1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-09-11 23:09:22 +02:00

Compare commits

...

20 Commits

Author SHA1 Message Date
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
a43bfb4a0d feat(portfolio): add Fusey 2025-05-11 17:28:02 +02:00
69af1bccc3 build: update to Node.js v24.0.0, pnpm v10, Tailwind CSS v4 2025-05-11 16:59:11 +02:00
cec70161f7 fix(blog): add useful command to Git cheatsheet post 2025-05-07 19:54:06 +02:00
f70a66e251 fix: replace standard open source contributions by nodejs.org 2025-02-08 20:56:56 +01:00
38eb296088 feat: themeColor metadata 2025-02-08 20:51:22 +01:00
43d91bfc28 chore: fix storybook configuration 2025-02-08 20:48:19 +01:00
b63cc3a66e chore: cleaner setup 2025-02-08 20:00:47 +01:00
semantic-release-bot
270920111a chore(release): 4.1.3 [skip ci] 2025-01-23 12:17:18 +00:00
d91feb8de4 fix: use Plausible 2025-01-23 13:16:20 +01:00
e68cb08a6f fix: update Node.js to v22.13.1 (security release)
Ref: https://nodejs.org/en/blog/release/v22.13.1
2025-01-23 13:11:57 +01:00
162 changed files with 5809 additions and 9035 deletions

View File

@@ -9,3 +9,6 @@ end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.md]
indent_size = 4

View File

@@ -1,2 +1,2 @@
TZ=UTC TZ=Europe/Paris
WEBSITE_PORT=3000 WEBSITE_PORT=3000

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

@@ -10,17 +10,24 @@ jobs:
chromatic: chromatic:
timeout-minutes: 30 timeout-minutes: 30
runs-on: "ubuntu-latest" 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: steps:
- uses: "actions/checkout@v4.2.2" - uses: "actions/checkout@v5.0.0"
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: "pnpm/action-setup@v4.0.0" - uses: "pnpm/action-setup@v4.1.0"
- name: "Setup Node.js" - name: "Setup Node.js"
uses: "actions/setup-node@v4.1.0" uses: "actions/setup-node@v4.4.0"
with: with:
node-version: "22.x" node-version: "24.x"
cache: "pnpm" cache: "pnpm"
- name: "Install dependencies" - name: "Install dependencies"

View File

@@ -10,15 +10,22 @@ jobs:
ci: ci:
timeout-minutes: 30 timeout-minutes: 30
runs-on: "ubuntu-latest" 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: steps:
- uses: "actions/checkout@v4.2.2" - uses: "actions/checkout@v5.0.0"
- uses: "pnpm/action-setup@v4.0.0" - uses: "pnpm/action-setup@v4.1.0"
- name: "Setup Node.js" - name: "Setup Node.js"
uses: "actions/setup-node@v4.1.0" uses: "actions/setup-node@v4.4.0"
with: with:
node-version: "22.x" node-version: "24.x"
cache: "pnpm" cache: "pnpm"
- name: "Install dependencies" - name: "Install dependencies"
@@ -34,10 +41,3 @@ jobs:
- run: "node --run lint:prettier" - run: "node --run lint:prettier"
- run: "node --run test" - run: "node --run test"
- run: "node --run build" - run: "node --run build"
commitlint:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.2.2"
- uses: "wagoid/commitlint-github-action@v6.1.2"

View File

@@ -1,45 +0,0 @@
name: "Release"
on:
push:
branches: [main, staging]
jobs:
release:
timeout-minutes: 30
runs-on: "ubuntu-latest"
permissions:
contents: "write"
issues: "write"
pull-requests: "write"
id-token: "write"
steps:
- uses: "actions/checkout@v4.2.2"
with:
fetch-depth: 0
persist-credentials: false
- name: "Import GPG key"
uses: "crazy-max/ghaction-import-gpg@v6.1.0"
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true
git_commit_gpgsign: true
- uses: "pnpm/action-setup@v4.0.0"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.1.0"
with:
node-version: "22.x"
cache: "pnpm"
- name: "Install dependencies"
run: "pnpm install --frozen-lockfile"
- name: "Release"
run: "node --run release"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }}

11
.gitignore vendored
View File

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

View File

@@ -1,12 +0,0 @@
{
"config": {
"extends": "markdownlint/style/prettier",
"default": true,
"relative-links": true,
"no-duplicate-heading": false,
"no-inline-html": false,
},
"globs": ["**/*.md"],
"ignores": ["**/node_modules"],
"customRules": ["markdownlint-rule-relative-links"],
}

16
.markdownlint-cli2.mjs Normal file
View File

@@ -0,0 +1,16 @@
import relativeLinksRule from "markdownlint-rule-relative-links"
const config = {
config: {
extends: "markdownlint/style/prettier",
default: true,
"relative-links": true,
"no-duplicate-heading": false,
"no-inline-html": false,
},
globs: ["**/*.md"],
ignores: ["**/node_modules"],
customRules: [relativeLinksRule],
}
export default config

View File

@@ -1,34 +0,0 @@
{
"branches": ["main", { "name": "staging", "prerelease": true }],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec",
{
"prepareCmd": "replace-in-files --regex='version\": *\"[^\"]*' --replacement='\"version\": \"${nextRelease.version}\"' '**/package.json' '!**/node_modules/**'"
}
],
[
"@semantic-release/git",
{
"assets": [
"package.json",
"apps/*/package.json",
"packages/*/package.json"
],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}
],
"@semantic-release/github",
[
"@saithodev/semantic-release-backmerge",
{
"branches": [
{ "from": "main", "to": "develop" },
{ "from": "staging", "to": "develop" }
]
}
]
]
}

View File

@@ -5,8 +5,6 @@
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"mikestead.dotenv",
"ms-azuretools.vscode-docker",
"antfu.pnpm-catalog-lens", "antfu.pnpm-catalog-lens",
"Lokalise.i18n-ally" "Lokalise.i18n-ally"
] ]

View File

