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

Compare commits

...

50 Commits

Author SHA1 Message Date
8a59e9034f chore(release): 3.2.6 [skip ci] 2024-05-21 18:23:28 +00:00
d7121ea833 style: fix tailwindcss linting 2024-05-21 20:18:05 +02:00
c10f690622 build(deps): update dependencies to latest 2024-05-21 20:15:57 +02:00
6915072ab9 chore: delete unused config 2024-05-21 19:31:45 +02:00
dd803bcc51 test: fix should display hello-world blog post 2024-05-21 19:17:29 +02:00
efa33f26ec fix(blog): headings should be aligned with the text, not shifted 2024-05-21 19:06:12 +02:00
5f3dfad988 chore(release): 3.2.5 [skip ci] 2024-05-16 08:09:25 +00:00
b231381cb3 fix: client-side age calculation, more glanular check for isMounted
Allows to render as much as possible on the server side.
While keeping the calculation of the age on the client side to avoid hydratation mismatch.
2024-05-16 10:06:43 +02:00
bbb2e56512 fix: usage of correct heading levels and html tags 2024-05-16 09:56:19 +02:00
66cf6d7438 fix: add scroll behavior: smooth 2024-05-16 09:32:20 +02:00
2a635bf3ba fix: add hover effects 2024-05-16 09:26:05 +02:00
9f79b88202 chore(release): 3.2.4 [skip ci] 2024-04-13 17:17:11 +00:00
23d9caf578 style: fix eslint 2024-04-13 19:13:48 +02:00
7febe6d1f9 fix(blog): typos in posts 2024-04-13 19:03:18 +02:00
c4650c34d9 build(deps): update latest 2024-04-13 18:54:36 +02:00
0eb780485c fix(footer): show 0.0.0-development version in Footer in development 2024-04-06 20:40:25 +02:00
cd5e92b64a fix: hydratation error with age calculation 2024-04-06 20:32:09 +02:00
982b148329 Revert "fix(portfolio): update link to Carolo (carolo.org)"
This reverts commit c2c9b59c7a.
2024-04-06 20:27:04 +02:00
0febee5b51 refactor: rename to primary color 2024-04-06 20:25:02 +02:00
3502f51735 chore(release): 3.2.3 [skip ci] 2024-02-15 08:41:01 +00:00
493df4e2f2 style: fix prettier 2024-02-15 09:35:58 +01:00
c2c9b59c7a fix(portfolio): update link to Carolo (carolo.org) 2024-02-15 09:34:02 +01:00
f6e3008ab9 fix(blog): add command to commit in the past in Git Ultimate Guide 2024-02-15 09:30:34 +01:00
15e94cec64 fix: update dependencies to latest to address security issues Node.js v20.11.1
Ref: https://nodejs.org/en/blog/vulnerability/february-2024-security-releases
2024-02-15 09:27:03 +01:00
5185c6758b chore(release): 3.2.2 [skip ci] 2024-02-02 16:31:35 +00:00
b633eef833 fix: remove npm vulnerability by updating html-w3c-validator 2024-02-02 17:30:25 +01:00
d2e627ff13 chore: cleaner configs 2024-01-29 21:26:59 +01:00
1e0567b538 chore(release): 3.2.1 [skip ci] 2024-01-28 15:15:48 +00:00
c8d32c6acc test: correct selector for Main Title of page 2024-01-28 16:13:02 +01:00
05503cda26 chore: only report errors for html validation 2024-01-28 16:04:00 +01:00
303b6f3011 fix: correct responsive for Header Title 2024-01-28 15:45:45 +01:00
0272cf7080 docs(license): add email address 2024-01-28 15:42:23 +01:00
e8ea42a260 fix: wrong font weight on hover link 2024-01-28 15:42:01 +01:00
f337e14260 chore(release): 3.2.0 [skip ci] 2024-01-28 12:16:11 +00:00
f5020cad19 chore: usage of eslint-plugin-tailwindcss 2024-01-28 03:21:11 +01:00
b8ceefb2f6 feat: new logo v1 2024-01-28 01:56:47 +01:00
1523c8cac0 fix: wording typos 2024-01-25 14:46:03 +01:00
548ddc8425 style: format JSONC files with Prettier correctly 2024-01-24 21:49:30 +01:00
bac65ad61a fix: improve wording 2024-01-23 23:59:10 +01:00
b91f3165b7 fix(blog): add depreciation notice on Thream post 2024-01-23 22:29:21 +01:00
5478e202a7 fix(open source): replace nrwl/nx by DefinitelyTyped 2024-01-23 22:13:39 +01:00
a89b5932c2 fix: update dependencies to latest 2024-01-23 22:01:50 +01:00
339e42acfa chore(release): 3.1.2 [skip ci] 2023-12-28 05:24:14 +00:00
c123815a86 fix(portfolio): remove Thream project as it is now deprecated 2023-12-28 06:21:32 +01:00
dd26a277a2 fix: update dependencies to latest 2023-12-28 06:21:24 +01:00
62222dbb0c chore(release): 3.1.1 [skip ci] 2023-11-07 20:34:11 +00:00
ee0a02bc8b chore: downgrade commitlint temporarily to release new versions 2023-11-07 21:33:05 +01:00
2e04053ec3 fix: update CV with latest education courses 2023-11-07 21:14:43 +01:00
45a9a69122 fix: update dependencies to latest 2023-11-07 20:57:52 +01:00
e566ef6c38 chore: better Prettier config for easier reviews 2023-10-23 23:11:59 +02:00
132 changed files with 11625 additions and 8669 deletions

View File

@ -1 +0,0 @@
FROM mcr.microsoft.com/devcontainers/javascript-node:20

View File

@ -1,9 +0,0 @@
services:
workspace:
build:
context: './'
dockerfile: './Dockerfile'
volumes:
- '..:/workspace:cached'
command: 'sleep infinity'
network_mode: 'host'

View File

@ -1,24 +0,0 @@
{
"name": "theoludwig",
"dockerComposeFile": "./compose.yaml",
"service": "workspace",
"workspaceFolder": "/workspace",
"customizations": {
"vscode": {
"settings": {
"remote.autoForwardPorts": false,
"remote.localPortHost": "allInterfaces"
}
},
"extensions": [
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"davidanson.vscode-markdownlint",
"bradlc.vscode-tailwindcss",
"mikestead.dotenv",
"ms-azuretools.vscode-docker"
]
},
"remoteUser": "node"
}

View File

@ -1,4 +1,21 @@
build **/.turbo
.next **/.next
coverage **/out
node_modules **/build
**/coverage
**/node_modules
# envs
.env
.env.production
.env.development
secrets
# misc
.DS_Store
*.pem
Dockerfile
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -1,4 +1,4 @@
# For more information see: https://editorconfig.org/ # https://editorconfig.org/
root = true root = true

View File

@ -1,2 +1,4 @@
COMPOSE_PROJECT_NAME=theoludwig COMPOSE_PROJECT_NAME=theoludwig
HOSTNAME=0.0.0.0
PORT=3000 PORT=3000
NEXT_TELEMETRY_DISABLED=1

View File

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

View File

@ -1,8 +1,8 @@
--- ---
name: '🐛 Bug Report' name: "🐛 Bug Report"
about: 'Report an unexpected problem or unintended behavior.' about: "Report an unexpected problem or unintended behavior."
title: '[Bug]' title: "[Bug]"
labels: 'bug' labels: "bug"
--- ---
<!-- <!--

View File

@ -1,8 +1,8 @@
--- ---
name: '📜 Documentation' name: "📜 Documentation"
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).' about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
title: '[Documentation]' title: "[Documentation]"
labels: 'documentation' labels: "documentation"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '✨ Feature Request' name: "✨ Feature Request"
about: 'Suggest a new feature idea.' about: "Suggest a new feature idea."
title: '[Feature]' title: "[Feature]"
labels: 'feature request' labels: "feature request"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🔧 Improvement' name: "🔧 Improvement"
about: 'Improve structure/format/performance/refactor/tests of the code.' about: "Improve structure/format/performance/refactor/tests of the code."
title: '[Improvement]' title: "[Improvement]"
labels: 'improvement' labels: "improvement"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🙋 Question' name: "🙋 Question"
about: 'Further information is requested.' about: "Further information is requested."
title: '[Question]' title: "[Question]"
labels: 'question' labels: "question"
--- ---
### Question ### Question

View File

@ -1,4 +1,4 @@
name: 'Build' name: "Build"
on: on:
push: push:
@ -8,18 +8,18 @@ on:
jobs: jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v4.0.0' - uses: "actions/checkout@v4.1.1"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.8.1' uses: "actions/setup-node@v4.0.2"
with: with:
node-version: '20.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "npm run build"

View File

@ -1,4 +1,4 @@
name: 'Lint' name: "Lint"
on: on:
push: push:
@ -8,35 +8,35 @@ on:
jobs: jobs:
lint: lint:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v4.0.0' - uses: "actions/checkout@v4.1.1"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.8.1' uses: "actions/setup-node@v4.0.2"
with: with:
node-version: '20.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'lint:commit' - name: "lint:commit"
run: 'npm run lint:commit -- --to "${{ github.sha }}"' run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- name: 'lint:editorconfig' - name: "lint:editorconfig"
run: 'npm run lint:editorconfig' run: "npm run lint:editorconfig"
- name: 'lint:markdown' - name: "lint:markdown"
run: 'npm run lint:markdown' run: "npm run lint:markdown"
- name: 'lint:eslint' - name: "lint:eslint"
run: 'npm run lint:eslint' run: "npm run lint:eslint"
- name: 'lint:prettier' - name: "lint:prettier"
run: 'npm run lint:prettier' run: "npm run lint:prettier"
- name: 'lint:dotenv' - name: "lint:dotenv"
uses: 'dotenv-linter/action-dotenv-linter@v2.18.0' uses: "dotenv-linter/action-dotenv-linter@v2.18.0"
with: with:
github_token: ${{ secrets.github_token }} github_token: ${{ secrets.github_token }}

View File

@ -1,4 +1,4 @@
name: 'Release' name: "Release"
on: on:
push: push:
@ -6,31 +6,31 @@ on:
jobs: jobs:
release: release:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v4.0.0' - uses: "actions/checkout@v4.1.1"
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: 'Import GPG key' - name: "Import GPG key"
uses: 'crazy-max/ghaction-import-gpg@v6.0.0' uses: "crazy-max/ghaction-import-gpg@v6.0.0"
with: with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true git_user_signingkey: true
git_commit_gpgsign: true git_commit_gpgsign: true
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.8.1' uses: "actions/setup-node@v4.0.2"
with: with:
node-version: '20.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Release' - name: "Release"
run: 'npm run release' run: "npm 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 }}

View File

@ -1,4 +1,4 @@
name: 'Test' name: "Test"
on: on:
push: push:
@ -8,41 +8,41 @@ on:
jobs: jobs:
test-unit: test-unit:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v4.0.0' - uses: "actions/checkout@v4.1.1"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.8.1' uses: "actions/setup-node@v4.0.2"
with: with:
node-version: '20.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Unit Test' - name: "Unit Test"
run: 'npm run test:unit' run: "npm run test:unit"
test-e2e: test-e2e:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v4.0.0' - uses: "actions/checkout@v4.1.1"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.8.1' uses: "actions/setup-node@v4.0.2"
with: with:
node-version: '20.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "npm run build"
- name: 'html-w3c-validator' - name: "html-w3c-validator"
run: 'npm run test:html-w3c-validator' run: "npm run test:html-w3c-validator"
- name: 'End To End (e2e) Test' - name: "End To End (e2e) Test"
run: 'npm run test:e2e' run: "npm run test:e2e"

View File

@ -1,20 +0,0 @@
image: 'gitpod/workspace-full'
tasks:
- before: 'cp .env.example .env'
init: 'npm clean-install'
command: 'npm run dev'
ports:
- port: 3000
onOpen: 'open-preview'
github:
prebuilds:
master: true
branches: true
pullRequests: true
pullRequestsFromForks: true
addComment: true
addBadge: true
addLabel: true

View File

@ -1,4 +1,6 @@
{ {
"$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"], "urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/blog"],
"files": ["./public/curriculum-vitae/index.html"] "files": ["./public/curriculum-vitae/index.html"],
"severities": ["error"]
} }

View File

@ -1,4 +1,3 @@
#!/bin/sh #!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:commit -- --edit npm run lint:commit -- --edit

View File

@ -1,4 +1,3 @@
#!/bin/sh #!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:staged npm run lint:staged

View File

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

View File

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

View File

@ -1,6 +1,4 @@
{ {
"singleQuote": true,
"jsxSingleQuote": true,
"semi": false, "semi": false,
"trailingComma": "none" "plugins": ["prettier-plugin-tailwindcss"]
} }

View File

