1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-02-08 21:59:39 +01:00

Compare commits

...

39 Commits

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

View File

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

View File

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

View File

@ -1,4 +1,2 @@
COMPOSE_PROJECT_NAME=theoludwig
HOSTNAME=0.0.0.0
PORT=3000
NEXT_TELEMETRY_DISABLED=1
TZ=Europe/Paris
WEBSITE_PORT=3000

View File

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

View File

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

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

@ -0,0 +1,37 @@
name: "Chromatic"
on:
push:
branches: [develop]
pull_request:
branches: [develop, staging, main]
jobs:
chromatic:
timeout-minutes: 30
runs-on: "ubuntu-latest"
env:
DO_NOT_TRACK: "1"
TURBO_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps:
- uses: "actions/checkout@v4.2.2"
with:
fetch-depth: 0
- uses: "pnpm/action-setup@v4.1.0"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.2.0"
with:
node-version: "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"

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

@ -0,0 +1,47 @@
name: "CI"
on:
push:
branches: [develop]
pull_request:
branches: [develop, staging, main]
jobs:
ci:
timeout-minutes: 30
runs-on: "ubuntu-latest"
env:
DO_NOT_TRACK: "1"
TURBO_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps:
- uses: "actions/checkout@v4.2.2"
- uses: "pnpm/action-setup@v4.1.0"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.2.0"
with:
node-version: "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:typescript"
- run: "node --run lint:eslint"
- run: "node --run lint:prettier"
- run: "node --run test"
- run: "node --run build"
commitlint:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.2.2"
- uses: "wagoid/commitlint-github-action@v6.1.2"

View File

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

View File

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

View File

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

58
.gitignore vendored
View File

@ -1,33 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
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/
# 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 +40,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

View File

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

View File

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

View File

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

View File

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

View File

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

16
.markdownlint-cli2.mjs Normal file
View File

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

1
.npmrc
View File

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

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

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

View File

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

View File

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

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

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

19
.vscode/settings.json vendored
View File

@ -1,14 +1,23 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"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": "never"
},
"eslint.options": {
"ignorePath": ".gitignore"
},
"prettier.ignorePath": ".gitignore"
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
],
"i18n-ally.localesPaths": ["./packages/i18n/src/translations/"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": false,
"i18n-ally.sourceLanguage": "en-US",
"i18n-ally.displayLanguage": "en-US",
"i18n-ally.enabledFrameworks": ["next-intl", "general"],
"i18n-ally.extract.autoDetect": true
}

View File