@@ -20,7 +20,7 @@
"scope": "typescriptreact", "scope": "typescriptreact",
"prefix": "rfcs", "prefix": "rfcs",
"body": [ "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\"", "import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}.tsx\"",
"", "",

17
.vscode/settings.json vendored
View File

@@ -9,13 +9,22 @@
"source.fixAll": "explicit", "source.fixAll": "explicit",
"source.organizeImports": "never" "source.organizeImports": "never"
}, },
"tailwindCSS.experimental.classRegex": [ "eslint.workingDirectories": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], {
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] "mode": "auto"
}
], ],
"files.associations": {
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": "on"
},
"tailwindCSS.classFunctions": ["classNames", "cva"],
"tailwindCSS.experimental.configFile": "./configs/config-tailwind/styles.css",
"i18n-ally.localesPaths": ["./packages/i18n/src/translations/"], "i18n-ally.localesPaths": ["./packages/i18n/src/translations/"],
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true, "i18n-ally.sortKeys": false,
"i18n-ally.sourceLanguage": "en-US", "i18n-ally.sourceLanguage": "en-US",
"i18n-ally.displayLanguage": "en-US", "i18n-ally.displayLanguage": "en-US",
"i18n-ally.enabledFrameworks": ["next-intl", "general"], "i18n-ally.enabledFrameworks": ["next-intl", "general"],

View File

