1
0
mirror of https://github.com/theoludwig/theoludwig.git synced 2026-02-20 03:09:20 +01:00

Compare commits

...

24 Commits

Author SHA1 Message Date
42c79ef49e chore: add missing editorconfig-checker 2026-02-17 01:41:57 +01:00
74a9e039d4 feat(portfolio): separate Fusey and Fusey ARK 2026-02-17 01:38:35 +01:00
d0cde4538c chore: migrate from ESLint/Prettier to Oxc 2026-02-17 01:26:49 +01:00
57026d652c ci: chromatic with fetch-depth: 0 2025-12-12 20:45:26 +01:00
dbe7a73d1f fix: use new plausible/analytics script tag 2025-12-12 20:42:17 +01:00
203fee5416 fix: update React.js to v19.2.3 (security release)
Ref: https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components
2025-12-12 20:41:47 +01:00
3c92889f12 fix: update React.js to v19.2.1 (security release)
Ref: https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components
2025-12-05 10:35:14 +01:00
c86a039c1f build(deps): update to Next.js v16 + use image digests instead of tags
Ref:
- https://docs.docker.com/dhi/core-concepts/digests/
- https://github.com/goldbergyoni/nodebestpractices/blob/master/sections/docker/image-tags.md
- https://emmer.dev/blog/what-is-a-docker-digest/
2025-11-01 00:11:08 +01:00
338741d252 fix: update portfolio/Fusey.webp 2025-10-11 16:59:52 +02:00
be738a1c8d build(deps): update latest 2025-10-11 16:59:42 +02:00
9c20844d89 fix: update Curriculum Vitae 2025-09-23 10:15:03 +02:00
eceeca8b69 fix: update Curriculum Vitae 2025-09-22 19:29:02 +02:00
92eca82127 fix: update Curriculum Vitae 2025-09-09 19:32:06 +02:00
4502e32250 build(deps): update latest 2025-09-09 19:10:52 +02:00
d6f2d9e3a6 fix: add more details to tasks in Work experiences 2025-09-02 00:25:18 +02:00
eeb5e8872f feat: update skills 2025-09-01 23:34:03 +02:00
7d22880905 fix: update X/Twitter logo 2025-09-01 21:17:17 +02:00
49896bbaeb fix: rename portfolio to projects + reorganize home page ordering of sections 2025-08-31 20:51:55 +02:00
b3140dd977 fix: update Curriculum Vitae + add Fusey interest 2025-08-31 20:38:14 +02:00
39ef3e80ce chore: remove deprecated warning Next.js image un-configured qualities
Ref: https://nextjs.org/docs/messages/next-image-unconfigured-qualities
2025-08-31 19:57:46 +02:00
64a5ca73ad chore: clean up 2025-08-31 19:35:14 +02:00
b16699f5aa build(deps): update latest 2025-05-29 20:58:40 +02:00
e044b4caea build(deps): update Node.js to v24.0.2 to address security issue
Ref: https://nodejs.org/en/blog/release/v24.0.2
2025-05-20 21:13:02 +02:00
d795025860 chore: remove not needed --experimental-strip-types 2025-05-11 18:04:26 +02:00
149 changed files with 5359 additions and 7795 deletions

View File

@@ -1,20 +0,0 @@
---
name: "🐛 Bug Report"
about: "Report an unexpected problem or unintended behavior."
title: "[Bug]"
labels: "bug"
---
<!--
Please provide a clear and concise description of what the bug is. Include
screenshots if needed. Please make sure your issue has not already been fixed.
-->
## Steps To Reproduce
1. Step 1
2. Step 2
## The current behavior
## The expected behavior

View File

@@ -1,18 +0,0 @@
---
name: "📜 Documentation"
about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
title: "[Documentation]"
labels: "documentation"
---
<!-- Please make sure your issue has not already been fixed. -->
## Documentation
<!-- Please uncomment the type of documentation problem this issue address -->
<!-- Documentation is Missing -->
<!-- Documentation is Confusing -->
<!-- Documentation has Typo errors -->
## Proposal

View File

@@ -1,20 +0,0 @@
---
name: "✨ Feature Request"
about: "Suggest a new feature idea."
title: "[Feature]"
labels: "feature request"
---
<!-- Please make sure your issue has not already been fixed. -->
## Description
<!-- A clear and concise description of the problem or missing capability... -->
## Describe the solution you'd like
<!-- If you have a solution in mind, please describe it. -->
## Describe alternatives you've considered
<!-- Have you considered any alternative solutions or workarounds? -->

View File

@@ -1,20 +0,0 @@
---
name: "🔧 Improvement"
about: "Improve structure/format/performance/refactor/tests of the code."
title: "[Improvement]"
labels: "improvement"
---
<!-- Please make sure your issue has not already been fixed. -->
## Type of Improvement
<!-- Please uncomment the type of improvements this issue address -->
<!-- Files and Folders Structure -->
<!-- Performance -->
<!-- Refactoring code -->
<!-- Tests -->
<!-- Not Sure? -->
## Proposal

View File

@@ -1,8 +0,0 @@
---
name: "🙋 Question"
about: "Further information is requested."
title: "[Question]"
labels: "question"
---
### Question

View File

@@ -1,7 +0,0 @@
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
# What changes this PR introduce?
## List any relevant issue numbers
## Is there anything you'd like reviewers to focus on?

View File

@@ -11,27 +11,32 @@ jobs:
timeout-minutes: 30
runs-on: "ubuntu-latest"
env:
CI: "1"
TZ: "Europe/Paris"
DO_NOT_TRACK: "1"
TURBO_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
STORYBOOK_DISABLE_TELEMETRY: "1"
steps:
- uses: "actions/checkout@v4.2.2"
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
with:
fetch-depth: 0
- uses: "pnpm/action-setup@v4.1.0"
- uses: "pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061" # v4.2.0
- uses: "pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061" # v4.2.0
- name: "Setup Node.js"
uses: "actions/setup-node@v4.2.0"
uses: "actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238" # v6.2.0
with:
node-version: "24.x"
node-version: "24.13.1"
cache: "pnpm"
- name: "Install dependencies"
run: "pnpm install --frozen-lockfile"
- name: "Run Chromatic"
uses: "chromaui/action@latest"
uses: "chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0" # latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
workingDir: "apps/storybook"

View File

@@ -11,18 +11,21 @@ jobs:
timeout-minutes: 30
runs-on: "ubuntu-latest"
env:
CI: "1"
TZ: "Europe/Paris"
DO_NOT_TRACK: "1"
TURBO_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
STORYBOOK_DISABLE_TELEMETRY: "1"
steps:
- uses: "actions/checkout@v4.2.2"
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
- uses: "pnpm/action-setup@v4.1.0"
- uses: "pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061" # v4.2.0
- name: "Setup Node.js"
uses: "actions/setup-node@v4.2.0"
uses: "actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238" # v6.2.0
with:
node-version: "24.x"
node-version: "24.13.1"
cache: "pnpm"
- name: "Install dependencies"
@@ -33,15 +36,9 @@ jobs:
- run: "node --run lint:editorconfig"
- run: "node --run lint:markdown"
- run: "node --run lint:typescript"
- run: "node --run lint:eslint"
- run: "node --run lint:prettier"
- run: "node --run lint:turbo"
# - run: "node --run lint:typescript" # already covered by oxlint
- run: "node --run lint:oxlint"
- run: "node --run lint:oxfmt"
- run: "node --run test"
- run: "node --run build"
commitlint:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.2.2"
- uses: "wagoid/commitlint-github-action@v6.1.2"

11
.gitignore vendored
View File

@@ -1,8 +1,10 @@
# dependencies
node_modules
.npm
.cache
package-lock.json
.pnpm-store
.apt
.pnp
.pnp.js
.yarn/install-state.gz
@@ -15,12 +17,16 @@ coverage
out/
dist/
build/
.swc/
.react-email/
# misc
.DS_Store
*.pem
*.bak
.turbo
bin/
dump_*
# debug
npm-debug.log*
@@ -30,6 +36,7 @@ yarn-error.log*
# Storybook
*storybook.log
storybook-static
/public
# IDEs and editors
.idea
@@ -42,6 +49,7 @@ storybook-static
# local env files
.env
.env.dev
.env.production
.env*.local
@@ -50,4 +58,5 @@ storybook-static
# typescript
*.tsbuildinfo
# next-env.d.ts
next-env.d.ts
packages/i18n/src/translations/*.d.json.ts

View File

@@ -1,4 +1,4 @@
import relativeLinksRule, { markdownIt } from "markdownlint-rule-relative-links"
import relativeLinksRule from "markdownlint-rule-relative-links"
const config = {
config: {
@@ -11,9 +11,6 @@ const config = {
globs: ["**/*.md"],
ignores: ["**/node_modules"],
customRules: [relativeLinksRule],
markdownItFactory: () => {
return markdownIt
},
}
export default config

9
.oxfmtrc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": ["pnpm-lock.yaml"],
"semi": false,
"experimentalTailwindcss": {
"stylesheet": "./configs/config-tailwind/styles.css",
"functions": ["classNames"]
}
}

16
.oxlintrc.json Normal file
View File

@@ -0,0 +1,16 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["node_modules/eslint-config-conventions/.oxlintrc.json"],
"rules": {
"import-x/extensions": [
"error",
"ignorePackages",
{
"ts": "always",
"tsx": "always",
"js": "never",
"jsx": "never"
}
]
}
}

View File

@@ -1,13 +0,0 @@
{
"semi": false,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["classNames", "cva"],
"overrides": [
{
"files": "pnpm-lock.yaml",
"options": {
"rangeEnd": 0
}
}
]
}

View File

