1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-05-29 22:37:44 +02:00

Compare commits

...

44 Commits

Author SHA1 Message Date
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
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
09d677bd37 chore(release): 4.1.2 [skip ci] 2024-12-06 22:22:48 +00:00
db1159f20c fix(blog): issue with CSS loading
Ref: https://github.com/nodejs/node/issues/56155
2024-12-06 23:16:40 +01:00
af5c845e4b chore(release): 4.1.1 [skip ci] 2024-12-06 08:35:37 +00:00
3f66dfe46e fix: error 404 not found 2024-12-06 09:30:49 +01:00
d52a0c6f08 build(deps): update latest 2024-12-06 09:30:44 +01:00
251b0b4038 chore(release): 4.1.0 [skip ci] 2024-11-09 20:10:23 +00:00
a5baffe9eb fix(ui): button outline, hover effect 2024-11-09 21:01:31 +01:00
1351e4122d fix(blog): shiki syntax highlighting for txt 2024-11-09 20:21:09 +01:00
4c69d5a852 refactor(ui): allow all tailwind css colors 2024-11-09 20:14:30 +01:00
9e840b8dae build(deps): update Next.js to v15 and ESLint to v9 2024-11-09 19:50:22 +01:00
59153a7a69 style: fix ESLint 2024-10-13 00:00:44 +02:00
0a7094005c chore: config updates 2024-10-12 23:51:58 +02:00
12f1d6cdf2 chore: try to fix CI with Playwright 2024-09-12 00:14:58 +02:00
0d7b33727b feat: add in progress Engineering study + IRCAD until 2027 on CV 2024-09-12 00:05:59 +02:00
386f407f21 chore: simplify TypeScript config 2024-09-11 23:53:26 +02:00
6853ac6884 chore(release): 4.0.0 [skip ci] 2024-07-31 22:55:33 +00:00
b199aedf77 feat: add Locale switch to Curriculum Vitae (not visible in print mode) 2024-08-01 00:48:22 +02:00
da4b483a3c fix: a11y issues with curriculum-vitae 2024-08-01 00:36:45 +02:00
012fea869f feat: translate Curriculum Vitae in both English and French 2024-08-01 00:26:46 +02:00
a596d1c443 feat: components structure Curriculum Vitae 2024-07-31 22:27:51 +02:00
b4611e4a7f feat: init Curriculum Vitae 2024-07-31 19:23:14 +02:00
b5c50728de refactor: components struture 2024-07-31 11:41:39 +02:00
ceeeb2f9c5 fix: add missing unstable_setRequestLocale to enable static rendering 2024-07-31 11:00:30 +02:00
c094b37bca chore(release): 4.0.0-staging.1 [skip ci] 2024-07-30 22:57:33 +00:00
8dde7f5b42 ci: install playwright --with-deps 2024-07-31 00:47:05 +02:00
cef5ead09f chore: ignore curriculum-vitae 2024-07-30 23:59:33 +02:00
7bde328b96 perf!: monorepo setup + fully static + webp images
BREAKING CHANGE: minimum supported Node.js >= 22.0.0 and pnpm >= 9.5.0
2024-07-30 23:59:06 +02:00
0f44e64c0c chore(release): 3.3.2 [skip ci] 2024-07-20 08:34:00 +00:00
84c192bbef chore: usage of main instead of master 2024-07-20 10:31:14 +02:00
6f78a0686c fix: locale text with font-semibold 2024-07-20 10:28:17 +02:00
2897d181c5 Revert "fix: stop using flag image, use emoji instead"
This reverts commit f64acb68c7.
2024-07-20 10:26:19 +02:00
f94ce7d7bc fix: update Node.js to v20.15.1 (security release) 2024-07-20 10:21:16 +02:00
dd09092842 chore(release): 3.3.1 [skip ci] 2024-07-06 21:01:51 +00:00
f64acb68c7 fix: stop using flag image, use emoji instead 2024-07-06 02:33:49 +02:00
358 changed files with 21113 additions and 27001 deletions

View File

@ -1 +0,0 @@
{ "extends": ["@commitlint/config-conventional"] }

View File

@ -1,7 +1,10 @@
**/.git
**/.turbo **/.turbo
**/.next **/.next
**/out **/out
**/dist
**/build **/build
**/storybook-static
**/coverage **/coverage
**/node_modules **/node_modules
@ -11,10 +14,24 @@
.env.development .env.development
secrets secrets
# IDEs and editors
.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
.vscode
# misc # misc
.DS_Store .DS_Store
*.pem *.pem
Dockerfile Dockerfile
README.md
# vercel
.vercel
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo

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,4 +1,2 @@
COMPOSE_PROJECT_NAME=theoludwig TZ=Europe/Paris
HOSTNAME=0.0.0.0 WEBSITE_PORT=3000
PORT=3000
NEXT_TELEMETRY_DISABLED=1

View File