@@ -1,132 +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**! 🎉 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.
## Types of contributions ## Types of contributions
@@ -31,8 +27,8 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
### Prerequisites ### Prerequisites
- [Node.js](https://nodejs.org/) >= 22.0.0 - [Node.js](https://nodejs.org/) >= v24.0.0 [(`nvm install 24`)](https://nvm.sh)
- [pnpm](https://pnpm.io/) >= 9.15.0 [(`corepack enable`)](https://nodejs.org/docs/latest-v22.x/api/corepack.html) - [pnpm](https://pnpm.io/) v10.15.0 [(`npm install --global corepack@0.34.0 && corepack enable`)](https://github.com/nodejs/corepack)
- [Docker](https://www.docker.com/) - [Docker](https://www.docker.com/)
### Installation ### Installation
@@ -83,9 +79,9 @@ node --run test
```sh ```sh
# Setup and run all the services for you # Setup and run all the services for you
docker compose up --build VERSION=$(git describe --tags) docker compose up --build --detach
``` ```
#### Services started #### Services started
`theoludwig`: <http://127.0.0.1:3000> `theoludwig`: <http://localhost:3000>

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

@@ -1,45 +1,63 @@
<h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</a></h1> <h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</a></h1>
<p align="center"> <p align="center">
<strong>Developer Full Stack • Open-Source Enthusiast</strong> <strong>Developer Full Stack • Open-Source Enthusiast</strong>
</p> </p>
<p align="center"> <p align="center">
<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://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://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://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.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://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> <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>
<a href="mailto:contact@theoludwig.fr"><img alt="Email" src="https://img.shields.io/badge/-contact@theoludwig.fr-2F7EBE?style=flat&labelColor=2F7EBE&logo=minutemailer&logoColor=white"/></a> <a href="mailto:contact@theoludwig.fr"><img alt="Email" src="https://img.shields.io/badge/-contact@theoludwig.fr-2F7EBE?style=flat&labelColor=2F7EBE&logo=minutemailer&logoColor=white"/></a>
</p> </p>
<hr /> <hr />
## 📜 About ## About
```json ```json
{ {
"name": "Théo LUDWIG", "name": "Théo LUDWIG",
"pronouns": "He/Him", "pronouns": "He/Him",
"birthDate": "2003-03-31", "birthDate": "2003-03-31",
"nationality": "Alsace, France", "nationality": "Alsace, France",
"interests": ["Developer Full Stack", "Open-Source Enthusiast"], "interests": ["Developer Full Stack", "Open-Source Enthusiast"],
"skills": { "skills": {
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"], "software-development": [
"frontend": ["HTML/CSS", "Tailwind CSS", "React.js/Next.js"], "TypeScript",
"backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"], "React.js (+ Next.js)",
"tools": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git", "Docker"] "Tailwind CSS",
} "Node.js",
"tRPC/oRPC",
"PostgreSQL"
],
"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 /> <hr />
## 📈 Statistics ## Statistics
<p align=center> <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?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" /> <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> </p>

View File

@@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@@ -14,11 +14,8 @@ const config: StorybookConfig = {
], ],
addons: [ addons: [
"@chromatic-com/storybook", "@chromatic-com/storybook",
"@storybook/addon-essentials", "@storybook/addon-docs",
"@storybook/addon-storysource",
"@storybook/addon-a11y", "@storybook/addon-a11y",
"@storybook/addon-interactions",
"storybook-dark-mode",
], ],
framework: { framework: {
name: "@storybook/nextjs", name: "@storybook/nextjs",

View File

@@ -1,13 +1,24 @@
import "@repo/config-tailwind/styles.css" import "@repo/config-tailwind/styles.css"
import "./storybook-css-overrides.css"
import i18nMessages from "@repo/i18n/translations/en-US.json" import i18nMessages from "@repo/i18n/translations/en-US.json"
import { LOCALE_DEFAULT, TIMEZONE } from "@repo/utils/constants" 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 { NextIntlClientProvider } from "next-intl"
import { ThemeProvider as NextThemeProvider } from "next-themes" import { ThemeProvider as NextThemeProvider } from "next-themes"
import React from "react"
const preview: Preview = { const preview: Preview = {
initialGlobals: {
a11y: {
manual: true,
},
},
parameters: { parameters: {
a11y: {
test: "error",
},
docs: {
codePanel: true,
},
nextjs: { nextjs: {
appDirectory: true, appDirectory: true,
}, },

View File

@@ -0,0 +1,3 @@
body {
overflow: auto !important;
}

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,5 +1,5 @@
import typescriptESLint from "typescript-eslint" import typescriptESLint from "typescript-eslint"
import config from "@repo/eslint-config" import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, { export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"], files: ["**/*.ts", "**/*.tsx"],

View File

@@ -0,0 +1,84 @@
import http from "node:http"
import fs from "node:fs"
import path from "node:path"
import util from "node:util"
import mime from "mime"
const MIMETYPE_DEFAULT = "application/octet-stream"
const args = util.parseArgs({
options: {
path: { type: "string", default: "public", required: true },
port: { type: "string", default: "3000", required: true },
host: { type: "string", default: "0.0.0.0" },
},
})
const host = args.values.host
const basePath = args.values.path
const port = Number.parseInt(args.values.port, 10)
if (Number.isNaN(port)) {
console.error("Error: Invalid port number.")
process.exit(1)
}
const serverURL = `http://${host}:${port}`
const server = http.createServer(async (request, response) => {
if (request.url == null) {
response.writeHead(400, { "Content-Type": "text/plain" })
response.end("Bad Request")
return
}
const url = new URL(request.url, serverURL)
const urlPath = url.pathname
const filePath = path.join(process.cwd(), basePath, urlPath)
try {
const stat = await fs.promises.stat(filePath)
if (stat.isDirectory()) {
const indexFile = path.join(filePath, "index.html")
try {
const fileContent = await fs.promises.readFile(indexFile)
response.writeHead(200, { "Content-Type": "text/html" })
response.end(fileContent)
} catch {
response.writeHead(403, { "Content-Type": "text/plain" })
response.end("Error: Directory listing not allowed.")
}
} else {
const mimeType = mime.getType(filePath) ?? MIMETYPE_DEFAULT
const fileContent = await fs.promises.readFile(filePath)
response.writeHead(200, { "Content-Type": mimeType })
response.end(fileContent)
}
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
response.writeHead(404, { "Content-Type": "text/plain" })
response.end("Error: File not found.")
} else {
response.writeHead(500, { "Content-Type": "text/plain" })
response.end("Error: Internal Server Error.")
}
}
})
const gracefulShutdown = (): void => {
server.close()
process.exit(0)
}
process.on("SIGTERM", gracefulShutdown)
process.on("SIGINT", gracefulShutdown)
server.listen(
{
host,
port,
},
() => {
console.log(
`HTTP Server is listening at ${util.styleText("cyan", serverURL)}`,
)
console.log(`Serving files from: \`${basePath}\``)
},
)

View File

@@ -1,14 +1,14 @@
{ {
"name": "@repo/storybook", "name": "@repo/storybook",
"version": "4.1.2", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "storybook build", "build": "storybook build",
"dev": "storybook dev --port 6006 --no-open", "dev": "storybook dev --port 6006 --no-open",
"start": "http-server \"storybook-static\" --port 6006 --silent", "start": "node http-server.ts --path=storybook-static --port=6006",
"test": "start-server-and-test \"start\" http://127.0.0.1:6006 \"test:storybook\"", "test": "start-server-and-test \"start\" http://localhost:6006 \"test:storybook\"",
"test:dev": "start-server-and-test \"dev\" http://127.0.0.1:6006 \"test:storybook\"", "test:dev": "start-server-and-test \"dev\" http://localhost:6006 \"test:storybook\"",
"test:storybook": "test-storybook --testTimeout=60000 --maxWorkers=2", "test:storybook": "test-storybook --testTimeout=60000 --maxWorkers=2",
"chromatic": "chromatic" "chromatic": "chromatic"
}, },
@@ -22,35 +22,29 @@
"next-intl": "catalog:", "next-intl": "catalog:",
"next-themes": "catalog:", "next-themes": "catalog:",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:" "react-dom": "catalog:",
"mime": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*", "@repo/config-typescript": "workspace:*",
"@chromatic-com/storybook": "catalog:", "@chromatic-com/storybook": "catalog:",
"@playwright/test": "catalog:", "@playwright/test": "catalog:",
"@storybook/addon-essentials": "catalog:", "@storybook/addon-docs": "catalog:",
"@storybook/addon-storysource": "catalog:",
"@storybook/addon-a11y": "catalog:", "@storybook/addon-a11y": "catalog:",
"@storybook/addon-interactions": "catalog:",
"@storybook/addon-themes": "catalog:", "@storybook/addon-themes": "catalog:",
"@storybook/blocks": "catalog:",
"@storybook/nextjs": "catalog:", "@storybook/nextjs": "catalog:",
"@storybook/react": "catalog:",
"@storybook/test": "catalog:",
"@storybook/test-runner": "catalog:", "@storybook/test-runner": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/react": "catalog:", "@types/react": "catalog:",
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"axe-playwright": "catalog:",
"chromatic": "catalog:", "chromatic": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"http-server": "catalog:",
"start-server-and-test": "catalog:", "start-server-and-test": "catalog:",
"storybook": "catalog:", "storybook": "catalog:",
"storybook-dark-mode": "catalog:",
"postcss": "catalog:", "postcss": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "catalog:", "typescript-eslint": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }

View File

@@ -1,7 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
},
}
export default config

View File

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

View File

@@ -2,11 +2,6 @@ import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */ /** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = { const config = {
content: [
".storybook/preview.tsx",
"../../packages/ui/src/**/*.tsx",
"../../packages/blog/src/**/*.tsx",
],
presets: [sharedConfig], presets: [sharedConfig],
} }

View File

@@ -3,5 +3,5 @@
"compilerOptions": { "compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext"] "lib": ["DOM", "DOM.Iterable", "ESNext"]
}, },
"include": ["./.storybook/**/*.ts", "./.storybook/**/*.tsx"] "include": ["http-server.ts", "./.storybook/**/*.ts", "./.storybook/**/*.tsx"]
} }

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://turbo.build/schema.json", "$schema": "../../node_modules/turbo/schema.json",
"extends": ["//"], "extends": ["//"],
"tasks": { "tasks": {
"test": { "test": {

View File

@@ -1,4 +1,4 @@
TZ=UTC TZ=Europe/Paris
HOSTNAME=0.0.0.0 HOSTNAME=0.0.0.0
PORT=3000 PORT=3000
NEXT_TELEMETRY_DISABLED=1 NEXT_TELEMETRY_DISABLED=1

View File

@@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@@ -1,14 +1,15 @@
FROM node:22.12.0-slim AS node-pnpm FROM node:24.7.0-slim AS node-pnpm
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN npm install --global corepack@0.34.0 && corepack enable
ENV TURBO_TELEMETRY_DISABLED=1 ENV TURBO_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV DO_NOT_TRACK=1 ENV DO_NOT_TRACK=1
WORKDIR /usr/src/app WORKDIR /usr/src/app
FROM node-pnpm AS builder FROM node-pnpm AS builder
COPY ./ ./ COPY ./ ./
RUN pnpm install --global turbo@2.3.3 RUN pnpm install --global turbo@2.5.6
RUN turbo prune @repo/website --docker RUN turbo prune @repo/website --docker
FROM node-pnpm AS installer FROM node-pnpm AS installer
@@ -20,17 +21,20 @@ COPY --from=builder /usr/src/app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY --from=builder /usr/src/app/out/full/ ./ COPY --from=builder /usr/src/app/out/full/ ./
COPY turbo.json turbo.json COPY turbo.json turbo.json
ARG VERSION="0.0.0-develop"
RUN pnpm install --global replace-in-files-cli@3.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 RUN pnpm --filter=@repo/website... exec turbo run build
FROM node-pnpm AS runner FROM node-pnpm AS runner
ENV NODE_ENV=production ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0 ENV HOSTNAME=0.0.0.0
ENV NEXT_TELEMETRY_DISABLED=1
ENV IS_STANDALONE=true ENV IS_STANDALONE=true
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner
USER 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 /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/standalone ./
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/static ./apps/website/.next/static COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/static ./apps/website/.next/static

View File

@@ -1,19 +1,23 @@
import "@repo/config-tailwind/styles.css" 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 { Footer } from "@repo/ui/Layout/Footer"
import { Header } from "@repo/ui/Layout/Header" import { Header } from "@repo/ui/Layout/Header"
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme" import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { VERSION } from "@repo/utils/constants" import { VERSION } from "@repo/utils/constants"
import { setRequestLocale } from "next-intl/server" 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 MainLayout: React.FC<MainLayoutProps> = async (props) => {
const { children, params } = props const { children, params } = props
const { locale } = await params const { locale } = await params
// Enable static rendering // Enable static rendering
setRequestLocale(locale) setRequestLocale(locale as Locale)
return ( return (
<ThemeProvider> <ThemeProvider>

View File

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

View File

@@ -1,11 +1,13 @@
import "@repo/config-tailwind/styles.css" 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 { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { setRequestLocale } from "next-intl/server" import { setRequestLocale } from "next-intl/server"
interface CurriculumVitaeLayoutProps interface CurriculumVitaeLayoutProps extends React.PropsWithChildren {
extends React.PropsWithChildren, params: Promise<{
LocaleProps {} locale: string
}>
}
const CurriculumVitaeLayout: React.FC<CurriculumVitaeLayoutProps> = async ( const CurriculumVitaeLayout: React.FC<CurriculumVitaeLayoutProps> = async (
props, props,
@@ -14,7 +16,7 @@ const CurriculumVitaeLayout: React.FC<CurriculumVitaeLayoutProps> = async (
const { locale } = await params const { locale } = await params
// Enable static rendering // Enable static rendering
setRequestLocale(locale) setRequestLocale(locale as Locale)
return <ThemeProvider forcedTheme="light">{children}</ThemeProvider> return <ThemeProvider forcedTheme="light">{children}</ThemeProvider>
} }

View File

@@ -1,24 +1,34 @@
import "@repo/config-tailwind/styles.css" import "@repo/config-tailwind/styles.css"
import type { LocaleProps } from "@repo/i18n/routing"
import type { Locale } from "@repo/utils/constants" import type { Locale } from "@repo/utils/constants"
import { LOCALES } from "@repo/utils/constants" import { LOCALES } from "@repo/utils/constants"
import type { Metadata } from "next" import type { Metadata, Viewport } from "next"
import { NextIntlClientProvider } from "next-intl" import { NextIntlClientProvider } from "next-intl"
import { import {
getMessages, getMessages,
getTranslations, getTranslations,
setRequestLocale, setRequestLocale,
} from "next-intl/server" } from "next-intl/server"
import Script from "next/script"
const DOMAIN = "theoludwig.fr"
export const viewport: Viewport = {
themeColor: "#00aeff",
}
export const generateMetadata = async ({ export const generateMetadata = async ({
params, params,
}: LocaleProps): Promise<Metadata> => { }: {
params: Promise<{
locale: string
}>
}): Promise<Metadata> => {
const { locale } = await params const { locale } = await params
const t = await getTranslations({ locale }) const t = await getTranslations({ locale: locale as Locale })
const title = t("meta.title") const title = t("meta.title")
const description = `${title} - ${t("meta.description")}` const description = `${title} - ${t("meta.description")}`
const image = "/images/logo.webp" const image = "/images/logo.webp"
const url = new URL("https://theoludwig.fr") const url = new URL(`https://${DOMAIN}`)
const locales = LOCALES.join(", ") const locales = LOCALES.join(", ")
return { return {
@@ -57,14 +67,18 @@ 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 LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
const { children, params } = props const { children, params } = props
const { locale } = await params const { locale } = await params
// Enable static rendering // Enable static rendering
setRequestLocale(locale) setRequestLocale(locale as Locale)
const messages = await getMessages() const messages = await getMessages()
@@ -74,6 +88,12 @@ const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
{children} {children}
</NextIntlClientProvider> </NextIntlClientProvider>
<Script
defer
data-domain={DOMAIN}
src="https://plausible.theoludwig.fr/js/script.js"
/>
</body> </body>
</html> </html>
) )

View File

@@ -1,7 +1,7 @@
import typescriptESLint from "typescript-eslint" import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/eslint-config/nextjs" import config from "@repo/config-eslint"
export default typescriptESLint.config(...configNextjs, { export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"], files: ["**/*.ts", "**/*.tsx"],
languageOptions: { languageOptions: {
parser: typescriptESLint.parser, parser: typescriptESLint.parser,

View File

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

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/building-your-application/configuring/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,37 @@
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,
},
eslint: {
ignoreDuringBuilds: 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

@@ -1,6 +1,6 @@
{ {
"name": "@repo/website", "name": "@repo/website",
"version": "4.1.2", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"imports": { "imports": {
@@ -8,10 +8,11 @@
}, },
"scripts": { "scripts": {
"dev": "next dev --port 3000 --turbopack", "dev": "next dev --port 3000 --turbopack",
"build": "next build", "build": "next build --turbopack",
"start": "next start --port 3000", "start": "next start --port 3000",
"typegen": "next typegen",
"lint:eslint": "eslint . --max-warnings 0", "lint:eslint": "eslint . --max-warnings 0",
"lint:typescript": "tsc --noEmit" "lint:typescript": "next typegen && tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@repo/blog": "workspace:*", "@repo/blog": "workspace:*",
@@ -28,7 +29,7 @@
"react-dom": "catalog:" "react-dom": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*", "@repo/config-typescript": "workspace:*",
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/react": "catalog:", "@types/react": "catalog:",
@@ -37,6 +38,7 @@
"eslint": "catalog:", "eslint": "catalog:",
"postcss": "catalog:", "postcss": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "catalog:", "typescript-eslint": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }

View File

@@ -1,7 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
},
}
export default config

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 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

@@ -2,11 +2,6 @@ import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */ /** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = { const config = {
content: [
"./app/**/*.tsx",
"../../packages/ui/src/**/*.tsx",
"../../packages/blog/src/**/*.tsx",
],
presets: [sharedConfig], presets: [sharedConfig],
} }

View File

@@ -14,5 +14,5 @@
] ]
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", ".next"] "exclude": ["node_modules"]
} }

View File

@@ -6,6 +6,8 @@ services:
build: build:
context: "./" context: "./"
dockerfile: "./apps/website/Dockerfile" dockerfile: "./apps/website/Dockerfile"
args:
VERSION: ${VERSION-0.0.0-develop}
ports: ports:
- "${WEBSITE_PORT-3000}:${WEBSITE_PORT-3000}" - "${WEBSITE_PORT-3000}:${WEBSITE_PORT-3000}"
environment: environment:

View File

@@ -0,0 +1,85 @@
import nextPlugin from "@next/eslint-plugin-next"
import configConventions from "eslint-config-conventions"
import importXPlugin from "eslint-plugin-import-x"
import reactPlugin from "eslint-plugin-react"
import reactHooksPlugin from "eslint-plugin-react-hooks"
import typescriptESLint from "typescript-eslint"
export default typescriptESLint.config(
...configConventions,
reactHooksPlugin.configs.recommended,
reactPlugin.configs.flat.recommended,
{
ignores: [
".next",
"**/next.config.ts",
"**/eslint.config.js",
"**/tailwind.config.js",
"**/postcss.config.js",
"**/kysely.config.ts",
],
},
{
name: "config-eslint",
settings: {
react: {
version: "19.1.1",
},
},
plugins: {
"@next/next": nextPlugin,
"import-x": importXPlugin,
},
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs["core-web-vitals"].rules,
"@next/next/no-html-link-for-pages": "off",
"@next/next/no-img-element": "off",
"react-hooks/react-compiler": "error",
"react/jsx-no-target-blank": "off",
"react/no-unknown-property": "off",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/self-closing-comp": [
"error",
{
component: true,
html: true,
},
],
"react/void-dom-elements-no-children": "error",
"react/jsx-boolean-value": "error",
"react/iframe-missing-sandbox": "off",
"no-restricted-imports": [
"error",
{
paths: [
{
name: "next/navigation",
importNames: [
"redirect",
"permanentRedirect",
"useRouter",
"usePathname",
],
message: "Please import from `@repo/i18n/routing` instead.",
},
],
},
],
"import-x/extensions": [
"error",
"ignorePackages",
{
ts: "always",
tsx: "always",
js: "never",
jsx: "never",
},
],
},
},
)

View File

@@ -1,6 +1,6 @@
{ {
"name": "@repo/eslint-config", "name": "@repo/config-eslint",
"version": "4.1.2", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
@@ -9,25 +9,18 @@
"import": "./eslint.config.js", "import": "./eslint.config.js",
"require": "./eslint.config.js", "require": "./eslint.config.js",
"default": "./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": { "devDependencies": {
"@eslint/eslintrc": "catalog:",
"typescript-eslint": "catalog:", "typescript-eslint": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"eslint-config-conventions": "catalog:", "eslint-config-conventions": "catalog:",
"eslint-plugin-promise": "catalog:", "eslint-plugin-promise": "catalog:",
"eslint-plugin-unicorn": "catalog:", "eslint-plugin-unicorn": "catalog:",
"eslint-config-next": "catalog:",
"eslint-plugin-storybook": "catalog:",
"eslint-plugin-tailwindcss": "catalog:",
"eslint-plugin-import-x": "catalog:", "eslint-plugin-import-x": "catalog:",
"@next/eslint-plugin-next": "catalog:",
"eslint-plugin-react": "catalog:",
"eslint-plugin-react-hooks": "catalog:",
"typescript": "catalog:", "typescript": "catalog:",
"globals": "catalog:" "globals": "catalog:"
} }

View File

@@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@@ -1,4 +1,5 @@
import { type ClassValue, clsx } from "clsx" import type { ClassValue } from "clsx"
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
export const classNames = (...inputs: ClassValue[]): string => { export const classNames = (...inputs: ClassValue[]): string => {

View File

@@ -1,5 +1,5 @@
import typescriptESLint from "typescript-eslint" import typescriptESLint from "typescript-eslint"
import config from "@repo/eslint-config" import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, { export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"], files: ["**/*.ts", "**/*.tsx"],

5
configs/config-tailwind/index.d.ts vendored Normal file
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@repo/config-tailwind", "name": "@repo/config-tailwind",
"version": "4.1.2", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./tailwind.config.js", "main": "./tailwind.config.js",
@@ -21,12 +21,13 @@
"tailwind-merge": "catalog:" "tailwind-merge": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*", "@repo/config-typescript": "workspace:*",
"@tailwindcss/typography": "catalog:", "@tailwindcss/typography": "catalog:",
"typescript-eslint": "catalog:", "typescript-eslint": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"postcss": "catalog:", "postcss": "catalog:",
"tailwindcss": "catalog:" "tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:"
} }
} }

View File

@@ -1,16 +1,27 @@
@import "@fontsource/montserrat/400.css"; @import "tailwindcss";
@import "@fontsource/montserrat/500.css"; @config "./tailwind.config.js";
@import "@fontsource/montserrat/600.css";
@import "@fontsource/montserrat/700.css";
@import "@fontsource/montserrat/800.css";
@tailwind base; @source "../../apps/website/app";
@tailwind components; @source "../../apps/website/components";
@tailwind utilities; @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);
@layer base { @layer base {
* { [type="search"]::-webkit-search-decoration,
min-width: 0; [type="search"]::-webkit-search-cancel-button {
appearance: none;
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
} }
b, b,
@@ -162,9 +173,8 @@ code .line::before {
} }
.curriculum-vitae { .curriculum-vitae {
background: #f0f0f0;
color: #333; color: #333;
font-family: Arial, sans-serif; font-family: Montserrat, sans-serif;
hr { hr {
margin-top: 15px; margin-top: 15px;
@@ -240,8 +250,8 @@ code .line::before {
padding: 10px 0; padding: 10px 0;
} }
.profile-pic img { .profile-pic img {
width: 100px; width: 80px;
height: 100px; height: 80px;
border-radius: 50%; border-radius: 50%;
vertical-align: middle; vertical-align: middle;
border: 0; border: 0;

View File

@@ -1,6 +1,6 @@
{ {
"name": "@repo/config-typescript", "name": "@repo/config-typescript",
"version": "4.1.2", "version": "0.0.0-develop",
"private": true, "private": true,
"files": [ "files": [
"tsconfig.json" "tsconfig.json"

View File

@@ -11,6 +11,7 @@
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"isolatedModules": true, "isolatedModules": true,
@@ -18,7 +19,7 @@
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"skipLibCheck": true, "skipLibCheck": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": false,
"noEmit": true, "noEmit": true,
"target": "ESNext", "target": "ESNext",

View File

@@ -1,29 +1,24 @@
{ {
"name": "repo", "name": "repo",
"version": "4.1.2", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c", "packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b",
"engines": { "engines": {
"node": ">=22.0.0", "node": ">=24.0.0"
"pnpm": ">=9.15.0"
}, },
"scripts": { "scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel", "dev": "turbo run dev --parallel",
"start": "turbo run start --parallel", "start": "turbo run start --parallel",
"build": "turbo run build",
"test": "turbo run test", "test": "turbo run test",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint-cli2", "lint:markdown": "markdownlint-cli2",
"lint:typescript": "turbo run lint:typescript", "lint:typescript": "turbo run lint:typescript",
"lint:eslint": "turbo run lint:eslint", "lint:eslint": "turbo run lint:eslint",
"lint:prettier": "prettier . --check", "lint:prettier": "prettier . --check"
"release": "semantic-release"
}, },
"devDependencies": { "devDependencies": {
"@saithodev/semantic-release-backmerge": "catalog:",
"@semantic-release/exec": "catalog:",
"@semantic-release/git": "catalog:",
"editorconfig-checker": "catalog:", "editorconfig-checker": "catalog:",
"playwright": "catalog:", "playwright": "catalog:",
"prettier": "catalog:", "prettier": "catalog:",
@@ -31,8 +26,6 @@
"markdownlint-cli2": "catalog:", "markdownlint-cli2": "catalog:",
"markdownlint": "catalog:", "markdownlint": "catalog:",
"markdownlint-rule-relative-links": "catalog:", "markdownlint-rule-relative-links": "catalog:",
"replace-in-files-cli": "catalog:",
"semantic-release": "catalog:",
"turbo": "catalog:", "turbo": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }

View File

@@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@@ -1,7 +1,7 @@
import typescriptESLint from "typescript-eslint" import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/eslint-config/nextjs" import config from "@repo/config-eslint"
export default typescriptESLint.config(...configNextjs, { export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"], files: ["**/*.ts", "**/*.tsx"],
languageOptions: { languageOptions: {
parser: typescriptESLint.parser, parser: typescriptESLint.parser,

View File

@@ -1,6 +1,6 @@
{ {
"name": "@repo/blog", "name": "@repo/blog",
"version": "4.1.2", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
@@ -37,18 +37,18 @@
"react-icons": "catalog:" "react-icons": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*", "@repo/config-typescript": "workspace:*",
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/react": "catalog:", "@types/react": "catalog:",
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:", "@total-typescript/ts-reset": "catalog:",
"@storybook/blocks": "catalog:", "storybook": "catalog:",
"@storybook/react": "catalog:", "@storybook/nextjs": "catalog:",
"@storybook/test": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"postcss": "catalog:", "postcss": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "catalog:", "typescript-eslint": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }

View File

@@ -1,7 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
},
}
export default config

View File

@@ -109,11 +109,11 @@ const transaction = charge(user, subscription)
```typescript ```typescript
interface Car { interface Car {
carModel: string carModel: string
carColor: "red" | "blue" | "yellow" carColor: "red" | "blue" | "yellow"
} }
const printCar = (car: Car): void => { const printCar = (car: Car): void => {
console.log(`${car.carModel} (${car.carColor})`) console.log(`${car.carModel} (${car.carColor})`)
} }
``` ```
@@ -121,11 +121,11 @@ const printCar = (car: Car): void => {
```typescript ```typescript
interface Car { interface Car {
model: string model: string
color: "red" | "blue" | "yellow" color: "red" | "blue" | "yellow"
} }
const printCar = (car: Car): void => { const printCar = (car: Car): void => {
console.log(`${car.model} (${car.color})`) console.log(`${car.model} (${car.color})`)
} }
``` ```
@@ -174,13 +174,13 @@ import fs from "node:fs"
import path from "node:path" import path from "node:path"
const createFile = async ( const createFile = async (
name: string, name: string,
isTemporary: boolean = false, isTemporary: boolean = false,
): Promise<void> => { ): Promise<void> => {
if (isTemporary) { if (isTemporary) {
return await fs.promises.writeFile(path.join("temporary", name), "") return await fs.promises.writeFile(path.join("temporary", name), "")
} }
return await fs.promises.writeFile(name, "") return await fs.promises.writeFile(name, "")
} }
``` ```
@@ -193,11 +193,11 @@ import fs from "node:fs"
import path from "node:path" import path from "node:path"
const createFile = async (name: string): Promise<void> => { const createFile = async (name: string): Promise<void> => {
await fs.promises.writeFile(name, "") await fs.promises.writeFile(name, "")
} }
const createTemporaryFile = async (name: string): Promise<void> => { const createTemporaryFile = async (name: string): Promise<void> => {
await createFile(path.join("temporary", name)) await createFile(path.join("temporary", name))
} }
``` ```

View File

@@ -40,8 +40,8 @@ These configurations are stored in the `.gitconfig` file in your home directory
```sh ```sh
[user] [user]
name = Username name = Username
email = email@example.com email = email@example.com
``` ```
You can find more information and useful `git` configurations in the [official documentation](https://git-scm.com/docs/git-config). You can find more information and useful `git` configurations in the [official documentation](https://git-scm.com/docs/git-config).
@@ -88,6 +88,8 @@ git commit -m "Commit message"
# Commit changes in the past # Commit changes in the past
git commit --date "10 day ago" -m "Commit message" git commit --date "10 day ago" -m "Commit message"
# Also update the committer date for the last commit
git filter-branch --env-filter 'export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"' HEAD^..HEAD
# Add remote repository # Add remote repository
git remote add <remote> <url> git remote add <remote> <url>
@@ -266,7 +268,7 @@ Sometimes, you want to compare what commits have been made between two branches,
```sh ```sh
[alias] [alias]
diff-commits = !sh -c 'echo -n "Commits in $2 not in $1 \\(" && printf "%d" $(git cherry -v $1 $2 | wc -l) && echo "\\)" && git cherry -v $1 $2 && echo "" && echo -n "Commits in $1 not in $2 \\(" && printf "%d" $(git cherry -v $2 $1 | wc -l) && echo "\\)" && git cherry -v $2 $1' - diff-commits = !sh -c 'echo -n "Commits in $2 not in $1 \\(" && printf "%d" $(git cherry -v $1 $2 | wc -l) && echo "\\)" && git cherry -v $1 $2 && echo "" && echo -n "Commits in $1 not in $2 \\(" && printf "%d" $(git cherry -v $2 $1 | wc -l) && echo "\\)" && git cherry -v $2 $1' -
``` ```
With this alias, we can compare the commits between `main` and `develop` branches for example: With this alias, we can compare the commits between `main` and `develop` branches for example:

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! 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 ## Project based learning
@@ -39,23 +39,23 @@ The code of this website is open source on [GitHub](https://github.com/theoludwi
- [Next.js](https://nextjs.org/) - [Next.js](https://nextjs.org/)
It allows to have a server-side rendered website, that means that it is faster and easier to have a good <abbr title="Search Engine Optimization">SEO</abbr> than a <abbr title="Single Page Application">SPA</abbr>. It allows to have a server-side rendered website, that means that it is faster and easier to have a good <abbr title="Search Engine Optimization">SEO</abbr> than a <abbr title="Single Page Application">SPA</abbr>.
- [MDX](https://mdxjs.com/) - [MDX](https://mdxjs.com/)
MDX is an extension of Markdown that allows you to use custom React components. MDX is an extension of Markdown that allows you to use custom React components.
Here's what Markdown looks like: Here's what Markdown looks like:
```md ```md
A simple paragraph, with some **bold** text and some `inline code`. A simple paragraph, with some **bold** text and some `inline code`.
``` ```
When using Markdown in a web application, there's a "compile" step; the Markdown needs to be transformed into HTML, so that it can be understood by the browser. Those asterisks get turned into a `<strong>` tag, and each paragraph gets a `<p>` tag etc. When using Markdown in a web application, there's a "compile" step; the Markdown needs to be transformed into HTML, so that it can be understood by the browser. Those asterisks get turned into a `<strong>` tag, and each paragraph gets a `<p>` tag etc.
- [Tailwind CSS](https://tailwindcss.com/) - [Tailwind CSS](https://tailwindcss.com/)
Tailwind is a CSS framework to rapidly build modern websites without ever leaving HTML. Tailwind is a CSS framework to rapidly build modern websites without ever leaving HTML.
## Conclusion ## Conclusion

View File

@@ -37,7 +37,7 @@ The idea is that a user can create an account to authenticate with an email addr
## History ## 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. 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

@@ -95,7 +95,6 @@ export const BlogPostContent: React.FC<BlogPostContentProps> = async (
alt={alt} alt={alt}
width={1000} width={1000}
height={1000} height={1000}
quality={100}
className="size-auto" className="size-auto"
/> />
</span> </span>

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 { BLOG_POST_MOCK } from "../BlogPost.ts"
import { BlogPostUI as BlogPostUIComponent } from "../BlogPostUI.tsx" 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 { BLOG_POST_MOCK } from "../BlogPost.ts"
import { BlogPosts as BlogPostsComponent } from "../BlogPosts.tsx" import { BlogPosts as BlogPostsComponent } from "../BlogPosts.tsx"

View File

@@ -2,7 +2,6 @@ import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */ /** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = { const config = {
content: ["./src/**/*.tsx"],
presets: [sharedConfig], presets: [sharedConfig],
} }

View File

@@ -1,36 +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",
"**/vitest.config.ts",
"**/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,61 +0,0 @@
import { FlatCompat } from "@eslint/eslintrc"
import storybook from "eslint-plugin-storybook"
import tailwind from "eslint-plugin-tailwindcss"
import typescriptESLint from "typescript-eslint"
import config from "../eslint.config.js"
const flatCompat = new FlatCompat()
export default typescriptESLint.config(
...config,
...flatCompat.extends("next/core-web-vitals"),
...tailwind.configs["flat/recommended"],
...storybook.configs["flat/recommended"],
{
name: "config-eslint/nextjs",
settings: {
tailwindcss: {
callees: ["classNames", "cva"],
},
react: {
version: "detect",
},
},
rules: {
"tailwindcss/classnames-order": "off",
"tailwindcss/no-custom-classname": "off",
"@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,3 +0,0 @@
import type { Config } from "tailwindcss"
export default Config

View File

@@ -1,7 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
},
}
export default config

View File

@@ -1,5 +1,5 @@
import typescriptESLint from "typescript-eslint" import typescriptESLint from "typescript-eslint"
import config from "@repo/eslint-config" import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, { export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"], files: ["**/*.ts", "**/*.tsx"],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@repo/i18n", "name": "@repo/i18n",
"version": "4.1.2", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
@@ -11,26 +11,23 @@
}, },
"scripts": { "scripts": {
"lint:eslint": "eslint src --max-warnings 0", "lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit", "lint:typescript": "tsc --noEmit"
"test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@repo/utils": "workspace:*", "@repo/utils": "workspace:*",
"deepmerge": "catalog:",
"next": "catalog:", "next": "catalog:",
"next-intl": "catalog:", "next-intl": "catalog:",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:" "react-dom": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*", "@repo/config-typescript": "workspace:*",
"@types/react": "catalog:", "@types/react": "catalog:",
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:", "@total-typescript/ts-reset": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"typescript-eslint": "catalog:", "typescript-eslint": "catalog:",
"typescript": "catalog:", "typescript": "catalog:"
"vitest": "catalog:"
} }
} }

View File

@@ -1,10 +1,9 @@
import type en from "./translations/en-US.json" import type { routing } from "./routing.ts"
import type messages from "./translations/en-US.json"
type Messages = typeof en declare module "next-intl" {
interface AppConfig {
declare global { Locale: (typeof routing.locales)[number]
/** Messages: typeof messages
* Use type safe message keys with `next-intl`. }
*/
interface IntlMessages extends Messages {}
} }

View File

@@ -1,21 +1,22 @@
import deepmerge from "deepmerge"
import type { AbstractIntlMessages } from "next-intl" import type { AbstractIntlMessages } from "next-intl"
import { hasLocale } from "next-intl"
import { getRequestConfig } from "next-intl/server" import { getRequestConfig } from "next-intl/server"
import { routing } from "./routing.ts"
import type { Locale } from "@repo/utils/constants" import { LOCALE_DEFAULT } from "@repo/utils/constants"
import { LOCALE_DEFAULT, LOCALES } from "@repo/utils/constants" import { deepMerge } from "@repo/utils/objects"
export default getRequestConfig(async ({ requestLocale }) => { export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale const requested = await requestLocale
if (!LOCALES.includes(locale as Locale)) { const locale = hasLocale(routing.locales, requested)
locale = LOCALE_DEFAULT ? requested
} : routing.defaultLocale
const userMessages = (await import(`./translations/${locale}.json`)).default const userMessages = (await import(`./translations/${locale}.json`)).default
const defaultMessages = ( const defaultMessages = (
await import(`./translations/${LOCALE_DEFAULT}.json`) await import(`./translations/${LOCALE_DEFAULT}.json`)
).default ).default
const messages = deepmerge<AbstractIntlMessages>( const messages = deepMerge<AbstractIntlMessages>(
defaultMessages, defaultMessages,
userMessages, userMessages,
) )

View File

@@ -4,6 +4,13 @@ import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/utils/constants"
import { defineRouting } from "next-intl/routing" import { defineRouting } from "next-intl/routing"
import type { Locale } from "@repo/utils/constants" import type { Locale } from "@repo/utils/constants"
// Countries: https://github.com/umpirsky/country-list/blob/master/data/en/country.json
// Country flag picture: https://purecatamphetamine.github.io/country-flag-icons/3x2/US.svg
// Locale codes: https://simplelocalize.io/data/locales/
// Locale code is a combination of ISO 639-1 language code and ISO 3166-1 country code.
// For example, `fr-FR` is a locale code for French language in France.
export interface LocaleProps { export interface LocaleProps {
params: Promise<{ params: Promise<{
locale: Locale locale: Locale

View File

@@ -1,7 +0,0 @@
import { expectTypeOf, test } from "vitest"
import en from "../translations/en-US.json"
import fr from "../translations/fr-FR.json"
test("translations types should match", () => {
expectTypeOf(en).toEqualTypeOf(fr)
})

View File

@@ -88,7 +88,7 @@
"position": "Stagiaire Développeur Web Full Stack", "position": "Stagiaire Développeur Web Full Stack",
"startDate": "2023-04-11", "startDate": "2023-04-11",
"endDate": "2023-07-26", "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.", // "summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.",

View File

@@ -1,4 +1,24 @@
{ {
"meta": {
"description": "Developer Full Stack • Open-Source Enthusiast",
"title": "Théo LUDWIG"
},
"locales": {
"en-US": "English",
"fr-FR": "French"
},
"loading": "Loading...",
"errors": {
"error": "Error",
"not-found": "Not Found",
"page-doesnt-exist": "This page doesn't exist!",
"return-to-home-page": "Return to the home page?",
"server-error": "Internal Server Error!",
"try-again": "Try again?"
},
"footer": {
"all-rights-reserved": "All rights reserved"
},
"curriculum-vitae": { "curriculum-vitae": {
"about": { "about": {
"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.", "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.",
@@ -67,34 +87,32 @@
"title": "Studies" "title": "Studies"
}, },
"interests": { "interests": {
"high-tech": "Passionate about High-Tech", "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": "Open-Source Enthusiast" "open-source": "Open-Source Enthusiast"
}, },
"work": { "work": {
"ircad": { "ircad": {
"duration": "4 ans", "duration": "4 years",
"position": "Full Stack Web Developer Apprentice", "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": "Integration of Figma mockups for WebSurg, website dedicated to medical-surgical training.",
"IRCAD-Core": "IRCAD Core, an internal tool for managing medical training sessions and their requirements (anatomicals models, medications, staff training, etc.).",
"feature-logs": "History and traceability of data modifications (what? who? when?) in IRCAD Core.",
"feature-permissions": "Advanced permissions system and OAuth2 authentication, with read, write, and delete access restricted for specific users of the IRCAD Core application.",
"feature-search": "Search engine with filters, sorting, and customizable display for each user of IRCAD Core.",
"feature-architecture": "IRCAD Core project architecture in TypeScript Monorepo with Turborepo, and automatic deployment (CI/CD) with Docker Compose, self-hosted internally."
}
}, },
"numerize": { "numerize": {
"duration": "4 months", "duration": "3 months",
"position": "Full Stack Web Developer Intern", "position": "Full Stack Web Developer Intern",
"summary": "Development of an DMS (Document Management System) tool in React.js, Laravel and GraphQL." "summary": "Development of an DMS (Document Management System) tool in React.js, Laravel and GraphQL."
}, },
"title": "Work experiences" "title": "Work experiences"
} }
}, },
"errors": {
"error": "Error",
"not-found": "Not Found",
"page-doesnt-exist": "This page doesn't exist!",
"return-to-home-page": "Return to the home page?",
"server-error": "Internal Server Error!",
"try-again": "Try again?"
},
"footer": {
"all-rights-reserved": "All rights reserved"
},
"home": { "home": {
"about": { "about": {
"birth-date": { "birth-date": {
@@ -131,6 +149,10 @@
"title": "Open-Source" "title": "Open-Source"
}, },
"portfolio": { "portfolio": {
"fusey": {
"description": "Website I'm developing for the game ARK that tracks the number of players connected to the servers in real time and has over ~5,000 visitors each month, ~100,000 members on Discord, and ~120,000 followers on X/Twitter.",
"title": "Fusey"
},
"carolo": { "carolo": {
"description": "Strategy board game similar to chess which allows grandiose moves (only available in French).", "description": "Strategy board game similar to chess which allows grandiose moves (only available in French).",
"title": "Carolo" "title": "Carolo"
@@ -139,9 +161,13 @@
"description": "Leon is your open-source personal assistant.", "description": "Leon is your open-source personal assistant.",
"title": "Leon" "title": "Leon"
}, },
"title": "Portfolio" "title": "Projects"
}, },
"skills": { "skills": {
"software-development": "Software Development",
"sys-admin": "SysAdmin",
"systems-programming": "Systems Programming",
"backend": "Backend", "backend": "Backend",
"driving-license": "Driving license", "driving-license": "Driving license",
"frontend": "Frontend", "frontend": "Frontend",
@@ -150,13 +176,5 @@
"software-tools": "Software and tools", "software-tools": "Software and tools",
"title": "Skills" "title": "Skills"
} }
},
"locales": {
"en-US": "English",
"fr-FR": "French"
},
"meta": {
"description": "Developer Full Stack • Open-Source Enthusiast",
"title": "Théo LUDWIG"
} }
} }

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