@ -31,8 +31,9 @@ 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.12.0 [(`nvm install 22`)](https://nvm.sh)
- [pnpm](https://pnpm.io/) >= 10.2.1 [(`corepack enable`)](https://nodejs.org/docs/latest-v22.x/api/corepack.html)
- [Docker](https://www.docker.com/)
### Installation
@ -45,25 +46,46 @@ 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:typescript
node --run lint:eslint
node --run lint:prettier
# Tests
node --run test
# Build
node --run build
# To execute a command in a specific package (e.g: packages/utils)
cd packages/utils
node --run test
```
### Production environment with [Docker](https://www.docker.com/)
```sh
# Setup and run all the services for you
docker compose up --build
VERSION=$(git describe --tags) docker compose up --build --detach
```
### Service started
#### Services started
`website`: <http://127.0.0.1:3000>
`theoludwig`: <http://localhost:3000>

View File

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

View File

@ -8,7 +8,7 @@
<a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
<a href="https://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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,37 @@
import type { StorybookConfig } from "@storybook/nextjs"
const config: StorybookConfig = {
core: {
disableTelemetry: true,
},
docs: {
defaultName: "Documentation",
},
stories: [
"../../../packages/ui/src/**/*.stories.tsx",
"../../../packages/blog/src/**/*.stories.tsx",
"../stories/*.mdx",
],
addons: [
"@chromatic-com/storybook",
"@storybook/addon-essentials",
"@storybook/addon-storysource",
"@storybook/addon-a11y",
"@storybook/addon-interactions",
"storybook-dark-mode",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
features: {
experimentalRSC: true,
},
typescript: {
check: false,
reactDocgen: "react-docgen-typescript",
},
staticDirs: ["../../website/public"],
}
export default config

View File

@ -0,0 +1,57 @@
import "@repo/config-tailwind/styles.css"
import "./storybook-css-overrides.css"
import i18nMessages from "@repo/i18n/translations/en-US.json"
import { LOCALE_DEFAULT, TIMEZONE } from "@repo/utils/constants"
import type { Preview } from "@storybook/react"
import { NextIntlClientProvider } from "next-intl"
import { ThemeProvider as NextThemeProvider } from "next-themes"
import React from "react"
const preview: Preview = {
globals: {
a11y: {
manual: true,
},
},
parameters: {
nextjs: {
appDirectory: true,
},
options: {
storySort: {
order: ["Design System", "Layout", "Errors"],
},
},
backgrounds: { disable: true },
darkMode: {
darkClass: "dark",
lightClass: "light",
classTarget: "html",
stylePreview: true,
},
controls: {
disableSaveFromUI: true,
matchers: {
color: /(background|color)$/i,
date: /date$/i,
},
},
},
decorators: [
(Story) => {
return (
<NextThemeProvider enableColorScheme={false}>
<NextIntlClientProvider
messages={i18nMessages}
locale={LOCALE_DEFAULT}
timeZone={TIMEZONE}
>
<Story />
</NextIntlClientProvider>
</NextThemeProvider>
)
},
],
}
export default preview

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
module.exports = {
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
export default config

View File

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

View File

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

View File

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

View File

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

View File

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

42
apps/website/Dockerfile Normal file
View File

@ -0,0 +1,42 @@
FROM node:22.13.1-slim AS node-pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN npm install --global corepack@0.31.0 && corepack enable
ENV TURBO_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV DO_NOT_TRACK=1
WORKDIR /usr/src/app
FROM node-pnpm AS builder
COPY ./ ./
RUN pnpm install --global turbo@2.4.0
RUN turbo prune @repo/website --docker
FROM node-pnpm AS installer
ENV IS_STANDALONE=true
COPY .gitignore .gitignore
COPY --from=builder /usr/src/app/out/json/ ./
COPY --from=builder /usr/src/app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY --from=builder /usr/src/app/out/full/ ./
COPY turbo.json turbo.json
ARG VERSION="0.0.0-develop"
RUN pnpm install --global replace-in-files-cli@3.0.0
RUN VERSION_STRIPPED=${VERSION#v} && replace-in-files --regex='version": *"[^"]*' --replacement='"version": "'"$VERSION_STRIPPED"'"' '**/package.json' '!**/node_modules/**'
RUN pnpm --filter=@repo/website... exec turbo run build
FROM node-pnpm AS runner
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV IS_STANDALONE=true
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner
USER applicationrunner
COPY --from=installer /usr/src/app/apps/website/next.config.js ./
COPY --from=installer /usr/src/app/apps/website/package.json ./
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/standalone ./
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/static ./apps/website/.next/static
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/public ./apps/website/public
CMD ["node", "apps/website/server.js"]

View File

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

View File

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

View File

@ -0,0 +1,57 @@
import { getBlogPosts } from "@repo/blog"
import { BlogPosts } from "@repo/blog/BlogPosts"
import type { LocaleProps } from "@repo/i18n/routing"
import { MainLayout } from "@repo/ui/Layout/MainLayout"
import {
Section,
SectionDescription,
SectionTitle,
} from "@repo/ui/Layout/Section"
import { LOCALE_DEFAULT } from "@repo/utils/constants"
import type { Metadata } from "next"
import { setRequestLocale } from "next-intl/server"
const title = "Blog | Théo LUDWIG"
const description =
"The latest news about my journey of learning computer science."
export const generateMetadata = async (): Promise<Metadata> => {
return {
title,
description,
openGraph: {
title,
description,
locale: LOCALE_DEFAULT,
},
twitter: {
title,
description,
},
}
}
interface BlogPageProps extends LocaleProps {}
const BlogPage: React.FC<BlogPageProps> = async (props) => {
const { params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
const posts = await getBlogPosts()
return (
<MainLayout>
<Section verticalSpacing horizontalSpacing>
<SectionTitle>Blog</SectionTitle>
<SectionDescription>{description}</SectionDescription>
<BlogPosts posts={posts} />
</Section>
</MainLayout>
)
}
export default BlogPage

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

@ -0,0 +1,43 @@
{
"name": "@repo/website",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"imports": {
"#*": "./*"
},
"scripts": {
"dev": "next dev --port 3000 --turbopack",
"build": "next build",
"start": "next start --port 3000",
"lint:eslint": "eslint . --max-warnings 0",
"lint:typescript": "tsc --noEmit"
},
"dependencies": {
"@repo/blog": "workspace:*",
"@repo/config-tailwind": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*",
"@mdx-js/mdx": "catalog:",
"next-mdx-remote": "catalog:",
"shiki": "catalog:",
"next": "catalog:",
"next-intl": "catalog:",
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"eslint": "catalog:",
"postcss": "catalog:",
"tailwindcss": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:"
}
}

View File

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

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

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