@ -1,38 +0,0 @@
{
"root": true,
"extends": [
"conventions",
"next/core-web-vitals",
"plugin:tailwindcss/recommended"
],
"plugins": ["import", "promise", "unicorn"],
"settings": {
"tailwindcss": {
"callees": ["classNames"]
},
"react": {
"version": "detect"
}
},
"rules": {
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
],
"react/void-dom-elements-no-children": "error",
"react/jsx-boolean-value": "error"
},
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"parserOptions": {
"project": "./tsconfig.json"
}
}
]
}

View File

@ -6,8 +6,8 @@ labels: "bug"
--- ---
<!-- <!--
Please provide a clear and concise description of what the bug is. Include 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. screenshots if needed. Please make sure your issue has not already been fixed.
--> -->
## Steps To Reproduce ## Steps To Reproduce

View File

@ -1,25 +0,0 @@
name: "Build"
on:
push:
branches: [develop]
pull_request:
branches: [master, develop]
jobs:
build:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.6"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: "20.x"
cache: "npm"
- name: "Install dependencies"
run: "npm clean-install"
- name: "Build"
run: "npm run build"

37
.github/workflows/chromatic.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: "Chromatic"
on:
push:
branches: [develop]
pull_request:
branches: [develop, staging, main]
jobs:
chromatic:
timeout-minutes: 30
runs-on: "ubuntu-latest"
env:
DO_NOT_TRACK: "1"
TURBO_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps:
- uses: "actions/checkout@v4.2.2"
with:
fetch-depth: 0
- uses: "pnpm/action-setup@v4.1.0"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.2.0"
with:
node-version: "24.x"
cache: "pnpm"
- name: "Install dependencies"
run: "pnpm install --frozen-lockfile"
- name: "Run Chromatic"
uses: "chromaui/action@latest"
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
workingDir: "apps/storybook"

47
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: "CI"
on:
push:
branches: [develop]
pull_request:
branches: [develop, staging, main]
jobs:
ci:
timeout-minutes: 30
runs-on: "ubuntu-latest"
env:
DO_NOT_TRACK: "1"
TURBO_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps:
- uses: "actions/checkout@v4.2.2"
- uses: "pnpm/action-setup@v4.1.0"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.2.0"
with:
node-version: "24.x"
cache: "pnpm"
- name: "Install dependencies"
run: "pnpm install --frozen-lockfile"
- name: "Install Playwright"
run: "pnpm exec playwright install --with-deps"
- 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 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"

View File

@ -1,42 +0,0 @@
name: "Lint"
on:
push:
branches: [develop]
pull_request:
branches: [master, develop]
jobs:
lint:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.6"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: "20.x"
cache: "npm"
- name: "Install dependencies"
run: "npm clean-install"
- name: "lint:commit"
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- name: "lint:editorconfig"
run: "npm run lint:editorconfig"
- name: "lint:markdown"
run: "npm run lint:markdown"
- name: "lint:eslint"
run: "npm run lint:eslint"
- name: "lint:prettier"
run: "npm run lint:prettier"
- name: "lint:dotenv"
uses: "dotenv-linter/action-dotenv-linter@v2.21.0"
with:
github_token: ${{ secrets.github_token }}

View File

@ -1,37 +0,0 @@
name: "Release"
on:
push:
branches: [master]
jobs:
release:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.6"
with:
fetch-depth: 0
persist-credentials: false
- name: "Import GPG key"
uses: "crazy-max/ghaction-import-gpg@v6.0.0"
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: "20.x"
cache: "npm"
- name: "Install dependencies"
run: "npm clean-install"
- name: "Release"
run: "npm run release"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }}

View File

@ -1,48 +0,0 @@
name: "Test"
on:
push:
branches: [develop]
pull_request:
branches: [master, develop]
jobs:
test-unit:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.6"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: "20.x"
cache: "npm"
- name: "Install dependencies"
run: "npm clean-install"
- name: "Unit Test"
run: "npm run test:unit"
test-e2e:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.6"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: "20.x"
cache: "npm"
- name: "Install dependencies"
run: "npm clean-install"
- name: "Build"
run: "npm run build"
- name: "html-w3c-validator"
run: "npm run test:html-w3c-validator"
- name: "End To End (e2e) Test"
run: "npm run test:e2e"

58
.gitignore vendored
View File