@ -5,7 +5,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll": "explicit"
}, },
"eslint.options": { "eslint.options": {
"ignorePath": ".gitignore" "ignorePath": ".gitignore"

View File

@ -29,12 +29,10 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
## Getting Started ## Getting Started
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/theoludwig/theoludwig)
### Prerequisites ### Prerequisites
- [Node.js](https://nodejs.org/) >= 20.0.0 - [Node.js](https://nodejs.org/) >= 20.0.0
- [npm](https://www.npmjs.com/) >= 9.0.0 - [npm](https://www.npmjs.com/) >= 10.0.0
### Installation ### Installation
@ -66,6 +64,6 @@ npm run dev
docker compose up --build docker compose up --build
``` ```
### Services started ### Service started
- `website`: <http://127.0.0.1:3000> `website`: <http://127.0.0.1:3000>

View File

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

View File

@ -1,6 +1,6 @@
MIT License # MIT License
Copyright (c) Théo LUDWIG Copyright (c) Théo LUDWIG <contact@theoludwig.fr>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,7 +1,7 @@
<h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</a></h1> <h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</a></h1>
<p align="center"> <p align="center">
<strong>Developer Full Stack • Open-Source enthusiast</strong> <strong>Developer Full Stack • Open-Source Enthusiast</strong>
</p> </p>
<p align="center"> <p align="center">
@ -25,10 +25,10 @@
"pronouns": "He/Him", "pronouns": "He/Him",
"birthDate": "31/03/2003", "birthDate": "31/03/2003",
"nationality": "Alsace, France", "nationality": "Alsace, France",
"interests": ["Developer Full Stack", "Open-Source enthusiast"], "interests": ["Developer Full Stack", "Open-Source Enthusiast"],
"skills": { "skills": {
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"], "programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
"frontend": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"], "frontend": ["HTML/CSS", "Tailwind CSS", "React.js/Next.js"],
"backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"], "backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
"tools": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git", "Docker"] "tools": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git", "Docker"]
} }
@ -40,6 +40,6 @@
## 📈 Statistics ## 📈 Statistics
<p align=center> <p align=center>
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=theoludwig&show_icons=true&theme=dark" /> <img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=theoludwig&show_icons=true&theme=dark" alt="Théo LUDWIG's GitHub Stats" />
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=theoludwig&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" /> <img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=theoludwig&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" alt="Théo LUDWIG's Programming Languages" />
</p> </p>

View File

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

View File

@ -1,10 +1,10 @@
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 "katex/dist/katex.min.css"
import { getBlogPostBySlug } from '@/blog/blog' import { getBlogPostBySlug } from "@/blog/blog"
import { BlogPost } from '@/blog/BlogPost' import { BlogPost } from "@/blog/BlogPost"
interface BlogPostPageProps { interface BlogPostPageProps {
params: { params: {
@ -13,7 +13,7 @@ interface BlogPostPageProps {
} }
export const generateMetadata = async ( export const generateMetadata = async (
props: BlogPostPageProps props: BlogPostPageProps,
): Promise<Metadata> => { ): Promise<Metadata> => {
const blogPost = await getBlogPostBySlug(props.params.slug) const blogPost = await getBlogPostBySlug(props.params.slug)
if (blogPost == null) { if (blogPost == null) {
@ -26,12 +26,12 @@ export const generateMetadata = async (
description, description,
openGraph: { openGraph: {
title, title,
description description,
}, },
twitter: { twitter: {
title, title,
description description,
} },
} }
} }

View File

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

View File

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

View File

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

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -7,6 +7,10 @@
word-break: break-word; word-break: break-word;
} }
.text-base {
line-height: 1.75rem;
}
.prose { .prose {
@apply !max-w-5xl scroll-smooth text-gray dark:text-gray-300; @apply !max-w-5xl scroll-smooth text-gray dark:text-gray-300;
} }
@ -16,7 +20,7 @@
} }
.prose [id]::before { .prose [id]::before {
content: ''; content: "";
display: block; display: block;
height: 90px; height: 90px;
margin-top: -90px; margin-top: -90px;
@ -25,7 +29,12 @@
.prose a, .prose a,
.prose strong { .prose strong {
@apply text-yellow dark:text-yellow-dark; @apply !font-semibold text-primary dark:text-primary-dark;
}
strong,
b {
@apply font-bold;
} }
.prose h2, .prose h2,
@ -39,9 +48,9 @@
.prose code { .prose code {
color: #ce9178; color: #ce9178;
} }
.prose :where(code):not(:where([class~='not-prose'] *))::before, .prose :where(code):not(:where([class~="not-prose"] *))::before,
.prose :where(code):not(:where([class~='not-prose'] *))::after { .prose :where(code):not(:where([class~="not-prose"] *))::after {
content: ''; content: "";
} }
.shiki { .shiki {
white-space: pre-wrap !important; white-space: pre-wrap !important;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { notFound } from 'next/navigation' import { notFound } from "next/navigation"
import date from 'date-and-time' import date from "date-and-time"
import 'katex/dist/katex.min.css' import "katex/dist/katex.min.css"
import { getBlogPostBySlug } from '@/blog/blog' import { getBlogPostBySlug } from "@/blog/blog"
import { BlogPostContent } from '@/blog/BlogPostContent' import { BlogPostContent } from "@/blog/BlogPostContent"
export interface BlogPostProps { export interface BlogPostProps {
slug: string slug: string
@ -19,13 +19,15 @@ export const BlogPost = async (props: BlogPostProps): Promise<JSX.Element> => {
} }
return ( return (
<main className='break-wrap-words flex flex-1 flex-col flex-wrap items-center justify-center'> <main className="break-wrap-words flex flex-1 flex-col flex-wrap items-center justify-center">
<div className='my-10 flex flex-col items-center text-center'> <div className="my-10 flex flex-col items-center text-center">
<h1 className='text-3xl font-semibold'>{blogPost.frontmatter.title}</h1> <h1 className="text-3xl font-semibold text-primary dark:text-primary-dark">
<p className='mt-2' data-cy='blog-post-date'> {blogPost.frontmatter.title}
</h1>
<p className="mt-2" data-cy="blog-post-date">
{date.format( {date.format(
new Date(blogPost.frontmatter.publishedOn), new Date(blogPost.frontmatter.publishedOn),
'DD/MM/YYYY' "DD/MM/YYYY",
)} )}
</p> </p>
</div> </div>

View File

@ -1,9 +1,9 @@
'use client' "use client"
import Giscus from '@giscus/react' import Giscus from "@giscus/react"
import { useTheme } from '@/theme/theme.client' import { useTheme } from "@/theme/theme.client"
import type { CookiesStore } from '@/utils/constants' import type { CookiesStore } from "@/utils/constants"
interface BlogPostCommentsProps { interface BlogPostCommentsProps {
cookiesStore: CookiesStore cookiesStore: CookiesStore
@ -16,18 +16,18 @@ export const BlogPostComments = (props: BlogPostCommentsProps): JSX.Element => {
return ( return (
<Giscus <Giscus
id='comments' id="comments"
repo='theoludwig/theoludwig' repo="theoludwig/theoludwig"
repoId='MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ=' repoId="MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ="
category='General' category="General"
categoryId='DIC_kwDOFWUewM4CQ_WK' categoryId="DIC_kwDOFWUewM4CQ_WK"
mapping='pathname' mapping="pathname"
reactionsEnabled='1' reactionsEnabled="1"
emitMetadata='0' emitMetadata="0"
inputPosition='top' inputPosition="top"
theme={theme} theme={theme}
lang='en' lang="en"
loading='lazy' loading="lazy"
/> />
) )
} }

View File

@ -1,39 +1,39 @@
import Image from 'next/image' import Image from "next/image"
import Link from 'next/link' import Link from "next/link"
import { cookies } from 'next/headers' import { cookies } from "next/headers"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faLink } from '@fortawesome/free-solid-svg-icons' import { faLink } from "@fortawesome/free-solid-svg-icons"
import { MDXRemote } from 'next-mdx-remote/rsc' import { MDXRemote } from "next-mdx-remote/rsc"
import { nodeTypes } from '@mdx-js/mdx' import { nodeTypes } from "@mdx-js/mdx"
import rehypeRaw from 'rehype-raw' import rehypeRaw from "rehype-raw"
import remarkGfm from 'remark-gfm' import remarkGfm from "remark-gfm"
import rehypeSlug from 'rehype-slug' import rehypeSlug from "rehype-slug"
import remarkMath from 'remark-math' import remarkMath from "remark-math"
import rehypeKatex from 'rehype-katex' import rehypeKatex from "rehype-katex"
import { getHighlighter } from 'shiki' import { getHighlighter } from "shiki"
import 'katex/dist/katex.min.css' import "katex/dist/katex.min.css"
import { getTheme } from '@/theme/theme.server' import { getTheme } from "@/theme/theme.server"
import { remarkSyntaxHighlightingPlugin } from '@/blog/remarkSyntaxHighlightingPlugin' import { remarkSyntaxHighlightingPlugin } from "@/blog/remarkSyntaxHighlightingPlugin"
import { BlogPostComments } from '@/blog/BlogPostComments' import { BlogPostComments } from "@/blog/BlogPostComments"
const Heading = ( const Heading = (
props: React.DetailedHTMLProps< props: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLHeadingElement>, React.HTMLAttributes<HTMLHeadingElement>,
HTMLHeadingElement HTMLHeadingElement
> >,
): JSX.Element => { ): JSX.Element => {
const { children, id = '' } = props const { children, id = "" } = props
return ( return (
<h2 {...props} className='group'> <h2 {...props}>
<Link <Link href={`#${id}`} className="group relative hover:no-underline">
href={`#${id}`} <FontAwesomeIcon
className='invisible !text-black group-hover:visible dark:!text-white' className="absolute bottom-2 left-[-26px] mr-2 hidden size-4 !text-black group-hover:inline dark:!text-white"
> icon={faLink}
<FontAwesomeIcon className='mr-2 inline h-4 w-4' icon={faLink} /> />
{children}
</Link> </Link>
{children}
</h2> </h2>
) )
} }
@ -43,7 +43,7 @@ export interface BlogPostContentProps {
} }
export const BlogPostContent = async ( export const BlogPostContent = async (
props: BlogPostContentProps props: BlogPostContentProps,
): Promise<JSX.Element> => { ): Promise<JSX.Element> => {
const { content } = props const { content } = props
@ -51,12 +51,12 @@ export const BlogPostContent = async (
const theme = getTheme() const theme = getTheme()
const highlighter = await getHighlighter({ const highlighter = await getHighlighter({
theme: `${theme}-plus` theme: `${theme}-plus`,
}) })
return ( return (
<div className='prose mb-10'> <div className="prose mb-10">
<div className='px-8'> <div className="px-8">
<MDXRemote <MDXRemote
source={content} source={content}
options={{ options={{
@ -64,14 +64,14 @@ export const BlogPostContent = async (
remarkPlugins: [ remarkPlugins: [
remarkGfm, remarkGfm,
[remarkSyntaxHighlightingPlugin, { highlighter }], [remarkSyntaxHighlightingPlugin, { highlighter }],
remarkMath remarkMath,
], ],
rehypePlugins: [ rehypePlugins: [
rehypeSlug, rehypeSlug,
[rehypeRaw, { passThrough: nodeTypes }], [rehypeRaw, { passThrough: nodeTypes }],
rehypeKatex rehypeKatex,
] ],
} },
}} }}
components={{ components={{
h1: Heading, h1: Heading,
@ -81,27 +81,37 @@ export const BlogPostContent = async (
h5: Heading, h5: Heading,
h6: Heading, h6: Heading,
img: (properties) => { img: (properties) => {
const { src = '', alt = 'Blog Image' } = properties const { src = "", alt = "Blog Image" } = properties
const source = src.replace('../../public/', '/') const source = src.replace("../../public/", "/")
return ( return (
<span className='flex flex-col items-center justify-center'> <span className="flex flex-col items-center justify-center">
<Image <Image
src={source} src={source}
alt={alt} alt={alt}
width={1000} width={1000}
height={1000} height={1000}
className='h-auto w-auto' className="size-auto"
/> />
</span> </span>
) )
}, },
a: (props) => { a: (props) => {
const { href = '' } = props const { href = "", ...rest } = props
if (href.startsWith('#')) { if (href.startsWith("#")) {
return <a {...props} /> return <a {...props} />
} }
return <a target='_blank' rel='noopener noreferrer' {...props} /> if (href.startsWith("../posts/")) {
} return (
<a
href={href
.replace("../posts/", "/blog/")
.replace(".md", "")}
{...rest}
/>
)
}
return <a target="_blank" rel="noopener noreferrer" {...props} />
},
}} }}
/> />
<BlogPostComments cookiesStore={cookiesStore.toString()} /> <BlogPostComments cookiesStore={cookiesStore.toString()} />

View File

@ -1,42 +1,42 @@
import Link from 'next/link' import Link from "next/link"
import date from 'date-and-time' import date from "date-and-time"
import { ShadowContainer } from '@/components/design/ShadowContainer' import { ShadowContainer } from "@/components/design/ShadowContainer"
import { getBlogPosts } from '@/blog/blog' import { getBlogPosts } from "@/blog/blog"
export const BlogPosts = async (): Promise<JSX.Element> => { export const BlogPosts = async (): Promise<JSX.Element> => {
const posts = await getBlogPosts() const posts = await getBlogPosts()
return ( return (
<div className='flex w-full items-center justify-center p-8'> <div className="flex w-full items-center justify-center p-8">
<div className='w-[1600px]' data-cy='blog-posts'> <ul className="w-[1600px]" data-cy="blog-posts">
{posts.map((post, index) => { {posts.map((post) => {
const postPublishedOn = date.format( const postPublishedOn = date.format(
new Date(post.frontmatter.publishedOn), new Date(post.frontmatter.publishedOn),
'DD/MM/YYYY' "DD/MM/YYYY",
) )
return ( return (
<Link <li key={post.slug}>
href={`/blog/${post.slug}`} <Link href={`/blog/${post.slug}`} locale="en" data-cy={post.slug}>
key={index} <ShadowContainer className="cursor-pointer p-6 transition-all duration-300 ease-in-out hover:scale-[1.02]">
locale='en' <h2
data-cy={post.slug} data-cy="blog-post-title"
> className="text-xl font-semibold text-primary dark:text-primary-dark"
<ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'> >
<h2 data-cy='blog-post-title' className='text-xl font-semibold'> {post.frontmatter.title}
{post.frontmatter.title} </h2>
</h2> <p data-cy="blog-post-date" className="mt-2">
<p data-cy='blog-post-date' className='mt-2'> {postPublishedOn}
{postPublishedOn} </p>
</p> <p data-cy="blog-post-description" className="mt-3">
<p data-cy='blog-post-description' className='mt-3'> {post.frontmatter.description}
{post.frontmatter.description} </p>
</p> </ShadowContainer>
</ShadowContainer> </Link>
</Link> </li>
) )
})} })}
</div> </ul>
</div> </div>
) )
} }

View File

@ -1,10 +1,10 @@
import fs from 'node:fs' import fs from "node:fs"
import path from 'node:path' import path from "node:path"
import { cache } from 'react' import { cache } from "react"
import matter from 'gray-matter' import matter from "gray-matter"
export const BLOG_POSTS_PATH = path.join(process.cwd(), 'blog', 'posts') export const BLOG_POSTS_PATH = path.join(process.cwd(), "blog", "posts")
export interface FrontMatter { export interface FrontMatter {
title: string title: string
@ -23,13 +23,13 @@ export const getBlogPosts = cache(async (): Promise<BlogPost[]> => {
const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH) const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH)
const blogPostsWithTime = await Promise.all( const blogPostsWithTime = await Promise.all(
blogPosts.map(async (blogPostFilename) => { blogPosts.map(async (blogPostFilename) => {
const [slug, extension] = blogPostFilename.split('.') const [slug, extension] = blogPostFilename.split(".")
if (slug == null || extension == null) { if (slug == null || extension == null) {
throw new Error('Invalid blog post filename.') throw new Error("Invalid blog post filename.")
} }
const blogPostPath = path.join(BLOG_POSTS_PATH, `${slug}.${extension}`) const blogPostPath = path.join(BLOG_POSTS_PATH, `${slug}.${extension}`)
const blogPostContent = await fs.promises.readFile(blogPostPath, { const blogPostContent = await fs.promises.readFile(blogPostPath, {
encoding: 'utf8' encoding: "utf8",
}) })
const { data, content } = matter(blogPostContent) as unknown as { const { data, content } = matter(blogPostContent) as unknown as {
data: FrontMatter data: FrontMatter
@ -40,9 +40,9 @@ export const getBlogPosts = cache(async (): Promise<BlogPost[]> => {
slug, slug,
content, content,
frontmatter: data, frontmatter: data,
time: date.getTime() time: date.getTime(),
} }
}) }),
) )
const blogPostsSortedByPublicationDate = blogPostsWithTime const blogPostsSortedByPublicationDate = blogPostsWithTime
.filter((post) => { .filter((post) => {
@ -61,5 +61,5 @@ export const getBlogPostBySlug = cache(
return blogPost.slug === slug && blogPost.frontmatter.isPublished return blogPost.slug === slug && blogPost.frontmatter.isPublished
}) })
return blogPost return blogPost
} },
) )

View File

@ -1,8 +1,8 @@
--- ---
title: '🧼 Clean Code' title: "🧼 Clean Code"
description: 'What is "Clean Code", what are "Design Patterns", and why is it so important today? Tips and tricks to make your code more readable and maintainable in the long term.' description: 'What is "Clean Code", what are "Design Patterns", and why is it so important today? Tips and tricks to make your code more readable and maintainable in the long term.'
isPublished: true isPublished: true
publishedOn: '2022-02-23T08:00:18.758Z' publishedOn: "2022-02-23T08:00:18.758Z"
--- ---
Hello! 👋 Hello! 👋
@ -19,13 +19,13 @@ A clean code is a code that is **easy** to **read** and easy to **understand**.
But I promise it is not a code that is easy to write, in fact it is really **hard to write Clean Code**. But I promise it is not a code that is easy to write, in fact it is really **hard to write Clean Code**.
We could ask ourselves, what is **easy** to **read** and easy to **understand** ? We could ask ourselves, what is **easy** to **read** and easy to **understand**?
It depends of many factors, and is somewhat relative to each one of us. The **perfect** Clean code **doesn't exist**, but we can try to be **as perfect as possible**. It depends of many factors, and is somewhat relative to each one of us. The **perfect** Clean code **doesn't exist**, but we can try to be **as perfect as possible**.
## Why is it so important? ## Why is it so important?
Code like that works great, but it is not enough, even if the code will be read by the computer and understood by the machine, we should not forget that the code is **written by human** and will be also **read by human** not only a machine. Code that works is great, but not enough, even if the code will be read and understood by the computer, we should not forget that the code is **written by human** and will be also **read by human** not only a machine.
For example the [Linux kernel](https://www.kernel.org/), is one of the biggest open source project with many contributors worldwide. Last data shows that it is about **20 millions** lines of code. For example the [Linux kernel](https://www.kernel.org/), is one of the biggest open source project with many contributors worldwide. Last data shows that it is about **20 millions** lines of code.
@ -110,7 +110,7 @@ const transaction = charge(user, subscription)
```typescript ```typescript
interface Car { interface Car {
carModel: string carModel: string
carColor: 'red' | 'blue' | 'yellow' carColor: "red" | "blue" | "yellow"
} }
const printCar = (car: Car): void => { const printCar = (car: Car): void => {
console.log(`${car.carModel} (${car.carColor})`) console.log(`${car.carModel} (${car.carColor})`)
@ -122,7 +122,7 @@ const printCar = (car: Car): void => {
```typescript ```typescript
interface Car { interface Car {
model: string model: string
color: 'red' | 'blue' | 'yellow' color: "red" | "blue" | "yellow"
} }
const printCar = (car: Car): void => { const printCar = (car: Car): void => {
console.log(`${car.model} (${car.color})`) console.log(`${car.model} (${car.color})`)
@ -170,17 +170,17 @@ We have to keep it as simple as possible, not to implement features that are not
### Example (bad way) ### Example (bad way)
```typescript ```typescript
import fs from 'node:fs' import fs from "node:fs"
import path from 'node:path' import path from "node:path"
const createFile = async ( const createFile = async (
name: string, name: string,
isTemporary: boolean = false isTemporary: boolean = false,
): Promise<void> => { ): Promise<void> => {
if (isTemporary) { if (isTemporary) {
return await fs.promises.writeFile(path.join('temporary', name), '') return await fs.promises.writeFile(path.join("temporary", name), "")
} }
return await fs.promises.writeFile(name, '') return await fs.promises.writeFile(name, "")
} }
``` ```
@ -189,15 +189,15 @@ const createFile = async (
### Example (good way) ### Example (good way)
```typescript ```typescript
import fs from 'node:fs' import fs from "node:fs"
import path from 'node:path' import path from "node:path"
const createFile = async (name: string): Promise<void> => { const createFile = async (name: string): Promise<void> => {
await fs.promises.writeFile(name, '') await fs.promises.writeFile(name, "")
} }
const createTemporaryFile = async (name: string): Promise<void> => { const createTemporaryFile = async (name: string): Promise<void> => {
await createFile(path.join('temporary', name)) await createFile(path.join("temporary", name))
} }
``` ```

View File

@ -1,8 +1,8 @@
--- ---
title: '🗓️ Git version control: Ultimate Guide' title: "🗓️ Git version control: Ultimate Guide"
description: 'What is `git`, what are the most used commands, best practices, and tips and tricks. The Ultimate guide to master `git` in your daily workflow.' description: "What is `git`, what are the most used commands, best practices, and tips and tricks. The Ultimate guide to master `git` in your daily workflow."
isPublished: true isPublished: true
publishedOn: '2022-10-27T14:33:07.465Z' publishedOn: "2022-10-27T14:33:07.465Z"
--- ---
Hello! 👋 Hello! 👋
@ -84,7 +84,10 @@ git add .
git add <file> git add <file>
# Commit changes # Commit changes
git commit -m "chore: initial commit" git commit -m "Commit message"
# Commit changes in the past
git commit --date "10 day ago" -m "Commit message"
# Add remote repository # Add remote repository
git remote add <remote> <url> git remote add <remote> <url>

View File

@ -1,8 +1,8 @@
--- ---
title: '👋 Hello, world!' title: "👋 Hello, world!"
description: 'First post of the blog, introduction and explanation of how this blog is made.' description: "First post of the blog, introduction and explanation of how this blog is made."
isPublished: true isPublished: true
publishedOn: '2022-02-20T08:00:18.758Z' publishedOn: "2022-02-20T08:00:18.758Z"
--- ---
Hello, world! 👋 Hello, world! 👋

View File

@ -1,8 +1,8 @@
--- ---
title: '❌ Mistakes I made as a junior developer' title: "❌ Mistakes I made as a junior developer"
description: 'Here are mistakes I made when I started, to prevent you from making the same mistakes.' description: "Here are mistakes I made when I started, to prevent you from making the same mistakes."
isPublished: true isPublished: true
publishedOn: '2022-03-14T07:42:52.989Z' publishedOn: "2022-03-14T07:42:52.989Z"
--- ---
Hello! 👋 Hello! 👋
@ -41,13 +41,13 @@ Find the right balance, between abstraction and simple implementation, start sim
When you start a new project, you should focus on the core of the project, not on the details, to release as soon as possible, a working usable version of your project also called a [**Minimum Viable Product** (MVP)](https://en.wikipedia.org/wiki/Minimum_viable_product), it is better than a half-functioning, over-engineered project. When you start a new project, you should focus on the core of the project, not on the details, to release as soon as possible, a working usable version of your project also called a [**Minimum Viable Product** (MVP)](https://en.wikipedia.org/wiki/Minimum_viable_product), it is better than a half-functioning, over-engineered project.
I made this mistake while developing [Thream](https://thream.theoludwig.fr), your **open source** platform to stay close with your friends and communities, **talk**, chat, **collaborate**, share and **have fun**. I made this mistake while developing [Thream](../posts/thream-v1-0-0.md), your **open source** platform to stay close with your friends and communities, **talk**, chat, **collaborate**, share and **have fun**.
Basically, I thought it was cool, to do a "big" v1.0.0 release with a lot of features, but in fact, it was not, because I could not even show what I was developing (to the end-users, not technical people) as I was making multiple features at the same time and also mainly focused on the **REST API** side and not at all the **website (frontend)**. Basically, I thought it was cool, to do a "big" v1.0.0 release with a lot of features, but in fact, it was not, because I could not even show what I was developing (to the end-users, not technical people) as I was making multiple features at the same time and also mainly focused on the **REST API** side and not at all the **website (frontend)**.
What I recommend you to do is to start with a **v1.0.0** release as soon as possible with the minimum required features needed for your project idea, and then gradually add new features and release new versions. What I recommend you to do is to start with a **v1.0.0** release as soon as possible with the minimum required features needed for your project idea, and then gradually add new features and release new versions.
In my example for [Thream](https://thream.theoludwig.fr), I could release a v1.0.0 without these features: In my example for [Thream](../posts/thream-v1-0-0.md), I could release a v1.0.0 without these features:
- English/French translation (could be only English) - English/French translation (could be only English)
- Light/Dark theme (could be only Dark) - Light/Dark theme (could be only Dark)
@ -55,7 +55,7 @@ In my example for [Thream](https://thream.theoludwig.fr), I could release a v1.0
- User public profile - User public profile
- Channels (maybe could be only one channel per guild to start with) - Channels (maybe could be only one channel per guild to start with)
And probably more, what was really required with [Thream](https://thream.theoludwig.fr), is that users could authenticate, create a community of friends, and then they could communicate with each other with messages in real-time, really that was enough. And probably more, what was really required with [Thream](../posts/thream-v1-0-0.md), is that users could authenticate, create a community of friends, and then they could communicate with each other with messages in real-time, really that was enough.
And then with this basis, I could release, v1.1.0, v1.2.0 etc. with more features, and release new versions more often to show the progress of the project, it is also more motivating to have users testing our project and to **get feedback sooner**. And then with this basis, I could release, v1.1.0, v1.2.0 etc. with more features, and release new versions more often to show the progress of the project, it is also more motivating to have users testing our project and to **get feedback sooner**.

View File

@ -1,8 +1,8 @@
--- ---
title: '🧠 Programming Challenges' title: "🧠 Programming Challenges"
description: 'What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation.' description: "What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation."
isPublished: true isPublished: true
publishedOn: '2023-05-21T10:20:18.837Z' publishedOn: "2023-05-21T10:20:18.837Z"
--- ---
Hello! 👋 Hello! 👋
@ -15,7 +15,7 @@ We don't want to "reinvent the wheel" and rewrite everything from scratch for ea
However, it is important to draw a line between what dependencies are worth the cost and which are not. However, it is important to draw a line between what dependencies are worth the cost and which are not.
Most likely adding a [JavaScript npm package `is-odd`](https://www.npmjs.com/package/is-odd) to check if a number is odd or even for example, is not worth it. Writing it ourselves is easier and allows a better maintenance in the long term. Most likely adding a [JavaScript npm package `is-odd`](https://www.npmjs.com/package/is-odd) to check if a number is odd or even for example, is not worth it. Writing it ourselves allows a better maintenance in the long term.
Learning **how to solve problems** and how to write efficient code is very important and also a very broad and complicated topic, so this blog post will only be an **introduction to the subject**, and will not go in depth. Learning **how to solve problems** and how to write efficient code is very important and also a very broad and complicated topic, so this blog post will only be an **introduction to the subject**, and will not go in depth.
@ -240,7 +240,7 @@ Here is a list of classes of functions that are commonly encountered when analyz
### Estimating efficiency ### Estimating efficiency
By checking the time complexity of an algorithm, it is possible to check before implementing the algorithm,that it is efficient enough for the problem. By checking the time complexity of an algorithm, it is possible to check before implementing the algorithm, that it is efficient enough for the problem.
Example: assume that the time limit for a problem is 1 second and the input size is $n = 10^5$. If the time complexity is $O(n^2)$, the algorithm will perform about $(10^5)^2 = 10^{10}$ operations. Example: assume that the time limit for a problem is 1 second and the input size is $n = 10^5$. If the time complexity is $O(n^2)$, the algorithm will perform about $(10^5)^2 = 10^{10}$ operations.
@ -286,7 +286,7 @@ Contiguous subarray is any sub series of elements in a given array that are cont
**Explanation:** The subarray with the largest sum is `[2, 4, -3, 5, 2]` which has a sum of `10`. **Explanation:** The subarray with the largest sum is `[2, 4, -3, 5, 2]` which has a sum of `10`.
### Worst solution: Brute force ### Worst solution: Brute force ($O(n^3)$)
```python ```python
def maximum_subarray_sum_cubic(array: list[int]) -> int: def maximum_subarray_sum_cubic(array: list[int]) -> int:
@ -309,7 +309,7 @@ def maximum_subarray_sum_cubic(array: list[int]) -> int:
return best_sum return best_sum
``` ```
### Better solution: Linear time ### Better solution: Linear time ($O(n)$)
```python ```python
def maximum_subarray_sum_linear(array: list[int]) -> int: def maximum_subarray_sum_linear(array: list[int]) -> int:

View File

@ -1,19 +1,29 @@
--- ---
title: '🟢 Thream v1.0.0' title: "🟢 Thream v1.0.0"
description: 'Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.' description: "Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun."
isPublished: true isPublished: true
publishedOn: '2022-04-11T10:24:55.206Z' publishedOn: "2022-04-11T10:24:55.206Z"
---
⚠️ **Thream** is **not maintained anymore**, and is no longer accessible on ~~[thream.theoludwig.fr](https://thream.theoludwig.fr)`~~.
While the project taught me a lot, it had too much ambitions for new features, with nearly no users, and no contributors.
You can still use the code as you wish and fork it to maintain it yourself, as the code is completely open source on [GitHub](https://github.com/Thream).
This blog post is still available to explain the project, and how it was implemented.
--- ---
Hello! 👋 Hello! 👋
After months of hard work, [Thream v1.0.0](https://thream.theoludwig.fr/) has been released! 🎉 After months of hard work, [Thream v1.0.0](https://github.com/Thream) has been released! 🎉
[**Thream**](https://thream.theoludwig.fr/) is your open-source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun. [**Thream**](https://github.com/Thream) is your open-source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.
## Presentation ## Presentation
[**Thream**](https://thream.theoludwig.fr/) is a social network to stay close with your friends and communities to talk, chat, collaborate and share. [**Thream**](https://github.com/Thream) is a social network to stay close with your friends and communities to talk, chat, collaborate and share.
The project is largely inspired by [Discord](https://discord.com), a proprietary instant messaging service, but differentiates itself by its **non-profit open source philosophy** and will integrate special features. The project is largely inspired by [Discord](https://discord.com), a proprietary instant messaging service, but differentiates itself by its **non-profit open source philosophy** and will integrate special features.
@ -23,7 +33,7 @@ The idea is that a user can create an account to authenticate with an email addr
![The Thream app on a community page](../../public/images/posts/thream-v1-0-0/thream-ui.png) ![The Thream app on a community page](../../public/images/posts/thream-v1-0-0/thream-ui.png)
[**Thream**](https://thream.theoludwig.fr/) is a website that works on any recent browser, accessible on [thream.theoludwig.fr](https://thream.theoludwig.fr/). [**Thream**](https://github.com/Thream) is a website that works on any recent browser.
## History ## History
@ -115,5 +125,3 @@ The other interest of the project is that it is completely **open-source**, and
**Thream** is **non-profit** and therefore has no financial goal, deadline or specific feature target, which makes the design of the project a hobby and a way to learn new concepts. **Thream** is **non-profit** and therefore has no financial goal, deadline or specific feature target, which makes the design of the project a hobby and a way to learn new concepts.
Feel free to give feebacks and suggestions to improve the project, and to report any bug you find. Feel free to give feebacks and suggestions to improve the project, and to report any bug you find.
**Thream** is available: [**thream.theoludwig.fr**](https://thream.theoludwig.fr/).

View File

@ -1,7 +1,7 @@
import type { Plugin, Transformer } from 'unified' import type { Plugin, Transformer } from "unified"
import type { Literal, Node } from 'unist' import type { Literal, Node } from "unist"
import { visit } from 'unist-util-visit' import { visit } from "unist-util-visit"
import type { Highlighter } from 'shiki' import type { Highlighter } from "shiki"
export interface RemarkSyntaxHighlightingPluginOptions { export interface RemarkSyntaxHighlightingPluginOptions {
highlighter: Highlighter highlighter: Highlighter
@ -20,11 +20,11 @@ export const remarkSyntaxHighlightingPlugin: Plugin<
Literal Literal
> = (options) => { > = (options) => {
const transformer: Transformer<RemarkSyntaxHighlightingNode> = (tree) => { const transformer: Transformer<RemarkSyntaxHighlightingNode> = (tree) => {
visit<RemarkSyntaxHighlightingNode, string>(tree, 'code', (node) => { visit<RemarkSyntaxHighlightingNode, string>(tree, "code", (node) => {
node.type = 'html' node.type = "html"
node.children = undefined node.children = undefined
node.value = options.highlighter.codeToHtml(node.value, { node.value = options.highlighter.codeToHtml(node.value, {
lang: node.lang lang: node.lang,
}) })
}) })
} }

View File

@ -1,6 +1,6 @@
import Link from 'next/link' import Link from "next/link"
import { getI18n } from '@/i18n/i18n.server' import { getI18n } from "@/i18n/i18n.server"
export const FooterText = (): JSX.Element => { export const FooterText = (): JSX.Element => {
const i18n = getI18n() const i18n = getI18n()
@ -8,12 +8,12 @@ export const FooterText = (): JSX.Element => {
return ( return (
<p> <p>
<Link <Link
href='/' href="/"
className='text-yellow hover:underline dark:text-yellow-dark' className="font-semibold text-primary hover:underline dark:text-primary-dark"
> >
Théo LUDWIG Théo LUDWIG
</Link>{' '} </Link>{" "}
| {i18n.translate('common.all-rights-reserved')} | {i18n.translate("common.all-rights-reserved")}
</p> </p>
) )
} }

View File

@ -1,4 +1,4 @@
import { useMemo } from 'react' import { useMemo } from "react"
interface FooterVersionProps { interface FooterVersionProps {
version: string version: string
@ -12,14 +12,14 @@ export const FooterVersion = (props: FooterVersionProps): JSX.Element => {
}, [version]) }, [version])
return ( return (
<p className='mt-1'> <p className="mt-1">
Version{' '} Version{" "}
<a <a
data-cy='version-link' data-cy="version-link"
className='text-yellow hover:underline dark:text-yellow-dark' className="font-semibold text-primary hover:underline dark:text-primary-dark"
href={versionLink} href={versionLink}
target='_blank' target="_blank"
rel='noopener noreferrer' rel="noopener noreferrer"
> >
{version} {version}
</a> </a>

View File

@ -1,12 +1,13 @@
import { FooterText } from './FooterText' import { getVersion } from "@/utils/getVersion"
import { FooterVersion } from './FooterVersion'
import { FooterText } from "./FooterText"
import { FooterVersion } from "./FooterVersion"
export const Footer = async (): Promise<JSX.Element> => { export const Footer = async (): Promise<JSX.Element> => {
const { readPackage } = await import('read-pkg') const version = await getVersion()
const { version } = await readPackage()
return ( return (
<footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'> <footer className="flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black">
<FooterText /> <FooterText />
<FooterVersion version={version} /> <FooterVersion version={version} />
</footer> </footer>

View File

@ -1,15 +1,15 @@
export const Arrow = (): JSX.Element => { export const Arrow = (): JSX.Element => {
return ( return (
<svg <svg
width='12' width="12"
height='8' height="8"
viewBox='0 0 12 8' viewBox="0 0 12 8"
fill='none' fill="none"
xmlns='http://www.w3.org/2000/svg' xmlns="http://www.w3.org/2000/svg"
> >
<path <path
className='fill-current text-black dark:text-white' className="fill-current text-black dark:text-white"
d='M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z' d="M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z"
/> />
</svg> </svg>
) )

View File

@ -1,7 +1,7 @@
import Image from 'next/image' import Image from "next/image"
import type { CookiesStore } from '@/utils/constants' import type { CookiesStore } from "@/utils/constants"
import { useI18n } from '@/i18n/i18n.client' import { useI18n } from "@/i18n/i18n.client"
export interface LocaleFlagProps { export interface LocaleFlagProps {
locale: string locale: string
@ -22,7 +22,7 @@ export const LocaleFlag = (props: LocaleFlagProps): JSX.Element => {
src={`/images/locales/${locale}.svg`} src={`/images/locales/${locale}.svg`}
alt={locale} alt={locale}
/> />
<p data-cy='locale-flag-text' className='mx-2 text-base'> <p data-cy="locale-flag-text" className="mx-2 text-base">
{i18n.translate(`common.${locale}`)} {i18n.translate(`common.${locale}`)}
</p> </p>
</> </>

View File

@ -1,14 +1,14 @@
'use client' "use client"
import { usePathname } from 'next/navigation' import { usePathname } from "next/navigation"
import { useCallback, useEffect, useState, useRef } from 'react' import { useCallback, useEffect, useState, useRef } from "react"
import classNames from 'clsx' import classNames from "clsx"
import type { Locale as LocaleType, CookiesStore } from '@/utils/constants' import type { Locale as LocaleType, CookiesStore } from "@/utils/constants"
import { LOCALES } from '@/utils/constants' import { LOCALES } from "@/utils/constants"
import { Arrow } from './Arrow' import { Arrow } from "./Arrow"
import { LocaleFlag } from './LocaleFlag' import { LocaleFlag } from "./LocaleFlag"
export interface LocalesProps { export interface LocalesProps {
currentLocale: string currentLocale: string
@ -38,28 +38,28 @@ export const Locales = (props: LocalesProps): JSX.Element => {
} }
} }
window.document.addEventListener('click', handleClickEvent) window.document.addEventListener("click", handleClickEvent)
return () => { return () => {
return window.removeEventListener('click', handleClickEvent) return window.removeEventListener("click", handleClickEvent)
} }
}, []) }, [])
const handleLocale = async (locale: LocaleType): Promise<void> => { const handleLocale = async (locale: LocaleType): Promise<void> => {
const { setLocale } = await import('@/i18n/i18n.server') const { setLocale } = await import("@/i18n/i18n.server")
setLocale(locale) setLocale(locale)
} }
if (pathname.startsWith('/blog')) { if (pathname.startsWith("/blog")) {
return <></> return <></>
} }
return ( return (
<div className='flex cursor-pointer flex-col items-center justify-center'> <div className="flex cursor-pointer flex-col items-center justify-center">
<div <div
ref={languageClickRef} ref={languageClickRef}
data-cy='locale-click' data-cy="locale-click"
className='mr-5 flex items-center' className="mr-5 flex items-center"
onClick={handleHiddenMenu} onClick={handleHiddenMenu}
> >
<LocaleFlag <LocaleFlag
@ -70,10 +70,10 @@ export const Locales = (props: LocalesProps): JSX.Element => {
</div> </div>
<ul <ul
data-cy='locales-list' data-cy="locales-list"
className={classNames( className={classNames(
'absolute top-14 z-10 mr-4 mt-3 flex w-32 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag', "absolute top-14 z-10 mr-4 mt-3 flex w-32 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag",
{ hidden: hiddenMenu } { hidden: hiddenMenu },
)} )}
> >
{LOCALES.filter((locale) => { {LOCALES.filter((locale) => {
@ -82,7 +82,7 @@ export const Locales = (props: LocalesProps): JSX.Element => {
return ( return (
<li <li
key={locale} key={locale}
className='flex h-12 w-full items-center justify-center hover:bg-[#4f545c] hover:bg-opacity-20' className="flex h-12 w-full items-center justify-center hover:bg-[#4f545c]/20"
onClick={async () => { onClick={async () => {
return await handleLocale(locale) return await handleLocale(locale)
}} }}

View File

@ -1,9 +1,9 @@
'use client' "use client"
import classNames from 'clsx' import classNames from "clsx"
import { useTheme } from '@/theme/theme.client' import { useTheme } from "@/theme/theme.client"
import type { CookiesStore } from '@/utils/constants' import type { CookiesStore } from "@/utils/constants"
export interface SwitchThemeProps { export interface SwitchThemeProps {
cookiesStore: CookiesStore cookiesStore: CookiesStore
@ -14,63 +14,63 @@ export const SwitchTheme = (props: SwitchThemeProps): JSX.Element => {
const theme = useTheme(cookiesStore) const theme = useTheme(cookiesStore)
const handleClick = async (): Promise<void> => { const handleClick = async (): Promise<void> => {
const { setTheme } = await import('@/theme/theme.server') const { setTheme } = await import("@/theme/theme.server")
const newTheme = theme === 'dark' ? 'light' : 'dark' const newTheme = theme === "dark" ? "light" : "dark"
setTheme(newTheme) setTheme(newTheme)
} }
return ( return (
<div <div
className='flex items-center' className="flex items-center"
data-cy='switch-theme-click' data-cy="switch-theme-click"
onClick={handleClick} onClick={handleClick}
> >
<div className='relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0'> <div className="relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0">
<div className='h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out'> <div className="h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out">
<div <div
data-cy='switch-theme-dark' data-cy="switch-theme-dark"
className={classNames( className={classNames(
'absolute bottom-0 left-[8px] top-0 mb-auto mt-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out', "absolute inset-y-0 left-[8px] my-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out",
{ {
'opacity-100': theme === 'dark', "opacity-100": theme === "dark",
'opacity-0': theme === 'light' "opacity-0": theme === "light",
} },
)} )}
> >
<span className='relative flex h-[10px] w-[10px] items-center justify-center'> <span className="relative flex size-[10px] items-center justify-center">
🌜 🌜
</span> </span>
</div> </div>
<div <div
data-cy='switch-theme-light' data-cy="switch-theme-light"
className={classNames( className={classNames(
'absolute bottom-0 right-[10px] top-0 mb-auto mt-auto h-[10px] w-[10px] leading-[0]', "absolute inset-y-0 right-[10px] my-auto size-[10px] leading-[0]",
{ {
'opacity-100': theme === 'light', "opacity-100": theme === "light",
'opacity-0': theme === 'dark' "opacity-0": theme === "dark",
} },
)} )}
> >
<span className='relative flex h-[10px] w-[10px] items-center justify-center'> <span className="relative flex size-[10px] items-center justify-center">
🌞 🌞
</span> </span>
</div> </div>
</div> </div>
<div <div
className={classNames( className={classNames(
'absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out', "absolute top-px box-border size-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out",
{ {
'left-[27px]': theme === 'dark', "left-[27px]": theme === "dark",
'left-0': theme === 'light' "left-0": theme === "light",
} },
)} )}
style={{ border: '1px solid #4d4d4d' }} style={{ border: "1px solid #4d4d4d" }}
/> />
<input <input
data-cy='switch-theme-input' data-cy="switch-theme-input"
type='checkbox' type="checkbox"
aria-label='Dark mode toggle' aria-label="Dark mode toggle"
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0 hidden' className="absolute -m-px hidden size-px overflow-hidden border-0 p-0"
defaultChecked defaultChecked
/> />
</div> </div>

View File

@ -1,39 +1,42 @@
import { cookies } from 'next/headers' import { cookies } from "next/headers"
import Link from 'next/link' import Link from "next/link"
import Image from 'next/image' import Image from "next/image"
import { getI18n } from '@/i18n/i18n.server' import Logo from "@/public/images/logo.png"
import { getI18n } from "@/i18n/i18n.server"
import { Locales } from './Locales' import { Locales } from "./Locales"
import { SwitchTheme } from './SwitchTheme' import { SwitchTheme } from "./SwitchTheme"
export const Header = (): JSX.Element => { export const Header = (): JSX.Element => {
const cookiesStore = cookies() const cookiesStore = cookies()
const i18n = getI18n() const i18n = getI18n()
return ( return (
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'> <header className="sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black">
<Link href='/'> <h1>
<div className='flex items-center justify-center'> <Link
href="/"
className="flex items-center justify-center transition-all duration-300 ease-in-out hover:scale-[1.03]"
>
<Image <Image
quality={100} quality={100}
width={60} className="size-16"
height={60} src={Logo}
src='/images/icon_small.png' alt="Théo LUDWIG"
alt='Théo LUDWIG'
priority priority
/> />
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'> <strong className="ml-1 hidden font-headline font-semibold text-primary dark:text-primary-dark sm:block sm:text-xl">
Théo LUDWIG Théo LUDWIG
</strong> </strong>
</div> </Link>
</Link> </h1>
<div className='flex justify-between'> <div className="flex justify-between">
<div className='flex flex-col items-center justify-center px-6'> <div className="flex flex-col items-center justify-center px-6">
<Link <Link
href='/blog' href="/blog"
data-cy='header-blog-link' data-cy="header-blog-link"
className='text-yellow hover:underline dark:text-yellow-dark' className="font-semibold text-primary hover:underline dark:text-primary-dark"
> >
Blog Blog
</Link> </Link>

View File

@ -1,24 +1,30 @@
import htmlParser from 'html-react-parser' import htmlParser from "html-react-parser"
import { faCode, faMicrochip } from "@fortawesome/free-solid-svg-icons"
import { faGit } from "@fortawesome/free-brands-svg-icons"
export const InterestsIcons = {
code: faCode,
"open-source": faGit,
"high-tech": faMicrochip,
} as const
export interface InterestParagraphProps { export interface InterestParagraphProps {
title: string title: string
description: string description: string
id: keyof typeof InterestsIcons
} }
export const InterestParagraph = ( export const InterestParagraph = (
props: InterestParagraphProps props: InterestParagraphProps,
): JSX.Element => { ): JSX.Element => {
const { title, description } = props const { title, description } = props
return ( return (
<> <div className="my-6 text-center text-gray dark:text-gray-dark">
<p className='my-6 text-center text-gray dark:text-gray-dark'> <h3 className="text-lg font-semibold text-primary dark:text-primary-dark">
<strong className='text-lg font-semibold text-yellow dark:text-yellow-dark'> {title}
{title} </h3>
</strong> <p className="my-2">{htmlParser(description)}</p>
<br /> </div>
<span>{htmlParser(description)}</span>
</p>
</>
) )
} }

View File

@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons' import type { IconDefinition } from "@fortawesome/free-solid-svg-icons"
interface InterestItemProps { interface InterestItemProps {
title: string title: string
@ -10,9 +10,9 @@ export const InterestItem = (props: InterestItemProps): JSX.Element => {
const { fontAwesomeIcon, title } = props const { fontAwesomeIcon, title } = props
return ( return (
<li className='interest-item mx-2 my-2 h-8 w-8' title={title}> <li className="m-2 size-8" title={title}>
<FontAwesomeIcon <FontAwesomeIcon
className='block h-full w-full text-yellow dark:text-yellow-dark' className="block size-full text-primary dark:text-primary-dark"
icon={fontAwesomeIcon} icon={fontAwesomeIcon}
/> />
</li> </li>

View File

@ -1,18 +1,28 @@
import { faCode, faMicrochip } from '@fortawesome/free-solid-svg-icons' import { getI18n } from "@/i18n/i18n.server"
import { faGit } from '@fortawesome/free-brands-svg-icons'
import { InterestItem } from './InterestItem' import {
InterestsIcons,
type InterestParagraphProps,
} from "../InterestParagraph"
import { InterestItem } from "./InterestItem"
export const InterestsList = (): JSX.Element => { export const InterestsList = (): JSX.Element => {
const i18n = getI18n()
let paragraphs = i18n.translate<InterestParagraphProps[]>(
"home.interests.paragraphs",
)
if (!Array.isArray(paragraphs)) {
paragraphs = []
}
return ( return (
<div className='my-4 flex justify-center'> <div className="my-4 flex justify-center">
<ul className='m-0 flex w-96 list-none justify-around p-0'> <ul className="m-0 flex w-96 list-none justify-around p-0">
<InterestItem title='Developer Full Stack' fontAwesomeIcon={faCode} /> {paragraphs.map(({ title, id }) => {
<InterestItem const icon = InterestsIcons[id]
title='Passionate about High-Tech' return <InterestItem key={id} title={title} fontAwesomeIcon={icon} />
fontAwesomeIcon={faMicrochip} })}
/>
<InterestItem title='Open-Source enthusiast' fontAwesomeIcon={faGit} />
</ul> </ul>
</div> </div>
) )

View File

@ -1,23 +1,23 @@
import { getI18n } from '@/i18n/i18n.server' import { getI18n } from "@/i18n/i18n.server"
import type { InterestParagraphProps } from './InterestParagraph' import type { InterestParagraphProps } from "./InterestParagraph"
import { InterestParagraph } from './InterestParagraph' import { InterestParagraph } from "./InterestParagraph"
import { InterestsList } from './InterestsList' import { InterestsList } from "./InterestsList"
export const Interests = (): JSX.Element => { export const Interests = (): JSX.Element => {
const i18n = getI18n() const i18n = getI18n()
let paragraphs = i18n.translate<InterestParagraphProps[]>( let paragraphs = i18n.translate<InterestParagraphProps[]>(
'home.interests.paragraphs' "home.interests.paragraphs",
) )
if (!Array.isArray(paragraphs)) { if (!Array.isArray(paragraphs)) {
paragraphs = [] paragraphs = []
} }
return ( return (
<div className='max-w-full'> <div className="max-w-full">
{paragraphs.map((paragraph, index) => { {paragraphs.map((paragraph) => {
return <InterestParagraph key={index} {...paragraph} /> return <InterestParagraph key={paragraph.id} {...paragraph} />
})} })}
<InterestsList /> <InterestsList />
</div> </div>

View File

@ -1,5 +1,5 @@
import { ShadowContainer } from '@/components/design/ShadowContainer' import { ShadowContainer } from "@/components/design/ShadowContainer"
import { GitHubIcon } from '@/components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon' import { GitHubIcon } from "@/components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon"
export interface RepositoryProps { export interface RepositoryProps {
name: string name: string
@ -11,14 +11,18 @@ export const Repository = (props: RepositoryProps): JSX.Element => {
const { name, description, href } = props const { name, description, href } = props
return ( return (
<ShadowContainer className='relative !mb-4 max-h-32 cursor-pointer p-6 transition-transform duration-200 ease-in-out hover:-translate-y-2'> <li>
<a href={href} target='_blank' rel='noopener noreferrer'> <a href={href} target="_blank" rel="noopener noreferrer">
<div className='flex'> <ShadowContainer className="relative !mb-4 max-h-32 cursor-pointer p-6 transition-all duration-300 ease-in-out hover:scale-[1.03]">
<GitHubIcon className='mr-2 h-6' /> <h3 className="flex">
<span className='text-yellow dark:text-yellow-dark'>{name}</span> <GitHubIcon className="mr-2 h-6" />
</div> <span className="font-semibold text-primary dark:text-primary-dark">
<p className='my-4'>{description}</p> {name}
</span>
</h3>
<p className="my-4">{description}</p>
</ShadowContainer>
</a> </a>
</ShadowContainer> </li>
) )
} }

View File

@ -1,37 +1,37 @@
import { getI18n } from '@/i18n/i18n.server' import { getI18n } from "@/i18n/i18n.server"
import { Repository } from './Repository' import { Repository } from "./Repository"
export const OpenSource = (): JSX.Element => { export const OpenSource = (): JSX.Element => {
const i18n = getI18n() const i18n = getI18n()
return ( return (
<div className='mt-0 flex max-w-full flex-col items-center'> <div className="mt-0 flex max-w-full flex-col items-center">
<p className='text-center'> <p className="text-center">
{i18n.translate('home.open-source.description')} {i18n.translate("home.open-source.description")}
</p> </p>
<div className='my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2'> <ul className="my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2">
<Repository <Repository
name='nodejs/node' name="nodejs/node"
description='Node.js JavaScript runtime ✨🐢🚀✨' description="Node.js JavaScript runtime ✨🐢🚀✨"
href='https://github.com/nodejs/node/commits?author=theoludwig' href="https://github.com/nodejs/node/commits?author=theoludwig"
/> />
<Repository <Repository
name='standard/standard' name="standard/standard"
description='🌟 JavaScript Style Guide, with linter & automatic code fixer' description="🌟 JavaScript Style Guide, with linter & automatic code fixer"
href='https://github.com/standard/standard/commits?author=theoludwig' href="https://github.com/standard/standard/commits?author=theoludwig"
/> />
<Repository <Repository
name='nrwl/nx' name="DefinitelyTyped/DefinitelyTyped"
description='Smart, Fast and Extensible Build System' description="High quality TypeScript type definitions."
href='https://github.com/nrwl/nx/commits?author=theoludwig' href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits?author=theoludwig"
/> />
<Repository <Repository
name='vercel/next.js' name="vercel/next.js"
description='The React Framework' description="The React Framework"
href='https://github.com/vercel/next.js/commits?author=theoludwig' href="https://github.com/vercel/next.js/commits?author=theoludwig"
/> />
</div> </ul>
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
import Image from 'next/image' import Image from "next/image"
import { ShadowContainer } from '@/components/design/ShadowContainer' import { ShadowContainer } from "@/components/design/ShadowContainer"
export interface PortfolioItemProps { export interface PortfolioItemProps {
title: string title: string
@ -13,31 +13,33 @@ export const PortfolioItem = (props: PortfolioItemProps): JSX.Element => {
const { title, description, link, image } = props const { title, description, link, image } = props
return ( return (
<ShadowContainer className='relative cursor-pointer items-center sm:ml-10'> <li>
<a <a
className='group inline-flex justify-center' className="group inline-flex justify-center"
target='_blank' target="_blank"
rel='noopener noreferrer' rel="noopener noreferrer"
href={link} href={link}
aria-label={title} aria-label={title}
> >
<div className='flex justify-center'> <ShadowContainer className="relative cursor-pointer items-center sm:ml-10">
<Image <div className="flex justify-center">
quality={100} <Image
className='h-auto w-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5' quality={100}
width={300} className="size-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5"
height={300} width={300}
src={image} height={300}
alt={title} src={image}
/> alt={title}
</div> />
<div className='absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100'> </div>
<h3 className='my-6 text-xl font-semibold text-yellow dark:text-yellow-dark'> <div className="absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100">
{title} <h3 className="my-6 text-2xl font-semibold text-primary dark:text-primary-dark">
</h3> {title}
<p className='my-6'>{description}</p> </h3>
</div> <p className="mx-4 my-6 font-semibold">{description}</p>
</div>
</ShadowContainer>
</a> </a>
</ShadowContainer> </li>
) )
} }

View File

@ -1,21 +1,21 @@
import { getI18n } from '@/i18n/i18n.server' import { getI18n } from "@/i18n/i18n.server"
import type { PortfolioItemProps } from './PortfolioItem' import type { PortfolioItemProps } from "./PortfolioItem"
import { PortfolioItem } from './PortfolioItem' import { PortfolioItem } from "./PortfolioItem"
export const Portfolio = (): JSX.Element => { export const Portfolio = (): JSX.Element => {
const i18n = getI18n() const i18n = getI18n()
let items = i18n.translate<PortfolioItemProps[]>('home.portfolio.items') let items = i18n.translate<PortfolioItemProps[]>("home.portfolio.items")
if (!Array.isArray(items)) { if (!Array.isArray(items)) {
items = [] items = []
} }
return ( return (
<div className='flex w-full flex-wrap justify-center px-3'> <ul className="flex w-full flex-wrap justify-center px-3">
{items.map((item, index) => { {items.map((item) => {
return <PortfolioItem key={index} {...item} /> return <PortfolioItem key={item.title} {...item} />
})} })}
</div> </ul>
) )
} }

View File

@ -1,23 +1,21 @@
import { getI18n } from '@/i18n/i18n.server' import htmlParser from "html-react-parser"
import { getI18n } from "@/i18n/i18n.server"
export const ProfileDescriptionBottom = (): JSX.Element => { export const ProfileDescriptionBottom = (): JSX.Element => {
const i18n = getI18n() const i18n = getI18n()
return ( return (
<p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'> <div className="my-6 max-w-md text-center text-base text-gray dark:text-gray-dark">
{i18n.translate('home.about.description-bottom')} <p>{htmlParser(i18n.translate("home.about.description-bottom"))}</p>
{i18n.locale === 'fr-FR' ? (
<> <br />
<br /> <a
<br /> href="/curriculum-vitae/index.html"
<a className="font-semibold text-primary hover:underline dark:text-primary-dark"
href='/curriculum-vitae/index.html' >
className='text-yellow hover:underline dark:text-yellow-dark' Curriculum vitæ ({i18n.translate("common.fr-FR")})
> </a>
Curriculum vitæ </div>
</a>
</>
) : null}
</p>
) )
} }

View File

@ -1,15 +1,15 @@
import { getI18n } from '@/i18n/i18n.server' import { getI18n } from "@/i18n/i18n.server"
export const ProfileInformation = (): JSX.Element => { export const ProfileInformation = (): JSX.Element => {
const i18n = getI18n() const i18n = getI18n()
return ( return (
<div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'> <div className="mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400">
<h1 className='mb-2 text-4xl font-semibold text-yellow dark:text-yellow-dark'> <h1 className="mb-2 text-4xl font-semibold text-primary dark:text-primary-dark">
Théo LUDWIG Théo LUDWIG
</h1> </h1>
<h2 className='mb-3 text-base'> <h2 className="mb-3 text-base">
{i18n.translate('home.about.description')} {i18n.translate("home.about.description")}
</h2> </h2>
</div> </div>
) )

View File

@ -8,14 +8,14 @@ export const ProfileItem = (props: ProfileItemProps): JSX.Element => {
const { title, value, link } = props const { title, value, link } = props
return ( return (
<li className='mb-3 before:table after:clear-both after:table'> <li className="mb-3 before:table after:clear-both after:table">
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'> <strong className="float-left block w-28 text-sm font-bold text-black dark:text-white">
{title} {title}
</strong> </strong>
<span className='mb-4 ml-0 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'> <span className="mb-4 ml-0 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32">
{link != null ? ( {link != null ? (
<a <a
className='text-gray hover:underline dark:text-gray-dark' className="text-gray hover:underline dark:text-gray-dark"
href={link} href={link}
> >
{value} {value}

View File

@ -1,12 +1,13 @@
'use client' "use client"
import { useMemo } from 'react' import { useMemo } from "react"
import { useI18n } from '@/i18n/i18n.client' import { useI18n } from "@/i18n/i18n.client"
import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from '@/utils/getAge' import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from "@/utils/getAge"
import type { CookiesStore } from '@/utils/constants' import type { CookiesStore } from "@/utils/constants"
import { useIsMounted } from "@/hooks/useIsMounted"
import { ProfileItem } from './ProfileItem' import { ProfileItem } from "./ProfileItem"
export interface ProfileListProps { export interface ProfileListProps {
cookiesStore: CookiesStore cookiesStore: CookiesStore
@ -21,26 +22,33 @@ export const ProfileList = (props: ProfileListProps): JSX.Element => {
return getAge(BIRTH_DATE) return getAge(BIRTH_DATE)
}, []) }, [])
const { isMounted } = useIsMounted()
return ( return (
<ul className='m-0 list-none p-0'> <ul className="m-0 list-none p-0">
<ProfileItem <ProfileItem
title={i18n.translate('home.about.pronouns')} title={i18n.translate("home.about.pronouns")}
value={i18n.translate('home.about.pronouns-value')} value={i18n.translate("home.about.pronouns-value")}
/> />
<ProfileItem <ProfileItem
title={i18n.translate('home.about.birth-date')} title={i18n.translate("home.about.birth-date")}
value={`${BIRTH_DATE_STRING} (${age} ${i18n.translate( value={
'home.about.years-old' isMounted
)})`} ? `${BIRTH_DATE_STRING} (${age} ${i18n.translate(
"home.about.years-old",
)})`
: BIRTH_DATE_STRING
}
/>
<ProfileItem
title={i18n.translate("home.about.nationality")}
value="Alsace, France"
/> />
<ProfileItem <ProfileItem
title={i18n.translate('home.about.nationality')} title="Email"
value='Alsace, France' value="contact@theoludwig.fr"
/> link="mailto:contact@theoludwig.fr"
<ProfileItem
title='Email'
value='contact@theoludwig.fr'
link='mailto:contact@theoludwig.fr'
/> />
</ul> </ul>
) )

View File

@ -1,11 +1,11 @@
import Image from 'next/image' import Image from "next/image"
import Logo from 'public/images/logo.png' import Logo from "@/public/images/logo.png"
export const ProfileLogo = (): JSX.Element => { export const ProfileLogo = (): JSX.Element => {
return ( return (
<div className='max-h-[370px] max-w-[370px] px-2 py-6'> <div className="max-h-[370px] max-w-[370px] px-2 py-6">
<Image quality={100} src={Logo} alt='Théo LUDWIG' priority /> <Image quality={100} src={Logo} alt="Théo LUDWIG" priority />
</div> </div>
) )
} }

View File

@ -1,12 +1,12 @@
import { Icon } from './Icon' import { Icon } from "./Icon"
export const EmailIcon = ( export const EmailIcon = (
props: React.SVGProps<SVGSVGElement> props: React.SVGProps<SVGSVGElement>,
): JSX.Element => { ): JSX.Element => {
return ( return (
<Icon {...props}> <Icon {...props}>
<title>Email</title> <title>Email</title>
<path d='M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12' /> <path d="M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12" />
</Icon> </Icon>
) )
} }

View File

@ -1,12 +1,12 @@
import { Icon } from './Icon' import { Icon } from "./Icon"
export const GitHubIcon = ( export const GitHubIcon = (
props: React.SVGProps<SVGSVGElement> props: React.SVGProps<SVGSVGElement>,
): JSX.Element => { ): JSX.Element => {
return ( return (
<Icon {...props}> <Icon {...props}>
<title>GitHub</title> <title>GitHub</title>
<path d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12' /> <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</Icon> </Icon>
) )
} }

View File

@ -1,12 +1,12 @@
import { Icon } from './Icon' import { Icon } from "./Icon"
export const GitLabIcon = ( export const GitLabIcon = (
props: React.SVGProps<SVGSVGElement> props: React.SVGProps<SVGSVGElement>,
): JSX.Element => { ): JSX.Element => {
return ( return (
<Icon {...props}> <Icon {...props}>
<title>GitLab</title> <title>GitLab</title>
<path d='M4.845.904c-.435 0-.82.28-.955.692C2.639 5.449 1.246 9.728.07 13.335a1.437 1.437 0 00.522 1.607l11.071 8.045c.2.145.472.144.67-.004l11.073-8.04a1.436 1.436 0 00.522-1.61c-1.285-3.942-2.683-8.256-3.817-11.746a1.004 1.004 0 00-.957-.684.987.987 0 00-.949.69l-2.405 7.408H8.203l-2.41-7.408a.987.987 0 00-.942-.69h-.006zm-.006 1.42l2.173 6.678H2.675zm14.326 0l2.168 6.678h-4.341zm-10.593 7.81h6.862c-1.142 3.52-2.288 7.04-3.434 10.559L8.572 10.135zm-5.514.005h4.321l3.086 9.5zm13.567 0h4.325c-2.467 3.17-4.95 6.328-7.411 9.502 1.028-3.167 2.059-6.334 3.086-9.502zM2.1 10.762l6.977 8.947-7.817-5.682a.305.305 0 01-.112-.341zm19.798 0l.952 2.922a.305.305 0 01-.11.341v.002l-7.82 5.68.026-.035z' /> <path d="M4.845.904c-.435 0-.82.28-.955.692C2.639 5.449 1.246 9.728.07 13.335a1.437 1.437 0 00.522 1.607l11.071 8.045c.2.145.472.144.67-.004l11.073-8.04a1.436 1.436 0 00.522-1.61c-1.285-3.942-2.683-8.256-3.817-11.746a1.004 1.004 0 00-.957-.684.987.987 0 00-.949.69l-2.405 7.408H8.203l-2.41-7.408a.987.987 0 00-.942-.69h-.006zm-.006 1.42l2.173 6.678H2.675zm14.326 0l2.168 6.678h-4.341zm-10.593 7.81h6.862c-1.142 3.52-2.288 7.04-3.434 10.559L8.572 10.135zm-5.514.005h4.321l3.086 9.5zm13.567 0h4.325c-2.467 3.17-4.95 6.328-7.411 9.502 1.028-3.167 2.059-6.334 3.086-9.502zM2.1 10.762l6.977 8.947-7.817-5.682a.305.305 0 01-.112-.341zm19.798 0l.952 2.922a.305.305 0 01-.11.341v.002l-7.82 5.68.026-.035z" />
</Icon> </Icon>
) )
} }

View File

@ -1,15 +1,15 @@
import classNames from 'clsx' import classNames from "clsx"
export const Icon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => { export const Icon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
const { children, className, ...rest } = props const { children, className, ...rest } = props
return ( return (
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns="http://www.w3.org/2000/svg"
viewBox='0 0 24 24' viewBox="0 0 24 24"
className={classNames( className={classNames(
'h-8 w-8 fill-current text-black dark:text-white', "size-8 fill-current text-black dark:text-white",
className className,
)} )}
{...rest} {...rest}
> >

View File

@ -1,10 +1,10 @@
import { Icon } from './Icon' import { Icon } from "./Icon"
export const NPMIcon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => { export const NPMIcon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
return ( return (
<Icon {...props}> <Icon {...props}>
<title>npm</title> <title>npm</title>
<path d='M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z' /> <path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z" />
</Icon> </Icon>
) )
} }

View File

@ -1,12 +1,12 @@
import { Icon } from './Icon' import { Icon } from "./Icon"
export const TwitchIcon = ( export const TwitchIcon = (
props: React.SVGProps<SVGSVGElement> props: React.SVGProps<SVGSVGElement>,
): JSX.Element => { ): JSX.Element => {
return ( return (
<Icon {...props}> <Icon {...props}>
<title>Twitch</title> <title>Twitch</title>
<path d='M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z' /> <path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z" />
</Icon> </Icon>
) )
} }

View File

@ -1,12 +1,12 @@
import { Icon } from './Icon' import { Icon } from "./Icon"
export const TwitterIcon = ( export const TwitterIcon = (
props: React.SVGProps<SVGSVGElement> props: React.SVGProps<SVGSVGElement>,
): JSX.Element => { ): JSX.Element => {
return ( return (
<Icon {...props}> <Icon {...props}>
<title>Twitter</title> <title>Twitter</title>
<path d='M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z' /> <path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" />
</Icon> </Icon>
) )
} }

View File

@ -1,12 +1,12 @@
import { Icon } from './Icon' import { Icon } from "./Icon"
export const YouTubeIcon = ( export const YouTubeIcon = (
props: React.SVGProps<SVGSVGElement> props: React.SVGProps<SVGSVGElement>,
): JSX.Element => { ): JSX.Element => {
return ( return (
<Icon {...props}> <Icon {...props}>
<title>YouTube</title> <title>YouTube</title>
<path d='M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z' /> <path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</Icon> </Icon>
) )
} }

View File

@ -7,13 +7,13 @@ export const SocialMediaItem = (props: SocialMediaItemProps): JSX.Element => {
const { link, ariaLabel, children } = props const { link, ariaLabel, children } = props
return ( return (
<li className='mx-4 my-1 inline-block'> <li className="mx-4 my-1 inline-block">
<a <a
href={link} href={link}
aria-label={ariaLabel} aria-label={ariaLabel}
target='_blank' target="_blank"
rel='noopener noreferrer' rel="noopener noreferrer"
className='relative inline-block bg-transparent' className="relative inline-block bg-transparent transition-all duration-300 ease-in-out hover:scale-110"
> >
{children} {children}
</a> </a>

View File

@ -1,43 +1,43 @@
import { SocialMediaItem } from './SocialMediaItem' import { SocialMediaItem } from "./SocialMediaItem"
import { TwitterIcon } from './SocialMediaIcons/TwitterIcon' import { TwitterIcon } from "./SocialMediaIcons/TwitterIcon"
import { GitHubIcon } from './SocialMediaIcons/GitHubIcon' import { GitHubIcon } from "./SocialMediaIcons/GitHubIcon"
import { GitLabIcon } from './SocialMediaIcons/GitLabIcon' import { GitLabIcon } from "./SocialMediaIcons/GitLabIcon"
import { YouTubeIcon } from './SocialMediaIcons/YouTubeIcon' import { YouTubeIcon } from "./SocialMediaIcons/YouTubeIcon"
import { TwitchIcon } from './SocialMediaIcons/TwitchIcon' import { TwitchIcon } from "./SocialMediaIcons/TwitchIcon"
import { EmailIcon } from './SocialMediaIcons/EmailIcon' import { EmailIcon } from "./SocialMediaIcons/EmailIcon"
import { NPMIcon } from './SocialMediaIcons/NPMIcon' import { NPMIcon } from "./SocialMediaIcons/NPMIcon"
export const SocialMediaList = (): JSX.Element => { export const SocialMediaList = (): JSX.Element => {
return ( return (
<ul className='social-media-list m-0 mt-2 list-none py-4 text-center'> <ul className="m-0 mt-2 list-none py-4 text-center">
<SocialMediaItem link='https://github.com/theoludwig' ariaLabel='GitHub'> <SocialMediaItem link="https://github.com/theoludwig" ariaLabel="GitHub">
<GitHubIcon /> <GitHubIcon />
</SocialMediaItem> </SocialMediaItem>
<SocialMediaItem link='https://gitlab.com/theoludwig' ariaLabel='GitLab'> <SocialMediaItem link="https://gitlab.com/theoludwig" ariaLabel="GitLab">
<GitLabIcon /> <GitLabIcon />
</SocialMediaItem> </SocialMediaItem>
<SocialMediaItem link='https://www.npmjs.com/~theoludwig' ariaLabel='npm'> <SocialMediaItem link="https://www.npmjs.com/~theoludwig" ariaLabel="npm">
<NPMIcon /> <NPMIcon />
</SocialMediaItem> </SocialMediaItem>
<SocialMediaItem <SocialMediaItem
link='https://twitter.com/theoludwig_' link="https://twitter.com/theoludwig_"
ariaLabel='Twitter' ariaLabel="Twitter"
> >
<TwitterIcon /> <TwitterIcon />
</SocialMediaItem> </SocialMediaItem>
<SocialMediaItem <SocialMediaItem
link='https://www.youtube.com/@theo_ludwig' link="https://www.youtube.com/@theo_ludwig"
ariaLabel='YouTube' ariaLabel="YouTube"
> >
<YouTubeIcon /> <YouTubeIcon />
</SocialMediaItem> </SocialMediaItem>
<SocialMediaItem <SocialMediaItem
link='https://www.twitch.tv/theoludwig' link="https://www.twitch.tv/theoludwig"
ariaLabel='Twitch' ariaLabel="Twitch"
> >
<TwitchIcon /> <TwitchIcon />
</SocialMediaItem> </SocialMediaItem>
<SocialMediaItem link='mailto:contact@theoludwig.fr' ariaLabel='Email'> <SocialMediaItem link="mailto:contact@theoludwig.fr" ariaLabel="Email">
<EmailIcon /> <EmailIcon />
</SocialMediaItem> </SocialMediaItem>
</ul> </ul>

View File

@ -1,15 +1,15 @@
import { cookies } from 'next/headers' import { cookies } from "next/headers"
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom' import { ProfileDescriptionBottom } from "./ProfileDescriptionBottom"
import { ProfileInformation } from './ProfileInfo' import { ProfileInformation } from "./ProfileInfo"
import { ProfileList } from './ProfileList' import { ProfileList } from "./ProfileList"
import { ProfileLogo } from './ProfileLogo' import { ProfileLogo } from "./ProfileLogo"
export const Profile = (): JSX.Element => { export const Profile = (): JSX.Element => {
const cookiesStore = cookies() const cookiesStore = cookies()
return ( return (
<div className='flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10'> <div className="flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10">
<ProfileLogo /> <ProfileLogo />
<div> <div>
<ProfileInformation /> <ProfileInformation />

View File

@ -1,9 +1,9 @@
import Image from 'next/image' import Image from "next/image"
import { getTheme } from '@/theme/theme.server' import { getTheme } from "@/theme/theme.server"
import type { SkillName } from './skills' import type { SkillName } from "./skills"
import { skills } from './skills' import { skills } from "./skills"
export interface SkillComponentProps { export interface SkillComponentProps {
skill: SkillName skill: SkillName
@ -17,33 +17,33 @@ export const SkillComponent = (props: SkillComponentProps): JSX.Element => {
const theme = getTheme() const theme = getTheme()
const getImage = (): string => { const getImage = (): string => {
if (typeof skillProperties.image === 'string') { if (typeof skillProperties.image === "string") {
return skillProperties.image return skillProperties.image
} }
if (theme === 'light') { if (theme === "light") {
return skillProperties.image.light return skillProperties.image.light
} }
return skillProperties.image.dark return skillProperties.image.dark
} }
return ( return (
<a <li>
href={skillProperties.link} <a
className='mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark' href={skillProperties.link}
target='_blank' className="mx-2 flex max-w-xl flex-col items-center justify-center text-center text-primary hover:underline dark:text-primary-dark"
rel='noopener noreferrer' target="_blank"
> rel="noopener noreferrer"
<div className='text-center'> >
<Image <Image
className='inline h-16 w-16' className="inline size-16"
quality={100} quality={100}
width={64} width={64}
height={64} height={64}
alt={skill} alt={skill}
src={getImage()} src={getImage()}
/> />
<p className='mt-1'>{skill}</p> <p className="mt-1 font-semibold">{skill}</p>
</div> </a>
</a> </li>
) )
} }

View File

@ -1,4 +1,4 @@
import { ShadowContainer } from '@/components/design/ShadowContainer' import { ShadowContainer } from "@/components/design/ShadowContainer"
export interface SkillsSectionProps { export interface SkillsSectionProps {
title: string title: string
@ -10,15 +10,15 @@ export const SkillsSection = (props: SkillsSectionProps): JSX.Element => {
return ( return (
<ShadowContainer> <ShadowContainer>
<div className='mx-auto w-full px-4'> <div className="mx-auto w-full px-4">
<div className='flex flex-wrap px-4 py-6'> <div className="flex flex-wrap px-4 py-6">
<div className='flex-1'> <div className="flex-1">
<div className='mb-8 border-b border-gray-600 dark:border-white dark:border-opacity-10'> <div className="mb-8 border-b border-gray-600 dark:border-white/10">
<h3 className='my-3 text-xl font-semibold text-yellow dark:text-yellow-dark'> <h3 className="my-3 text-xl font-semibold text-primary dark:text-primary-dark">
{title} {title}
</h3> </h3>
</div> </div>
<div className='flex flex-wrap justify-around'>{children}</div> <ul className="flex flex-wrap justify-around">{children}</ul>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,40 +1,40 @@
import { getI18n } from '@/i18n/i18n.server' import { getI18n } from "@/i18n/i18n.server"
import { SkillComponent } from './Skill' import { SkillComponent } from "./Skill"
import { SkillsSection } from './SkillsSection' import { SkillsSection } from "./SkillsSection"
export const Skills = (): JSX.Element => { export const Skills = (): JSX.Element => {
const i18n = getI18n() const i18n = getI18n()
return ( return (
<> <>
<SkillsSection title={i18n.translate('home.skills.languages')}> <SkillsSection title={i18n.translate("home.skills.languages")}>
<SkillComponent skill='TypeScript' /> <SkillComponent skill="TypeScript" />
<SkillComponent skill='Python' /> <SkillComponent skill="Python" />
<SkillComponent skill='C/C++' /> <SkillComponent skill="C/C++" />
<SkillComponent skill='PHP' /> <SkillComponent skill="PHP" />
</SkillsSection> </SkillsSection>
<SkillsSection title='Frontend'> <SkillsSection title="Frontend">
<SkillComponent skill='HTML' /> <SkillComponent skill="HTML" />
<SkillComponent skill='CSS' /> <SkillComponent skill="CSS" />
<SkillComponent skill='Tailwind CSS' /> <SkillComponent skill="Tailwind CSS" />
<SkillComponent skill='React.js (+ Next.js)' /> <SkillComponent skill="React.js (+ Next.js)" />
</SkillsSection> </SkillsSection>
<SkillsSection title='Backend'> <SkillsSection title="Backend">
<SkillComponent skill='Laravel' /> <SkillComponent skill="Laravel" />
<SkillComponent skill='Node.js' /> <SkillComponent skill="Node.js" />
<SkillComponent skill='Fastify' /> <SkillComponent skill="Fastify" />
<SkillComponent skill='PostgreSQL' /> <SkillComponent skill="PostgreSQL" />
</SkillsSection> </SkillsSection>
<SkillsSection title={i18n.translate('home.skills.software-tools')}> <SkillsSection title={i18n.translate("home.skills.software-tools")}>
<SkillComponent skill='GNU/Linux' /> <SkillComponent skill="GNU/Linux" />
<SkillComponent skill='Arch Linux' /> <SkillComponent skill="Arch Linux" />
<SkillComponent skill='Visual Studio Code' /> <SkillComponent skill="Visual Studio Code" />
<SkillComponent skill='Git' /> <SkillComponent skill="Git" />
<SkillComponent skill='Docker' /> <SkillComponent skill="Docker" />
</SkillsSection> </SkillsSection>
</> </>
) )

View File

@ -5,111 +5,111 @@ export interface Skill {
export const skills = { export const skills = {
JavaScript: { JavaScript: {
link: 'https://developer.mozilla.org/docs/Web/JavaScript', link: "https://developer.mozilla.org/docs/Web/JavaScript",
image: '/images/skills/JavaScript.png' image: "/images/skills/JavaScript.png",
}, },
TypeScript: { TypeScript: {
link: 'https://www.typescriptlang.org/', link: "https://www.typescriptlang.org/",
image: '/images/skills/TypeScript.png' image: "/images/skills/TypeScript.png",
}, },
Python: { Python: {
link: 'https://www.python.org/', link: "https://www.python.org/",
image: '/images/skills/Python.png' image: "/images/skills/Python.png",
}, },
'C/C++': { "C/C++": {
link: 'https://isocpp.org/', link: "https://isocpp.org/",
image: '/images/skills/C-Cpp.png' image: "/images/skills/C-Cpp.png",
}, },
PHP: { PHP: {
link: 'https://www.php.net/', link: "https://www.php.net/",
image: '/images/skills/PHP.png' image: "/images/skills/PHP.png",
}, },
Laravel: { Laravel: {
link: 'https://laravel.com/', link: "https://laravel.com/",
image: '/images/skills/Laravel.png' image: "/images/skills/Laravel.png",
}, },
Dart: { Dart: {
link: 'https://dart.dev/', link: "https://dart.dev/",
image: '/images/skills/Dart.png' image: "/images/skills/Dart.png",
}, },
Flutter: { Flutter: {
link: 'https://flutter.dev/', link: "https://flutter.dev/",
image: '/images/skills/Flutter.webp' image: "/images/skills/Flutter.webp",
}, },
HTML: { HTML: {
link: 'https://developer.mozilla.org/docs/Web/HTML', link: "https://developer.mozilla.org/docs/Web/HTML",
image: '/images/skills/HTML.png' image: "/images/skills/HTML.png",
}, },
CSS: { CSS: {
link: 'https://developer.mozilla.org/docs/Web/CSS', link: "https://developer.mozilla.org/docs/Web/CSS",
image: '/images/skills/CSS.png' image: "/images/skills/CSS.png",
}, },
'Tailwind CSS': { "Tailwind CSS": {
link: 'https://tailwindcss.com/', link: "https://tailwindcss.com/",
image: '/images/skills/TailwindCSS.png' image: "/images/skills/TailwindCSS.png",
}, },
SASS: { SASS: {
link: 'https://sass-lang.com/', link: "https://sass-lang.com/",
image: '/images/skills/SASS.svg' image: "/images/skills/SASS.svg",
}, },
'React.js (+ Next.js)': { "React.js (+ Next.js)": {
link: 'https://reactjs.org/', link: "https://reactjs.org/",
image: '/images/skills/ReactJS.png' image: "/images/skills/ReactJS.png",
}, },
'Node.js': { "Node.js": {
link: 'https://nodejs.org/', link: "https://nodejs.org/",
image: '/images/skills/NodeJS.png' image: "/images/skills/NodeJS.png",
}, },
Fastify: { Fastify: {
link: 'https://www.fastify.io/', link: "https://www.fastify.io/",
image: { image: {
light: '/images/skills/Fastify-light.png', light: "/images/skills/Fastify-light.png",
dark: '/images/skills/Fastify-dark.png' dark: "/images/skills/Fastify-dark.png",
} },
}, },
Prisma: { Prisma: {
link: 'https://www.prisma.io/', link: "https://www.prisma.io/",
image: { image: {
light: '/images/skills/Prisma-light.png', light: "/images/skills/Prisma-light.png",
dark: '/images/skills/Prisma-dark.png' dark: "/images/skills/Prisma-dark.png",
} },
}, },
PostgreSQL: { PostgreSQL: {
link: 'https://www.postgresql.org/', link: "https://www.postgresql.org/",
image: '/images/skills/PostgreSQL.png' image: "/images/skills/PostgreSQL.png",
}, },
MySQL: { MySQL: {
link: 'https://www.mysql.com/', link: "https://www.mysql.com/",
image: '/images/skills/MySQL.png' image: "/images/skills/MySQL.png",
}, },
Strapi: { Strapi: {
link: 'https://strapi.io/', link: "https://strapi.io/",
image: '/images/skills/Strapi.png' image: "/images/skills/Strapi.png",
}, },
'Visual Studio Code': { "Visual Studio Code": {
link: 'https://code.visualstudio.com/', link: "https://code.visualstudio.com/",
image: '/images/skills/VisualStudioCode.png' image: "/images/skills/VisualStudioCode.png",
}, },
Git: { Git: {
link: 'https://git-scm.com/', link: "https://git-scm.com/",
image: '/images/skills/Git.png' image: "/images/skills/Git.png",
}, },
Ubuntu: { Ubuntu: {
link: 'https://ubuntu.com/', link: "https://ubuntu.com/",
image: '/images/skills/Ubuntu.png' image: "/images/skills/Ubuntu.png",
}, },
'Arch Linux': { "Arch Linux": {
link: 'https://archlinux.org/', link: "https://archlinux.org/",
image: '/images/skills/ArchLinux.png' image: "/images/skills/ArchLinux.png",
}, },
'GNU/Linux': { "GNU/Linux": {
link: 'https://www.gnu.org/', link: "https://www.gnu.org/",
image: '/images/skills/GNU-Linux.png' image: "/images/skills/GNU-Linux.png",
}, },
Docker: { Docker: {
link: 'https://www.docker.com/', link: "https://www.docker.com/",
image: '/images/skills/Docker.png' image: "/images/skills/Docker.png",
} },
} as const } as const
export type SkillName = keyof typeof skills export type SkillName = keyof typeof skills

View File

@ -1,4 +1,4 @@
import classNames from 'clsx' import classNames from "clsx"
export interface LoaderProps { export interface LoaderProps {
width?: number width?: number
@ -13,16 +13,16 @@ export const Loader = (props: LoaderProps): JSX.Element => {
<div <div
style={{ style={{
width, width,
height height,
}} }}
className={classNames( className={classNames(
'animate-spin inline-block border-[3px] border-current border-t-transparent text-yellow dark:text-yellow-dark rounded-full', "inline-block animate-spin rounded-full border-[3px] border-current border-t-transparent text-primary dark:text-primary-dark",
className className,
)} )}
role='status' role="status"
aria-label='loading' aria-label="loading"
> >
<span className='sr-only'>Loading...</span> <span className="sr-only">Loading...</span>
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
'use client' "use client"
import { useEffect, useRef } from 'react' import { useEffect, useRef } from "react"
export type RevealFadeProps = React.PropsWithChildren export type RevealFadeProps = React.PropsWithChildren
@ -15,22 +15,22 @@ export const RevealFade = (props: RevealFadeProps): JSX.Element => {
for (const entry of entries) { for (const entry of entries) {
if (entry.isIntersecting) { if (entry.isIntersecting) {
entry.target.className = entry.target.className =
'opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out' "opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out"
observer.unobserve(entry.target) observer.unobserve(entry.target)
} }
} }
}, },
{ {
root: null, root: null,
rootMargin: '0px', rootMargin: "0px",
threshold: 0.28 threshold: 0.28,
} },
) )
observer.observe(htmlElement.current as HTMLDivElement) observer.observe(htmlElement.current as HTMLDivElement)
}, []) }, [])
return ( return (
<div ref={htmlElement} className='invisible -translate-y-7 opacity-0'> <div ref={htmlElement} className="invisible -translate-y-7 opacity-0">
{children} {children}
</div> </div>
) )

View File

@ -1,10 +1,13 @@
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'> type SectionHeadingProps = React.ComponentPropsWithRef<"h2">
export const SectionHeading = (props: SectionHeadingProps): JSX.Element => { export const SectionHeading = (props: SectionHeadingProps): JSX.Element => {
const { children, ...rest } = props const { children, ...rest } = props
return ( return (
<h2 {...rest} className='mb-3 mt-1 text-center text-4xl font-semibold'> <h2
{...rest}
className="mb-3 mt-1 text-center text-4xl font-semibold text-primary dark:text-primary-dark"
>
{children} {children}
</h2> </h2>
) )

View File

@ -1,7 +1,7 @@
import { ShadowContainer } from '@/components/design/ShadowContainer' import { ShadowContainer } from "@/components/design/ShadowContainer"
import { SectionHeading } from '@/components/design/Section/SectionHeading' import { SectionHeading } from "@/components/design/Section/SectionHeading"
type SectionProps = React.ComponentPropsWithRef<'section'> & { type SectionProps = React.ComponentPropsWithRef<"section"> & {
heading?: string heading?: string
description?: string description?: string
isMain?: boolean isMain?: boolean
@ -20,13 +20,13 @@ export const Section = (props: SectionProps): JSX.Element => {
if (isMain) { if (isMain) {
return ( return (
<div className='w-full px-3'> <div className="w-full px-3">
<ShadowContainer style={{ marginTop: 50 }}> <ShadowContainer style={{ marginTop: 50 }}>
<section {...rest}> <section {...rest}>
{heading != null ? ( {heading != null ? (
<SectionHeading>{heading}</SectionHeading> <SectionHeading>{heading}</SectionHeading>
) : null} ) : null}
<div className='w-full px-3'>{children}</div> <div className="w-full px-3">{children}</div>
</section> </section>
</ShadowContainer> </ShadowContainer>
</div> </div>
@ -37,7 +37,7 @@ export const Section = (props: SectionProps): JSX.Element => {
return ( return (
<section {...rest}> <section {...rest}>
{heading != null ? <SectionHeading>{heading}</SectionHeading> : null} {heading != null ? <SectionHeading>{heading}</SectionHeading> : null}
<div className='w-full px-3'>{children}</div> <div className="w-full px-3">{children}</div>
</section> </section>
) )
} }
@ -52,13 +52,13 @@ export const Section = (props: SectionProps): JSX.Element => {
</SectionHeading> </SectionHeading>
) : null} ) : null}
{description != null ? ( {description != null ? (
<p style={{ marginTop: 7 }} className='text-center'> <p style={{ marginTop: 7 }} className="text-center">
{description} {description}
</p> </p>
) : null} ) : null}
<div className='w-full px-3'> <div className="w-full px-3">
<ShadowContainer> <ShadowContainer className="w-full px-2 py-4 leading-8 sm:px-16">
<div className='w-full px-16 py-4 leading-8'>{children}</div> {children}
</ShadowContainer> </ShadowContainer>
</div> </div>
</section> </section>

View File

@ -1,6 +1,6 @@
import classNames from 'clsx' import classNames from "clsx"
type ShadowContainerProps = React.ComponentPropsWithRef<'div'> type ShadowContainerProps = React.ComponentPropsWithRef<"div">
export const ShadowContainer = (props: ShadowContainerProps): JSX.Element => { export const ShadowContainer = (props: ShadowContainerProps): JSX.Element => {
const { children, className, ...rest } = props const { children, className, ...rest } = props
@ -8,8 +8,8 @@ export const ShadowContainer = (props: ShadowContainerProps): JSX.Element => {
return ( return (
<div <div
className={classNames( className={classNames(
'mb-12 h-full max-w-full break-words rounded-2xl border border-solid border-[#000] shadow-light dark:shadow-dark ', "mb-12 h-full max-w-full break-words rounded-2xl border border-solid border-[#000] shadow-light dark:shadow-dark ",
className className,
)} )}
{...rest} {...rest}
> >

View File

@ -1,11 +1,12 @@
services: services:
theoludwig: theoludwig:
container_name: ${COMPOSE_PROJECT_NAME} container_name: ${COMPOSE_PROJECT_NAME}
image: 'theoludwig' image: "theoludwig"
restart: 'unless-stopped' restart: "unless-stopped"
build: build:
context: './' context: "./"
network_mode: 'host' ports:
- "${PORT-3000}:${PORT-3000}"
environment: environment:
PORT: ${PORT-3000} PORT: ${PORT-3000}
env_file: '.env' env_file: ".env"

View File

@ -1,20 +1,20 @@
import { fileURLToPath } from 'node:url' import { fileURLToPath } from "node:url"
import fs from 'node:fs' import fs from "node:fs"
import { build } from 'vite' import { build } from "vite"
const curriculumVitae = new URL('./', import.meta.url) const curriculumVitae = new URL("./", import.meta.url)
const curriculumVitaeDist = new URL('./dist', curriculumVitae) const curriculumVitaeDist = new URL("./dist", curriculumVitae)
const publicCurriculumVitaeOutputURL = new URL( const publicCurriculumVitaeOutputURL = new URL(
'../public/curriculum-vitae', "../public/curriculum-vitae",
import.meta.url import.meta.url,
) )
await build({ await build({
root: fileURLToPath(curriculumVitae), root: fileURLToPath(curriculumVitae),
base: '/curriculum-vitae/' base: "/curriculum-vitae/",
}) })
await fs.promises.cp(curriculumVitaeDist, publicCurriculumVitaeOutputURL, { await fs.promises.cp(curriculumVitaeDist, publicCurriculumVitaeOutputURL, {
recursive: true recursive: true,
}) })

View File

@ -3,14 +3,14 @@
"basics": { "basics": {
"name": "Théo LUDWIG", "name": "Théo LUDWIG",
"label": "Développeur Full Stack • Étudiant", "label": "Développeur Full Stack • Étudiant",
"image": "https://theoludwig.fr/images/logo_orange.png", "image": "https://theoludwig.fr/images/logo_background.png",
"email": "contact@theoludwig.fr", "email": "contact@theoludwig.fr",
"age": "31/03/2003", "age": "31/03/2003",
"location": { "location": {
"address": "Alsace, France" "address": "Alsace, France",
}, },
"url": "https://theoludwig.fr", "url": "https://theoludwig.fr",
"summary": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne. <br/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets (disponible sur <a href=\"https://theoludwig.fr\">theoludwig.fr</a>)." "summary": "Je me demande constamment comment améliorer notre présent, afin de rendre notre futur meilleur, particulièrement grâce aux progrès de l'informatique. <br /> Ma priorité réside dans la création d'expériences utilisateurs (UX) intuitives, répondant aux besoins des utilisateurs de la manière la plus efficace que possible.",
}, },
"education": [ "education": [
{ {
@ -22,9 +22,10 @@
"courses": [ "courses": [
"Développement Web en Node.js et React.js", "Développement Web en Node.js et React.js",
"Intégration/Déploiement Continue et Docker", "Intégration/Déploiement Continue et Docker",
"Projet développement LLM (Large Language Model) et NLP (Natural Language Processing)", "Complexité Algorithmique Théorique et Pratique en C++",
"Base de données NoSQL (MongoDB)" // "Projet développement LLM (Large Language Model) et NLP (Natural Language Processing)",
] "Base de données NoSQL (Redis, MongoDB, Cassandra)",
],
}, },
{ {
"startDate": "2022", "startDate": "2022",
@ -34,11 +35,11 @@
"score": "2ème année", "score": "2ème année",
"courses": [ "courses": [
"Développement Web avec le framework Laravel en PHP", "Développement Web avec le framework Laravel en PHP",
"Qualité de développement et Tests unitaires, d'intégration, fonctionnels/systèmes, d'acceptation etc.", "Qualité de développement et Tests automatisés",
"Patrons et Principes de conceptions (Code maintenable et réutilisable) en UML", "Patrons et Principes de conceptions (Code maintenable et réutilisable) en UML",
"Programmation systèmes en C (Multi-Thread, Serveur/Client UDP/TCP)", "Programmation systèmes en C (Multi-Thread, Serveur/Client UDP/TCP)",
"Sécurisation des accès à la base de données et PL/SQL" "Sécurisation des accès à la base de données et PL/SQL",
] ],
}, },
{ {
"startDate": "2021", "startDate": "2021",
@ -50,16 +51,16 @@
"Développement Orientée Objet en Java", "Développement Orientée Objet en Java",
"Programmation systèmes en C (Allocation mémoire, Pointeurs, Structures)", "Programmation systèmes en C (Allocation mémoire, Pointeurs, Structures)",
"Développement d'application Windows Forms (.NET Framework) en C#", "Développement d'application Windows Forms (.NET Framework) en C#",
"Base de données relationnelles et langage SQL" "Base de données relationnelles et langage SQL",
] ],
}, },
{ {
"startDate": "2019", "startDate": "2019",
"endDate": "2021", "endDate": "2021",
"studyType": "Baccalauréat Général (Mathématiques et Numériques Sciences Informatiques)", "studyType": "Baccalauréat Général (Mathématiques et Numériques Sciences Informatiques)",
"institution": "Lycée Heinrich Nessel à Haguenau", "institution": "Lycée Heinrich Nessel à Haguenau",
"score": "Mention Assez Bien" "score": "Mention Assez Bien",
} },
// { // {
// "startDate": "2014", // "startDate": "2014",
// "endDate": "2018", // "endDate": "2018",
@ -77,7 +78,7 @@
"position": "Alternant Développeur Web Full Stack", "position": "Alternant Développeur Web Full Stack",
"startDate": "2023-08-28", "startDate": "2023-08-28",
"endDate": "2024-09-02", "endDate": "2024-09-02",
"duration": "1 an" "duration": "1 an",
}, },
{ {
"summary": "Développement d'un outil GED (Gestion Électronique de Documents) en React.js, Laravel et GraphQL.", "summary": "Développement d'un outil GED (Gestion Électronique de Documents) en React.js, Laravel et GraphQL.",
@ -87,7 +88,7 @@
"position": "Stagiaire Développeur Web Full Stack", "position": "Stagiaire Développeur Web Full Stack",
"startDate": "2023-04-11", "startDate": "2023-04-11",
"endDate": "2023-07-26", "endDate": "2023-07-26",
"duration": "4 mois" "duration": "4 mois",
}, },
// { // {
// "summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.", // "summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.",
@ -107,7 +108,7 @@
"position": "Stage initiation métier développeur web", "position": "Stage initiation métier développeur web",
"startDate": "2019-06-17", "startDate": "2019-06-17",
"endDate": "2019-06-21", "endDate": "2019-06-21",
"duration": "1 semaine" "duration": "1 semaine",
}, },
{ {
"description": "interests", "description": "interests",
@ -117,7 +118,7 @@
"position": "Participation en équipe de 5 personnes", "position": "Participation en équipe de 5 personnes",
"startDate": "2021-12-02", "startDate": "2021-12-02",
"endDate": "2021-12-03", "endDate": "2021-12-03",
"duration": "1 semaine" "duration": "1 semaine",
}, },
{ {
"description": "interests", "description": "interests",
@ -128,8 +129,8 @@
"position": "Initiation métier Développeur web", "position": "Initiation métier Développeur web",
"startDate": "2019-06-24", "startDate": "2019-06-24",
"endDate": "2019-06-28", "endDate": "2019-06-28",
"duration": "1 semaine" "duration": "1 semaine",
} },
// { // {
// "summary": "Apprentissage du métier \"Chargé de communication\" et des logiciels de graphisme tels que \"Adobe Photoshop\".", // "summary": "Apprentissage du métier \"Chargé de communication\" et des logiciels de graphisme tels que \"Adobe Photoshop\".",
// "website": "https://es.fr/", // "website": "https://es.fr/",
@ -143,24 +144,24 @@
], ],
"interests": [ "interests": [
{ {
"name": "Enthousiaste de l'Open-Source" "name": "Enthousiaste de l'Open-Source",
}, },
{ {
"name": "Passionné de High-Tech" "name": "Passionné de High-Tech",
} },
], ],
"skills": [ "skills": [
{ {
"keywords": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"], "keywords": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
"name": "Langages de programmation" "name": "Langages de programmation",
}, },
{ {
"keywords": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"], "keywords": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"],
"name": "Frontend" "name": "Frontend",
}, },
{ {
"keywords": ["Laravel", "Node.js", "Fastify", "PostgreSQL"], "keywords": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
"name": "Backend" "name": "Backend",
}, },
{ {
"keywords": [ "keywords": [
@ -168,13 +169,13 @@
"Arch Linux", "Arch Linux",
"Visual Studio Code", "Visual Studio Code",
"Git", "Git",
"Docker" "Docker",
], ],
"name": "Logiciels et outils" "name": "Logiciels et outils",
}, },
{ {
"keywords": ["Permis B", "Anglais"], "keywords": ["Permis B", "Anglais"],
"name": "Autres" "name": "Autres",
} },
] ],
} }

View File

@ -8,20 +8,36 @@
"name": "curriculum-vitae", "name": "curriculum-vitae",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"jsonc-parser": "3.2.0", "jsonc-parser": "3.2.1",
"modern-normalize": "2.0.0" "modern-normalize": "2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.6.2", "@types/node": "20.12.12",
"date-and-time": "3.0.3", "date-and-time": "3.3.0",
"vite": "4.4.9", "vite": "5.2.11",
"vite-plugin-html": "3.2.0" "vite-plugin-html": "3.2.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -35,9 +51,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -51,9 +67,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -67,9 +83,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -83,9 +99,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -99,9 +115,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -115,9 +131,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -131,9 +147,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -147,9 +163,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -163,9 +179,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -179,9 +195,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -195,9 +211,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -211,9 +227,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -227,9 +243,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -243,9 +259,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -259,9 +275,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -275,9 +291,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -291,9 +307,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -307,9 +323,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -323,9 +339,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -339,9 +355,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -355,9 +371,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -371,45 +387,45 @@
} }
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.0.1", "@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9" "@jridgewell/trace-mapping": "^0.3.24"
}, },
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/set-array": { "node_modules/@jridgewell/set-array": {
"version": "1.1.2", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/source-map": { "node_modules/@jridgewell/source-map": {
"version": "0.3.5", "version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.9" "@jridgewell/trace-mapping": "^0.3.25"
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
@ -419,9 +435,9 @@
"dev": true "dev": true
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.19", "version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@ -476,16 +492,221 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/@types/node": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "20.6.2", "version": "4.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.2.tgz",
"integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", "integrity": "sha512-ahxSgCkAEk+P/AVO0vYr7DxOD3CwAQrT0Go9BJyGQ9Ef0QxVOfjDZMiF4Y2s3mLyPrjonchIMH/tbWHucJMykQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.2.tgz",
"integrity": "sha512-lAarIdxZWbFSHFSDao9+I/F5jDaKyCqAPMq5HqnfpBw8dKDiCaaqM0lq5h1pQTLeIqueeay4PieGR5jGZMWprw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.2.tgz",
"integrity": "sha512-SWsr8zEUk82KSqquIMgZEg2GE5mCSfr9sE/thDROkX6pb3QQWPp8Vw8zOq2GyxZ2t0XoSIUlvHDkrf5Gmf7x3Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.2.tgz",
"integrity": "sha512-o/HAIrQq0jIxJAhgtIvV5FWviYK4WB0WwV91SLUnsliw1lSAoLsmgEEgRWzDguAFeUEUUoIWXiJrPqU7vGiVkA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.2.tgz",
"integrity": "sha512-nwlJ65UY9eGq91cBi6VyDfArUJSKOYt5dJQBq8xyLhvS23qO+4Nr/RreibFHjP6t+5ap2ohZrUJcHv5zk5ju/g==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.2.tgz",
"integrity": "sha512-Pg5TxxO2IVlMj79+c/9G0LREC9SY3HM+pfAwX7zj5/cAuwrbfj2Wv9JbMHIdPCfQpYsI4g9mE+2Bw/3aeSs2rQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.2.tgz",
"integrity": "sha512-cAOTjGNm84gc6tS02D1EXtG7tDRsVSDTBVXOLbj31DkwfZwgTPYZ6aafSU7rD/4R2a34JOwlF9fQayuTSkoclA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.2.tgz",
"integrity": "sha512-4RyT6v1kXb7C0fn6zV33rvaX05P0zHoNzaXI/5oFHklfKm602j+N4mn2YvoezQViRLPnxP8M1NaY4s/5kXO5cw==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.2.tgz",
"integrity": "sha512-KNUH6jC/vRGAKSorySTyc/yRYlCwN/5pnMjXylfBniwtJx5O7X17KG/0efj8XM3TZU7raYRXJFFReOzNmL1n1w==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.2.tgz",
"integrity": "sha512-xPV4y73IBEXToNPa3h5lbgXOi/v0NcvKxU0xejiFw6DtIYQqOTMhZ2DN18/HrrP0PmiL3rGtRG9gz1QE8vFKXQ==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.2.tgz",
"integrity": "sha512-QBhtr07iFGmF9egrPOWyO5wciwgtzKkYPNLVCFZTmr4TWmY0oY2Dm/bmhHjKRwZoGiaKdNcKhFtUMBKvlchH+Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.2.tgz",
"integrity": "sha512-8zfsQRQGH23O6qazZSFY5jP5gt4cFvRuKTpuBsC1ZnSWxV8ZKQpPqOZIUtdfMOugCcBvFGRa1pDC/tkf19EgBw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.2.tgz",
"integrity": "sha512-H4s8UjgkPnlChl6JF5empNvFHp77Jx+Wfy2EtmYPe9G22XV+PMuCinZVHurNe8ggtwoaohxARJZbaH/3xjB/FA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.2.tgz",
"integrity": "sha512-djqpAjm/i8erWYF0K6UY4kRO3X5+T4TypIqw60Q8MTqSBaQNpNXDhxdjpZ3ikgb+wn99svA7jxcXpiyg9MUsdw==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.14.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.2.tgz",
"integrity": "sha512-teAqzLT0yTYZa8ZP7zhFKEx4cotS8Tkk5XiqNMJhD4CpaWB1BHARE4Qy+RzwnXvSAYv+Q3jAqCVBS+PS+Yee8Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true "dev": true
}, },
"node_modules/@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.10.0", "version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true, "dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -510,9 +731,9 @@
} }
}, },
"node_modules/async": { "node_modules/async": {
"version": "3.2.4", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
"dev": true "dev": true
}, },
"node_modules/balanced-match": { "node_modules/balanced-match": {
@ -582,9 +803,9 @@
} }
}, },
"node_modules/clean-css": { "node_modules/clean-css": {
"version": "5.3.2", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
"integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"source-map": "~0.6.0" "source-map": "~0.6.0"
@ -676,10 +897,11 @@
} }
}, },
"node_modules/date-and-time": { "node_modules/date-and-time": {
"version": "3.0.3", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.0.3.tgz", "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.3.0.tgz",
"integrity": "sha512-CmHCeTixc3KA5pcLTVs9JCFhmJMFTBsmSsgHnNed4YDNw9yUOrjjRn3zALy8eMgqmTO+4U8k5jl1peC7IoezfA==", "integrity": "sha512-UguWfh9LkUecVrGSE0B7SpAnGRMPATmpwSoSij24/lDnwET3A641abfDBD/TdL0T+E04f8NWlbMkD9BscVvIZg==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/dom-serializer": { "node_modules/dom-serializer": {
"version": "1.4.1", "version": "1.4.1",
@ -747,15 +969,15 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.3.1", "version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/motdotla/dotenv?sponsor=1" "url": "https://dotenvx.com"
} }
}, },
"node_modules/dotenv-expand": { "node_modules/dotenv-expand": {
@ -768,9 +990,9 @@
} }
}, },
"node_modules/ejs": { "node_modules/ejs": {
"version": "3.1.9", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"jake": "^10.8.5" "jake": "^10.8.5"
@ -792,9 +1014,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.18.20", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
@ -804,28 +1026,29 @@
"node": ">=12" "node": ">=12"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/android-arm": "0.18.20", "@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm64": "0.18.20", "@esbuild/android-arm": "0.20.2",
"@esbuild/android-x64": "0.18.20", "@esbuild/android-arm64": "0.20.2",
"@esbuild/darwin-arm64": "0.18.20", "@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-x64": "0.18.20", "@esbuild/darwin-arm64": "0.20.2",
"@esbuild/freebsd-arm64": "0.18.20", "@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/linux-arm": "0.18.20", "@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-x64": "0.18.20", "@esbuild/linux-s390x": "0.20.2",
"@esbuild/netbsd-x64": "0.18.20", "@esbuild/linux-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.18.20", "@esbuild/netbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.18.20", "@esbuild/openbsd-x64": "0.20.2",
"@esbuild/win32-arm64": "0.18.20", "@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-x64": "0.18.20" "@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
} }
}, },
"node_modules/estree-walker": { "node_modules/estree-walker": {
@ -835,9 +1058,9 @@
"dev": true "dev": true
}, },
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.1", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.stat": "^2.0.2",
@ -851,9 +1074,9 @@
} }
}, },
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.15.0", "version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"reusify": "^1.0.4" "reusify": "^1.0.4"
@ -1035,9 +1258,9 @@
} }
}, },
"node_modules/jsonc-parser": { "node_modules/jsonc-parser": {
"version": "3.2.0", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA=="
}, },
"node_modules/jsonfile": { "node_modules/jsonfile": {
"version": "6.1.0", "version": "6.1.0",
@ -1106,9 +1329,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.6", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1200,9 +1423,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.29", "version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1219,9 +1442,9 @@
} }
], ],
"dependencies": { "dependencies": {
"nanoid": "^3.3.6", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.2.0"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@ -1267,18 +1490,36 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.29.2", "version": "4.14.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.2.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.2.tgz",
"integrity": "sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==", "integrity": "sha512-WkeoTWvuBoFjFAhsEOHKRoZ3r9GfTyhh7Vff1zwebEFLEFjT1lG3784xEgKiTa7E+e70vsC81roVL2MP4tgEEQ==",
"dev": true, "dev": true,
"dependencies": {
"@types/estree": "1.0.5"
},
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
"engines": { "engines": {
"node": ">=14.18.0", "node": ">=18.0.0",
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.14.2",
"@rollup/rollup-android-arm64": "4.14.2",
"@rollup/rollup-darwin-arm64": "4.14.2",
"@rollup/rollup-darwin-x64": "4.14.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.14.2",
"@rollup/rollup-linux-arm64-gnu": "4.14.2",
"@rollup/rollup-linux-arm64-musl": "4.14.2",
"@rollup/rollup-linux-powerpc64le-gnu": "4.14.2",
"@rollup/rollup-linux-riscv64-gnu": "4.14.2",
"@rollup/rollup-linux-s390x-gnu": "4.14.2",
"@rollup/rollup-linux-x64-gnu": "4.14.2",
"@rollup/rollup-linux-x64-musl": "4.14.2",
"@rollup/rollup-win32-arm64-msvc": "4.14.2",
"@rollup/rollup-win32-ia32-msvc": "4.14.2",
"@rollup/rollup-win32-x64-msvc": "4.14.2",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -1315,9 +1556,9 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.0.2", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -1346,9 +1587,9 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.19.4", "version": "5.30.3",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz",
"integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==", "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
@ -1387,39 +1628,46 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true "dev": true
}, },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/universalify": { "node_modules/universalify": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.4.9", "version": "5.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.20.1",
"postcss": "^8.4.27", "postcss": "^8.4.38",
"rollup": "^3.27.1" "rollup": "^4.13.0"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^18.0.0 || >=20.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://github.com/vitejs/vite?sponsor=1"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.2" "fsevents": "~2.3.3"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": ">= 14", "@types/node": "^18.0.0 || >=20.0.0",
"less": "*", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
@ -1452,9 +1700,9 @@
} }
}, },
"node_modules/vite-plugin-html": { "node_modules/vite-plugin-html": {
"version": "3.2.0", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.0.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz",
"integrity": "sha512-2VLCeDiHmV/BqqNn5h2V+4280KRgQzCFN47cst3WiNK848klESPQnzuC3okH5XHtgwHH/6s1Ho/YV6yIO0pgoQ==", "integrity": "sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^4.2.0", "@rollup/pluginutils": "^4.2.0",

View File

@ -9,13 +9,13 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"jsonc-parser": "3.2.0", "jsonc-parser": "3.2.1",
"modern-normalize": "2.0.0" "modern-normalize": "2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.6.2", "@types/node": "20.12.12",
"date-and-time": "3.0.3", "date-and-time": "3.3.0",
"vite": "4.4.9", "vite": "5.2.11",
"vite-plugin-html": "3.2.0" "vite-plugin-html": "3.2.2"
} }
} }

View File

@ -1,5 +1,5 @@
import { BIRTH_DATE, getAge } from '../../utils/getAge.ts' import { BIRTH_DATE, getAge } from "../../utils/getAge.ts"
const yearOld = document.getElementById('year-old') const yearOld = document.getElementById("year-old")
yearOld.textContent = getAge(BIRTH_DATE).toString() yearOld.textContent = getAge(BIRTH_DATE).toString()

View File

@ -1,7 +1,7 @@
@import 'modern-normalize/modern-normalize.css'; @import "modern-normalize/modern-normalize.css";
body { body {
font-family: 'Montserrat', 'Arial', 'sans-serif'; font-family: "Montserrat", "Arial", "sans-serif";
background: #f0f0f0; background: #f0f0f0;
color: #333; color: #333;
line-height: 1.42857143; line-height: 1.42857143;

View File

@ -1,19 +1,19 @@
import fs from 'node:fs' import fs from "node:fs"
import { defineConfig } from 'vite' import { defineConfig } from "vite"
import { parse as JSONCParser } from 'jsonc-parser' import { parse as JSONCParser } from "jsonc-parser"
import { createHtmlPlugin } from 'vite-plugin-html' import { createHtmlPlugin } from "vite-plugin-html"
import date from 'date-and-time' import date from "date-and-time"
const jsonCurriculumVitaeURL = new URL( const jsonCurriculumVitaeURL = new URL(
'./curriculum-vitae.jsonc', "./curriculum-vitae.jsonc",
import.meta.url import.meta.url,
) )
const dataCurriculumVitaeStringJSON = await fs.promises.readFile( const dataCurriculumVitaeStringJSON = await fs.promises.readFile(
jsonCurriculumVitaeURL, jsonCurriculumVitaeURL,
{ {
encoding: 'utf-8' encoding: "utf-8",
} },
) )
const curriculumVitae = JSONCParser(dataCurriculumVitaeStringJSON) const curriculumVitae = JSONCParser(dataCurriculumVitaeStringJSON)
@ -22,7 +22,7 @@ const curriculumVitae = JSONCParser(dataCurriculumVitaeStringJSON)
*/ */
export default defineConfig({ export default defineConfig({
build: { build: {
assetsDir: './' assetsDir: "./",
}, },
plugins: [ plugins: [
createHtmlPlugin({ createHtmlPlugin({
@ -30,13 +30,13 @@ export default defineConfig({
data: { data: {
date, date,
locals: { locals: {
...curriculumVitae ...curriculumVitae,
} },
} },
} },
}) }),
], ],
css: { css: {
postcss: {} postcss: {},
} },
}) })

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