@@ -1,11 +1,9 @@
{
"recommendations": [
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"oxc.oxc-vscode",
"davidanson.vscode-markdownlint",
"bradlc.vscode-tailwindcss",
"mikestead.dotenv",
"antfu.pnpm-catalog-lens",
"Lokalise.i18n-ally"
]

View File

@@ -4,7 +4,6 @@
"prefix": "rfc",
"body": [
"export interface ${1:ComponentName}Props {}",
"",
"export const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = () => {",
" return (",
" <div>",
@@ -20,7 +19,7 @@
"scope": "typescriptreact",
"prefix": "rfcs",
"body": [
"import type { Meta, StoryObj } from \"@storybook/react\"",
"import type { Meta, StoryObj } from \"@storybook/nextjs\"",
"",
"import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}.tsx\"",
"",

27
.vscode/settings.json vendored
View File

@@ -2,22 +2,29 @@
"typescript.tsdk": "node_modules/typescript/lib",
"editor.bracketPairColorization.enabled": true,
"editor.wordWrap": "on",
"prettier.configPath": ".prettierrc.json",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll": "never",
"source.organizeImports": "never"
},
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
],
"oxc.enable": true,
"oxc.typeAware": true,
"oxc.unusedDisableDirectives": "deny",
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true,
"typescript.experimental.useTsgo": false,
"editor.quickSuggestions": {
"strings": "on"
},
"files.associations": {
"*.css": "tailwindcss"
},
"tailwindCSS.classFunctions": ["classNames"],
"tailwindCSS.experimental.configFile": "./configs/config-tailwind/styles.css",
"i18n-ally.localesPaths": ["./packages/i18n/src/translations/"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": false,
"i18n-ally.sourceLanguage": "en-US",
"i18n-ally.displayLanguage": "en-US",
"i18n-ally.sourceLanguage": "fr-FR",
"i18n-ally.displayLanguage": "fr-FR",
"i18n-ally.enabledFrameworks": ["next-intl", "general"],
"i18n-ally.extract.autoDetect": true
}

View File

@@ -1,127 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<contact@theoludwig.fr>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][mozilla coc].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][faq]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,10 +1,6 @@
# 💡 Contributing
# Contributing
Thanks a lot for your interest in contributing to **theoludwig.fr**! 🎉
## Code of Conduct
**theoludwig.fr** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
Thanks a lot for your interest in contributing to **theoludwig.fr**!
## Types of contributions
@@ -32,7 +28,7 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
### Prerequisites
- [Node.js](https://nodejs.org/) >= v24.0.0 [(`nvm install 24`)](https://nvm.sh)
- [pnpm](https://pnpm.io/) v10.10.0 [(`npm install --global corepack@0.32.0 && corepack enable`)](https://github.com/nodejs/corepack)
- [pnpm](https://pnpm.io/) [(`npm install --global corepack@0.34.6 && corepack enable`)](https://github.com/nodejs/corepack)
- [Docker](https://www.docker.com/)
### Installation
@@ -64,9 +60,10 @@ node --run dev
# Lint
node --run lint:editorconfig
node --run lint:markdown
node --run lint:typescript
node --run lint:eslint
node --run lint:prettier
node --run lint:turbo
# node --run lint:typescript # already covered by oxlint
node --run lint:oxlint
node --run lint:oxfmt
# Tests
node --run test

21
LICENSE
View File

@@ -1,21 +0,0 @@
# MIT License
Copyright (c) Théo LUDWIG <contact@theoludwig.fr>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -8,7 +8,7 @@
<a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
<a href="https://gitlab.com/theoludwig"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
<a href="https://www.npmjs.com/~theoludwig"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
<a href="https://twitter.com/theoludwig_"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=x&logoColor=white"/></a>
<a href="https://twitter.com/theoludwig_"><img alt="X/Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=x&logoColor=white"/></a>
<a href="https://www.youtube.com/@theo_ludwig"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a>
<a href="https://www.twitch.tv/theoludwig"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a>
<a href="https://theoludwig.fr/"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a>
@@ -17,7 +17,7 @@
<hr />
## 📜 About
## About
```json
{
@@ -27,30 +27,17 @@
"nationality": "Alsace, France",
"interests": ["Developer Full Stack", "Open-Source Enthusiast"],
"skills": {
"programmingLanguages": [
"JavaScript/TypeScript",
"Python",
"C/C++",
"PHP"
"software-development": [
"TypeScript",
"React.js (+ Next.js)",
"Tailwind CSS",
"Node.js",
"tRPC/oRPC",
"PostgreSQL"
],
"frontend": ["HTML/CSS", "Tailwind CSS", "React.js/Next.js"],
"backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
"tools": [
"GNU/Linux",
"Arch Linux",
"Visual Studio Code",
"Git",
"Docker"
]
"sys-admin": ["Docker", "Proxmox", "Caddy", "GitHub Actions", "GitLab CI/CD"],
"software-tools": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git"],
"systems-programming": ["C/C++", "Rust", "Go"]
}
}
```
<hr />
## 📈 Statistics
<p align=center>
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=theoludwig&show_icons=true&theme=dark" alt="Théo LUDWIG's GitHub Stats" />
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=theoludwig&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" alt="Théo LUDWIG's Programming Languages" />
</p>

View File

@@ -12,14 +12,7 @@ const config: StorybookConfig = {
"../../../packages/blog/src/**/*.stories.tsx",
"../stories/*.mdx",
],
addons: [
"@chromatic-com/storybook",
"@storybook/addon-essentials",
"@storybook/addon-storysource",
"@storybook/addon-a11y",
"@storybook/addon-interactions",
"storybook-dark-mode",
],
addons: ["@chromatic-com/storybook", "@storybook/addon-docs", "@storybook/addon-a11y"],
framework: {
name: "@storybook/nextjs",
options: {},

View File

@@ -2,18 +2,23 @@ import "@repo/config-tailwind/styles.css"
import "./storybook-css-overrides.css"
import i18nMessages from "@repo/i18n/translations/en-US.json"
import { LOCALE_DEFAULT, TIMEZONE } from "@repo/utils/constants"
import type { Preview } from "@storybook/react"
import type { Preview } from "@storybook/nextjs"
import { NextIntlClientProvider } from "next-intl"
import { ThemeProvider as NextThemeProvider } from "next-themes"
import React from "react"
const preview: Preview = {
globals: {
initialGlobals: {
a11y: {
manual: true,
},
},
parameters: {
a11y: {
test: "error",
},
docs: {
codePanel: true,
},
nextjs: {
appDirectory: true,
},

View File

@@ -1,35 +0,0 @@
import type { TestRunnerConfig } from "@storybook/test-runner"
import { getStoryContext } from "@storybook/test-runner"
import { checkA11y, configureAxe, injectAxe } from "axe-playwright"
/*
* See https://storybook.js.org/docs/writing-tests/test-runner#test-hook-api
*/
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page)
},
async postVisit(page, context) {
const storyContext = await getStoryContext(page, context)
const isA11yDisabled = storyContext.parameters?.a11y?.disable as boolean
if (isA11yDisabled) {
return
}
await configureAxe(page, {
rules: storyContext.parameters?.a11y?.config?.rules,
})
await checkA11y(page, "#storybook-root", {
verbose: false,
detailedReport: true,
detailedReportOptions: {
html: true,
},
})
},
}
export default config

View File

@@ -1,13 +0,0 @@
import typescriptESLint from "typescript-eslint"
import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@@ -76,9 +76,7 @@ server.listen(
port,
},
() => {
console.log(
`HTTP Server is listening at ${util.styleText("cyan", serverURL)}`,
)
console.log(`HTTP Server is listening at ${util.styleText("cyan", serverURL)}`)
console.log(`Serving files from: \`${basePath}\``)
},
)

View File

@@ -6,53 +6,43 @@
"scripts": {
"build": "storybook build",
"dev": "storybook dev --port 6006 --no-open",
"start": "node --experimental-strip-types http-server.ts --path=storybook-static --port=6006",
"start": "node http-server.ts --path=storybook-static --port=6006",
"test": "start-server-and-test \"start\" http://localhost:6006 \"test:storybook\"",
"test:dev": "start-server-and-test \"dev\" http://localhost:6006 \"test:storybook\"",
"test:storybook": "test-storybook --testTimeout=60000 --maxWorkers=2",
"chromatic": "chromatic"
},
"dependencies": {
"@repo/blog": "workspace:*",
"@repo/config-tailwind": "workspace:*",
"@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/blog": "workspace:*",
"mime": "catalog:",
"next": "catalog:",
"next-intl": "catalog:",
"next-themes": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"mime": "catalog:"
"react-dom": "catalog:"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@chromatic-com/storybook": "catalog:",
"@playwright/test": "catalog:",
"@storybook/addon-essentials": "catalog:",
"@storybook/addon-storysource": "catalog:",
"@repo/config-typescript": "workspace:*",
"@storybook/addon-a11y": "catalog:",
"@storybook/addon-interactions": "catalog:",
"@storybook/addon-docs": "catalog:",
"@storybook/addon-themes": "catalog:",
"@storybook/blocks": "catalog:",
"@storybook/nextjs": "catalog:",
"@storybook/react": "catalog:",
"@storybook/test": "catalog:",
"@storybook/test-runner": "catalog:",
"@tailwindcss/postcss": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"axe-playwright": "catalog:",
"chromatic": "catalog:",
"eslint": "catalog:",
"postcss": "catalog:",
"start-server-and-test": "catalog:",
"storybook": "catalog:",
"storybook-dark-mode": "catalog:",
"postcss": "catalog:",
"tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:"
}
}

View File

@@ -1,4 +1,4 @@
import { Meta, Title, ColorPalette, ColorItem } from "@storybook/blocks"
import { Meta, Title, ColorPalette, ColorItem } from "@storybook/addon-docs/blocks"
import tailwindConfig from "@repo/config-tailwind"
<Meta title="Design System/Colors" />

View File

@@ -1,7 +1,7 @@
FROM node:24.0.0-slim AS node-pnpm
FROM node:24.13.1-slim@sha256:a81a03dd965b4052269a57fac857004022b522a4bf06e7a739e25e18bce45af2 AS node-pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN npm install --global corepack@0.32.0 && corepack enable
RUN npm install --global corepack@0.34.6 && corepack enable
ENV TURBO_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV DO_NOT_TRACK=1
@@ -9,7 +9,7 @@ WORKDIR /usr/src/app
FROM node-pnpm AS builder
COPY ./ ./
RUN pnpm install --global turbo@2.5.3
RUN pnpm install --global turbo@2.8.9
RUN turbo prune @repo/website --docker
FROM node-pnpm AS installer
@@ -23,7 +23,7 @@ COPY --from=builder /usr/src/app/out/full/ ./
COPY turbo.json turbo.json
ARG VERSION="0.0.0-develop"
RUN pnpm install --global replace-in-files-cli@3.0.0
RUN pnpm install --global replace-in-files-cli@4.0.0
RUN VERSION_STRIPPED=${VERSION#v} && replace-in-files --regex='version": *"[^"]*' --replacement='"version": "'"$VERSION_STRIPPED"'"' '**/package.json' '!**/node_modules/**'
RUN pnpm --filter=@repo/website... exec turbo run build
@@ -34,7 +34,7 @@ ENV IS_STANDALONE=true
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner
USER applicationrunner
COPY --from=installer /usr/src/app/apps/website/next.config.js ./
COPY --from=installer /usr/src/app/apps/website/next.config.ts ./
COPY --from=installer /usr/src/app/apps/website/package.json ./
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/standalone ./
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/static ./apps/website/.next/static

View File

@@ -13,9 +13,7 @@ interface BlogPostPageProps {
}>
}
export const generateMetadata = async (
props: BlogPostPageProps,
): Promise<Metadata> => {
export const generateMetadata = async (props: BlogPostPageProps): Promise<Metadata> => {
const { slug } = await props.params
const blogPost = await getBlogPostBySlug(slug)
if (blogPost == null) {
@@ -37,9 +35,7 @@ export const generateMetadata = async (
}
}
export const generateStaticParams = async (): Promise<
Array<{ slug: string }>
> => {
export const generateStaticParams = async (): Promise<Array<{ slug: string }>> => {
const posts = await getBlogPosts()
return posts.map((post) => {
return {

View File

@@ -2,18 +2,13 @@ import { getBlogPosts } from "@repo/blog"
import { BlogPosts } from "@repo/blog/BlogPosts"
import type { LocaleProps } from "@repo/i18n/routing"
import { MainLayout } from "@repo/ui/Layout/MainLayout"
import {
Section,
SectionDescription,
SectionTitle,
} from "@repo/ui/Layout/Section"
import { Section, SectionDescription, SectionTitle } from "@repo/ui/Layout/Section"
import { LOCALE_DEFAULT } from "@repo/utils/constants"
import type { Metadata } from "next"
import { setRequestLocale } from "next-intl/server"
const title = "Blog | Théo LUDWIG"
const description =
"The latest news about my journey of learning computer science."
const description = "The latest news about my journey of learning computer science."
export const generateMetadata = async (): Promise<Metadata> => {
return {

View File

@@ -1,19 +1,23 @@
import "@repo/config-tailwind/styles.css"
import type { LocaleProps } from "@repo/i18n/routing"
import type { Locale } from "@repo/utils/constants"
import { Footer } from "@repo/ui/Layout/Footer"
import { Header } from "@repo/ui/Layout/Header"
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { VERSION } from "@repo/utils/constants"
import { setRequestLocale } from "next-intl/server"
interface MainLayoutProps extends React.PropsWithChildren, LocaleProps {}
interface MainLayoutProps extends React.PropsWithChildren {
params: Promise<{
locale: string
}>
}
const MainLayout: React.FC<MainLayoutProps> = async (props) => {
const { children, params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
setRequestLocale(locale as Locale)
return (
<ThemeProvider>

View File

@@ -21,21 +21,21 @@ const HomePage: React.FC<HomePageProps> = async (props) => {
<MainLayout>
<About />
<RevealFade>
<Interests />
</RevealFade>
<RevealFade>
<Skills />
</RevealFade>
<RevealFade>
<Portfolio />
</RevealFade>
<RevealFade>
<Interests />
</RevealFade>
<RevealFade>
<OpenSource />
</RevealFade>
<RevealFade>
<Skills />
</RevealFade>
</MainLayout>
)
}

View File

@@ -1,20 +1,20 @@
import "@repo/config-tailwind/styles.css"
import type { LocaleProps } from "@repo/i18n/routing"
import type { Locale } from "@repo/utils/constants"
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { setRequestLocale } from "next-intl/server"
interface CurriculumVitaeLayoutProps
extends React.PropsWithChildren,
LocaleProps {}
interface CurriculumVitaeLayoutProps extends React.PropsWithChildren {
params: Promise<{
locale: string
}>
}
const CurriculumVitaeLayout: React.FC<CurriculumVitaeLayoutProps> = async (
props,
) => {
const CurriculumVitaeLayout: React.FC<CurriculumVitaeLayoutProps> = async (props) => {
const { children, params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
setRequestLocale(locale as Locale)
return <ThemeProvider forcedTheme="light">{children}</ThemeProvider>
}

View File

@@ -1,14 +1,9 @@
import "@repo/config-tailwind/styles.css"
import type { LocaleProps } from "@repo/i18n/routing"
import type { Locale } from "@repo/utils/constants"
import { LOCALES } from "@repo/utils/constants"
import type { Metadata, Viewport } from "next"
import { NextIntlClientProvider } from "next-intl"
import {
getMessages,
getTranslations,
setRequestLocale,
} from "next-intl/server"
import { getMessages, getTranslations, setRequestLocale } from "next-intl/server"
import Script from "next/script"
const DOMAIN = "theoludwig.fr"
@@ -19,9 +14,13 @@ export const viewport: Viewport = {
export const generateMetadata = async ({
params,
}: LocaleProps): Promise<Metadata> => {
}: {
params: Promise<{
locale: string
}>
}): Promise<Metadata> => {
const { locale } = await params
const t = await getTranslations({ locale })
const t = await getTranslations({ locale: locale as Locale })
const title = t("meta.title")
const description = `${title} - ${t("meta.description")}`
const image = "/images/logo.webp"
@@ -64,29 +63,32 @@ export const generateStaticParams = (): Array<{ locale: Locale }> => {
})
}
interface LocaleLayoutProps extends React.PropsWithChildren, LocaleProps {}
interface LocaleLayoutProps extends React.PropsWithChildren {
params: Promise<{
locale: string
}>
}
const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
const { children, params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
setRequestLocale(locale as Locale)
const messages = await getMessages()
return (
<html lang={locale} suppressHydrationWarning>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
<Script
defer
data-domain={DOMAIN}
src="https://plausible.theoludwig.fr/js/script.js"
/>
<Script async src="https://analytics.theoludwig.fr/js/pa-MToGPBn2iXgorPLkh6f_s.js" />
<Script id="analytics-init">
{`
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
plausible.init()`}
</Script>
</body>
</html>
)

View File

@@ -1,13 +0,0 @@
import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/config-eslint/nextjs"
export default typescriptESLint.config(...configNextjs, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -1,22 +0,0 @@
import createNextIntlPlugin from "next-intl/plugin"
const IS_STANDALONE = process.env.IS_STANDALONE === "true"
/** @type {import('next').NextConfig} */
const nextConfig = {
output: IS_STANDALONE ? "standalone" : undefined,
images: {
unoptimized: true,
},
compress: false,
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
}
const withNextIntl = createNextIntlPlugin()
export default withNextIntl(nextConfig)

View File

@@ -0,0 +1,34 @@
import { LOCALE_DEFAULT } from "@repo/utils/constants"
import type { NextConfig } from "next"
import createNextIntlPlugin from "next-intl/plugin"
import path from "node:path"
const IS_STANDALONE = process.env.IS_STANDALONE === "true"
const nextConfig: NextConfig = {
output: IS_STANDALONE ? "standalone" : undefined,
typedRoutes: true,
images: {
unoptimized: true,
},
typescript: {
ignoreBuildErrors: true,
},
}
const withNextIntl = createNextIntlPlugin({
experimental: {
createMessagesDeclaration: path.join(
process.cwd(),
"..",
"..",
"packages",
"i18n",
"src",
"translations",
`${LOCALE_DEFAULT}.json`,
),
},
})
export default withNextIntl(nextConfig)

View File

@@ -7,38 +7,35 @@
"#*": "./*"
},
"scripts": {
"dev": "next dev --port 3000 --turbopack",
"dev": "next dev --port 3000",
"build": "next build",
"start": "next start --port 3000",
"lint:eslint": "eslint . --max-warnings 0",
"lint:typescript": "tsc --noEmit"
"typegen": "next typegen",
"lint:typescript": "next typegen && tsc --noEmit"
},
"dependencies": {
"@mdx-js/mdx": "catalog:",
"@repo/blog": "workspace:*",
"@repo/config-tailwind": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*",
"@mdx-js/mdx": "catalog:",
"next-mdx-remote": "catalog:",
"shiki": "catalog:",
"@repo/utils": "workspace:*",
"next": "catalog:",
"next-intl": "catalog:",
"next-mdx-remote": "catalog:",
"react": "catalog:",
"react-dom": "catalog:"
"react-dom": "catalog:",
"shiki": "catalog:"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@tailwindcss/postcss": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"eslint": "catalog:",
"postcss": "catalog:",
"tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:"
}
}

View File

@@ -21,6 +21,6 @@ export const config = {
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
"/((?!api|_next/static|_next/image|images|favicon.ico).*)",
"/((?!api|_next/static|_next/image|images|favicon.ico|robots.txt).*)",
],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 945 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -4,9 +4,6 @@
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["@total-typescript/ts-reset", "@repo/i18n/messages.d.ts"],
"allowJs": true,
"paths": {
"#*": ["./*"]
},
"plugins": [
{
"name": "next"
@@ -14,5 +11,5 @@
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", ".next"]
"exclude": ["node_modules"]
}

View File

@@ -1,35 +0,0 @@
import typescriptESLint from "typescript-eslint"
import configConventions from "eslint-config-conventions"
import importX from "eslint-plugin-import-x"
export default typescriptESLint.config(
{
ignores: [
".next",
"**/next.config.js",
"**/eslint.config.js",
"**/tailwind.config.js",
"**/postcss.config.js",
"**/kysely.config.ts",
],
},
...configConventions,
{
name: "config-eslint",
plugins: {
"import-x": importX,
},
rules: {
"import-x/extensions": [
"error",
"ignorePackages",
{
ts: "always",
tsx: "always",
js: "never",
jsx: "never",
},
],
},
},
)

View File

@@ -1,7 +0,0 @@
import type typescriptESLint from "typescript-eslint"
declare const eslintConfigConventions: ReturnType<
typeof typescriptESLint.config
>
export default eslintConfigConventions

View File

@@ -1,57 +0,0 @@
import { FlatCompat } from "@eslint/eslintrc"
import typescriptESLint from "typescript-eslint"
import config from "../eslint.config.js"
const flatCompat = new FlatCompat({
baseDirectory: import.meta.dirname,
})
export default typescriptESLint.config(
...config,
...flatCompat.extends("next/core-web-vitals"),
{
name: "config-eslint/nextjs",
settings: {
tailwindcss: {
callees: ["classNames", "cva"],
},
react: {
version: "detect",
},
},
rules: {
"@next/next/no-html-link-for-pages": "off",
"@next/next/no-img-element": "off",
"react/self-closing-comp": [
"error",
{
component: true,
html: true,
},
],
"react/void-dom-elements-no-children": "error",
"react/jsx-boolean-value": "error",
"no-restricted-imports": [
"error",
{
paths: [
{
name: "next/link",
message: "Please import from `@repo/i18n/routing` instead.",
},
{
name: "next/navigation",
importNames: [
"redirect",
"permanentRedirect",
"useRouter",
"usePathname",
],
message: "Please import from `@repo/i18n/routing` instead.",
},
],
},
],
},
},
)

View File

@@ -1,32 +0,0 @@
{
"name": "@repo/config-eslint",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./eslint.config.js",
"require": "./eslint.config.js",
"default": "./eslint.config.js"
},
"./nextjs": {
"types": "./index.d.ts",
"import": "./nextjs/eslint.config.js",
"require": "./nextjs/eslint.config.js",
"default": "./nextjs/eslint.config.js"
}
},
"devDependencies": {
"@eslint/eslintrc": "catalog:",
"typescript-eslint": "catalog:",
"eslint": "catalog:",
"eslint-config-conventions": "catalog:",
"eslint-plugin-promise": "catalog:",
"eslint-plugin-unicorn": "catalog:",
"eslint-config-next": "catalog:",
"eslint-plugin-import-x": "catalog:",
"typescript": "catalog:",
"globals": "catalog:"
}
}

View File

@@ -1,13 +0,0 @@
import typescriptESLint from "typescript-eslint"
import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@@ -1,3 +1,5 @@
import type { Config } from "tailwindcss"
export default Config
declare const config: Config
export default config

View File

@@ -21,13 +21,10 @@
"tailwind-merge": "catalog:"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@tailwindcss/postcss": "catalog:",
"@tailwindcss/typography": "catalog:",
"typescript-eslint": "catalog:",
"eslint": "catalog:",
"postcss": "catalog:",
"tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:"
"tailwindcss": "catalog:"
}
}

View File

@@ -1,16 +1,17 @@
@import "tailwindcss";
@config "./tailwind.config.js";
@source "../../packages/ui";
@source "../../packages/blog";
@source "../../apps/website";
@source "../../apps/website/app";
@source "../../apps/website/components";
@source "../../apps/storybook/.storybook";
@source "../../packages/ui/src";
@source "../../packages/blog/src";
@import "@fontsource/montserrat/400.css" layer(base);
@import "@fontsource/montserrat/500.css" layer(base);
@import "@fontsource/montserrat/600.css" layer(base);
@import "@fontsource/montserrat/700.css" layer(base);
@import "@fontsource/montserrat/800.css" layer(base);
@import "@fontsource/montserrat/400.css";
@import "@fontsource/montserrat/500.css";
@import "@fontsource/montserrat/600.css";
@import "@fontsource/montserrat/700.css";
@import "@fontsource/montserrat/800.css";
@layer base {
[type="search"]::-webkit-search-decoration,
@@ -47,7 +48,7 @@
q,
blockquote {
@apply italic tracking-wider;
@apply tracking-wider italic;
}
blockquote {
@apply border-gray-lighter border-l-4 pl-3 italic;
@@ -73,6 +74,10 @@
@apply font-semibold italic;
cursor: help;
}
small {
font-size: 87%;
}
}
body {
@@ -172,9 +177,8 @@ code .line::before {
}
.curriculum-vitae {
background: #f0f0f0;
color: #333;
font-family: Arial, sans-serif;
font-family: Montserrat, sans-serif;
hr {
margin-top: 15px;

View File

@@ -17,9 +17,10 @@
"isolatedModules": true,
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"allowArbitraryExtensions": true,
"skipLibCheck": true,
"jsx": "preserve",
"incremental": true,
"incremental": false,
"noEmit": true,
"target": "ESNext",

View File

@@ -3,30 +3,34 @@
"version": "0.0.0-develop",
"private": true,
"type": "module",
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"engines": {
"node": ">=24.0.0"
},
"scripts": {
"dev": "turbo run dev --parallel",
"start": "turbo run start --parallel",
"build": "turbo run build",
"test": "turbo run test",
"lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint-cli2",
"lint:turbo": "turbo boundaries",
"lint:typescript": "turbo run lint:typescript",
"lint:eslint": "turbo run lint:eslint",
"lint:prettier": "prettier . --check"
"lint:oxlint": "turbo run typegen && oxlint . --type-aware --type-check --deny-warnings --report-unused-disable-directives",
"lint:oxfmt": "oxfmt . --check",
"oxfmt": "oxfmt .",
"test": "turbo run test",
"build": "turbo run build"
},
"devDependencies": {
"editorconfig-checker": "catalog:",
"playwright": "catalog:",
"prettier": "catalog:",
"prettier-plugin-tailwindcss": "catalog:",
"markdownlint-cli2": "catalog:",
"eslint-config-conventions": "catalog:",
"markdownlint": "catalog:",
"markdownlint-cli2": "catalog:",
"markdownlint-rule-relative-links": "catalog:",
"oxfmt": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"playwright": "catalog:",
"turbo": "catalog:",
"typescript": "catalog:"
}
},
"engines": {
"node": ">=24.0.0"
},
"packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
}

View File

@@ -1,13 +0,0 @@
import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/config-eslint/nextjs"
export default typescriptESLint.config(...configNextjs, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@@ -9,48 +9,42 @@
"./BlogPostUI": "./src/BlogPostUI.tsx"
},
"scripts": {
"lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit"
},
"dependencies": {
"@giscus/react": "catalog:",
"@mdx-js/mdx": "catalog:",
"@repo/config-tailwind": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*",
"@repo/react-hooks": "workspace:*",
"@giscus/react": "catalog:",
"@repo/utils": "workspace:*",
"@shikijs/rehype": "catalog:",
"@mdx-js/mdx": "catalog:",
"gray-matter": "catalog:",
"katex": "catalog:",
"next": "catalog:",
"next-intl": "catalog:",
"next-mdx-remote": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-icons": "catalog:",
"rehype-katex": "catalog:",
"rehype-raw": "catalog:",
"rehype-slug": "catalog:",
"remark-gfm": "catalog:",
"remark-math": "catalog:",
"shiki": "catalog:",
"next": "catalog:",
"next-mdx-remote": "catalog:",
"next-intl": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"react-icons": "catalog:"
"shiki": "catalog:"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@storybook/nextjs": "catalog:",
"@tailwindcss/postcss": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"@storybook/blocks": "catalog:",
"@storybook/react": "catalog:",
"@storybook/test": "catalog:",
"eslint": "catalog:",
"postcss": "catalog:",
"storybook": "catalog:",
"tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:"
}
}

View File

@@ -173,10 +173,7 @@ We have to keep it as simple as possible, not to implement features that are not
import fs from "node:fs"
import path from "node:path"
const createFile = async (
name: string,
isTemporary: boolean = false,
): Promise<void> => {
const createFile = async (name: string, isTemporary: boolean = false): Promise<void> => {
if (isTemporary) {
return await fs.promises.writeFile(path.join("temporary", name), "")
}

View File

@@ -17,7 +17,7 @@ Keep in mind that I will not translate the posts in French, all the posts will b
I plan to publish new posts when I have something new to share. There's no schedule, so stay tuned!
To stay informed of new blog post and to ask questions, feel free to follow me on Twitter: [@theoludwig\_](https://twitter.com/theoludwig_).
To stay informed of new blog post and to ask questions, feel free to follow me on X/Twitter: [@theoludwig\_](https://twitter.com/theoludwig_).
## Project based learning

View File

@@ -37,7 +37,7 @@ The idea is that a user can create an account to authenticate with an email addr
## History
The idea for the project has existed since May 13, 2020, symbolized by a [publication on Twitter](https://twitter.com/theoludwig_/status/1260638175246135296) by the creator: Théo LUDWIG.
The idea for the project has existed since May 13, 2020, symbolized by a [publication on X/Twitter](https://twitter.com/theoludwig_/status/1260638175246135296) by the creator: Théo LUDWIG.
The main goal is to put into **practice knowledge in web development** and computer science in general on a concrete project that can **easily evolve over time** where you can add many features.

View File

@@ -17,8 +17,7 @@ export const BLOG_POST_MOCK = {
"\nHello, world! 👋\n\n## Introduction\n\nThis blog is here to document my journey of learning computer science.",
frontmatter: {
title: "👋 Hello, world!",
description:
"First post of the blog, introduction and explanation of how this blog is made.",
description: "First post of the blog, introduction and explanation of how this blog is made.",
isPublished: true,
publishedOn: "2022-02-20T08:00:18.758Z",
},

View File

@@ -14,10 +14,9 @@ import "katex/dist/katex.min.css"
import { BlogPostComments } from "./BlogPostComments.tsx"
const Heading: React.FC<
React.DetailedHTMLProps<
React.HTMLAttributes<HTMLHeadingElement>,
HTMLHeadingElement
> & { as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" }
React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement> & {
as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
}
> = (props) => {
const { children, as, id = "", ...rest } = props
@@ -37,9 +36,7 @@ export interface BlogPostContentProps {
content: string
}
export const BlogPostContent: React.FC<BlogPostContentProps> = async (
props,
) => {
export const BlogPostContent: React.FC<BlogPostContentProps> = async (props) => {
const { content } = props
return (
@@ -90,14 +87,7 @@ export const BlogPostContent: React.FC<BlogPostContentProps> = async (
const source = src.replace("../../../apps/website/public/", "/")
return (
<span className="flex flex-col items-center justify-center">
<Image
src={source}
alt={alt}
width={1000}
height={1000}
quality={100}
className="size-auto"
/>
<Image src={source} alt={alt} width={1000} height={1000} className="size-auto" />
</span>
)
},
@@ -110,9 +100,7 @@ export const BlogPostContent: React.FC<BlogPostContentProps> = async (
if (hrefString.startsWith("../posts/")) {
return (
<Link
href={hrefString
.replace("../posts/", "/blog/")
.replace(".md", "")}
href={hrefString.replace("../posts/", "/blog/").replace(".md", "")}
{...rest}
/>
)

View File

@@ -19,9 +19,7 @@ export const BlogPostUI: React.FC<BlogPostUIProps> = (props) => {
<Typography variant="h2" as="h1">
{blogPost.frontmatter.title}
</Typography>
<p className="mt-2">
{getISODate(new Date(blogPost.frontmatter.publishedOn))}
</p>
<p className="mt-2">{getISODate(new Date(blogPost.frontmatter.publishedOn))}</p>
</div>
<BlogPostContent content={blogPost.content} />
</MainLayout>

View File

@@ -14,9 +14,7 @@ export const BlogPosts: React.FC<BlogPostsProps> = (props) => {
return (
<ul className="list-none">
{posts.map((post) => {
const postPublishedOn = getISODate(
new Date(post.frontmatter.publishedOn),
)
const postPublishedOn = getISODate(new Date(post.frontmatter.publishedOn))
return (
<li key={post.slug}>

View File

@@ -4,14 +4,7 @@ import path from "node:path"
import matter from "gray-matter"
import type { BlogPost, FrontMatter } from "./BlogPost.tsx"
export const BLOG_POSTS_PATH = path.join(
process.cwd(),
"..",
"..",
"packages",
"blog",
"posts",
)
export const BLOG_POSTS_PATH = path.join(process.cwd(), "..", "..", "packages", "blog", "posts")
export const getBlogPosts = async (): Promise<BlogPost[]> => {
const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH)
@@ -48,9 +41,7 @@ export const getBlogPosts = async (): Promise<BlogPost[]> => {
return blogPostsSortedByPublicationDate
}
export const getBlogPostBySlug = async (
slug: string,
): Promise<BlogPost | undefined> => {
export const getBlogPostBySlug = async (slug: string): Promise<BlogPost | undefined> => {
const blogPosts = await getBlogPosts()
const blogPost = blogPosts.find((blogPost) => {
return blogPost.slug === slug

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react"
import type { Meta, StoryObj } from "@storybook/nextjs"
import { BLOG_POST_MOCK } from "../BlogPost.ts"
import { BlogPostUI as BlogPostUIComponent } from "../BlogPostUI.tsx"

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react"
import type { Meta, StoryObj } from "@storybook/nextjs"
import { BLOG_POST_MOCK } from "../BlogPost.ts"
import { BlogPosts as BlogPostsComponent } from "../BlogPosts.tsx"

View File

@@ -2,10 +2,6 @@
"extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": [
"@total-typescript/ts-reset",
"@types/node",
"@repo/i18n/messages.d.ts"
]
"types": ["@total-typescript/ts-reset", "@types/node", "@repo/i18n/messages.d.ts"]
}
}

View File

@@ -1,13 +0,0 @@
import typescriptESLint from "typescript-eslint"
import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@@ -10,7 +10,6 @@
"./routing": "./src/routing.ts"
},
"scripts": {
"lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit"
},
"dependencies": {
@@ -21,13 +20,10 @@
"react-dom": "catalog:"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@total-typescript/ts-reset": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"eslint": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:"
}
}

View File

@@ -8,18 +8,11 @@ import { deepMerge } from "@repo/utils/objects"
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
const userMessages = (await import(`./translations/${locale}.json`)).default
const defaultMessages = (
await import(`./translations/${LOCALE_DEFAULT}.json`)
).default
const messages = deepMerge<AbstractIntlMessages>(
defaultMessages,
userMessages,
)
const defaultMessages = (await import(`./translations/${LOCALE_DEFAULT}.json`)).default
const messages = deepMerge<AbstractIntlMessages>(defaultMessages, userMessages)
return {
locale,

View File

@@ -23,11 +23,5 @@ export const routing = defineRouting({
localePrefix: LOCALE_PREFIX,
})
export const {
Link,
redirect,
usePathname,
useRouter,
getPathname,
permanentRedirect,
} = createNavigation(routing)
export const { Link, redirect, usePathname, useRouter, getPathname, permanentRedirect } =
createNavigation(routing)

View File

@@ -88,7 +88,7 @@
"position": "Stagiaire Développeur Web Full Stack",
"startDate": "2023-04-11",
"endDate": "2023-07-26",
"duration": "4 mois",
"duration": "3 mois",
},
// {
// "summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.",
@@ -164,13 +164,7 @@
"name": "Backend",
},
{
"keywords": [
"GNU/Linux",
"Arch Linux",
"Visual Studio Code",
"Git",
"Docker",
],
"keywords": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git", "Docker"],
"name": "Logiciels et outils",
},
{

View File

@@ -7,6 +7,7 @@
"en-US": "English",
"fr-FR": "French"
},
"fr-FR-main": "French (primary language)",
"loading": "Loading...",
"errors": {
"error": "Error",
@@ -24,7 +25,7 @@
"description": "I constantly wonder how to improve our present, to make our future better, particularly thanks to the advancements in computer science. <br></br> My priority is to craft intuitive user experiences (UX), that meet the needs of the users in the most efficient way possible.",
"title": "About"
},
"description": "Developer Full Stack • Student",
"description": "Developer Full Stack • French Student",
"education": {
"cnam": {
"institution": "Conservatoire National des Arts et Métiers (CNAM), in Eckbolsheim - UIMM Alsace - ITII Alsace",
@@ -41,10 +42,11 @@
"institution": "IUT Robert Schuman in Illkirch-Graffenstaden",
"study-type": "University Bachelor of Technology (BUT) Computer Science",
"years": {
"title": "2021 - 2024",
"2021-2022": {
"courses": {
"java": "Object Oriented Development in Java",
"sql": "Relational database and SQL language",
"sql": "<strong>Relational database and SQL language</strong>",
"systems-c": "Systems programming in C (Memory allocation, Pointers, Structures)",
"windows-forms": "Windows Forms (.NET Framework) Application Development in C#"
},
@@ -53,10 +55,10 @@
},
"2022-2023": {
"courses": {
"clean-code": "Design Patterns and Principles (Maintainable and Reusable Code) in UML",
"clean-code": "Design Patterns and Principles (<strong>Maintainable and Reusable Code</strong>) in UML",
"sql-security": "Securing database access and PL/SQL",
"systems-c": "Systems programming in C (Multi-Thread, Server/Client UDP/TCP)",
"tests": "Development Quality and Automated Testing",
"systems-c": "Systems programming in <strong>C (Multi-Thread, Server/Client UDP/TCP)</strong>",
"tests": "<strong>Development Quality and Automated Testing</strong>",
"web": "Web development with the Laravel framework in PHP"
},
"description": "2nd year",
@@ -67,7 +69,7 @@
"ci-cd": "Continuous Integration/Deployment (CI/CD) and Docker",
"complexity-algorithms": "Theoretical and Practical Algorithmic Complexity in C++",
"no-sql": "NoSQL database (Redis, MongoDB, Cassandra)",
"web": "Web development in Node.js and React.js"
"web": "<strong>Web development in Node.js and React.js</strong>"
},
"description": "3rd year",
"title": "2023 - 2024"
@@ -87,19 +89,28 @@
"title": "Studies"
},
"interests": {
"high-tech": "Passionate about High-Tech",
"open-source": "Open-Source Enthusiast"
"title": "Interests & hobbies",
"fusey": "<link>Fusey (fusey.gg)</link>: website I'm developing for the game ARK that tracks the number of players connected to the servers in real time and has <strong>over ~5,000 visitors each month, ~100,000 members on Discord, and ~120,000 followers on X/Twitter</strong>.",
"open-source": "<strong>Open-Source Contributor</strong>: publishing and <strong>contributing to npm packages</strong> and <link-github>GitHub</link-github> repositories, notably with <link-markdownlint>markdownlint-rule-relative-links</link-markdownlint> (around 20,000 downloads per week), contributions to <link-leon>Leon</link-leon>, an open-source personal assistant, among other projects."
},
"work": {
"ircad": {
"duration": "4 ans",
"duration": "4 years",
"position": "Full Stack Web Developer Apprentice",
"summary": "Development of WebSurg, a virtual university dedicated to medical-surgical training, in React.js/Next.js and API Platform with Symfony."
"tasks": {
"WebSurg": "Development of WebSurg, a virtual university dedicated to medical-surgical training, built with React.js/Next.js and API Platform with Symfony.",
"Figma": "<strong>Integration of Figma mockups</strong> for WebSurg, website dedicated to medical-surgical training.",
"IRCAD-Core": "IRCAD Core, an internal tool for managing medical training sessions and their requirements (anatomical models, medications, staff training, etc.).",
"feature-logs": "History and traceability of data modifications (what? who? when?) in IRCAD Core.",
"feature-permissions": "<strong>Advanced permissions system and OAuth2 authentication</strong>, with read, write, and delete access restricted for specific users of the IRCAD Core application.",
"feature-search": "<strong>Search engine with filters, sorting, and customizable display</strong> for each user of IRCAD Core.",
"feature-architecture": "IRCAD Core project architecture in <strong>TypeScript Monorepo</strong> with Turborepo, <strong>automatic deployment (CI/CD)</strong> and <strong>self-hosted internally with Docker Compose</strong>."
}
},
"numerize": {
"duration": "4 months",
"duration": "3 months",
"position": "Full Stack Web Developer Intern",
"summary": "Development of an DMS (Document Management System) tool in React.js, Laravel and GraphQL."
"summary": "Development of a DMS (Document Management System) tool in React.js, Laravel and GraphQL."
},
"title": "Work experiences"
}
@@ -141,27 +152,34 @@
},
"portfolio": {
"fusey": {
"description": "ARK: Survival Ascended Wiki and Player stats tracker.",
"description": "A platform connecting creators and game server owners through automated, safe, and verifiable paid promotions.",
"title": "Fusey"
},
"fusey-ark": {
"description": "Website I'm developing for the game ARK that tracks the number of players connected to the servers in real time.",
"title": "Fusey ARK"
},
"carolo": {
"description": "Strategy board game similar to chess which allows grandiose moves (only available in French).",
"title": "Carolo"
},
"leon": {
"description": "Leon is your open-source personal assistant.",
"title": "Leon"
},
"title": "Portfolio"
"title": "Projects"
},
"skills": {
"software-development": "Software Development",
"sys-admin": "SysAdmin",
"systems-programming": "Systems Programming",
"backend": "Backend",
"driving-license": "Driving license",
"frontend": "Frontend",
"others": "Others",
"programming-languages": "Programming languages",
"software-tools": "Software and tools",
"title": "Skills"
"title": "Skills",
"rust-advent-of-code": " (as hobby and <link-aoc>Advent Of Code</link-aoc>)",
"go-basics": " (basics and fundamentals)"
}
}
}

View File

@@ -7,6 +7,7 @@
"en-US": "Anglais",
"fr-FR": "Français"
},
"fr-FR-main": "Français (langue principale)",
"loading": "Chargement...",
"errors": {
"error": "Erreur",
@@ -41,10 +42,11 @@
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
"study-type": "Bachelor Universitaire de Technologie (BUT) Informatique",
"years": {
"title": "2021 - 2024",
"2021-2022": {
"courses": {
"java": "Développement Orientée Objet en Java",
"sql": "Base de données relationnelles et langage SQL",
"sql": "<strong>Base de données relationnelles et langage SQL</strong>",
"systems-c": "Programmation systèmes en C (Allocation mémoire, Pointeurs, Structures)",
"windows-forms": "Développement d'application Windows Forms (.NET Framework) en C#"
},
@@ -53,10 +55,10 @@
},
"2022-2023": {
"courses": {
"clean-code": "Patrons et Principes de conceptions (Code maintenable et réutilisable) en UML",
"clean-code": "Patrons et Principes de conceptions (<strong>Code maintenable et réutilisable</strong>) en UML",
"sql-security": "Sécurisation des accès à la base de données et PL/SQL",
"systems-c": "Programmation systèmes en C (Multi-Thread, Serveur/Client UDP/TCP)",
"tests": "Qualité de développement et Tests automatisés",
"systems-c": "Programmation systèmes en <strong>C (Multi-Thread, Serveur/Client UDP/TCP)</strong>",
"tests": "<strong>Qualité de développement et Tests automatisés</strong>",
"web": "Développement Web avec le framework Laravel en PHP"
},
"description": "2ème année",
@@ -67,7 +69,7 @@
"ci-cd": "Intégration/Déploiement Continue et Docker",
"complexity-algorithms": "Complexité Algorithmique Théorique et Pratique en C++",
"no-sql": "Base de données NoSQL (Redis, MongoDB, Cassandra)",
"web": "Développement Web en Node.js et React.js"
"web": "<strong>Développement Web en Node.js et React.js</strong>"
},
"description": "3ème année",
"title": "2023 - 2024"
@@ -87,17 +89,26 @@
"title": "Études"
},
"interests": {
"high-tech": "Passionné de High-Tech",
"open-source": "Enthousiaste de l'Open-Source"
"title": "Intérêts & loisirs",
"fusey": "<link>Fusey (fusey.gg)</link> : site web que je développe pour le jeu ARK qui permet de suivre en temps réel le nombre de joueurs connectés sur les serveurs et a plus de <strong>~5 000 visiteurs chaque mois, ~100 000 membres sur Discord et ~120 000 followers sur X/Twitter</strong>.",
"open-source": "<strong>Contributeur de l'Open-Source</strong> : publications et <strong>contributions sur npm</strong> et <link-github>GitHub</link-github> avec notamment <link-markdownlint>markdownlint-rule-relative-links</link-markdownlint> avec ~20 000 téléchargements par semaine, contributions à <link-leon>Leon</link-leon>, un assistant personnel open source, parmis d'autres projets."
},
"work": {
"ircad": {
"duration": "4 ans",
"position": "Apprenti Développeur Web Full Stack",
"summary": "Développement de WebSurg, une université virtuelle consacrée à la formation médico-chirurgicale, en React.js/Next.js et API Platform avec Symfony."
"tasks": {
"WebSurg": "Développement de WebSurg, une université virtuelle consacrée à la formation médico-chirurgicale, en React.js/Next.js et API Platform avec Symfony.",
"Figma": "<strong>Intégration des maquettes Figma</strong> pour WebSurg, site web consacrée à la formation médico-chirurgicale.",
"IRCAD-Core": "IRCAD Core, outil interne de gestion des formations médicales et leurs besoins (modèles anatomiques, médicaments, formations du personnel, etc.).",
"feature-logs": "Historique et traçabilité des modifications des données (quoi? qui? quand?) dans IRCAD Core.",
"feature-permissions": "<strong>Système de permissions avancé</strong> et authentification OAuth2, avec accès en lecture, écriture et suppression restreint pour des utilisateurs spécifiques de l'application IRCAD Core.",
"feature-search": "<strong>Moteur de recherche avec filtres, tris et ordre d'affichage personnalisable</strong> pour IRCAD Core.",
"feature-architecture": "Architecture du projet IRCAD Core en <strong>Monorepo TypeScript</strong> avec Turborepo, <strong>déploiement automatique (CI/CD)</strong> et <strong>hébergé en interne avec Docker Compose</strong>."
}
},
"numerize": {
"duration": "4 mois",
"duration": "3 mois",
"position": "Stagiaire Développeur Web Full Stack",
"summary": "Développement d'un outil GED (Gestion Électronique de Documents) en React.js, Laravel et GraphQL."
},
@@ -141,27 +152,34 @@
},
"portfolio": {
"fusey": {
"description": "ARK: Survival Ascended Wiki et suivi des statistiques des joueurs.",
"description": "Une plateforme mettant en relation les créateurs de contenu (e.g: YouTube) et les propriétaires de serveurs de jeux pour des promotions sécurisées et fiables.",
"title": "Fusey"
},
"fusey-ark": {
"description": "Site web que je développe pour le jeu ARK qui permet de suivre en temps réel le nombre de joueurs connectés sur les serveurs.",
"title": "Fusey ARK"
},
"carolo": {
"description": "Jeu de plateau stratégique similaire aux échecs qui permet des coups grandioses, reposant sur des enchaînements remarquables.",
"title": "Carolo"
},
"leon": {
"description": "Leon est votre assistant personnel open source.",
"title": "Leon"
},
"title": "Portfolio"
"title": "Projets"
},
"skills": {
"software-development": "Développement informatique",
"sys-admin": "SysAdmin",
"systems-programming": "Programmation Système",
"backend": "Backend",
"driving-license": "Permis B",
"frontend": "Frontend",
"others": "Autres",
"programming-languages": "Langages de programmation",
"software-tools": "Logiciels et outils",
"title": "Compétences"
"title": "Compétences",
"rust-advent-of-code": " (pendant temps libre et <link-aoc>Advent Of Code</link-aoc>)",
"go-basics": " (bases et fondamentaux)"
}
}
}

View File

@@ -1,13 +0,0 @@
import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/config-eslint/nextjs"
export default typescriptESLint.config(...configNextjs, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@@ -1,29 +0,0 @@
{
"name": "@repo/react-hooks",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {
"./useBoolean": "./src/useBoolean.ts",
"./useIsMounted": "./src/useIsMounted.ts"
},
"scripts": {
"lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit"
},
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"eslint": "catalog:",
"playwright": "catalog:",
"typescript": "catalog:",
"typescript-eslint": "catalog:"
}
}

View File

@@ -1,15 +0,0 @@
import { useEffect, useState } from "react"
export interface UseIsMountedOutput {
isMounted: boolean
}
export const useIsMounted = (): UseIsMountedOutput => {
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
}, [])
return { isMounted }
}

View File

@@ -1,7 +0,0 @@
{
"extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["@total-typescript/ts-reset"]
}
}

View File

@@ -1,13 +0,0 @@
import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/config-eslint/nextjs"
export default typescriptESLint.config(...configNextjs, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@@ -23,15 +23,12 @@
"./Layout/Section": "./src/Layout/Section/Section.tsx"
},
"scripts": {
"lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit"
},
"dependencies": {
"@repo/config-tailwind": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/i18n": "workspace:*",
"@repo/react-hooks": "workspace:*",
"cva": "catalog:",
"@repo/utils": "workspace:*",
"next": "catalog:",
"next-intl": "catalog:",
"next-themes": "catalog:",
@@ -40,19 +37,15 @@
"react-icons": "catalog:"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@storybook/nextjs": "catalog:",
"@tailwindcss/postcss": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"@storybook/blocks": "catalog:",
"@storybook/react": "catalog:",
"@storybook/test": "catalog:",
"eslint": "catalog:",
"postcss": "catalog:",
"storybook": "catalog:",
"tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:"
}
}

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react"
import type { Meta, StoryObj } from "@storybook/nextjs"
import { CurriculumVitae as CurriculumVitaeComponent } from "./CurriculumVitae.tsx"

View File

@@ -1,4 +1,3 @@
import { CurriculumVitaeAbout } from "./CurriculumVitaeAbout.tsx"
import { CurriculumVitaeEducation } from "./CurriculumVitaeEducation.tsx"
import { CurriculumVitaeInterests } from "./CurriculumVitaeInterests.tsx"
import { CurriculumVitaeProfile } from "./CurriculumVitaeProfile.tsx"
@@ -16,18 +15,14 @@ export const CurriculumVitae: React.FC<CurriculumVitaeProps> = () => {
<div className="card background-card">
<div className="background-details">
<CurriculumVitaeAbout />
<hr />
<section className="flex">
<section className="flex justify-between">
<CurriculumVitaeEducation />
<CurriculumVitaeSkills />
</section>
<hr />
<section className="flex">
<section className="flex justify-between">
<CurriculumVitaeWork />
<CurriculumVitaeInterests />
</section>

View File

@@ -1,25 +0,0 @@
import { useTranslations } from "next-intl"
import { FaUser } from "react-icons/fa"
import { CurriculumVitaeSection } from "./CurriculumVitaeSection.tsx"
export interface CurriculumVitaeAboutProps {}
export const CurriculumVitaeAbout: React.FC<CurriculumVitaeAboutProps> = () => {
const t = useTranslations()
return (
<CurriculumVitaeSection
id="about"
title={t("curriculum-vitae.about.title")}
icon={<FaUser size={24} />}
>
<p>
{t.rich("curriculum-vitae.about.description", {
br: () => {
return <br />
},
})}
</p>
</CurriculumVitaeSection>
)
}

View File

@@ -4,9 +4,7 @@ import { CurriculumVitaeSection } from "./CurriculumVitaeSection.tsx"
export interface CurriculumVitaeEducationProps {}
export const CurriculumVitaeEducation: React.FC<
CurriculumVitaeEducationProps
> = () => {
export const CurriculumVitaeEducation: React.FC<CurriculumVitaeEducationProps> = () => {
const t = useTranslations()
const educations = [
@@ -14,59 +12,76 @@ export const CurriculumVitaeEducation: React.FC<
years: t("curriculum-vitae.education.cnam.years.2024-2027.title"),
studyType: t("curriculum-vitae.education.cnam.study-type"),
institution: t("curriculum-vitae.education.cnam.institution"),
score: t("curriculum-vitae.education.cnam.years.2024-2027.description"),
// score: t("curriculum-vitae.education.cnam.years.2024-2027.description"),
courses: [],
},
{
years: t("curriculum-vitae.education.iut.years.2023-2024.title"),
years: t("curriculum-vitae.education.iut.years.title"),
studyType: t("curriculum-vitae.education.iut.study-type"),
institution: t("curriculum-vitae.education.iut.institution"),
score: t("curriculum-vitae.education.iut.years.2023-2024.description"),
courses: [
t("curriculum-vitae.education.iut.years.2023-2024.courses.web"),
t("curriculum-vitae.education.iut.years.2023-2024.courses.ci-cd"),
t(
"curriculum-vitae.education.iut.years.2023-2024.courses.complexity-algorithms",
),
t("curriculum-vitae.education.iut.years.2023-2024.courses.no-sql"),
{
year: t("curriculum-vitae.education.iut.years.2021-2022.description"),
title: t("curriculum-vitae.education.iut.years.2021-2022.title"),
courses: [
t("curriculum-vitae.education.iut.years.2021-2022.courses.java"),
t("curriculum-vitae.education.iut.years.2021-2022.courses.systems-c"),
// t(
// "curriculum-vitae.education.iut.years.2021-2022.courses.windows-forms",
// ),
t.rich("curriculum-vitae.education.iut.years.2021-2022.courses.sql", {
strong: (children) => {
return <strong>{children}</strong>
},
}),
],
},
{
year: t("curriculum-vitae.education.iut.years.2022-2023.description"),
title: t("curriculum-vitae.education.iut.years.2022-2023.title"),
courses: [
t("curriculum-vitae.education.iut.years.2022-2023.courses.web"),
t.rich("curriculum-vitae.education.iut.years.2022-2023.courses.tests", {
strong: (children) => {
return <strong>{children}</strong>
},
}),
t.rich("curriculum-vitae.education.iut.years.2022-2023.courses.clean-code", {
strong: (children) => {
return <strong>{children}</strong>
},
}),
// t("curriculum-vitae.education.iut.years.2022-2023.courses.sql-security"),
t.rich("curriculum-vitae.education.iut.years.2022-2023.courses.systems-c", {
strong: (children) => {
return <strong>{children}</strong>
},
}),
],
},
{
year: t("curriculum-vitae.education.iut.years.2023-2024.description"),
title: t("curriculum-vitae.education.iut.years.2023-2024.title"),
courses: [
t.rich("curriculum-vitae.education.iut.years.2023-2024.courses.web", {
strong: (children) => {
return <strong>{children}</strong>
},
}),
t("curriculum-vitae.education.iut.years.2023-2024.courses.ci-cd"),
t("curriculum-vitae.education.iut.years.2023-2024.courses.complexity-algorithms"),
t("curriculum-vitae.education.iut.years.2023-2024.courses.no-sql"),
],
},
],
},
{
years: t("curriculum-vitae.education.iut.years.2022-2023.title"),
studyType: t("curriculum-vitae.education.iut.study-type"),
institution: t("curriculum-vitae.education.iut.institution"),
score: t("curriculum-vitae.education.iut.years.2022-2023.description"),
courses: [
t("curriculum-vitae.education.iut.years.2022-2023.courses.web"),
t("curriculum-vitae.education.iut.years.2022-2023.courses.tests"),
t("curriculum-vitae.education.iut.years.2022-2023.courses.clean-code"),
t("curriculum-vitae.education.iut.years.2022-2023.courses.systems-c"),
t(
"curriculum-vitae.education.iut.years.2022-2023.courses.sql-security",
),
],
},
{
years: t("curriculum-vitae.education.iut.years.2021-2022.title"),
studyType: t("curriculum-vitae.education.iut.study-type"),
institution: t("curriculum-vitae.education.iut.institution"),
score: t("curriculum-vitae.education.iut.years.2021-2022.description"),
courses: [
t("curriculum-vitae.education.iut.years.2021-2022.courses.java"),
t("curriculum-vitae.education.iut.years.2021-2022.courses.systems-c"),
t(
"curriculum-vitae.education.iut.years.2021-2022.courses.windows-forms",
),
t("curriculum-vitae.education.iut.years.2021-2022.courses.sql"),
],
},
{
years: t("curriculum-vitae.education.lycee.years.2019-2021.title"),
studyType: t("curriculum-vitae.education.lycee.study-type"),
institution: t("curriculum-vitae.education.lycee.institution"),
score: t("curriculum-vitae.education.lycee.score"),
courses: [],
},
// {
// years: t("curriculum-vitae.education.lycee.years.2019-2021.title"),
// studyType: t("curriculum-vitae.education.lycee.study-type"),
// institution: t("curriculum-vitae.education.lycee.institution"),
// score: t("curriculum-vitae.education.lycee.score"),
// courses: [],
// },
]
return (
@@ -79,35 +94,47 @@ export const CurriculumVitaeEducation: React.FC<
{educations.map((education) => {
return (
<li key={education.years} className="card card-nested">
<div className="content">
<p className="relative m-0">
<strong>{education.studyType}</strong>
</p>
<p className="relative m-0">
<strong>{education.studyType}</strong>
</p>
{/* {education.score != null ? (
<p className="relative m-0">
<strong>{education.score}</strong>
</p>
) : (
<></>
)} */}
<p className="text-muted m-0">{education.institution}</p>
<p className="text-muted m-0">{education.institution}</p>
<p className="text-muted m-0">
<small>{education.years}</small>
</p>
<p className="text-muted m-0">
<small>{education.years}</small>
</p>
{education.courses.length > 0 ? (
<ul
style={{
paddingInlineStart: 20,
}}
>
{education.courses.map((course) => {
return <li key={course}>{course}</li>
})}
</ul>
) : (
<></>
)}
</div>
{education.courses.length > 0 ? (
<ul className="list-none">
{education.courses.map(({ year, title, courses }) => {
return (
<li key={year}>
<span className="font-medium">{year}</span>
<span> ({title})</span>
<ul
style={{
paddingInlineStart: 20,
}}
>
{courses.map((course, index) => {
return <li key={index}>{course}</li>
})}
</ul>
</li>
)
})}
</ul>
) : (
<></>
)}
</li>
)
})}

View File

@@ -4,29 +4,65 @@ import { CurriculumVitaeSection } from "./CurriculumVitaeSection.tsx"
export interface CurriculumVitaeInterestsProps {}
export const CurriculumVitaeInterests: React.FC<
CurriculumVitaeInterestsProps
> = () => {
export const CurriculumVitaeInterests: React.FC<CurriculumVitaeInterestsProps> = () => {
const t = useTranslations()
const interests = [
t("curriculum-vitae.interests.open-source"),
t("curriculum-vitae.interests.high-tech"),
t.rich("curriculum-vitae.interests.open-source", {
strong: (children) => {
return <strong>{children}</strong>
},
"link-github": (children) => {
return (
<a href="https://github.com/theoludwig" target="_blank" className="font-semibold">
{children}
</a>
)
},
"link-leon": (children) => {
return (
<a href="https://github.com/leon-ai/leon" target="_blank" className="font-semibold">
{children}
</a>
)
},
"link-markdownlint": (children) => {
return (
<a
href="https://www.npmjs.com/package/markdownlint-rule-relative-links"
target="_blank"
className="font-semibold"
>
{children}
</a>
)
},
}),
t.rich("curriculum-vitae.interests.fusey", {
link: (children) => {
return (
<a href="https://fusey.gg" target="_blank" className="font-semibold">
{children}
</a>
)
},
strong: (children) => {
return <strong>{children}</strong>
},
}),
]
return (
<CurriculumVitaeSection
id="interests"
title={t("home.interests.title")}
title={t("curriculum-vitae.interests.title")}
icon={<FaHeart size={24} />}
>
<ul className="list-unstyled m-0">
{interests.map((interest) => {
{interests.map((interest, index) => {
return (
<li key={interest} className="card card-nested">
<p>
<strong>{interest}</strong>
</p>
<li key={index} className="card card-nested max-w-2xl">
<p>{interest}</p>
</li>
)
})}

View File

@@ -6,9 +6,7 @@ import { Locales } from "../Layout/Header/Locales/Locales.tsx"
export interface CurriculumVitaeProfileProps {}
export const CurriculumVitaeProfile: React.FC<
CurriculumVitaeProfileProps
> = () => {
export const CurriculumVitaeProfile: React.FC<CurriculumVitaeProfileProps> = () => {
const t = useTranslations()
return (
@@ -21,16 +19,16 @@ export const CurriculumVitaeProfile: React.FC<
<Image
className="mx-auto block"
alt={t("meta.title")}
src="/images/logo_background.webp"
width={800}
height={800}
src="/images/logo.webp"
width={450}
height={450}
/>
</div>
<div className="name-and-profession text-center">
<h1 className="h3">
<h1 className="h3 my-1!">
<strong>{t("meta.title")}</strong>
</h1>
<h2 className="text-muted h5">{t("curriculum-vitae.description")}</h2>
<h2 className="text-muted h5 font-semibold">{t("curriculum-vitae.description")}</h2>
<h2 className="text-muted h5">
<BirthDate />
</h2>
@@ -39,18 +37,14 @@ export const CurriculumVitaeProfile: React.FC<
</div>
<div className="flex justify-center">
<div className="relative px-3">
<span className="info">
<a className="link-disguise" href="mailto:contact@theoludwig.fr">
contact@theoludwig.fr
</a>
</span>
<strong className="info font-bold">
<a href="mailto:contact@theoludwig.fr">contact@theoludwig.fr</a>
</strong>
</div>
<div className="detail">
<span className="info">
<Link className="link-disguise" href="/">
https://theoludwig.fr/
</Link>
</span>
<strong className="info font-bold">
<Link href="/">https://theoludwig.fr</Link>
</strong>
</div>
</div>
<hr />

View File

@@ -4,9 +4,7 @@ export interface CurriculumVitaeSectionProps extends React.PropsWithChildren {
title: string
}
export const CurriculumVitaeSection: React.FC<CurriculumVitaeSectionProps> = (
props,
) => {
export const CurriculumVitaeSection: React.FC<CurriculumVitaeSectionProps> = (props) => {
const { id, icon, title, children } = props
return (

View File

@@ -1,16 +1,11 @@
import { useTranslations } from "next-intl"
import { FaToolbox } from "react-icons/fa"
import {
SKILL_CATEGORIES,
SKILL_NAMES_BY_CATEGORY,
} from "../Home/Skills/skills.ts"
import { SKILL_CATEGORIES, SKILL_NAMES_BY_CATEGORY } from "../Home/Skills/skills.ts"
import { CurriculumVitaeSection } from "./CurriculumVitaeSection.tsx"
export interface CurriculumVitaeSkillsProps {}
export const CurriculumVitaeSkills: React.FC<
CurriculumVitaeSkillsProps
> = () => {
export const CurriculumVitaeSkills: React.FC<CurriculumVitaeSkillsProps> = () => {
const t = useTranslations()
const skills = [
@@ -23,7 +18,7 @@ export const CurriculumVitaeSkills: React.FC<
}),
{
category: "others",
skillNames: [t("locales.en-US"), t("home.skills.driving-license")],
skillNames: [t("fr-FR-main"), t("locales.en-US"), t("home.skills.driving-license")],
},
] as const
@@ -45,6 +40,23 @@ export const CurriculumVitaeSkills: React.FC<
return (
<p key={skillName} className="label label-keyword">
{skillName}
{skillName === "Rust"
? t.rich("home.skills.rust-advent-of-code", {
"link-aoc": (children) => {
return (
<a
href="https://github.com/theoludwig/advent_of_code"
target="_blank"
className="no-underline"
>
{children}
</a>
)
},
})
: skillName === "Go"
? t("home.skills.go-basics")
: ""}
</p>
)
})}

View File

@@ -9,13 +9,37 @@ export const CurriculumVitaeWork: React.FC<CurriculumVitaeWorkProps> = () => {
const workExperiences = [
{
summary: t("curriculum-vitae.work.ircad.summary"),
website: "https://ircad.fr/",
name: "IRCAD",
location: "1 Place de l'Hôpital, FR-67000 Strasbourg",
position: t("curriculum-vitae.work.ircad.position"),
dates: "28/08/2023 - 31/08/2027",
duration: t("curriculum-vitae.work.ircad.duration"),
tasks: [
// t("curriculum-vitae.work.ircad.tasks.WebSurg"),
t.rich("curriculum-vitae.work.ircad.tasks.Figma", {
strong: (children) => {
return <strong>{children}</strong>
},
}),
t("curriculum-vitae.work.ircad.tasks.IRCAD-Core"),
t("curriculum-vitae.work.ircad.tasks.feature-logs"),
t.rich("curriculum-vitae.work.ircad.tasks.feature-permissions", {
strong: (children) => {
return <strong>{children}</strong>
},
}),
t.rich("curriculum-vitae.work.ircad.tasks.feature-search", {
strong: (children) => {
return <strong>{children}</strong>
},
}),
t.rich("curriculum-vitae.work.ircad.tasks.feature-architecture", {
strong: (children) => {
return <strong>{children}</strong>
},
}),
],
},
{
summary: t("curriculum-vitae.work.numerize.summary"),
@@ -25,6 +49,7 @@ export const CurriculumVitaeWork: React.FC<CurriculumVitaeWorkProps> = () => {
position: t("curriculum-vitae.work.numerize.position"),
dates: "11/04/2023 - 26/07/2023",
duration: t("curriculum-vitae.work.numerize.duration"),
tasks: [],
},
]
@@ -50,16 +75,36 @@ export const CurriculumVitaeWork: React.FC<CurriculumVitaeWorkProps> = () => {
<strong>{workExperience.position}</strong>
</p>
<p className="text-muted">
<p className="text-muted m-0">
<small>
<span className="space-right">
{workExperience.dates} ({workExperience.duration})
</span>
</small>
</p>
<div className="mt-2">
<p>{workExperience.summary}</p>
</div>
{workExperience.tasks.length > 0 ? (
<ul
style={{
paddingInlineStart: 20,
}}
className="space-y-1"
>
{workExperience.tasks.map((task, index) => {
return <li key={index}>{task}</li>
})}
</ul>
) : (
<></>
)}
{workExperience.summary != null ? (
<div className="mt-2">
<p>{workExperience.summary}</p>
</div>
) : (
<></>
)}
</li>
)
})}

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react"
import { expect, fn, userEvent, within } from "@storybook/test"
import type { Meta, StoryObj } from "@storybook/nextjs"
import { expect, fn, userEvent, within } from "storybook/test"
import { FaCheck } from "react-icons/fa"
import type { ButtonLinkProps } from "./Button.tsx"
@@ -136,10 +136,7 @@ export const LinkWithIcons: Story = {
<Button leftIcon={<FaCheck size={18} />} {...(args as ButtonLinkProps)}>
Link Left Icon
</Button>
<Button
rightIcon={<FaCheck size={18} />}
{...(args as ButtonLinkProps)}
>
<Button rightIcon={<FaCheck size={18} />} {...(args as ButtonLinkProps)}>
Link Right Icon
</Button>
</ButtonContainer>

View File

@@ -1,32 +1,37 @@
import { classNames } from "@repo/config-tailwind/classNames"
import { Link as NextLink } from "@repo/i18n/routing"
import type { VariantProps } from "cva"
import { cva } from "cva"
import { Spinner } from "../Spinner/Spinner.tsx"
import { Ripple } from "./Ripple.tsx"
const buttonVariants = cva({
base: "relative inline-flex items-center justify-center overflow-hidden rounded-md text-base font-semibold transition duration-150 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
variants: {
variant: {
solid: "bg-primary hover:bg-primary/80 text-white",
outline:
"dark:border-primary-dark/60 dark:text-primary-dark dark:hover:border-primary-dark border-primary/60 text-primary hover:border-primary hover:bg-primary border bg-transparent hover:text-white",
},
size: {
small: "h-9 rounded-md px-3",
medium: "h-10 px-4 py-2",
large: "h-11 rounded-md px-8",
},
},
defaultVariants: {
variant: "solid",
size: "medium",
},
})
const BUTTON_VARIANTS = ["solid", "outline"] as const
type ButtonVariant = (typeof BUTTON_VARIANTS)[number]
const BUTTON_SIZES = ["small", "medium", "large"] as const
type ButtonSize = (typeof BUTTON_SIZES)[number]
const buttonVariants = (options?: { variant?: ButtonVariant; size?: ButtonSize }): string => {
const { variant = "solid", size = "medium" } = options ?? {}
return classNames(
"relative inline-flex items-center justify-center overflow-hidden rounded-md text-base font-semibold transition duration-150 ease-in-out focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
{
"bg-primary text-white hover:bg-primary/80": variant === "solid",
"border border-primary/60 bg-transparent text-primary hover:border-primary hover:bg-primary hover:text-white dark:border-primary-dark/60 dark:text-primary-dark dark:hover:border-primary-dark":
variant === "outline",
},
{
"h-9 rounded-md px-3": size === "small",
"h-10 px-4 py-2": size === "medium",
"h-11 rounded-md px-8": size === "large",
},
)
}
interface ButtonBaseProps {
variant?: ButtonVariant
size?: ButtonSize
interface ButtonBaseProps extends VariantProps<typeof buttonVariants> {
leftIcon?: React.ReactNode
rightIcon?: React.ReactNode
disabled?: boolean
@@ -34,13 +39,10 @@ interface ButtonBaseProps extends VariantProps<typeof buttonVariants> {
}
interface ButtonElementProps extends React.ComponentPropsWithoutRef<"button"> {}
interface LinkElementProps
extends React.ComponentPropsWithoutRef<typeof NextLink> {}
interface LinkElementProps extends React.ComponentPropsWithoutRef<typeof NextLink> {}
export type ButtonLinkProps = ButtonBaseProps &
LinkElementProps & { href: string }
export type ButtonButtonProps = ButtonBaseProps &
ButtonElementProps & { href?: never }
export type ButtonLinkProps = ButtonBaseProps & LinkElementProps & { href: string }
export type ButtonButtonProps = ButtonBaseProps & ButtonElementProps & { href?: never }
export type ButtonProps = ButtonButtonProps | ButtonLinkProps
@@ -50,18 +52,13 @@ export type ButtonProps = ButtonButtonProps | ButtonLinkProps
* @returns
*/
export const Button: React.FC<ButtonProps> = (props) => {
const rippleColor =
props.variant === "outline" ? "rgb(30, 64, 175)" : "rgb(229, 231, 235)"
const rippleColor = props.variant === "outline" ? "rgb(30, 64, 175)" : "rgb(229, 231, 235)"
if (typeof props.href === "string") {
const { variant, size, leftIcon, rightIcon, className, children, ...rest } =
props
const { variant, size, leftIcon, rightIcon, className, children, ...rest } = props
return (
<NextLink
className={classNames(buttonVariants({ variant, size }), className)}
{...rest}
>
<NextLink className={classNames(buttonVariants({ variant, size }), className)} {...rest}>
{leftIcon != null ? <span className="mr-2">{leftIcon}</span> : null}
<span>{children}</span>
{rightIcon != null ? <span className="ml-2">{rightIcon}</span> : null}
@@ -97,13 +94,9 @@ export const Button: React.FC<ButtonProps> = (props) => {
disabled={isDisabled}
{...rest}
>
{leftIconElement != null ? (
<span className="mr-2">{leftIconElement}</span>
) : null}
{leftIconElement != null ? <span className="mr-2">{leftIconElement}</span> : null}
<span>{children}</span>
{rightIcon != null && !isLoading ? (
<span className="ml-2">{rightIcon}</span>
) : null}
{rightIcon != null && !isLoading ? <span className="ml-2">{rightIcon}</span> : null}
<Ripple color={rippleColor} />
</button>

View File

@@ -1,4 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react"
import type { Meta, StoryObj } from "@storybook/nextjs"
import { Link } from "./Link.tsx"

Some files were not shown because too many files have changed in this diff Show More