@ -1,33 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
node_modules node_modules
.npm .npm
package-lock.json
# next.js .pnpm-store
.next .pnp
out .pnp.js
.yarn/install-state.gz
# production
build
dist
public/curriculum-vitae
# testing # testing
coverage coverage
cypress/screenshots
cypress/videos
cypress/downloads
# envs # production
.env .next/
.env.production out/
dist/
build/
# misc
.DS_Store
*.pem
.turbo
bin/
# debug # debug
npm-debug.log* npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Storybook
*storybook.log
storybook-static
# IDEs and editors # IDEs and editors
/.idea .idea
.project .project
.classpath .classpath
.c9/ .c9/
@ -35,17 +40,14 @@ npm-debug.log*
.settings/ .settings/
*.sublime-workspace *.sublime-workspace
# IDE - VSCode # local env files
.vscode/* .env
!.vscode/settings.json .env.production
!.vscode/tasks.json .env*.local
!.vscode/launch.json
!.vscode/extensions.json
# misc # vercel
.DS_Store .vercel
.lighthouseci
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts # next-env.d.ts

View File

@ -1,6 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/theoludwig/html-w3c-validator/master/schema/html-w3c-validatorrc-schema.json",
"urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/blog"],
"files": ["./public/curriculum-vitae/index.html"],
"severities": ["error"]
}

View File

@ -1,3 +0,0 @@
#!/usr/bin/env sh
npm run lint:commit -- --edit

View File

@ -1,3 +0,0 @@
#!/usr/bin/env sh
npm run lint:staged

View File

@ -1,7 +0,0 @@
{
"**/*": ["editorconfig-checker", "prettier --write --ignore-unknown"],
"**/*.md": ["markdownlint-cli2 --fix --no-globs"],
"**/*.{js,jsx,ts,tsx}": [
"eslint --fix --max-warnings 0 --report-unused-disable-directives"
]
}

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"],
}

19
.markdownlint-cli2.mjs Normal file
View File

@ -0,0 +1,19 @@
import relativeLinksRule, { markdownIt } 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],
markdownItFactory: () => {
return markdownIt
},
}
export default config

1
.npmrc
View File

@ -1 +0,0 @@
save-exact=true

11
.prettierrc.json Normal file → Executable file
View File

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

View File

@ -1,38 +0,0 @@
{
"branches": ["master"],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": ["package.json", "package-lock.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}
],
"@semantic-release/github",
[
"@saithodev/semantic-release-backmerge",
{
"branches": [{ "from": "master", "to": "develop" }],
"backmergeStrategy": "merge"
}
]
]
}

View File

@ -3,9 +3,10 @@
"editorconfig.editorconfig", "editorconfig.editorconfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"davidanson.vscode-markdownlint",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"mikestead.dotenv", "mikestead.dotenv",
"davidanson.vscode-markdownlint", "antfu.pnpm-catalog-lens",
"ms-azuretools.vscode-docker" "Lokalise.i18n-ally"
] ]
} }

43
.vscode/react.code-snippets vendored Normal file
View File

@ -0,0 +1,43 @@
{
"React Component": {
"scope": "typescriptreact",
"prefix": "rfc",
"body": [
"export interface ${1:ComponentName}Props {}",
"",
"export const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = () => {",
" return (",
" <div>",
" <h1>${1:ComponentName}</h1>",
" </div>",
" )",
"}",
"",
],
"description": "React Component",
},
"React Component Story": {
"scope": "typescriptreact",
"prefix": "rfcs",
"body": [
"import type { Meta, StoryObj } from \"@storybook/react\"",
"",
"import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}.tsx\"",
"",
"const meta = {",
" title: \"${1:ComponentName}\",",
" component: ${1:ComponentName}Component",
"} satisfies Meta<typeof ${1:ComponentName}Component>",
"",
"export default meta",
"",
"type Story = StoryObj<typeof meta>",
"",
"export const ${1:ComponentName}: Story = {",
" args: {}",
"}",
"",
],
"description": "React Component Story",
},
}

19
.vscode/settings.json vendored
View File

@ -1,14 +1,23 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"editor.bracketPairColorization.enabled": true, "editor.bracketPairColorization.enabled": true,
"editor.wordWrap": "on",
"prettier.configPath": ".prettierrc.json", "prettier.configPath": ".prettierrc.json",
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit" "source.fixAll": "explicit",
"source.organizeImports": "never"
}, },
"eslint.options": { "tailwindCSS.experimental.classRegex": [
"ignorePath": ".gitignore" ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
}, ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
"prettier.ignorePath": ".gitignore" ],
"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.enabledFrameworks": ["next-intl", "general"],
"i18n-ally.extract.autoDetect": true
} }

View File

@ -20,21 +20,16 @@ community include:
- Demonstrating empathy and kindness toward other people - Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences - Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback - Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or - The use of sexualized language or imagery, and sexual attention or advances of any kind
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks - Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment - Public or private harassment
- Publishing others' private information, such as a physical or email - Publishing others' private information, such as a physical or email address, without their explicit permission
address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities

View File

