perf!: monorepo setup + fully static + webp images
BREAKING CHANGE: minimum supported Node.js >= 22.0.0 and pnpm >= 9.5.0
| @@ -1 +0,0 @@ | ||||
| { "extends": ["@commitlint/config-conventional"] } | ||||
| @@ -1,7 +1,10 @@ | ||||
| **/.git | ||||
| **/.turbo | ||||
| **/.next | ||||
| **/out | ||||
| **/dist | ||||
| **/build | ||||
| **/storybook-static | ||||
| **/coverage | ||||
| **/node_modules | ||||
|  | ||||
| @@ -11,10 +14,24 @@ | ||||
| .env.development | ||||
| secrets | ||||
|  | ||||
| # IDEs and editors | ||||
| .idea | ||||
| .project | ||||
| .classpath | ||||
| .c9/ | ||||
| *.launch | ||||
| .settings/ | ||||
| *.sublime-workspace | ||||
| .vscode | ||||
|  | ||||
| # misc | ||||
| .DS_Store | ||||
| *.pem | ||||
| Dockerfile | ||||
| README.md | ||||
|  | ||||
| # vercel | ||||
| .vercel | ||||
|  | ||||
| # typescript | ||||
| *.tsbuildinfo | ||||
|   | ||||
| @@ -1,4 +1 @@ | ||||
| COMPOSE_PROJECT_NAME=theoludwig | ||||
| HOSTNAME=0.0.0.0 | ||||
| PORT=3000 | ||||
| NEXT_TELEMETRY_DISABLED=1 | ||||
| WEBSITE_PORT=3000 | ||||
|   | ||||
| @@ -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" | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										25
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,25 +0,0 @@ | ||||
| name: "Build" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [develop] | ||||
|   pull_request: | ||||
|     branches: [main, 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" | ||||
							
								
								
									
										33
									
								
								.github/workflows/chromatic.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| name: "Chromatic" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [develop] | ||||
|   pull_request: | ||||
|     branches: [develop, staging, main] | ||||
|  | ||||
| jobs: | ||||
|   chromatic: | ||||
|     timeout-minutes: 30 | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: "actions/checkout@v4.1.7" | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|  | ||||
|       - uses: "pnpm/action-setup@v4.0.0" | ||||
|  | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v4.0.3" | ||||
|         with: | ||||
|           node-version: "22.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" | ||||
							
								
								
									
										43
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,43 @@ | ||||
| name: "CI" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [develop] | ||||
|   pull_request: | ||||
|     branches: [develop, staging, main] | ||||
|  | ||||
| jobs: | ||||
|   ci: | ||||
|     timeout-minutes: 30 | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: "actions/checkout@v4.1.7" | ||||
|  | ||||
|       - uses: "pnpm/action-setup@v4.0.0" | ||||
|  | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v4.0.3" | ||||
|         with: | ||||
|           node-version: "22.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:prettier" | ||||
|       - run: "node --run lint:eslint" | ||||
|       - run: "node --run lint:typescript" | ||||
|       - run: "node --run test" | ||||
|       - run: "node --run build" | ||||
|  | ||||
|   commitlint: | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: "actions/checkout@v4.1.7" | ||||
|  | ||||
|       - uses: "wagoid/commitlint-github-action@v6.0.1" | ||||
							
								
								
									
										42
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,42 +0,0 @@ | ||||
| name: "Lint" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [develop] | ||||
|   pull_request: | ||||
|     branches: [main, 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 }} | ||||
							
								
								
									
										22
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -2,13 +2,19 @@ name: "Release" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [main] | ||||
|     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.1.6" | ||||
|       - uses: "actions/checkout@v4.1.7" | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|           persist-credentials: false | ||||
| @@ -20,17 +26,19 @@ jobs: | ||||
|           git_user_signingkey: true | ||||
|           git_commit_gpgsign: true | ||||
|  | ||||
|       - uses: "pnpm/action-setup@v4.0.0" | ||||
|  | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v4.0.2" | ||||
|         uses: "actions/setup-node@v4.0.3" | ||||
|         with: | ||||
|           node-version: "20.x" | ||||
|           cache: "npm" | ||||
|           node-version: "22.x" | ||||
|           cache: "pnpm" | ||||
|  | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|         run: "pnpm install --frozen-lockfile" | ||||
|  | ||||
|       - name: "Release" | ||||
|         run: "npm run release" | ||||
|         run: "node --run release" | ||||
|         env: | ||||
|           GH_TOKEN: ${{ secrets.GH_TOKEN }} | ||||
|           GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }} | ||||
|   | ||||
							
								
								
									
										48
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,48 +0,0 @@ | ||||
| name: "Test" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [develop] | ||||
|   pull_request: | ||||
|     branches: [main, 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" | ||||
							
								
								
									
										59
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,33 +1,39 @@ | ||||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||||
|  | ||||
| # dependencies | ||||
| node_modules | ||||
| .npm | ||||
|  | ||||
| # next.js | ||||
| .next | ||||
| out | ||||
|  | ||||
| # production | ||||
| build | ||||
| dist | ||||
| public/curriculum-vitae | ||||
| package-lock.json | ||||
| .pnpm-store | ||||
| .pnp | ||||
| .pnp.js | ||||
| .yarn/install-state.gz | ||||
|  | ||||
| # testing | ||||
| coverage | ||||
| cypress/screenshots | ||||
| cypress/videos | ||||
| cypress/downloads | ||||
|  | ||||
| # envs | ||||
| .env | ||||
| .env.production | ||||
| # production | ||||
| .next/ | ||||
| out/ | ||||
| dist/ | ||||
| build/ | ||||
| # apps/website/public/curriculum-vitae/ | ||||
|  | ||||
| # misc | ||||
| .DS_Store | ||||
| *.pem | ||||
| .turbo | ||||
| bin/ | ||||
|  | ||||
| # debug | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
|  | ||||
| # Storybook | ||||
| *storybook.log | ||||
| storybook-static | ||||
|  | ||||
| # IDEs and editors | ||||
| /.idea | ||||
| .idea | ||||
| .project | ||||
| .classpath | ||||
| .c9/ | ||||
| @@ -35,17 +41,14 @@ npm-debug.log* | ||||
| .settings/ | ||||
| *.sublime-workspace | ||||
|  | ||||
| # IDE - VSCode | ||||
| .vscode/* | ||||
| !.vscode/settings.json | ||||
| !.vscode/tasks.json | ||||
| !.vscode/launch.json | ||||
| !.vscode/extensions.json | ||||
| # local env files | ||||
| .env | ||||
| .env.production | ||||
| .env*.local | ||||
|  | ||||
| # misc | ||||
| .DS_Store | ||||
| .lighthouseci | ||||
| # vercel | ||||
| .vercel | ||||
|  | ||||
| # typescript | ||||
| *.tsbuildinfo | ||||
| next-env.d.ts | ||||
| # next-env.d.ts | ||||
|   | ||||
| @@ -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"] | ||||
| } | ||||
| @@ -1,3 +0,0 @@ | ||||
| #!/usr/bin/env sh | ||||
|  | ||||
| npm run lint:commit -- --edit | ||||
| @@ -1,3 +0,0 @@ | ||||
| #!/usr/bin/env sh | ||||
|  | ||||
| npm run lint:staged | ||||
| @@ -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" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										11
									
								
								.prettierrc.json
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						| @@ -1,4 +1,13 @@ | ||||
| { | ||||
|   "semi": false, | ||||
|   "plugins": ["prettier-plugin-tailwindcss"] | ||||
|   "plugins": ["prettier-plugin-tailwindcss"], | ||||
|   "tailwindFunctions": ["classNames", "cva"], | ||||
|   "overrides": [ | ||||
|     { | ||||
|       "files": "pnpm-lock.yaml", | ||||
|       "options": { | ||||
|         "rangeEnd": 0 | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -1,28 +1,22 @@ | ||||
| { | ||||
|   "branches": ["main"], | ||||
|   "branches": ["main", { "name": "staging", "prerelease": true }], | ||||
|   "plugins": [ | ||||
|     "@semantic-release/commit-analyzer", | ||||
|     "@semantic-release/release-notes-generator", | ||||
|     [ | ||||
|       "@semantic-release/commit-analyzer", | ||||
|       "@semantic-release/exec", | ||||
|       { | ||||
|         "preset": "conventionalcommits" | ||||
|       } | ||||
|     ], | ||||
|     [ | ||||
|       "@semantic-release/release-notes-generator", | ||||
|       { | ||||
|         "preset": "conventionalcommits" | ||||
|       } | ||||
|     ], | ||||
|     [ | ||||
|       "@semantic-release/npm", | ||||
|       { | ||||
|         "npmPublish": false | ||||
|         "prepareCmd": "replace-in-files --regex='version\": *\"[^\"]*' --replacement='\"version\": \"${nextRelease.version}\"' '**/package.json' '!**/node_modules/**'" | ||||
|       } | ||||
|     ], | ||||
|     [ | ||||
|       "@semantic-release/git", | ||||
|       { | ||||
|         "assets": ["package.json", "package-lock.json"], | ||||
|         "assets": [ | ||||
|           "package.json", | ||||
|           "apps/*/package.json", | ||||
|           "packages/*/package.json" | ||||
|         ], | ||||
|         "message": "chore(release): ${nextRelease.version} [skip ci]" | ||||
|       } | ||||
|     ], | ||||
| @@ -30,8 +24,10 @@ | ||||
|     [ | ||||
|       "@saithodev/semantic-release-backmerge", | ||||
|       { | ||||
|         "branches": [{ "from": "main", "to": "develop" }], | ||||
|         "backmergeStrategy": "merge" | ||||
|         "branches": [ | ||||
|           { "from": "main", "to": "develop" }, | ||||
|           { "from": "staging", "to": "develop" } | ||||
|         ] | ||||
|       } | ||||
|     ] | ||||
|   ] | ||||
|   | ||||
							
								
								
									
										1
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,5 +1,6 @@ | ||||
| { | ||||
|   "recommendations": [ | ||||
|     "Vercel.turbo-vsc", | ||||
|     "editorconfig.editorconfig", | ||||
|     "esbenp.prettier-vscode", | ||||
|     "dbaeumer.vscode-eslint", | ||||
|   | ||||
							
								
								
									
										43
									
								
								.vscode/react.code-snippets
									
									
									
									
										vendored
									
									
										Normal 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}\"", | ||||
|       "", | ||||
|       "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", | ||||
|   }, | ||||
| } | ||||
							
								
								
									
										10
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,14 +1,20 @@ | ||||
| { | ||||
|   "typescript.tsdk": "node_modules/typescript/lib", | ||||
|   "editor.bracketPairColorization.enabled": true, | ||||
|   "editor.wordWrap": "on", | ||||
|   "prettier.configPath": ".prettierrc.json", | ||||
|   "editor.defaultFormatter": "esbenp.prettier-vscode", | ||||
|   "editor.formatOnSave": true, | ||||
|   "editor.codeActionsOnSave": { | ||||
|     "source.fixAll": "explicit" | ||||
|     "source.fixAll": "explicit", | ||||
|     "source.organizeImports": "explicit" | ||||
|   }, | ||||
|   "eslint.options": { | ||||
|     "ignorePath": ".gitignore" | ||||
|   }, | ||||
|   "prettier.ignorePath": ".gitignore" | ||||
|   "prettier.ignorePath": ".gitignore", | ||||
|   "tailwindCSS.experimental.classRegex": [ | ||||
|     ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], | ||||
|     ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -31,8 +31,8 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv | ||||
|  | ||||
| ### Prerequisites | ||||
|  | ||||
| - [Node.js](https://nodejs.org/) >= 20.0.0 | ||||
| - [npm](https://www.npmjs.com/) >= 10.0.0 | ||||
| - [Node.js](https://nodejs.org/) >= 22.0.0 | ||||
| - [pnpm](https://pnpm.io/) >= 9.5.0 | ||||
|  | ||||
| ### Installation | ||||
|  | ||||
| @@ -40,21 +40,35 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv | ||||
| # Clone the repository | ||||
| git clone git@github.com:theoludwig/theoludwig.git | ||||
|  | ||||
| # Go to the project root | ||||
| cd theoludwig | ||||
|  | ||||
| # Configure environment variables | ||||
| cp .env.example .env | ||||
| cp apps/website/.env.example apps/website/.env | ||||
|  | ||||
| # Install | ||||
| npm clean-install | ||||
| # Install dependencies | ||||
| pnpm install --frozen-lockfile | ||||
|  | ||||
| # Install Playwright browser binaries and their dependencies (tests) | ||||
| pnpm exec playwright install --with-deps | ||||
| ``` | ||||
|  | ||||
| ### Local Development environment | ||||
| ### Development | ||||
|  | ||||
| ```sh | ||||
| # Run website | ||||
| npm run dev | ||||
| # Start the development server | ||||
| node --run dev | ||||
|  | ||||
| # Lint | ||||
| node --run lint:editorconfig | ||||
| node --run lint:markdown | ||||
| node --run lint:prettier | ||||
| node --run lint:eslint | ||||
| node --run lint:typescript | ||||
|  | ||||
| # Tests | ||||
| node --run test | ||||
|  | ||||
| # Build | ||||
| node --run build | ||||
| ``` | ||||
|  | ||||
| ### Production environment with [Docker](https://www.docker.com/) | ||||
| @@ -64,6 +78,6 @@ npm run dev | ||||
| docker compose up --build | ||||
| ``` | ||||
|  | ||||
| ### Service started | ||||
| #### Services started | ||||
|  | ||||
| `website`: <http://127.0.0.1:3000> | ||||
| `theoludwig`: <http://127.0.0.1:3000> | ||||
|   | ||||
							
								
								
									
										28
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						| @@ -1,28 +0,0 @@ | ||||
| FROM node:20.15.1 AS builder-dependencies | ||||
| WORKDIR /usr/src/application | ||||
| COPY ./package*.json ./ | ||||
| RUN npm clean-install | ||||
|  | ||||
| FROM node:20.15.1 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.15.1-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"] | ||||
| @@ -8,7 +8,7 @@ | ||||
|   <a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a> | ||||
|   <a href="https://gitlab.com/theoludwig"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a> | ||||
|   <a href="https://www.npmjs.com/~theoludwig"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a> | ||||
|   <a href="https://twitter.com/theoludwig_"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=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.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> | ||||
| @@ -23,7 +23,7 @@ | ||||
| { | ||||
|   "name": "Théo LUDWIG", | ||||
|   "pronouns": "He/Him", | ||||
|   "birthDate": "31/03/2003", | ||||
|   "birthDate": "2003-03-31", | ||||
|   "nationality": "Alsace, France", | ||||
|   "interests": ["Developer Full Stack", "Open-Source Enthusiast"], | ||||
|   "skills": { | ||||
|   | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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 | ||||
							
								
								
									
										59
									
								
								app/page.tsx
									
									
									
									
									
								
							
							
						
						| @@ -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 | ||||
							
								
								
									
										4
									
								
								apps/storybook/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "root": true, | ||||
|   "extends": ["@repo/eslint-config"] | ||||
| } | ||||
							
								
								
									
										34
									
								
								apps/storybook/.storybook/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | ||||
| import type { StorybookConfig } from "@storybook/nextjs" | ||||
|  | ||||
| const config: StorybookConfig = { | ||||
|   core: { | ||||
|     disableTelemetry: true, | ||||
|   }, | ||||
|   docs: { | ||||
|     defaultName: "Documentation", | ||||
|   }, | ||||
|   stories: ["../../../packages/**/*.stories.tsx", "../stories/*.mdx"], | ||||
|   addons: [ | ||||
|     "@storybook/addon-essentials", | ||||
|     "@storybook/addon-storysource", | ||||
|     "@storybook/addon-a11y", | ||||
|     "@storybook/addon-links", | ||||
|     "@chromatic-com/storybook", | ||||
|     "@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 | ||||
							
								
								
									
										52
									
								
								apps/storybook/.storybook/preview.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,52 @@ | ||||
| import "@repo/config-tailwind/styles.css" | ||||
| import { defaultTranslationValues, Locale } from "@repo/i18n/config" | ||||
| import i18nMessagesEnglish from "@repo/i18n/translations/en-US.json" | ||||
| 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 = { | ||||
|   parameters: { | ||||
|     nextjs: { | ||||
|       appDirectory: true, | ||||
|     }, | ||||
|     options: { | ||||
|       storySort: { | ||||
|         order: ["Design System", "User Interface", "Errors", "Feature"], | ||||
|       }, | ||||
|     }, | ||||
|     backgrounds: { disable: true }, | ||||
|     darkMode: { | ||||
|       darkClass: "dark", | ||||
|       lightClass: "light", | ||||
|       classTarget: "html", | ||||
|       stylePreview: true, | ||||
|     }, | ||||
|     controls: { | ||||
|       matchers: { | ||||
|         color: /(background|color)$/i, | ||||
|         date: /Date$/i, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   decorators: [ | ||||
|     (Story) => { | ||||
|       const locale = "en-US" satisfies Locale | ||||
|  | ||||
|       return ( | ||||
|         <NextThemeProvider enableColorScheme={false}> | ||||
|           <NextIntlClientProvider | ||||
|             messages={i18nMessagesEnglish} | ||||
|             locale={locale} | ||||
|             defaultTranslationValues={defaultTranslationValues} | ||||
|           > | ||||
|             <Story /> | ||||
|           </NextIntlClientProvider> | ||||
|         </NextThemeProvider> | ||||
|       ) | ||||
|     }, | ||||
|   ], | ||||
| } | ||||
|  | ||||
| export default preview | ||||
							
								
								
									
										35
									
								
								apps/storybook/.storybook/test-runner.ts
									
									
									
									
									
										Normal 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 | ||||
|  * to learn more about the test-runner hooks API. | ||||
|  */ | ||||
| const config: TestRunnerConfig = { | ||||
|   async preVisit(page) { | ||||
|     await injectAxe(page) | ||||
|   }, | ||||
|   async postVisit(page, context) { | ||||
|     const storyContext = await getStoryContext(page, context) | ||||
|  | ||||
|     if (storyContext.parameters?.a11y?.disable) { | ||||
|       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 | ||||
							
								
								
									
										7
									
								
								apps/storybook/chromatic.config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "projectId": "Project:66a7a85ea85df74afbec7682", | ||||
|   "buildScriptName": "build", | ||||
|   "storybookBaseDir": "apps/storybook", | ||||
|   "onlyChanged": true, | ||||
|   "zip": true | ||||
| } | ||||
							
								
								
									
										55
									
								
								apps/storybook/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,55 @@ | ||||
| { | ||||
|   "name": "@repo/storybook", | ||||
|   "version": "3.3.2", | ||||
|   "private": true, | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "build": "storybook build", | ||||
|     "dev": "storybook dev --port 6006 --no-open", | ||||
|     "start": "http-server \"storybook-static\" --port 6006 --silent", | ||||
|     "test": "start-server-and-test \"dev\" http://127.0.0.1:6006 \"test:storybook\"", | ||||
|     "test:storybook": "test-storybook", | ||||
|     "test:storybook-coverage": "test-storybook --coverage", | ||||
|     "chromatic": "chromatic" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@repo/config-tailwind": "workspace:*", | ||||
|     "@repo/i18n": "workspace:*", | ||||
|     "@repo/ui": "workspace:*", | ||||
|     "@repo/blog": "workspace:*", | ||||
|     "next": "catalog:", | ||||
|     "next-intl": "catalog:", | ||||
|     "next-themes": "catalog:", | ||||
|     "react": "catalog:", | ||||
|     "react-dom": "catalog:" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@repo/eslint-config": "workspace:*", | ||||
|     "@chromatic-com/storybook": "catalog:", | ||||
|     "@playwright/test": "catalog:", | ||||
|     "@storybook/addon-a11y": "catalog:", | ||||
|     "@storybook/addon-essentials": "catalog:", | ||||
|     "@storybook/addon-interactions": "catalog:", | ||||
|     "@storybook/addon-links": "catalog:", | ||||
|     "@storybook/addon-storysource": "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:", | ||||
|     "http-server": "catalog:", | ||||
|     "start-server-and-test": "catalog:", | ||||
|     "storybook": "catalog:", | ||||
|     "storybook-dark-mode": "catalog:", | ||||
|     "postcss": "catalog:", | ||||
|     "tailwindcss": "catalog:", | ||||
|     "typescript": "catalog:" | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| module.exports = { | ||||
| const config = { | ||||
|   plugins: { | ||||
|     tailwindcss: {}, | ||||
|     autoprefixer: {}, | ||||
|   }, | ||||
| } | ||||
| 
 | ||||
| export default config | ||||
							
								
								
									
										31
									
								
								apps/storybook/stories/Colors.mdx
									
									
									
									
									
										Normal 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.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> | ||||
							
								
								
									
										9
									
								
								apps/storybook/tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| import sharedConfig from "@repo/config-tailwind" | ||||
|  | ||||
| /** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */ | ||||
| const config = { | ||||
|   content: [".storybook/preview.tsx", "../../packages/**/*.tsx"], | ||||
|   presets: [sharedConfig], | ||||
| } | ||||
|  | ||||
| export default config | ||||
							
								
								
									
										3
									
								
								apps/website/.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| HOSTNAME=0.0.0.0 | ||||
| PORT=3000 | ||||
| NEXT_TELEMETRY_DISABLED=1 | ||||
							
								
								
									
										15
									
								
								apps/website/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| { | ||||
|   "root": true, | ||||
|   "extends": ["@repo/eslint-config/nextjs/.eslintrc.json"], | ||||
|   "ignorePatterns": ["public/"], | ||||
|   "overrides": [ | ||||
|     { | ||||
|       "files": ["*.ts", "*.tsx"], | ||||
|       "plugins": ["@typescript-eslint"], | ||||
|       "parser": "@typescript-eslint/parser", | ||||
|       "parserOptions": { | ||||
|         "project": true | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										36
									
								
								apps/website/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| FROM node:22.4.1-slim AS node-pnpm | ||||
| ENV PNPM_HOME="/pnpm" | ||||
| ENV PATH="$PNPM_HOME:$PATH" | ||||
| RUN corepack enable | ||||
| WORKDIR /usr/src/app | ||||
|  | ||||
| FROM node-pnpm AS builder | ||||
| RUN pnpm install --global turbo@2.0.9 | ||||
| COPY ./ ./ | ||||
| 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 | ||||
| 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 NEXT_TELEMETRY_DISABLED=1 | ||||
| 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"] | ||||
							
								
								
									
										7
									
								
								apps/website/app/[locale]/[...rest]/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| import { notFound } from "next/navigation" | ||||
|  | ||||
| const CatchAllPage: React.FC = () => { | ||||
|   return notFound() | ||||
| } | ||||
|  | ||||
| export default CatchAllPage | ||||
| @@ -1,10 +1,8 @@ | ||||
| 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" | ||||
| import { getBlogPostBySlug, getBlogPosts } from "@repo/blog" | ||||
| import { BlogPostUI } from "@repo/blog/BlogPostUI" | ||||
| 
 | ||||
| interface BlogPostPageProps { | ||||
|   params: { | ||||
| @@ -35,10 +33,26 @@ export const generateMetadata = async ( | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const BlogPostPage = async (props: BlogPostPageProps): Promise<JSX.Element> => { | ||||
| 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 | ||||
| 
 | ||||
|   return <BlogPost slug={params.slug} /> | ||||
|   const blogPost = await getBlogPostBySlug(params.slug) | ||||
|   if (blogPost == null) { | ||||
|     return notFound() | ||||
|   } | ||||
| 
 | ||||
|   return <BlogPostUI blogPost={blogPost} /> | ||||
| } | ||||
| 
 | ||||
| export default BlogPostPage | ||||
							
								
								
									
										55
									
								
								apps/website/app/[locale]/blog/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,55 @@ | ||||
| import { getBlogPosts } from "@repo/blog" | ||||
| import { BlogPosts } from "@repo/blog/BlogPosts" | ||||
| import { LOCALE_DEFAULT, type LocaleProps } from "@repo/i18n/config" | ||||
| import { | ||||
|   Section, | ||||
|   SectionDescription, | ||||
|   SectionTitle, | ||||
| } from "@repo/ui/design/Section" | ||||
| import { MainLayout } from "@repo/ui/MainLayout" | ||||
| import type { Metadata } from "next" | ||||
| import { unstable_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 | ||||
|  | ||||
|   // Enable static rendering | ||||
|   unstable_setRequestLocale(params.locale) | ||||
|  | ||||
|   const posts = await getBlogPosts() | ||||
|  | ||||
|   return ( | ||||
|     <MainLayout> | ||||
|       <Section verticalSpacing horizontalSpacing> | ||||
|         <SectionTitle>Blog</SectionTitle> | ||||
|         <SectionDescription>{description}</SectionDescription> | ||||
|  | ||||
|         <BlogPosts posts={posts} /> | ||||
|       </Section> | ||||
|     </MainLayout> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default BlogPage | ||||
							
								
								
									
										10
									
								
								apps/website/app/[locale]/error.tsx
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										91
									
								
								apps/website/app/[locale]/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,91 @@ | ||||
| import "@repo/config-tailwind/styles.css" | ||||
| import type { Locale, LocaleProps } from "@repo/i18n/config" | ||||
| import { LOCALES } from "@repo/i18n/config" | ||||
| import { Footer } from "@repo/ui/Footer" | ||||
| import { Header } from "@repo/ui/Header" | ||||
| import { ThemeProvider } from "@repo/ui/Header/SwitchTheme" | ||||
| import { VERSION } from "@repo/utils/constants" | ||||
| import type { Metadata } from "next" | ||||
| import { NextIntlClientProvider } from "next-intl" | ||||
| import { | ||||
|   getMessages, | ||||
|   getTranslations, | ||||
|   unstable_setRequestLocale, | ||||
| } from "next-intl/server" | ||||
|  | ||||
| export const generateMetadata = async ({ | ||||
|   params, | ||||
| }: LocaleProps): Promise<Metadata> => { | ||||
|   const t = await getTranslations({ locale: params.locale }) | ||||
|   const title = t("meta.title") | ||||
|   const description = `${title} - ${t("meta.description")}` | ||||
|   const image = "/images/logo.webp" | ||||
|   const url = new URL("https://theoludwig.fr") | ||||
|   const locale = LOCALES.join(", ") | ||||
|  | ||||
|   return { | ||||
|     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], | ||||
|     }, | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const generateStaticParams = (): Array<{ locale: Locale }> => { | ||||
|   return LOCALES.map((locale) => { | ||||
|     return { | ||||
|       locale, | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|  | ||||
| interface LocaleLayoutProps extends React.PropsWithChildren { | ||||
|   params: { | ||||
|     locale: Locale | ||||
|   } | ||||
| } | ||||
|  | ||||
| const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => { | ||||
|   const { children, params } = props | ||||
|  | ||||
|   // Enable static rendering | ||||
|   unstable_setRequestLocale(params.locale) | ||||
|  | ||||
|   const messages = await getMessages() | ||||
|  | ||||
|   return ( | ||||
|     <html lang={params.locale} suppressHydrationWarning> | ||||
|       <body> | ||||
|         <ThemeProvider> | ||||
|           <NextIntlClientProvider messages={messages}> | ||||
|             <Header /> | ||||
|             {children} | ||||
|             <Footer version={VERSION} /> | ||||
|           </NextIntlClientProvider> | ||||
|         </ThemeProvider> | ||||
|       </body> | ||||
|     </html> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default LocaleLayout | ||||
							
								
								
									
										12
									
								
								apps/website/app/[locale]/loading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| import { MainLayout } from "@repo/ui/MainLayout" | ||||
| import { Spinner } from "@repo/ui/design/Spinner" | ||||
|  | ||||
| const Loading: React.FC = () => { | ||||
|   return ( | ||||
|     <MainLayout center> | ||||
|       <Spinner size={50} /> | ||||
|     </MainLayout> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default Loading | ||||
							
								
								
									
										10
									
								
								apps/website/app/[locale]/not-found.tsx
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										42
									
								
								apps/website/app/[locale]/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | ||||
| import type { LocaleProps } from "@repo/i18n/config" | ||||
| import { About } from "@repo/ui/About" | ||||
| import { RevealFade } from "@repo/ui/design/Section" | ||||
| import { Interests } from "@repo/ui/Interests" | ||||
| import { MainLayout } from "@repo/ui/MainLayout" | ||||
| import { OpenSource } from "@repo/ui/OpenSource" | ||||
| import { Portfolio } from "@repo/ui/Portfolio" | ||||
| import { Skills } from "@repo/ui/Skills" | ||||
| import { unstable_setRequestLocale } from "next-intl/server" | ||||
|  | ||||
| interface HomePageProps extends LocaleProps {} | ||||
|  | ||||
| const HomePage: React.FC<HomePageProps> = (props) => { | ||||
|   const { params } = props | ||||
|  | ||||
|   // Enable static rendering | ||||
|   unstable_setRequestLocale(params.locale) | ||||
|  | ||||
|   return ( | ||||
|     <MainLayout> | ||||
|       <About /> | ||||
|  | ||||
|       <RevealFade> | ||||
|         <Interests /> | ||||
|       </RevealFade> | ||||
|  | ||||
|       <RevealFade> | ||||
|         <Skills /> | ||||
|       </RevealFade> | ||||
|  | ||||
|       <RevealFade> | ||||
|         <Portfolio /> | ||||
|       </RevealFade> | ||||
|  | ||||
|       <RevealFade> | ||||
|         <OpenSource /> | ||||
|       </RevealFade> | ||||
|     </MainLayout> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default HomePage | ||||
| Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB | 
							
								
								
									
										7
									
								
								apps/website/app/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| interface RootLayoutProps extends React.PropsWithChildren {} | ||||
|  | ||||
| const RootLayout = ({ children }: RootLayoutProps): React.ReactNode => { | ||||
|   return children | ||||
| } | ||||
|  | ||||
| export default RootLayout | ||||
							
								
								
									
										20
									
								
								apps/website/app/not-found.tsx
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										3
									
								
								apps/website/i18n.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| import i18nConfig from "@repo/i18n/i18n" | ||||
|  | ||||
| export default i18nConfig | ||||
							
								
								
									
										23
									
								
								apps/website/middleware.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/i18n/config" | ||||
| import createMiddleware from "next-intl/middleware" | ||||
|  | ||||
| export default createMiddleware({ | ||||
|   locales: LOCALES, | ||||
|   defaultLocale: LOCALE_DEFAULT, | ||||
|   localePrefix: LOCALE_PREFIX, | ||||
| }) | ||||
|  | ||||
| export const config = { | ||||
|   matcher: [ | ||||
|     // Enable a redirect to a matching locale at the root | ||||
|     "/", | ||||
|  | ||||
|     // Set a cookie to remember the previous locale for | ||||
|     // all requests that have a locale prefix | ||||
|     "/(en-US|fr-FR)/:path*", | ||||
|  | ||||
|     // Enable redirects that add missing locales | ||||
|     // (e.g. `/pathnames` -> `/en/pathnames`) | ||||
|     "/((?!_next|_vercel|.*\\..*).*)", | ||||
|   ], | ||||
| } | ||||
							
								
								
									
										5
									
								
								apps/website/next-env.d.ts
									
									
									
									
										vendored
									
									
										Normal 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/basic-features/typescript for more information. | ||||
							
								
								
									
										22
									
								
								apps/website/next.config.js
									
									
									
									
									
										Normal 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, | ||||
|  | ||||
|   // https://github.com/hashicorp/next-mdx-remote/issues/436#issuecomment-2066971842 | ||||
|   transpilePackages: ["next-mdx-remote", "shiki"], | ||||
|  | ||||
|   eslint: { | ||||
|     ignoreDuringBuilds: true, | ||||
|   }, | ||||
|   typescript: { | ||||
|     ignoreBuildErrors: true, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| const withNextIntl = createNextIntlPlugin() | ||||
|  | ||||
| export default withNextIntl(nextConfig) | ||||
							
								
								
									
										40
									
								
								apps/website/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,40 @@ | ||||
| { | ||||
|   "name": "@repo/website", | ||||
|   "version": "3.3.2", | ||||
|   "private": true, | ||||
|   "type": "module", | ||||
|   "imports": { | ||||
|     "#*": "./*" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "dev": "next dev --port 3000 --turbo", | ||||
|     "build": "next build", | ||||
|     "start": "next start --port 3000", | ||||
|     "lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives", | ||||
|     "lint:typescript": "tsc --noEmit" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@repo/blog": "workspace:*", | ||||
|     "@repo/config-tailwind": "workspace:*", | ||||
|     "@repo/utils": "workspace:*", | ||||
|     "@repo/i18n": "workspace:*", | ||||
|     "@repo/ui": "workspace:*", | ||||
|     "next": "catalog:", | ||||
|     "next-intl": "catalog:", | ||||
|     "react": "catalog:", | ||||
|     "react-dom": "catalog:", | ||||
|     "sharp": "catalog:" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@repo/eslint-config": "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:", | ||||
|     "typescript": "catalog:" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										7
									
								
								apps/website/postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| const config = { | ||||
|   plugins: { | ||||
|     tailwindcss: {}, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| export default config | ||||
							
								
								
									
										1
									
								
								apps/website/public/curriculum-vitae/index-BFUlhOmZ.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| /*! modern-normalize v2.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,:before,:after{box-sizing:border-box}html{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";line-height:1.15;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4}body{margin:0}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}body{font-family:Montserrat,Arial,"sans-serif";background:#f0f0f0;color:#333;line-height:1.42857143;font-size:14px}hr{margin-top:15px;margin-bottom:15px;border:0;border-top:1px solid #eee}p{margin:0}strong{font-weight:600}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}.link-disguise,.link-disguise:hover{color:inherit}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.clear-margin{margin:0}.relative{position:relative}.center-block{display:block;margin-right:auto;margin-left:auto}.text-center{text-align:center}.text-muted{color:#777}.text-uppercase{text-transform:uppercase}.list-unstyled{padding-left:0;list-style:none}.main{padding:5px}.title{font-weight:600}.profile-card-wrapper{position:relative}.card-wrapper{float:none!important;padding:5px}.profile-card-wrapper .profile-card{padding:10px}.card{background:#fff;border-radius:3px;padding:10px 0}.profile-pic{padding:10px 0}.profile-pic img{width:100px;height:100px;border-radius:50%;vertical-align:middle;border:0}.contact-details{display:flex;justify-content:center}.contact-details .detail{position:relative;min-height:1px;padding:10px}.social-links{line-height:2.5}.experience-description{margin-top:10px}.background-details .detail{display:table}.background-details .detail .icon,.background-details .detail .info{display:table-cell}.background-details .detail .icon{color:#707070}.background-details .detail .icon{min-width:45px;max-width:45px;text-align:center}.icon img{width:20px;height:20px}.background-details .detail .mobile-title{display:none}.card-nested{min-height:0;border-width:1px 0 0 0}.card-skills{position:relative}.labels{line-height:2}.space-top{margin-top:10px}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:600;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label-keyword{display:inline-block;font-size:.9em;padding:5px;border:1px solid #357ebd;margin-right:5px}.label-keyword p{margin:0}.section-separated{display:flex} | ||||
							
								
								
									
										1
									
								
								apps/website/public/curriculum-vitae/index-C1jSdDgn.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| (function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const c of t.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&r(c)}).observe(document,{childList:!0,subtree:!0});function s(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function r(e){if(e.ep)return;e.ep=!0;const t=s(e);fetch(e.href,t)}})();const i="31",l="03",u="2003",d=`${u}-${l}-${i}`,f=new Date(d),a=n=>{const o=new Date;let s=o.getFullYear()-n.getFullYear();const r=o.getMonth()-n.getMonth();return(r<0||r===0&&o.getDate()<n.getDate())&&s--,s},g=document.getElementById("year-old");g.textContent=a(f).toString(); | ||||
							
								
								
									
										1
									
								
								apps/website/public/curriculum-vitae/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB | 
| Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 659 B | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/logo.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/logo_background.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/portfolio/Carolo.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 23 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/portfolio/FunctionProject.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 40 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/portfolio/Leon.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 40 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/portfolio/Thream.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
| Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB | 
| After Width: | Height: | Size: 13 KiB | 
| After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/posts/thream-v1-0-0/thream-ui.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/ArchLinux.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/C-Cpp.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/CSS.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 10 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/Dart.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/Docker.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/Fastify-dark.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/Fastify-light.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.0 KiB | 
| Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/GNU-Linux.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 63 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/Git.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/HTML.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/JavaScript.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/Laravel.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 35 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/MySQL.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/NodeJS.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/PHP.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 50 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/PostgreSQL.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/Prisma-dark.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/Prisma-light.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 39 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/Python.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/website/public/images/skills/ReactJS.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.7 KiB | 
| Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |