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
|
**/.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
|
||||||
|
@ -1,4 +1 @@
|
|||||||
COMPOSE_PROJECT_NAME=theoludwig
|
WEBSITE_PORT=3000
|
||||||
HOSTNAME=0.0.0.0
|
|
||||||
PORT=3000
|
|
||||||
NEXT_TELEMETRY_DISABLED=1
|
|
||||||
|
@ -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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, staging]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
timeout-minutes: 30
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
permissions:
|
||||||
|
contents: "write"
|
||||||
|
issues: "write"
|
||||||
|
pull-requests: "write"
|
||||||
|
id-token: "write"
|
||||||
steps:
|
steps:
|
||||||
- uses: "actions/checkout@v4.1.6"
|
- uses: "actions/checkout@v4.1.7"
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@ -20,17 +26,19 @@ jobs:
|
|||||||
git_user_signingkey: true
|
git_user_signingkey: true
|
||||||
git_commit_gpgsign: true
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
|
- uses: "pnpm/action-setup@v4.0.0"
|
||||||
|
|
||||||
- name: "Setup Node.js"
|
- name: "Setup Node.js"
|
||||||
uses: "actions/setup-node@v4.0.2"
|
uses: "actions/setup-node@v4.0.3"
|
||||||
with:
|
with:
|
||||||
node-version: "20.x"
|
node-version: "22.x"
|
||||||
cache: "npm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: "Install dependencies"
|
||||||
run: "npm clean-install"
|
run: "pnpm install --frozen-lockfile"
|
||||||
|
|
||||||
- name: "Release"
|
- name: "Release"
|
||||||
run: "npm run release"
|
run: "node --run release"
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
|
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
|
# 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/
|
||||||
|
# apps/website/public/curriculum-vitae/
|
||||||
|
|
||||||
|
# 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 +41,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
|
||||||
|
@ -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,
|
"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": [
|
"plugins": [
|
||||||
[
|
|
||||||
"@semantic-release/commit-analyzer",
|
"@semantic-release/commit-analyzer",
|
||||||
{
|
|
||||||
"preset": "conventionalcommits"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/release-notes-generator",
|
||||||
{
|
|
||||||
"preset": "conventionalcommits"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"@semantic-release/npm",
|
"@semantic-release/exec",
|
||||||
{
|
{
|
||||||
"npmPublish": false
|
"prepareCmd": "replace-in-files --regex='version\": *\"[^\"]*' --replacement='\"version\": \"${nextRelease.version}\"' '**/package.json' '!**/node_modules/**'"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"@semantic-release/git",
|
"@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]"
|
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -30,8 +24,10 @@
|
|||||||
[
|
[
|
||||||
"@saithodev/semantic-release-backmerge",
|
"@saithodev/semantic-release-backmerge",
|
||||||
{
|
{
|
||||||
"branches": [{ "from": "main", "to": "develop" }],
|
"branches": [
|
||||||
"backmergeStrategy": "merge"
|
{ "from": "main", "to": "develop" },
|
||||||
|
{ "from": "staging", "to": "develop" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
1
.vscode/extensions.json
vendored
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
|
"Vercel.turbo-vsc",
|
||||||
"editorconfig.editorconfig",
|
"editorconfig.editorconfig",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"dbaeumer.vscode-eslint",
|
"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",
|
"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": "explicit"
|
||||||
},
|
},
|
||||||
"eslint.options": {
|
"eslint.options": {
|
||||||
"ignorePath": ".gitignore"
|
"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
|
### Prerequisites
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) >= 20.0.0
|
- [Node.js](https://nodejs.org/) >= 22.0.0
|
||||||
- [npm](https://www.npmjs.com/) >= 10.0.0
|
- [pnpm](https://pnpm.io/) >= 9.5.0
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@ -40,21 +40,35 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
|
|||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone git@github.com:theoludwig/theoludwig.git
|
git clone git@github.com:theoludwig/theoludwig.git
|
||||||
|
|
||||||
# Go to the project root
|
|
||||||
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: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/)
|
### Production environment with [Docker](https://www.docker.com/)
|
||||||
@ -64,6 +78,6 @@ npm run dev
|
|||||||
docker compose up --build
|
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://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,7 +23,7 @@
|
|||||||
{
|
{
|
||||||
"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": {
|
||||||
|
@ -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: {
|
plugins: {
|
||||||
tailwindcss: {},
|
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 type { Metadata } from "next"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import "katex/dist/katex.min.css"
|
import { getBlogPostBySlug, getBlogPosts } from "@repo/blog"
|
||||||
|
import { BlogPostUI } from "@repo/blog/BlogPostUI"
|
||||||
import { getBlogPostBySlug } from "@/blog/blog"
|
|
||||||
import { BlogPost } from "@/blog/BlogPost"
|
|
||||||
|
|
||||||
interface BlogPostPageProps {
|
interface BlogPostPageProps {
|
||||||
params: {
|
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
|
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
|
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 |