@ -31,8 +31,9 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
### Prerequisites ### Prerequisites
- [Node.js](https://nodejs.org/) >= 20.0.0 - [Node.js](https://nodejs.org/) >= v24.0.0 [(`nvm install 24`)](https://nvm.sh)
- [npm](https://www.npmjs.com/) >= 10.0.0 - [pnpm](https://pnpm.io/) v10.10.0 [(`npm install --global corepack@0.32.0 && corepack enable`)](https://github.com/nodejs/corepack)
- [Docker](https://www.docker.com/)
### Installation ### Installation
@ -45,25 +46,46 @@ cd theoludwig
# Configure environment variables # Configure environment variables
cp .env.example .env cp .env.example .env
cp apps/website/.env.example apps/website/.env
# Install # Install dependencies
npm clean-install pnpm install --frozen-lockfile
# Install Playwright browser binaries and their dependencies (tests)
pnpm exec playwright install --with-deps
``` ```
### Local Development environment ### Development
```sh ```sh
# Run website # Start the development server
npm run dev node --run dev
# Lint
node --run lint:editorconfig
node --run lint:markdown
node --run lint:typescript
node --run lint:eslint
node --run lint:prettier
# Tests
node --run test
# Build
node --run build
# To execute a command in a specific package (e.g: packages/utils)
cd packages/utils
node --run test
``` ```
### Production environment with [Docker](https://www.docker.com/) ### Production environment with [Docker](https://www.docker.com/)
```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
``` ```
### Service started #### Services started
`website`: <http://127.0.0.1:3000> `theoludwig`: <http://localhost:3000>

View File

@ -1,28 +0,0 @@
FROM node:20.12.2 AS builder-dependencies
WORKDIR /usr/src/application
COPY ./package*.json ./
RUN npm clean-install
FROM node:20.12.2 AS builder
ENV NEXT_TELEMETRY_DISABLED=1
ENV IS_STANDALONE=true
WORKDIR /usr/src/application
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
COPY ./ ./
RUN npm run build
FROM node:20.12.2-slim AS runner
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV NEXT_TELEMETRY_DISABLED=1
ENV IS_STANDALONE=true
WORKDIR /usr/src/application
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner
USER applicationrunner
COPY --from=builder-dependencies --chown=applicationrunner:nodejs /usr/src/application/node_modules ./node_modules
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/.next/standalone ./
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/.next/static ./.next/static
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/public ./public
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/i18n/translations ./i18n/translations
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/next.config.js ./next.config.js
CMD ["./server.js"]

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://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=twitter&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://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>
@ -23,14 +23,25 @@
{ {
"name": "Théo LUDWIG", "name": "Théo LUDWIG",
"pronouns": "He/Him", "pronouns": "He/Him",
"birthDate": "31/03/2003", "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"], "programmingLanguages": [
"JavaScript/TypeScript",
"Python",
"C/C++",
"PHP"
],
"frontend": ["HTML/CSS", "Tailwind CSS", "React.js/Next.js"], "frontend": ["HTML/CSS", "Tailwind CSS", "React.js/Next.js"],
"backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"], "backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
"tools": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git", "Docker"] "tools": [
"GNU/Linux",
"Arch Linux",
"Visual Studio Code",
"Git",
"Docker"
]
} }
} }
``` ```

View File

@ -1,11 +0,0 @@
import { Loader } from "@/components/design/Loader"
const Loading = (): JSX.Element => {
return (
<main className="flex flex-1 flex-col items-center justify-center">
<Loader />
</main>
)
}
export default Loading

View File

@ -1,44 +0,0 @@
import type { Metadata } from "next"
import { notFound } from "next/navigation"
import "katex/dist/katex.min.css"
import { getBlogPostBySlug } from "@/blog/blog"
import { BlogPost } from "@/blog/BlogPost"
interface BlogPostPageProps {
params: {
slug: string
}
}
export const generateMetadata = async (
props: BlogPostPageProps,
): Promise<Metadata> => {
const blogPost = await getBlogPostBySlug(props.params.slug)
if (blogPost == null) {
return notFound()
}
const title = `${blogPost.frontmatter.title} | Théo LUDWIG`
const description = blogPost.frontmatter.description
return {
title,
description,
openGraph: {
title,
description,
},
twitter: {
title,
description,
},
}
}
const BlogPostPage = async (props: BlogPostPageProps): Promise<JSX.Element> => {
const { params } = props
return <BlogPost slug={params.slug} />
}
export default BlogPostPage

View File

@ -1,11 +0,0 @@
import { Loader } from "@/components/design/Loader"
const Loading = (): JSX.Element => {
return (
<main className="flex flex-1 flex-col items-center justify-center">
<Loader />
</main>
)
}
export default Loading

View File

@ -1,42 +0,0 @@
import { Suspense } from "react"
import type { Metadata } from "next"
import { BlogPosts } from "@/blog/BlogPosts"
import { Loader } from "@/components/design/Loader"
const title = "Blog | Théo LUDWIG"
const description =
"The latest news about my journey of learning computer science."
export const metadata: Metadata = {
title,
description,
openGraph: {
title,
description,
},
twitter: {
title,
description,
},
}
const BlogPage = async (): Promise<JSX.Element> => {
return (
<main className="flex flex-1 flex-col flex-wrap items-center">
<div className="mt-10 flex flex-col items-center">
<h1 className="text-4xl font-semibold text-primary dark:text-primary-dark">
Blog
</h1>
<p className="mt-6 text-center" data-cy="blog-post-date">
{description}
</p>
</div>
<Suspense fallback={<Loader className="mt-8" />}>
<BlogPosts />
</Suspense>
</main>
)
}
export default BlogPage

View File

@ -1,32 +0,0 @@
"use client"
import { useEffect } from "react"
export interface ErrorHandlingProps {
error: Error
}
const ErrorHandling = (props: ErrorHandlingProps): JSX.Element => {
const { error } = props
useEffect(() => {
console.error(error)
}, [error])
return (
<main className="flex flex-1 flex-col items-center justify-center">
<h1 className="my-6 text-4xl font-semibold">
Error{" "}
<span
className="text-primary dark:text-primary-dark"
data-cy="status-code"
>
500
</span>
</h1>
<p className="text-center text-lg">Server error</p>
</main>
)
}
export default ErrorHandling

View File

@ -1,80 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.break-wrap-words {
word-wrap: break-word;
word-break: break-word;
}
.text-base {
line-height: 1.75rem;
}
.prose {
@apply !max-w-5xl scroll-smooth text-gray dark:text-gray-300;
}
.prose p {
@apply text-justify;
}
.prose [id]::before {
content: "";
display: block;
height: 90px;
margin-top: -90px;
visibility: hidden;
}
.prose a,
.prose strong {
@apply !font-semibold text-primary dark:text-primary-dark;
}
strong,
b {
@apply font-bold;
}
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
@apply mt-1 text-gray dark:text-gray-dark;
}
.prose code {
color: #ce9178;
}
.prose :where(code):not(:where([class~="not-prose"] *))::before,
.prose :where(code):not(:where([class~="not-prose"] *))::after {
content: "";
}
.shiki {
white-space: pre-wrap !important;
}
code {
counter-reset: step;
counter-increment: step 0;
}
code .line::before {
content: counter(step);
counter-increment: step;
margin-right: 1rem;
text-align: right;
color: rgba(133, 133, 133, 0.8);
word-wrap: normal;
word-break: normal;
}
code .line:last-child {
display: none;
}
.katex .base {
display: inline !important;
white-space: normal !important;
width: 100% !important;
}

View File

@ -1,80 +0,0 @@
import type { Metadata } from "next"
import classNames from "clsx"
import "@fontsource/montserrat/400.css"
import "@fontsource/montserrat/600.css"
import "./globals.css"
import { Header } from "@/components/Header"
import { Footer } from "@/components/Footer"
import { getI18n } from "@/i18n/i18n.server"
import { getTheme } from "@/theme/theme.server"
const title = "Théo LUDWIG"
const description =
"Théo LUDWIG - Developer Full Stack • Open-Source Enthusiast"
const image = "/images/logo.png"
const url = new URL("https://theoludwig.fr")
const locale = "fr-FR, en-US"
export const metadata: Metadata = {
title,
description,
metadataBase: url,
openGraph: {
title,
description,
url,
siteName: title,
images: [
{
url: image,
width: 96,
height: 96,
},
],
locale,
type: "website",
},
twitter: {
card: "summary",
title,
description,
images: [image],
},
}
interface RootLayoutProps {
children: React.ReactNode
}
const RootLayout = (props: RootLayoutProps): JSX.Element => {
const { children } = props
const i18n = getI18n()
const theme = getTheme()
return (
<html
lang={i18n.locale}
className={classNames(
{
dark: theme === "dark",
light: theme === "light",
},
"scroll-smooth",
)}
style={{
colorScheme: theme,
}}
>
<body className="flex min-h-screen flex-col bg-white font-headline text-black dark:bg-black dark:text-white">
<Header />
{children}
<Footer />
</body>
</html>
)
}
export default RootLayout

View File

@ -1,11 +0,0 @@
import { Loader } from "@/components/design/Loader"
const Loading = (): JSX.Element => {
return (
<main className="flex flex-1 flex-col items-center justify-center">
<Loader />
</main>
)
}
export default Loading

View File

@ -1,32 +0,0 @@
import Link from "next/link"
import { getI18n } from "@/i18n/i18n.server"
const NotFound = (): JSX.Element => {
const i18n = getI18n()
return (
<main className="flex flex-1 flex-col items-center justify-center">
<h1 className="my-6 text-4xl font-semibold">
{i18n.translate("errors.error")}{" "}
<span
className="text-primary dark:text-primary-dark"
data-cy="status-code"
>
404
</span>
</h1>
<p className="text-center text-lg">
{i18n.translate("errors.not-found")}{" "}
<Link
href="/"
className="text-primary hover:underline dark:text-primary-dark"
>
{i18n.translate("errors.return-to-home-page")}
</Link>
</p>
</main>
)
}
export default NotFound

View File

@ -1,59 +0,0 @@
import { RevealFade } from "@/components/design/RevealFade"
import { Section } from "@/components/design/Section"
import { Interests } from "@/components/Interests"
import { Portfolio } from "@/components/Portfolio"
import { Profile } from "@/components/Profile"
import { SocialMediaList } from "@/components/Profile/SocialMediaList"
import { Skills } from "@/components/Skills"
import { OpenSource } from "@/components/OpenSource"
import { getI18n } from "@/i18n/i18n.server"
const HomePage = (): JSX.Element => {
const i18n = getI18n()
return (
<main className="flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl">
<Section isMain id="about">
<Profile />
<SocialMediaList />
</Section>
<RevealFade>
<Section
id="interests"
heading={i18n.translate("home.interests.title")}
>
<Interests />
</Section>
</RevealFade>
<RevealFade>
<Section
id="skills"
heading={i18n.translate("home.skills.title")}
withoutShadowContainer
>
<Skills />
</Section>
</RevealFade>
<RevealFade>
<Section
id="portfolio"
heading={i18n.translate("home.portfolio.title")}
withoutShadowContainer
>
<Portfolio />
</Section>
</RevealFade>
<RevealFade>
<Section id="open-source" heading="Open source" withoutShadowContainer>
<OpenSource />
</Section>
</RevealFade>
</main>
)
}
export default HomePage

View File

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

View File

@ -0,0 +1,37 @@
import type { StorybookConfig } from "@storybook/nextjs"
const config: StorybookConfig = {
core: {
disableTelemetry: true,
},
docs: {
defaultName: "Documentation",
},
stories: [
"../../../packages/ui/src/**/*.stories.tsx",
"../../../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",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
features: {
experimentalRSC: true,
},
typescript: {
check: false,
reactDocgen: "react-docgen-typescript",
},
staticDirs: ["../../website/public"],
}
export default config

View File

@ -0,0 +1,57 @@
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 { NextIntlClientProvider } from "next-intl"
import { ThemeProvider as NextThemeProvider } from "next-themes"
import React from "react"
const preview: Preview = {
globals: {
a11y: {
manual: true,
},
},
parameters: {
nextjs: {
appDirectory: true,
},
options: {
storySort: {
order: ["Design System", "Layout", "Errors"],
},
},
backgrounds: { disable: true },
darkMode: {
darkClass: "dark",
lightClass: "light",
classTarget: "html",
stylePreview: true,
},
controls: {
disableSaveFromUI: true,
matchers: {
color: /(background|color)$/i,
date: /date$/i,
},
},
},
decorators: [
(Story) => {
return (
<NextThemeProvider enableColorScheme={false}>
<NextIntlClientProvider
messages={i18nMessages}
locale={LOCALE_DEFAULT}
timeZone={TIMEZONE}
>
<Story />
</NextIntlClientProvider>
</NextThemeProvider>
)
},
],
}
export default preview

View File

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

View File

@ -0,0 +1,35 @@
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

@ -0,0 +1,7 @@
{
"projectId": "Project:66a7a85ea85df74afbec7682",
"buildScriptName": "build",
"storybookBaseDir": "apps/storybook",
"onlyChanged": true,
"zip": true
}

View File

@ -0,0 +1,13 @@
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

@ -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

@ -0,0 +1,58 @@
{
"name": "@repo/storybook",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"scripts": {
"build": "storybook build",
"dev": "storybook dev --port 6006 --no-open",
"start": "node --experimental-strip-types 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/config-tailwind": "workspace:*",
"@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/blog": "workspace:*",
"next": "catalog:",
"next-intl": "catalog:",
"next-themes": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"mime": "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:",
"@storybook/addon-a11y": "catalog:",
"@storybook/addon-interactions": "catalog:",
"@storybook/addon-themes": "catalog:",
"@storybook/blocks": "catalog:",
"@storybook/nextjs": "catalog:",
"@storybook/react": "catalog:",
"@storybook/test": "catalog:",
"@storybook/test-runner": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"axe-playwright": "catalog:",
"chromatic": "catalog:",
"eslint": "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

@ -0,0 +1,31 @@
import { Meta, Title, ColorPalette, ColorItem } from "@storybook/blocks"
import tailwindConfig from "@repo/config-tailwind"
<Meta title="Design System/Colors" />
<Title>Colors</Title>
<ColorPalette>
{Object.entries(tailwindConfig.theme.extend.colors).map(
([colorName, colorValue]) => {
const colors = {}
if (typeof colorValue === "string") {
colors[colorName] = colorValue
} else {
colors.light = colorValue.DEFAULT
colors.dark = colorValue.dark
}
return (
<ColorItem
key={colorName}
title={colorName}
colors={colors}
/>
)
}
)}
</ColorPalette>

View File

@ -0,0 +1,8 @@
import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = {
presets: [sharedConfig],
}
export default config

View File

@ -0,0 +1,7 @@
{
"extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext"]
},
"include": ["http-server.ts", "./.storybook/**/*.ts", "./.storybook/**/*.tsx"]
}

View File

@ -0,0 +1,9 @@
{
"$schema": "../../node_modules/turbo/schema.json",
"extends": ["//"],
"tasks": {
"test": {
"dependsOn": ["^test", "build"]
}
}
}

View File

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

View File

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

42
apps/website/Dockerfile Normal file
View File

@ -0,0 +1,42 @@
FROM node:24.0.0-slim AS node-pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN npm install --global corepack@0.32.0 && corepack enable
ENV TURBO_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV DO_NOT_TRACK=1
WORKDIR /usr/src/app
FROM node-pnpm AS builder
COPY ./ ./
RUN pnpm install --global turbo@2.5.3
RUN turbo prune @repo/website --docker
FROM node-pnpm AS installer
ENV IS_STANDALONE=true
COPY .gitignore .gitignore
COPY --from=builder /usr/src/app/out/json/ ./
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
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 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
FROM node-pnpm AS runner
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
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/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
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/public ./apps/website/public
CMD ["node", "apps/website/server.js"]

View File

@ -0,0 +1,7 @@
import { notFound } from "next/navigation"
const CatchAllPage: React.FC = () => {
return notFound()
}
export default CatchAllPage

View File

@ -0,0 +1,66 @@
import type { Metadata } from "next"
import { notFound } from "next/navigation"
import { getBlogPostBySlug, getBlogPosts } from "@repo/blog"
import { BlogPostUI } from "@repo/blog/BlogPostUI"
import type { Locale } from "@repo/utils/constants"
import { setRequestLocale } from "next-intl/server"
interface BlogPostPageProps {
params: Promise<{
slug: string
locale: Locale
}>
}
export const generateMetadata = async (
props: BlogPostPageProps,
): Promise<Metadata> => {
const { slug } = await props.params
const blogPost = await getBlogPostBySlug(slug)
if (blogPost == null) {
return notFound()
}
const title = `${blogPost.frontmatter.title} | Théo LUDWIG`
const description = blogPost.frontmatter.description
return {
title,
description,
openGraph: {
title,
description,
},
twitter: {
title,
description,
},
}
}
export const generateStaticParams = async (): Promise<
Array<{ slug: string }>
> => {
const posts = await getBlogPosts()
return posts.map((post) => {
return {
slug: post.slug,
}
})
}
const BlogPostPage: React.FC<BlogPostPageProps> = async (props) => {
const { params } = props
const { locale, slug } = await params
// Enable static rendering
setRequestLocale(locale)
const blogPost = await getBlogPostBySlug(slug)
if (blogPost == null) {
return notFound()
}
return <BlogPostUI blogPost={blogPost} />
}
export default BlogPostPage

View File

@ -0,0 +1,57 @@
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 { 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."
export const generateMetadata = async (): Promise<Metadata> => {
return {
title,
description,
openGraph: {
title,
description,
locale: LOCALE_DEFAULT,
},
twitter: {
title,
description,
},
}
}
interface BlogPageProps extends LocaleProps {}
const BlogPage: React.FC<BlogPageProps> = async (props) => {
const { params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
const posts = await getBlogPosts()
return (
<MainLayout>
<Section verticalSpacing horizontalSpacing>
<SectionTitle>Blog</SectionTitle>
<SectionDescription>{description}</SectionDescription>
<BlogPosts posts={posts} />
</Section>
</MainLayout>
)
}
export default BlogPage

View File

@ -0,0 +1,10 @@
"use client"
import type { ErrorServerProps } from "@repo/ui/Errors/ErrorServer"
import { ErrorServer } from "@repo/ui/Errors/ErrorServer"
const ErrorBoundaryPage: React.FC<ErrorServerProps> = (props) => {
return <ErrorServer {...props} />
}
export default ErrorBoundaryPage

View File

@ -0,0 +1,27 @@
import "@repo/config-tailwind/styles.css"
import type { LocaleProps } from "@repo/i18n/routing"
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 {}
const MainLayout: React.FC<MainLayoutProps> = async (props) => {
const { children, params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
return (
<ThemeProvider>
<Header />
{children}
<Footer version={VERSION} />
</ThemeProvider>
)
}
export default MainLayout

View File

@ -0,0 +1,12 @@
import { Spinner } from "@repo/ui/Design/Spinner"
import { MainLayout } from "@repo/ui/Layout/MainLayout"
const Loading: React.FC = () => {
return (
<MainLayout center>
<Spinner size={50} />
</MainLayout>
)
}
export default Loading

View File

@ -0,0 +1,10 @@
import { ErrorNotFound } from "@repo/ui/Errors/ErrorNotFound"
/**
* Note that `app/[locale]/[...rest]/page.tsx` is necessary for this page to render.
*/
const NotFound: React.FC = () => {
return <ErrorNotFound />
}
export default NotFound

View File

@ -0,0 +1,43 @@
import type { LocaleProps } from "@repo/i18n/routing"
import { About } from "@repo/ui/Home/About"
import { Interests } from "@repo/ui/Home/Interests"
import { OpenSource } from "@repo/ui/Home/OpenSource"
import { Portfolio } from "@repo/ui/Home/Portfolio"
import { Skills } from "@repo/ui/Home/Skills"
import { MainLayout } from "@repo/ui/Layout/MainLayout"
import { RevealFade } from "@repo/ui/Layout/Section"
import { setRequestLocale } from "next-intl/server"
interface HomePageProps extends LocaleProps {}
const HomePage: React.FC<HomePageProps> = async (props) => {
const { params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
return (
<MainLayout>
<About />
<RevealFade>
<Interests />
</RevealFade>
<RevealFade>
<Skills />
</RevealFade>
<RevealFade>
<Portfolio />
</RevealFade>
<RevealFade>
<OpenSource />
</RevealFade>
</MainLayout>
)
}
export default HomePage

View File

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

View File

@ -0,0 +1,17 @@
import type { LocaleProps } from "@repo/i18n/routing"
import { CurriculumVitae } from "@repo/ui/CurriculumVitae"
import { setRequestLocale } from "next-intl/server"
interface CurriculumVitaeProps extends LocaleProps {}
const CurriculumVitaePage: React.FC<CurriculumVitaeProps> = async (props) => {
const { params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
return <CurriculumVitae />
}
export default CurriculumVitaePage

View File

@ -0,0 +1,95 @@
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 Script from "next/script"
const DOMAIN = "theoludwig.fr"
export const viewport: Viewport = {
themeColor: "#00aeff",
}
export const generateMetadata = async ({
params,
}: LocaleProps): Promise<Metadata> => {
const { locale } = await params
const t = await getTranslations({ locale })
const title = t("meta.title")
const description = `${title} - ${t("meta.description")}`
const image = "/images/logo.webp"
const url = new URL(`https://${DOMAIN}`)
const locales = LOCALES.join(", ")
return {
title,
description,
metadataBase: url,
openGraph: {
title,
description,
url,
siteName: title,
images: [
{
url: image,
width: 96,
height: 96,
},
],
locale: locales,
type: "website",
},
twitter: {
card: "summary",
title,
description,
images: [image],
},
}
}
export const generateStaticParams = (): Array<{ locale: Locale }> => {
return LOCALES.map((locale) => {
return {
locale,
}
})
}
interface LocaleLayoutProps extends React.PropsWithChildren, LocaleProps {}
const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
const { children, params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
const messages = await getMessages()
return (
<html lang={locale} suppressHydrationWarning>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
<Script
defer
data-domain={DOMAIN}
src="https://plausible.theoludwig.fr/js/script.js"
/>
</body>
</html>
)
}
export default LocaleLayout

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,7 @@
interface RootLayoutProps extends React.PropsWithChildren {}
const RootLayout = ({ children }: RootLayoutProps): React.ReactNode => {
return children
}
export default RootLayout

View File

@ -0,0 +1,20 @@
"use client"
import Error from "next/error"
/**
* Render the default Next.js 404 page when a route
* is requested that doesn't match the middleware and
* therefore doesn't have a locale associated with it.
*/
const NotFound: React.FC = () => {
return (
<html lang="en">
<body>
<Error statusCode={404} />
</body>
</html>
)
}
export default NotFound

View File

@ -0,0 +1,13 @@
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

@ -0,0 +1,3 @@
import i18nRequestConfig from "@repo/i18n/request"
export default i18nRequestConfig

View File

@ -0,0 +1,26 @@
import { routing } from "@repo/i18n/routing"
import createIntlMiddleware from "next-intl/middleware"
const intlMiddleware = createIntlMiddleware(routing)
export default intlMiddleware
export const config = {
matcher: [
// Enable a redirect to a matching locale at the root
"/",
// Next.js issue, middleware matcher should support template literals:
// https://github.com/vercel/next.js/issues/56398
"/(en-US|fr-FR)/:path*",
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
"/((?!api|_next/static|_next/image|images|favicon.ico).*)",
],
}

5
apps/website/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <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

@ -0,0 +1,22 @@
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)

44
apps/website/package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "@repo/website",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"imports": {
"#*": "./*"
},
"scripts": {
"dev": "next dev --port 3000 --turbopack",
"build": "next build",
"start": "next start --port 3000",
"lint:eslint": "eslint . --max-warnings 0",
"lint:typescript": "tsc --noEmit"
},
"dependencies": {
"@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:",
"next": "catalog:",
"next-intl": "catalog:",
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@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

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

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