mirror of
				https://github.com/theoludwig/theoludwig.git
				synced 2025-11-04 00:19:01 +01:00 
			
		
		
		
	chore: better Prettier config for easier reviews
This commit is contained in:
		@@ -1,9 +1,9 @@
 | 
				
			|||||||
services:
 | 
					services:
 | 
				
			||||||
  workspace:
 | 
					  workspace:
 | 
				
			||||||
    build:
 | 
					    build:
 | 
				
			||||||
      context: './'
 | 
					      context: "./"
 | 
				
			||||||
      dockerfile: './Dockerfile'
 | 
					      dockerfile: "./Dockerfile"
 | 
				
			||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - '..:/workspace:cached'
 | 
					      - "..:/workspace:cached"
 | 
				
			||||||
    command: 'sleep infinity'
 | 
					    command: "sleep infinity"
 | 
				
			||||||
    network_mode: 'host'
 | 
					    network_mode: "host"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/BUG.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/BUG.md
									
									
									
									
										vendored
									
									
								
							@@ -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"
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!--
 | 
					<!--
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/DOCUMENTATION.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/DOCUMENTATION.md
									
									
									
									
										vendored
									
									
								
							@@ -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. -->
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
									
									
									
									
										vendored
									
									
								
							@@ -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. -->
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/IMPROVEMENT.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/IMPROVEMENT.md
									
									
									
									
										vendored
									
									
								
							@@ -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. -->
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/QUESTION.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/QUESTION.md
									
									
									
									
										vendored
									
									
								
							@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										22
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@@ -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.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Setup Node.js'
 | 
					      - name: "Setup Node.js"
 | 
				
			||||||
        uses: 'actions/setup-node@v3.8.1'
 | 
					        uses: "actions/setup-node@v3.8.1"
 | 
				
			||||||
        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"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										40
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										40
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							@@ -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.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Setup Node.js'
 | 
					      - name: "Setup Node.js"
 | 
				
			||||||
        uses: 'actions/setup-node@v3.8.1'
 | 
					        uses: "actions/setup-node@v3.8.1"
 | 
				
			||||||
        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 }}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -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.0.0"
 | 
				
			||||||
        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@v3.8.1"
 | 
				
			||||||
        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 }}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										50
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										50
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							@@ -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.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Setup Node.js'
 | 
					      - name: "Setup Node.js"
 | 
				
			||||||
        uses: 'actions/setup-node@v3.8.1'
 | 
					        uses: "actions/setup-node@v3.8.1"
 | 
				
			||||||
        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.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Setup Node.js'
 | 
					      - name: "Setup Node.js"
 | 
				
			||||||
        uses: 'actions/setup-node@v3.8.1'
 | 
					        uses: "actions/setup-node@v3.8.1"
 | 
				
			||||||
        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"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								.gitpod.yml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.gitpod.yml
									
									
									
									
									
								
							@@ -1,13 +1,13 @@
 | 
				
			|||||||
image: 'gitpod/workspace-full'
 | 
					image: "gitpod/workspace-full"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
tasks:
 | 
					tasks:
 | 
				
			||||||
  - before: 'cp .env.example .env'
 | 
					  - before: "cp .env.example .env"
 | 
				
			||||||
    init: 'npm clean-install'
 | 
					    init: "npm clean-install"
 | 
				
			||||||
    command: 'npm run dev'
 | 
					    command: "npm run dev"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ports:
 | 
					ports:
 | 
				
			||||||
  - port: 3000
 | 
					  - port: 3000
 | 
				
			||||||
    onOpen: 'open-preview'
 | 
					    onOpen: "open-preview"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
github:
 | 
					github:
 | 
				
			||||||
  prebuilds:
 | 
					  prebuilds:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,3 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "singleQuote": true,
 | 
					  "semi": false
 | 
				
			||||||
  "jsxSingleQuote": true,
 | 
					 | 
				
			||||||
  "semi": false,
 | 
					 | 
				
			||||||
  "trailingComma": "none"
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,7 +34,7 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
 | 
				
			|||||||
### Prerequisites
 | 
					### Prerequisites
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [Node.js](https://nodejs.org/) >= 20.0.0
 | 
					- [Node.js](https://nodejs.org/) >= 20.0.0
 | 
				
			||||||
- [npm](https://www.npmjs.com/) >= 9.0.0
 | 
					- [npm](https://www.npmjs.com/) >= 10.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Installation
 | 
					### Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,15 @@
 | 
				
			|||||||
FROM node:20.6.1 AS builder-dependencies
 | 
					FROM node:20.9.0 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.9.0 AS builder
 | 
				
			||||||
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 gcr.io/distroless/nodejs20-debian12:latest AS runner
 | 
				
			||||||
WORKDIR /usr/src/application
 | 
					WORKDIR /usr/src/application
 | 
				
			||||||
ENV NODE_ENV=production
 | 
					ENV NODE_ENV=production
 | 
				
			||||||
ENV HOSTNAME=0.0.0.0
 | 
					ENV HOSTNAME=0.0.0.0
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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-col flex-1 items-center justify-center">
 | 
				
			||||||
      <Loader />
 | 
					      <Loader />
 | 
				
			||||||
    </main>
 | 
					    </main>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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-col flex-1 items-center justify-center">
 | 
				
			||||||
      <Loader />
 | 
					      <Loader />
 | 
				
			||||||
    </main>
 | 
					    </main>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,36 +1,36 @@
 | 
				
			|||||||
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">Blog</h1>
 | 
				
			||||||
        <p className='mt-6 text-center' data-cy='blog-post-date'>
 | 
					        <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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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-col flex-1 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-yellow dark:text-yellow-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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.prose [id]::before {
 | 
					.prose [id]::before {
 | 
				
			||||||
  content: '';
 | 
					  content: "";
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  height: 90px;
 | 
					  height: 90px;
 | 
				
			||||||
  margin-top: -90px;
 | 
					  margin-top: -90px;
 | 
				
			||||||
@@ -39,9 +39,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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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/icon-96x96.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,21 @@ export const metadata: Metadata = {
 | 
				
			|||||||
      {
 | 
					      {
 | 
				
			||||||
        url: image,
 | 
					        url: image,
 | 
				
			||||||
        width: 96,
 | 
					        width: 96,
 | 
				
			||||||
        height: 96
 | 
					        height: 96,
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    locale,
 | 
					    locale,
 | 
				
			||||||
    type: 'website'
 | 
					    type: "website",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  icons: {
 | 
					  icons: {
 | 
				
			||||||
    icon: '/images/icon-96x96.png'
 | 
					    icon: "/images/icon-96x96.png",
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  twitter: {
 | 
					  twitter: {
 | 
				
			||||||
    card: 'summary',
 | 
					    card: "summary",
 | 
				
			||||||
    title,
 | 
					    title,
 | 
				
			||||||
    description,
 | 
					    description,
 | 
				
			||||||
    images: [image]
 | 
					    images: [image],
 | 
				
			||||||
  }
 | 
					  },
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface RootLayoutProps {
 | 
					interface RootLayoutProps {
 | 
				
			||||||
@@ -61,14 +61,14 @@ const RootLayout = (props: RootLayoutProps): JSX.Element => {
 | 
				
			|||||||
    <html
 | 
					    <html
 | 
				
			||||||
      lang={i18n.locale}
 | 
					      lang={i18n.locale}
 | 
				
			||||||
      className={classNames({
 | 
					      className={classNames({
 | 
				
			||||||
        dark: theme === 'dark',
 | 
					        dark: theme === "dark",
 | 
				
			||||||
        light: theme === 'light'
 | 
					        light: theme === "light",
 | 
				
			||||||
      })}
 | 
					      })}
 | 
				
			||||||
      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="bg-white font-headline text-black dark:bg-black dark:text-white flex flex-col min-h-screen">
 | 
				
			||||||
        <Header />
 | 
					        <Header />
 | 
				
			||||||
        {children}
 | 
					        {children}
 | 
				
			||||||
        <Footer />
 | 
					        <Footer />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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-col flex-1 items-center justify-center">
 | 
				
			||||||
      <Loader />
 | 
					      <Loader />
 | 
				
			||||||
    </main>
 | 
					    </main>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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-col flex-1 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-yellow dark:text-yellow-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-yellow hover:underline dark:text-yellow-dark"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {i18n.translate('errors.return-to-home-page')}
 | 
					          {i18n.translate("errors.return-to-home-page")}
 | 
				
			||||||
        </Link>
 | 
					        </Link>
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
    </main>
 | 
					    </main>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										36
									
								
								app/page.tsx
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								app/page.tsx
									
									
									
									
									
								
							@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,13 @@ 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">{blogPost.frontmatter.title}</h1>
 | 
				
			||||||
        <p className='mt-2' data-cy='blog-post-date'>
 | 
					        <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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,37 +1,37 @@
 | 
				
			|||||||
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} className="group">
 | 
				
			||||||
      <Link
 | 
					      <Link
 | 
				
			||||||
        href={`#${id}`}
 | 
					        href={`#${id}`}
 | 
				
			||||||
        className='invisible !text-black group-hover:visible dark:!text-white'
 | 
					        className="invisible !text-black group-hover:visible dark:!text-white"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <FontAwesomeIcon className='mr-2 inline h-4 w-4' icon={faLink} />
 | 
					        <FontAwesomeIcon className="mr-2 inline h-4 w-4" icon={faLink} />
 | 
				
			||||||
      </Link>
 | 
					      </Link>
 | 
				
			||||||
      {children}
 | 
					      {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,27 @@ 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="h-auto w-auto"
 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </span>
 | 
					                </span>
 | 
				
			||||||
              )
 | 
					              )
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            a: (props) => {
 | 
					            a: (props) => {
 | 
				
			||||||
              const { href = '' } = props
 | 
					              const { href = "" } = props
 | 
				
			||||||
              if (href.startsWith('#')) {
 | 
					              if (href.startsWith("#")) {
 | 
				
			||||||
                return <a {...props} />
 | 
					                return <a {...props} />
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              return <a target='_blank' rel='noopener noreferrer' {...props} />
 | 
					              return <a target="_blank" rel="noopener noreferrer" {...props} />
 | 
				
			||||||
            }
 | 
					            },
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <BlogPostComments cookiesStore={cookiesStore.toString()} />
 | 
					        <BlogPostComments cookiesStore={cookiesStore.toString()} />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,35 +1,35 @@
 | 
				
			|||||||
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'>
 | 
					      <div className="w-[1600px]" data-cy="blog-posts">
 | 
				
			||||||
        {posts.map((post, index) => {
 | 
					        {posts.map((post, index) => {
 | 
				
			||||||
          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
 | 
					            <Link
 | 
				
			||||||
              href={`/blog/${post.slug}`}
 | 
					              href={`/blog/${post.slug}`}
 | 
				
			||||||
              key={index}
 | 
					              key={index}
 | 
				
			||||||
              locale='en'
 | 
					              locale="en"
 | 
				
			||||||
              data-cy={post.slug}
 | 
					              data-cy={post.slug}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'>
 | 
					              <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'>
 | 
					                <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>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										22
									
								
								blog/blog.ts
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								blog/blog.ts
									
									
									
									
									
								
							@@ -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
 | 
				
			||||||
  }
 | 
					  },
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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! 👋
 | 
				
			||||||
@@ -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))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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! 👋
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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! 👋
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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! 👋
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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! 👋
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
---
 | 
					---
 | 
				
			||||||
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"
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Hello! 👋
 | 
					Hello! 👋
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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="text-yellow hover:underline dark:text-yellow-dark"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        Théo LUDWIG
 | 
					        Théo LUDWIG
 | 
				
			||||||
      </Link>{' '}
 | 
					      </Link>{" "}
 | 
				
			||||||
      | {i18n.translate('common.all-rights-reserved')}
 | 
					      | {i18n.translate("common.all-rights-reserved")}
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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="text-yellow hover:underline dark:text-yellow-dark"
 | 
				
			||||||
        href={versionLink}
 | 
					        href={versionLink}
 | 
				
			||||||
        target='_blank'
 | 
					        target="_blank"
 | 
				
			||||||
        rel='noopener noreferrer'
 | 
					        rel="noopener noreferrer"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {version}
 | 
					        {version}
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
import { FooterText } from './FooterText'
 | 
					import { FooterText } from "./FooterText"
 | 
				
			||||||
import { FooterVersion } from './FooterVersion'
 | 
					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 { readPackage } = await import("read-pkg")
 | 
				
			||||||
  const { version } = await readPackage()
 | 
					  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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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] hover:bg-opacity-20"
 | 
				
			||||||
              onClick={async () => {
 | 
					              onClick={async () => {
 | 
				
			||||||
                return await handleLocale(locale)
 | 
					                return await handleLocale(locale)
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 bottom-0 left-[8px] top-0 mb-auto mt-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 h-[10px] w-[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 bottom-0 right-[10px] top-0 mb-auto mt-auto h-[10px] w-[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 h-[10px] w-[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-[1px] box-border h-[22px] w-[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-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0 hidden"
 | 
				
			||||||
          defaultChecked
 | 
					          defaultChecked
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,39 +1,39 @@
 | 
				
			|||||||
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 { 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='/'>
 | 
					      <Link href="/">
 | 
				
			||||||
        <div className='flex items-center justify-center'>
 | 
					        <div className="flex items-center justify-center">
 | 
				
			||||||
          <Image
 | 
					          <Image
 | 
				
			||||||
            quality={100}
 | 
					            quality={100}
 | 
				
			||||||
            width={60}
 | 
					            width={60}
 | 
				
			||||||
            height={60}
 | 
					            height={60}
 | 
				
			||||||
            src='/images/icon_small.png'
 | 
					            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-yellow dark:text-yellow-dark xs:block">
 | 
				
			||||||
            Théo LUDWIG
 | 
					            Théo LUDWIG
 | 
				
			||||||
          </strong>
 | 
					          </strong>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </Link>
 | 
					      </Link>
 | 
				
			||||||
      <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="text-yellow hover:underline dark:text-yellow-dark"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            Blog
 | 
					            Blog
 | 
				
			||||||
          </Link>
 | 
					          </Link>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import htmlParser from 'html-react-parser'
 | 
					import htmlParser from "html-react-parser"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface InterestParagraphProps {
 | 
					export interface InterestParagraphProps {
 | 
				
			||||||
  title: string
 | 
					  title: string
 | 
				
			||||||
@@ -6,14 +6,14 @@ export interface InterestParagraphProps {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const InterestParagraph = (
 | 
					export const InterestParagraph = (
 | 
				
			||||||
  props: InterestParagraphProps
 | 
					  props: InterestParagraphProps,
 | 
				
			||||||
): JSX.Element => {
 | 
					): JSX.Element => {
 | 
				
			||||||
  const { title, description } = props
 | 
					  const { title, description } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <p className='my-6 text-center text-gray dark:text-gray-dark'>
 | 
					      <p className="my-6 text-center text-gray dark:text-gray-dark">
 | 
				
			||||||
        <strong className='text-lg font-semibold text-yellow dark:text-yellow-dark'>
 | 
					        <strong className="text-lg font-semibold text-yellow dark:text-yellow-dark">
 | 
				
			||||||
          {title}
 | 
					          {title}
 | 
				
			||||||
        </strong>
 | 
					        </strong>
 | 
				
			||||||
        <br />
 | 
					        <br />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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="interest-item mx-2 my-2 h-8 w-8" title={title}>
 | 
				
			||||||
      <FontAwesomeIcon
 | 
					      <FontAwesomeIcon
 | 
				
			||||||
        className='block h-full w-full text-yellow dark:text-yellow-dark'
 | 
					        className="block h-full w-full text-yellow dark:text-yellow-dark"
 | 
				
			||||||
        icon={fontAwesomeIcon}
 | 
					        icon={fontAwesomeIcon}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </li>
 | 
					    </li>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,18 @@
 | 
				
			|||||||
import { faCode, faMicrochip } from '@fortawesome/free-solid-svg-icons'
 | 
					import { faCode, faMicrochip } from "@fortawesome/free-solid-svg-icons"
 | 
				
			||||||
import { faGit } from '@fortawesome/free-brands-svg-icons'
 | 
					import { faGit } from "@fortawesome/free-brands-svg-icons"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { InterestItem } from './InterestItem'
 | 
					import { InterestItem } from "./InterestItem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const InterestsList = (): JSX.Element => {
 | 
					export const InterestsList = (): JSX.Element => {
 | 
				
			||||||
  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} />
 | 
					        <InterestItem title="Developer Full Stack" fontAwesomeIcon={faCode} />
 | 
				
			||||||
        <InterestItem
 | 
					        <InterestItem
 | 
				
			||||||
          title='Passionate about High-Tech'
 | 
					          title="Passionate about High-Tech"
 | 
				
			||||||
          fontAwesomeIcon={faMicrochip}
 | 
					          fontAwesomeIcon={faMicrochip}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <InterestItem title='Open-Source enthusiast' fontAwesomeIcon={faGit} />
 | 
					        <InterestItem title="Open-Source enthusiast" fontAwesomeIcon={faGit} />
 | 
				
			||||||
      </ul>
 | 
					      </ul>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,21 @@
 | 
				
			|||||||
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, index) => {
 | 
				
			||||||
        return <InterestParagraph key={index} {...paragraph} />
 | 
					        return <InterestParagraph key={index} {...paragraph} />
 | 
				
			||||||
      })}
 | 
					      })}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,13 +11,13 @@ 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'>
 | 
					    <ShadowContainer className="relative !mb-4 max-h-32 cursor-pointer p-6 transition-transform duration-200 ease-in-out hover:-translate-y-2">
 | 
				
			||||||
      <a href={href} target='_blank' rel='noopener noreferrer'>
 | 
					      <a href={href} target="_blank" rel="noopener noreferrer">
 | 
				
			||||||
        <div className='flex'>
 | 
					        <div className="flex">
 | 
				
			||||||
          <GitHubIcon className='mr-2 h-6' />
 | 
					          <GitHubIcon className="mr-2 h-6" />
 | 
				
			||||||
          <span className='text-yellow dark:text-yellow-dark'>{name}</span>
 | 
					          <span className="text-yellow dark:text-yellow-dark">{name}</span>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <p className='my-4'>{description}</p>
 | 
					        <p className="my-4">{description}</p>
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
    </ShadowContainer>
 | 
					    </ShadowContainer>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,35 +1,35 @@
 | 
				
			|||||||
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'>
 | 
					      <div 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="nrwl/nx"
 | 
				
			||||||
          description='Smart, Fast and Extensible Build System'
 | 
					          description="Smart, Fast and Extensible Build System"
 | 
				
			||||||
          href='https://github.com/nrwl/nx/commits?author=theoludwig'
 | 
					          href="https://github.com/nrwl/nx/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>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,29 +13,29 @@ 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'>
 | 
					    <ShadowContainer className="relative cursor-pointer items-center sm:ml-10">
 | 
				
			||||||
      <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'>
 | 
					        <div className="flex justify-center">
 | 
				
			||||||
          <Image
 | 
					          <Image
 | 
				
			||||||
            quality={100}
 | 
					            quality={100}
 | 
				
			||||||
            className='h-auto w-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
 | 
					            className="h-auto w-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5"
 | 
				
			||||||
            width={300}
 | 
					            width={300}
 | 
				
			||||||
            height={300}
 | 
					            height={300}
 | 
				
			||||||
            src={image}
 | 
					            src={image}
 | 
				
			||||||
            alt={title}
 | 
					            alt={title}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div className='absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100'>
 | 
					        <div className="absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100">
 | 
				
			||||||
          <h3 className='my-6 text-xl font-semibold text-yellow dark:text-yellow-dark'>
 | 
					          <h3 className="my-6 text-xl font-semibold text-yellow dark:text-yellow-dark">
 | 
				
			||||||
            {title}
 | 
					            {title}
 | 
				
			||||||
          </h3>
 | 
					          </h3>
 | 
				
			||||||
          <p className='my-6'>{description}</p>
 | 
					          <p className="my-6">{description}</p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
    </ShadowContainer>
 | 
					    </ShadowContainer>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,18 @@
 | 
				
			|||||||
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'>
 | 
					    <div className="flex w-full flex-wrap justify-center px-3">
 | 
				
			||||||
      {items.map((item, index) => {
 | 
					      {items.map((item, index) => {
 | 
				
			||||||
        return <PortfolioItem key={index} {...item} />
 | 
					        return <PortfolioItem key={index} {...item} />
 | 
				
			||||||
      })}
 | 
					      })}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,18 @@
 | 
				
			|||||||
import { getI18n } from '@/i18n/i18n.server'
 | 
					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'>
 | 
					    <p className="mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark">
 | 
				
			||||||
      {i18n.translate('home.about.description-bottom')}
 | 
					      {i18n.translate("home.about.description-bottom")}
 | 
				
			||||||
      {i18n.locale === 'fr-FR' ? (
 | 
					      {i18n.locale === "fr-FR" ? (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
          <br />
 | 
					          <br />
 | 
				
			||||||
          <br />
 | 
					          <br />
 | 
				
			||||||
          <a
 | 
					          <a
 | 
				
			||||||
            href='/curriculum-vitae/index.html'
 | 
					            href="/curriculum-vitae/index.html"
 | 
				
			||||||
            className='text-yellow hover:underline dark:text-yellow-dark'
 | 
					            className="text-yellow hover:underline dark:text-yellow-dark"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            Curriculum vitæ
 | 
					            Curriculum vitæ
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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-yellow dark:text-yellow-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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
'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 { ProfileItem } from './ProfileItem'
 | 
					import { ProfileItem } from "./ProfileItem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ProfileListProps {
 | 
					export interface ProfileListProps {
 | 
				
			||||||
  cookiesStore: CookiesStore
 | 
					  cookiesStore: CookiesStore
 | 
				
			||||||
@@ -22,25 +22,25 @@ export const ProfileList = (props: ProfileListProps): JSX.Element => {
 | 
				
			|||||||
  }, [])
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  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={`${BIRTH_DATE_STRING} (${age} ${i18n.translate(
 | 
				
			||||||
          'home.about.years-old'
 | 
					          "home.about.years-old",
 | 
				
			||||||
        )})`}
 | 
					        )})`}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <ProfileItem
 | 
					      <ProfileItem
 | 
				
			||||||
        title={i18n.translate('home.about.nationality')}
 | 
					        title={i18n.translate("home.about.nationality")}
 | 
				
			||||||
        value='Alsace, France'
 | 
					        value="Alsace, France"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <ProfileItem
 | 
					      <ProfileItem
 | 
				
			||||||
        title='Email'
 | 
					        title="Email"
 | 
				
			||||||
        value='contact@theoludwig.fr'
 | 
					        value="contact@theoludwig.fr"
 | 
				
			||||||
        link='mailto:contact@theoludwig.fr'
 | 
					        link="mailto:contact@theoludwig.fr"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </ul>
 | 
					    </ul>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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',
 | 
					        "h-8 w-8 fill-current text-black dark:text-white",
 | 
				
			||||||
        className
 | 
					        className,
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
      {...rest}
 | 
					      {...rest}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {children}
 | 
					        {children}
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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="social-media-list 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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,10 +17,10 @@ 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
 | 
				
			||||||
@@ -29,20 +29,20 @@ export const SkillComponent = (props: SkillComponentProps): JSX.Element => {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <a
 | 
					    <a
 | 
				
			||||||
      href={skillProperties.link}
 | 
					      href={skillProperties.link}
 | 
				
			||||||
      className='mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark'
 | 
					      className="mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark"
 | 
				
			||||||
      target='_blank'
 | 
					      target="_blank"
 | 
				
			||||||
      rel='noopener noreferrer'
 | 
					      rel="noopener noreferrer"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div className='text-center'>
 | 
					      <div className="text-center">
 | 
				
			||||||
        <Image
 | 
					        <Image
 | 
				
			||||||
          className='inline h-16 w-16'
 | 
					          className="inline h-16 w-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">{skill}</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 dark:border-opacity-10">
 | 
				
			||||||
              <h3 className='my-3 text-xl font-semibold text-yellow dark:text-yellow-dark'>
 | 
					              <h3 className="my-3 text-xl font-semibold text-yellow dark:text-yellow-dark">
 | 
				
			||||||
                {title}
 | 
					                {title}
 | 
				
			||||||
              </h3>
 | 
					              </h3>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div className='flex flex-wrap justify-around'>{children}</div>
 | 
					            <div className="flex flex-wrap justify-around">{children}</div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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',
 | 
					        "animate-spin inline-block border-[3px] border-current border-t-transparent text-yellow dark:text-yellow-dark rounded-full",
 | 
				
			||||||
        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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,10 @@
 | 
				
			|||||||
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">
 | 
				
			||||||
      {children}
 | 
					      {children}
 | 
				
			||||||
    </h2>
 | 
					    </h2>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
          <div className='w-full px-16 py-4 leading-8'>{children}</div>
 | 
					          <div className="w-full px-16 py-4 leading-8">{children}</div>
 | 
				
			||||||
        </ShadowContainer>
 | 
					        </ShadowContainer>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								compose.yaml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								compose.yaml
									
									
									
									
									
								
							@@ -1,11 +1,11 @@
 | 
				
			|||||||
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'
 | 
					    network_mode: "host"
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      PORT: ${PORT-3000}
 | 
					      PORT: ${PORT-3000}
 | 
				
			||||||
    env_file: '.env'
 | 
					    env_file: ".env"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										51
									
								
								curriculum-vitae/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										51
									
								
								curriculum-vitae/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -12,9 +12,9 @@
 | 
				
			|||||||
        "modern-normalize": "2.0.0"
 | 
					        "modern-normalize": "2.0.0"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "devDependencies": {
 | 
					      "devDependencies": {
 | 
				
			||||||
        "@types/node": "20.6.2",
 | 
					        "@types/node": "20.8.7",
 | 
				
			||||||
        "date-and-time": "3.0.3",
 | 
					        "date-and-time": "3.0.3",
 | 
				
			||||||
        "vite": "4.4.9",
 | 
					        "vite": "4.5.0",
 | 
				
			||||||
        "vite-plugin-html": "3.2.0"
 | 
					        "vite-plugin-html": "3.2.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -419,9 +419,9 @@
 | 
				
			|||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@jridgewell/trace-mapping": {
 | 
					    "node_modules/@jridgewell/trace-mapping": {
 | 
				
			||||||
      "version": "0.3.19",
 | 
					      "version": "0.3.20",
 | 
				
			||||||
      "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.20.tgz",
 | 
				
			||||||
      "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
 | 
					      "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@jridgewell/resolve-uri": "^3.1.0",
 | 
					        "@jridgewell/resolve-uri": "^3.1.0",
 | 
				
			||||||
@@ -477,10 +477,13 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/node": {
 | 
					    "node_modules/@types/node": {
 | 
				
			||||||
      "version": "20.6.2",
 | 
					      "version": "20.8.7",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz",
 | 
				
			||||||
      "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==",
 | 
					      "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "undici-types": "~5.25.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/acorn": {
 | 
					    "node_modules/acorn": {
 | 
				
			||||||
      "version": "8.10.0",
 | 
					      "version": "8.10.0",
 | 
				
			||||||
@@ -1200,9 +1203,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/postcss": {
 | 
					    "node_modules/postcss": {
 | 
				
			||||||
      "version": "8.4.29",
 | 
					      "version": "8.4.31",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
 | 
				
			||||||
      "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
 | 
					      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "funding": [
 | 
					      "funding": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -1267,9 +1270,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/rollup": {
 | 
					    "node_modules/rollup": {
 | 
				
			||||||
      "version": "3.29.2",
 | 
					      "version": "3.29.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
 | 
				
			||||||
      "integrity": "sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==",
 | 
					      "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "rollup": "dist/bin/rollup"
 | 
					        "rollup": "dist/bin/rollup"
 | 
				
			||||||
@@ -1346,9 +1349,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/terser": {
 | 
					    "node_modules/terser": {
 | 
				
			||||||
      "version": "5.19.4",
 | 
					      "version": "5.22.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/terser/-/terser-5.22.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==",
 | 
					      "integrity": "sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@jridgewell/source-map": "^0.3.3",
 | 
					        "@jridgewell/source-map": "^0.3.3",
 | 
				
			||||||
@@ -1387,6 +1390,12 @@
 | 
				
			|||||||
      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
 | 
					      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/undici-types": {
 | 
				
			||||||
 | 
					      "version": "5.25.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==",
 | 
				
			||||||
 | 
					      "dev": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/universalify": {
 | 
					    "node_modules/universalify": {
 | 
				
			||||||
      "version": "2.0.0",
 | 
					      "version": "2.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
 | 
				
			||||||
@@ -1397,9 +1406,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/vite": {
 | 
					    "node_modules/vite": {
 | 
				
			||||||
      "version": "4.4.9",
 | 
					      "version": "4.5.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
 | 
					      "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "esbuild": "^0.18.10",
 | 
					        "esbuild": "^0.18.10",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,9 +13,9 @@
 | 
				
			|||||||
    "modern-normalize": "2.0.0"
 | 
					    "modern-normalize": "2.0.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@types/node": "20.6.2",
 | 
					    "@types/node": "20.8.7",
 | 
				
			||||||
    "date-and-time": "3.0.3",
 | 
					    "date-and-time": "3.0.3",
 | 
				
			||||||
    "vite": "4.4.9",
 | 
					    "vite": "4.5.0",
 | 
				
			||||||
    "vite-plugin-html": "3.2.0"
 | 
					    "vite-plugin-html": "3.2.0"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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: {},
 | 
				
			||||||
  }
 | 
					  },
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,17 @@
 | 
				
			|||||||
import { defineConfig } from 'cypress'
 | 
					import { defineConfig } from "cypress"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
  fixturesFolder: false,
 | 
					  fixturesFolder: false,
 | 
				
			||||||
  video: false,
 | 
					  video: false,
 | 
				
			||||||
  screenshotOnRunFailure: false,
 | 
					  screenshotOnRunFailure: false,
 | 
				
			||||||
  e2e: {
 | 
					  e2e: {
 | 
				
			||||||
    baseUrl: 'http://127.0.0.1:3000',
 | 
					    baseUrl: "http://127.0.0.1:3000",
 | 
				
			||||||
    supportFile: false
 | 
					    supportFile: false,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  component: {
 | 
					  component: {
 | 
				
			||||||
    devServer: {
 | 
					    devServer: {
 | 
				
			||||||
      framework: 'next',
 | 
					      framework: "next",
 | 
				
			||||||
      bundler: 'webpack'
 | 
					      bundler: "webpack",
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
  }
 | 
					  },
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,16 @@
 | 
				
			|||||||
import { getAge } from '@/utils/getAge'
 | 
					import { getAge } from "@/utils/getAge"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('utils/getAge', () => {
 | 
					describe("utils/getAge", () => {
 | 
				
			||||||
  it('should calculate the right age of a person', () => {
 | 
					  it("should calculate the right age of a person", () => {
 | 
				
			||||||
    cy.clock(new Date('2018-03-20')).then(() => {
 | 
					    cy.clock(new Date("2018-03-20")).then(() => {
 | 
				
			||||||
      const birthDate = new Date('1980-02-20')
 | 
					      const birthDate = new Date("1980-02-20")
 | 
				
			||||||
      expect(getAge(birthDate)).equal(38)
 | 
					      expect(getAge(birthDate)).equal(38)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should calculate the right age of a person (taking into account the months)', () => {
 | 
					  it("should calculate the right age of a person (taking into account the months)", () => {
 | 
				
			||||||
    cy.clock(new Date('2018-03-20')).then(() => {
 | 
					    cy.clock(new Date("2018-03-20")).then(() => {
 | 
				
			||||||
      const birthDate = new Date('1980-07-20')
 | 
					      const birthDate = new Date("1980-07-20")
 | 
				
			||||||
      expect(getAge(birthDate)).equal(37)
 | 
					      expect(getAge(birthDate)).equal(37)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,62 +1,62 @@
 | 
				
			|||||||
describe('Common > Header', () => {
 | 
					describe("Common > Header", () => {
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    return cy.visit('/')
 | 
					    return cy.visit("/")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should redirect to /blog on click of the blog link', () => {
 | 
					  it("should redirect to /blog on click of the blog link", () => {
 | 
				
			||||||
    cy.get('[data-cy=header-blog-link]')
 | 
					    cy.get("[data-cy=header-blog-link]")
 | 
				
			||||||
      .click()
 | 
					      .click()
 | 
				
			||||||
      .location('pathname')
 | 
					      .location("pathname")
 | 
				
			||||||
      .should('eq', '/blog')
 | 
					      .should("eq", "/blog")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should always be visible (sticky header)', () => {
 | 
					  it("should always be visible (sticky header)", () => {
 | 
				
			||||||
    cy.scrollTo('bottom').get('header').should('be.visible')
 | 
					    cy.scrollTo("bottom").get("header").should("be.visible")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('Switch theme color (dark/light)', () => {
 | 
					  describe("Switch theme color (dark/light)", () => {
 | 
				
			||||||
    it('should switch theme from `dark` (default) to `light`', () => {
 | 
					    it("should switch theme from `dark` (default) to `light`", () => {
 | 
				
			||||||
      cy.get('[data-cy=switch-theme-dark]').should('be.visible')
 | 
					      cy.get("[data-cy=switch-theme-dark]").should("be.visible")
 | 
				
			||||||
      cy.get('[data-cy=switch-theme-light]').should('not.be.visible')
 | 
					      cy.get("[data-cy=switch-theme-light]").should("not.be.visible")
 | 
				
			||||||
      cy.get('body').should(
 | 
					      cy.get("body").should(
 | 
				
			||||||
        'not.have.css',
 | 
					        "not.have.css",
 | 
				
			||||||
        'background-color',
 | 
					        "background-color",
 | 
				
			||||||
        'rgb(255, 255, 255)'
 | 
					        "rgb(255, 255, 255)",
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      cy.get('[data-cy=switch-theme-click]').click()
 | 
					      cy.get("[data-cy=switch-theme-click]").click()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      cy.get('[data-cy=switch-theme-dark]').should('not.be.visible')
 | 
					      cy.get("[data-cy=switch-theme-dark]").should("not.be.visible")
 | 
				
			||||||
      cy.get('[data-cy=switch-theme-light]').should('be.visible')
 | 
					      cy.get("[data-cy=switch-theme-light]").should("be.visible")
 | 
				
			||||||
      cy.get('body').should(
 | 
					      cy.get("body").should(
 | 
				
			||||||
        'have.css',
 | 
					        "have.css",
 | 
				
			||||||
        'background-color',
 | 
					        "background-color",
 | 
				
			||||||
        'rgb(255, 255, 255)'
 | 
					        "rgb(255, 255, 255)",
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('Switch Language', () => {
 | 
					  describe("Switch Language", () => {
 | 
				
			||||||
    it('should switch locale from English (default) to French', () => {
 | 
					    it("should switch locale from English (default) to French", () => {
 | 
				
			||||||
      cy.get('h1').contains('Théo LUDWIG')
 | 
					      cy.get("h1").contains("Théo LUDWIG")
 | 
				
			||||||
      cy.get('[data-cy=locale-flag-text]').contains('English')
 | 
					      cy.get("[data-cy=locale-flag-text]").contains("English")
 | 
				
			||||||
      cy.get('[data-cy=locales-list]').should('not.be.visible')
 | 
					      cy.get("[data-cy=locales-list]").should("not.be.visible")
 | 
				
			||||||
      cy.get('[data-cy=locale-click]').click()
 | 
					      cy.get("[data-cy=locale-click]").click()
 | 
				
			||||||
      cy.get('[data-cy=locales-list]').should('be.visible')
 | 
					      cy.get("[data-cy=locales-list]").should("be.visible")
 | 
				
			||||||
      cy.get('[data-cy=locales-list] > li:first-child')
 | 
					      cy.get("[data-cy=locales-list] > li:first-child")
 | 
				
			||||||
        .contains('French')
 | 
					        .contains("French")
 | 
				
			||||||
        .click()
 | 
					        .click()
 | 
				
			||||||
      cy.get('[data-cy=locales-list]').should('not.be.visible')
 | 
					      cy.get("[data-cy=locales-list]").should("not.be.visible")
 | 
				
			||||||
      cy.get('[data-cy=locale-flag-text]').contains('French')
 | 
					      cy.get("[data-cy=locale-flag-text]").contains("French")
 | 
				
			||||||
      cy.get('h1').contains('Théo LUDWIG')
 | 
					      cy.get("h1").contains("Théo LUDWIG")
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should close the locale list menu when clicking outside', () => {
 | 
					    it("should close the locale list menu when clicking outside", () => {
 | 
				
			||||||
      cy.get('[data-cy=locales-list]').should('not.be.visible')
 | 
					      cy.get("[data-cy=locales-list]").should("not.be.visible")
 | 
				
			||||||
      cy.get('[data-cy=locale-click]').click()
 | 
					      cy.get("[data-cy=locale-click]").click()
 | 
				
			||||||
      cy.get('[data-cy=locales-list]').should('be.visible')
 | 
					      cy.get("[data-cy=locales-list]").should("be.visible")
 | 
				
			||||||
      cy.get('h1').click()
 | 
					      cy.get("h1").click()
 | 
				
			||||||
      cy.get('[data-cy=locales-list]').should('not.be.visible')
 | 
					      cy.get("[data-cy=locales-list]").should("not.be.visible")
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,10 @@
 | 
				
			|||||||
describe('Page /404', () => {
 | 
					describe("Page /404", () => {
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    return cy.visit('/404', { failOnStatusCode: false })
 | 
					    return cy.visit("/404", { failOnStatusCode: false })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should display the statusCode of 404', () => {
 | 
					  it("should display the statusCode of 404", () => {
 | 
				
			||||||
    cy.get('[data-cy=status-code]').contains('404')
 | 
					    cy.get("[data-cy=status-code]").contains("404")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,14 @@
 | 
				
			|||||||
describe('Page /blog/[slug]', () => {
 | 
					describe("Page /blog/[slug]", () => {
 | 
				
			||||||
  it('should displays the first blog post (`hello-world`)', () => {
 | 
					  it("should displays the first blog post (`hello-world`)", () => {
 | 
				
			||||||
    cy.visit('/blog/hello-world')
 | 
					    cy.visit("/blog/hello-world")
 | 
				
			||||||
    cy.get('[data-cy=locale-flag-text]').should('not.exist')
 | 
					    cy.get("[data-cy=locale-flag-text]").should("not.exist")
 | 
				
			||||||
    cy.get('h1').should('have.text', '👋 Hello, world!')
 | 
					    cy.get("h1").should("have.text", "👋 Hello, world!")
 | 
				
			||||||
    cy.get('.prose a:visible').should('have.attr', 'target', '_blank')
 | 
					    cy.get(".prose a:visible").should("have.attr", "target", "_blank")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it("should redirect to /404 if the blog post doesn't exist", () => {
 | 
					  it("should redirect to /404 if the blog post doesn't exist", () => {
 | 
				
			||||||
    cy.visit('/blog/random-blog-post-not-found', { failOnStatusCode: false })
 | 
					    cy.visit("/blog/random-blog-post-not-found", { failOnStatusCode: false })
 | 
				
			||||||
    cy.get('[data-cy=status-code]').contains('404')
 | 
					    cy.get("[data-cy=status-code]").contains("404")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +1,23 @@
 | 
				
			|||||||
describe('Page /blog', () => {
 | 
					describe("Page /blog", () => {
 | 
				
			||||||
  it('should displays the blog posts sorted from newest to oldest', () => {
 | 
					  it("should displays the blog posts sorted from newest to oldest", () => {
 | 
				
			||||||
    cy.visit('/blog')
 | 
					    cy.visit("/blog")
 | 
				
			||||||
    cy.get('[data-cy=blog-posts] [data-cy=blog-post-title]')
 | 
					    cy.get("[data-cy=blog-posts] [data-cy=blog-post-title]")
 | 
				
			||||||
      .last()
 | 
					      .last()
 | 
				
			||||||
      .should('have.text', '👋 Hello, world!')
 | 
					      .should("have.text", "👋 Hello, world!")
 | 
				
			||||||
    cy.get('[data-cy=blog-posts] [data-cy=blog-post-description]')
 | 
					    cy.get("[data-cy=blog-posts] [data-cy=blog-post-description]")
 | 
				
			||||||
      .last()
 | 
					      .last()
 | 
				
			||||||
      .should(
 | 
					      .should(
 | 
				
			||||||
        'have.text',
 | 
					        "have.text",
 | 
				
			||||||
        'First post of the blog, introduction and explanation of how this blog is made.'
 | 
					        "First post of the blog, introduction and explanation of how this blog is made.",
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should redirect the user to the right blog post', () => {
 | 
					  it("should redirect the user to the right blog post", () => {
 | 
				
			||||||
    cy.visit('/blog')
 | 
					    cy.visit("/blog")
 | 
				
			||||||
    cy.get('[data-cy=hello-world]')
 | 
					    cy.get("[data-cy=hello-world]")
 | 
				
			||||||
      .click()
 | 
					      .click()
 | 
				
			||||||
      .location('pathname')
 | 
					      .location("pathname")
 | 
				
			||||||
      .should('eq', '/blog/hello-world')
 | 
					      .should("eq", "/blog/hello-world")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,16 @@
 | 
				
			|||||||
describe('Page /', () => {
 | 
					describe("Page /", () => {
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    return cy.visit('/')
 | 
					    return cy.visit("/")
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should reveals the sections while scrolling except the about section', () => {
 | 
					  it("should reveals the sections while scrolling except the about section", () => {
 | 
				
			||||||
    const sectionsReveals = ['#interests', '#skills', '#portfolio']
 | 
					    const sectionsReveals = ["#interests", "#skills", "#portfolio"]
 | 
				
			||||||
    cy.get('#about').should('be.visible')
 | 
					    cy.get("#about").should("be.visible")
 | 
				
			||||||
    for (const section of sectionsReveals) {
 | 
					    for (const section of sectionsReveals) {
 | 
				
			||||||
      cy.get(section)
 | 
					      cy.get(section)
 | 
				
			||||||
        .should('not.be.visible')
 | 
					        .should("not.be.visible")
 | 
				
			||||||
        .scrollIntoView()
 | 
					        .scrollIntoView()
 | 
				
			||||||
        .should('be.visible')
 | 
					        .should("be.visible")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import { mount } from 'cypress/react'
 | 
					import { mount } from "cypress/react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import './commands'
 | 
					import "./commands"
 | 
				
			||||||
import '../../app/globals.css'
 | 
					import "../../app/globals.css"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
  namespace Cypress {
 | 
					  namespace Cypress {
 | 
				
			||||||
@@ -11,4 +11,4 @@ declare global {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Cypress.Commands.add('mount', mount)
 | 
					Cypress.Commands.add("mount", mount)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
import UniversalCookie from 'universal-cookie'
 | 
					import UniversalCookie from "universal-cookie"
 | 
				
			||||||
import type { I18n } from 'i18n-js'
 | 
					import type { I18n } from "i18n-js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { CookiesStore } from '@/utils/constants'
 | 
					import type { CookiesStore } from "@/utils/constants"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { i18n } from './i18n'
 | 
					import { i18n } from "./i18n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useI18n = (cookiesStore: CookiesStore): I18n => {
 | 
					export const useI18n = (cookiesStore: CookiesStore): I18n => {
 | 
				
			||||||
  const universalCookie = new UniversalCookie(cookiesStore)
 | 
					  const universalCookie = new UniversalCookie(cookiesStore)
 | 
				
			||||||
  i18n.locale = universalCookie.get('locale') ?? i18n.defaultLocale
 | 
					  i18n.locale = universalCookie.get("locale") ?? i18n.defaultLocale
 | 
				
			||||||
  return i18n
 | 
					  return i18n
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,21 @@
 | 
				
			|||||||
'use server'
 | 
					"use server"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { cookies } from 'next/headers'
 | 
					import { cookies } from "next/headers"
 | 
				
			||||||
import type { I18n } from 'i18n-js'
 | 
					import type { I18n } from "i18n-js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { Locale } from '@/utils/constants'
 | 
					import type { Locale } from "@/utils/constants"
 | 
				
			||||||
import { COOKIE_MAX_AGE } from '@/utils/constants'
 | 
					import { COOKIE_MAX_AGE } from "@/utils/constants"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { i18n } from './i18n'
 | 
					import { i18n } from "./i18n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const setLocale = (locale: Locale): void => {
 | 
					export const setLocale = (locale: Locale): void => {
 | 
				
			||||||
  cookies().set('locale', locale, {
 | 
					  cookies().set("locale", locale, {
 | 
				
			||||||
    path: '/',
 | 
					    path: "/",
 | 
				
			||||||
    maxAge: COOKIE_MAX_AGE
 | 
					    maxAge: COOKIE_MAX_AGE,
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getI18n = (): I18n => {
 | 
					export const getI18n = (): I18n => {
 | 
				
			||||||
  i18n.locale = cookies().get('locale')?.value ?? i18n.defaultLocale
 | 
					  i18n.locale = cookies().get("locale")?.value ?? i18n.defaultLocale
 | 
				
			||||||
  return i18n
 | 
					  return i18n
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										30
									
								
								i18n/i18n.ts
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								i18n/i18n.ts
									
									
									
									
									
								
							@@ -1,30 +1,30 @@
 | 
				
			|||||||
import { I18n } from 'i18n-js'
 | 
					import { I18n } from "i18n-js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { Locale } from '@/utils/constants'
 | 
					import type { Locale } from "@/utils/constants"
 | 
				
			||||||
import { DEFAULT_LOCALE, LOCALES } from '@/utils/constants'
 | 
					import { DEFAULT_LOCALE, LOCALES } from "@/utils/constants"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import commonEnglish from './translations/en-US/common.json'
 | 
					import commonEnglish from "./translations/en-US/common.json"
 | 
				
			||||||
import errorsEnglish from './translations/en-US/errors.json'
 | 
					import errorsEnglish from "./translations/en-US/errors.json"
 | 
				
			||||||
import homeEnglish from './translations/en-US/home.json'
 | 
					import homeEnglish from "./translations/en-US/home.json"
 | 
				
			||||||
import commonFrench from './translations/fr-FR/common.json'
 | 
					import commonFrench from "./translations/fr-FR/common.json"
 | 
				
			||||||
import errorsFrench from './translations/fr-FR/errors.json'
 | 
					import errorsFrench from "./translations/fr-FR/errors.json"
 | 
				
			||||||
import homeFrench from './translations/fr-FR/home.json'
 | 
					import homeFrench from "./translations/fr-FR/home.json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const translations = {
 | 
					const translations = {
 | 
				
			||||||
  'en-US': {
 | 
					  "en-US": {
 | 
				
			||||||
    common: commonEnglish,
 | 
					    common: commonEnglish,
 | 
				
			||||||
    errors: errorsEnglish,
 | 
					    errors: errorsEnglish,
 | 
				
			||||||
    home: homeEnglish
 | 
					    home: homeEnglish,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  'fr-FR': {
 | 
					  "fr-FR": {
 | 
				
			||||||
    common: commonFrench,
 | 
					    common: commonFrench,
 | 
				
			||||||
    errors: errorsFrench,
 | 
					    errors: errorsFrench,
 | 
				
			||||||
    home: homeFrench
 | 
					    home: homeFrench,
 | 
				
			||||||
  }
 | 
					  },
 | 
				
			||||||
} satisfies Record<Locale, Record<string, unknown>>
 | 
					} satisfies Record<Locale, Record<string, unknown>>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const i18n = new I18n(translations, {
 | 
					export const i18n = new I18n(translations, {
 | 
				
			||||||
  defaultLocale: DEFAULT_LOCALE,
 | 
					  defaultLocale: DEFAULT_LOCALE,
 | 
				
			||||||
  availableLocales: LOCALES.slice(),
 | 
					  availableLocales: LOCALES.slice(),
 | 
				
			||||||
  enableFallback: true
 | 
					  enableFallback: true,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,43 +1,43 @@
 | 
				
			|||||||
import { NextResponse } from 'next/server'
 | 
					import { NextResponse } from "next/server"
 | 
				
			||||||
import type { NextRequest } from 'next/server'
 | 
					import type { NextRequest } from "next/server"
 | 
				
			||||||
import { match } from '@formatjs/intl-localematcher'
 | 
					import { match } from "@formatjs/intl-localematcher"
 | 
				
			||||||
import Negotiator from 'negotiator'
 | 
					import Negotiator from "negotiator"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { Locale, Theme } from '@/utils/constants'
 | 
					import type { Locale, Theme } from "@/utils/constants"
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  COOKIE_MAX_AGE,
 | 
					  COOKIE_MAX_AGE,
 | 
				
			||||||
  DEFAULT_LOCALE,
 | 
					  DEFAULT_LOCALE,
 | 
				
			||||||
  DEFAULT_THEME,
 | 
					  DEFAULT_THEME,
 | 
				
			||||||
  LOCALES,
 | 
					  LOCALES,
 | 
				
			||||||
  THEMES
 | 
					  THEMES,
 | 
				
			||||||
} from '@/utils/constants'
 | 
					} from "@/utils/constants"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const middleware = (request: NextRequest): NextResponse => {
 | 
					export const middleware = (request: NextRequest): NextResponse => {
 | 
				
			||||||
  const response = NextResponse.next()
 | 
					  const response = NextResponse.next()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let locale = request.cookies.get('locale')?.value
 | 
					  let locale = request.cookies.get("locale")?.value
 | 
				
			||||||
  if (locale == null || !LOCALES.includes(locale as Locale)) {
 | 
					  if (locale == null || !LOCALES.includes(locale as Locale)) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const headers = {
 | 
					      const headers = {
 | 
				
			||||||
        'accept-language':
 | 
					        "accept-language":
 | 
				
			||||||
          request.headers.get('accept-language') ?? DEFAULT_LOCALE
 | 
					          request.headers.get("accept-language") ?? DEFAULT_LOCALE,
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const languages = new Negotiator({ headers }).languages()
 | 
					      const languages = new Negotiator({ headers }).languages()
 | 
				
			||||||
      locale = match(languages, LOCALES.slice(), DEFAULT_LOCALE)
 | 
					      locale = match(languages, LOCALES.slice(), DEFAULT_LOCALE)
 | 
				
			||||||
    } catch {
 | 
					    } catch {
 | 
				
			||||||
      locale = DEFAULT_LOCALE
 | 
					      locale = DEFAULT_LOCALE
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    response.cookies.set('locale', locale, {
 | 
					    response.cookies.set("locale", locale, {
 | 
				
			||||||
      path: '/',
 | 
					      path: "/",
 | 
				
			||||||
      maxAge: COOKIE_MAX_AGE
 | 
					      maxAge: COOKIE_MAX_AGE,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const theme = request.cookies.get('theme')?.value
 | 
					  const theme = request.cookies.get("theme")?.value
 | 
				
			||||||
  if (theme == null || !THEMES.includes(theme as Theme)) {
 | 
					  if (theme == null || !THEMES.includes(theme as Theme)) {
 | 
				
			||||||
    response.cookies.set('theme', DEFAULT_THEME, {
 | 
					    response.cookies.set("theme", DEFAULT_THEME, {
 | 
				
			||||||
      path: '/',
 | 
					      path: "/",
 | 
				
			||||||
      maxAge: COOKIE_MAX_AGE
 | 
					      maxAge: COOKIE_MAX_AGE,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -53,6 +53,6 @@ export const config = {
 | 
				
			|||||||
     * - _next/image (image optimization files)
 | 
					     * - _next/image (image optimization files)
 | 
				
			||||||
     * - favicon.ico (favicon file)
 | 
					     * - favicon.ico (favicon file)
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    '/((?!api|_next/static|_next/image|favicon.ico).*)'
 | 
					    "/((?!api|_next/static|_next/image|favicon.ico).*)",
 | 
				
			||||||
  ]
 | 
					  ],
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,13 @@
 | 
				
			|||||||
/** @type {import('next').NextConfig} */
 | 
					/** @type {import('next').NextConfig} */
 | 
				
			||||||
const nextConfig = {
 | 
					const nextConfig = {
 | 
				
			||||||
  reactStrictMode: true,
 | 
					  reactStrictMode: true,
 | 
				
			||||||
  output: 'standalone',
 | 
					  output: "standalone",
 | 
				
			||||||
  eslint: {
 | 
					  eslint: {
 | 
				
			||||||
    ignoreDuringBuilds: true
 | 
					    ignoreDuringBuilds: true,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  experimental: {
 | 
					  experimental: {
 | 
				
			||||||
    serverActions: true
 | 
					    serverActions: true,
 | 
				
			||||||
  }
 | 
					  },
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = nextConfig
 | 
					module.exports = nextConfig
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2244
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2244
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										54
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								package.json
									
									
									
									
									
								
							@@ -8,7 +8,7 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "engines": {
 | 
					  "engines": {
 | 
				
			||||||
    "node": ">=20.0.0",
 | 
					    "node": ">=20.0.0",
 | 
				
			||||||
    "npm": ">=9.0.0"
 | 
					    "npm": ">=10.0.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "dev": "next dev",
 | 
					    "dev": "next dev",
 | 
				
			||||||
@@ -29,7 +29,7 @@
 | 
				
			|||||||
    "postinstall": "husky install"
 | 
					    "postinstall": "husky install"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@fontsource/montserrat": "5.0.8",
 | 
					    "@fontsource/montserrat": "5.0.15",
 | 
				
			||||||
    "@formatjs/intl-localematcher": "0.4.2",
 | 
					    "@formatjs/intl-localematcher": "0.4.2",
 | 
				
			||||||
    "@fortawesome/fontawesome-svg-core": "6.4.2",
 | 
					    "@fortawesome/fontawesome-svg-core": "6.4.2",
 | 
				
			||||||
    "@fortawesome/free-brands-svg-icons": "6.4.2",
 | 
					    "@fortawesome/free-brands-svg-icons": "6.4.2",
 | 
				
			||||||
@@ -39,11 +39,11 @@
 | 
				
			|||||||
    "clsx": "2.0.0",
 | 
					    "clsx": "2.0.0",
 | 
				
			||||||
    "date-and-time": "3.0.3",
 | 
					    "date-and-time": "3.0.3",
 | 
				
			||||||
    "gray-matter": "4.0.3",
 | 
					    "gray-matter": "4.0.3",
 | 
				
			||||||
    "html-react-parser": "4.2.2",
 | 
					    "html-react-parser": "4.2.9",
 | 
				
			||||||
    "i18n-js": "4.3.2",
 | 
					    "i18n-js": "4.3.2",
 | 
				
			||||||
    "katex": "0.16.8",
 | 
					    "katex": "0.16.9",
 | 
				
			||||||
    "negotiator": "0.6.3",
 | 
					    "negotiator": "0.6.3",
 | 
				
			||||||
    "next": "13.4.19",
 | 
					    "next": "13.5.6",
 | 
				
			||||||
    "next-mdx-remote": "4.4.1",
 | 
					    "next-mdx-remote": "4.4.1",
 | 
				
			||||||
    "react": "18.2.0",
 | 
					    "react": "18.2.0",
 | 
				
			||||||
    "react-dom": "18.2.0",
 | 
					    "react-dom": "18.2.0",
 | 
				
			||||||
@@ -53,47 +53,47 @@
 | 
				
			|||||||
    "rehype-slug": "5.1.0",
 | 
					    "rehype-slug": "5.1.0",
 | 
				
			||||||
    "remark-gfm": "3.0.1",
 | 
					    "remark-gfm": "3.0.1",
 | 
				
			||||||
    "remark-math": "5.1.1",
 | 
					    "remark-math": "5.1.1",
 | 
				
			||||||
    "sharp": "0.32.5",
 | 
					    "sharp": "0.32.6",
 | 
				
			||||||
    "shiki": "0.14.4",
 | 
					    "shiki": "0.14.5",
 | 
				
			||||||
    "unified": "10.1.2",
 | 
					    "unified": "10.1.2",
 | 
				
			||||||
    "unist-util-visit": "5.0.0",
 | 
					    "unist-util-visit": "5.0.0",
 | 
				
			||||||
    "universal-cookie": "6.1.1"
 | 
					    "universal-cookie": "6.1.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@commitlint/cli": "17.7.1",
 | 
					    "@commitlint/cli": "18.0.0",
 | 
				
			||||||
    "@commitlint/config-conventional": "17.7.0",
 | 
					    "@commitlint/config-conventional": "18.0.0",
 | 
				
			||||||
    "@saithodev/semantic-release-backmerge": "3.2.0",
 | 
					    "@saithodev/semantic-release-backmerge": "3.2.1",
 | 
				
			||||||
    "@semantic-release/git": "10.0.1",
 | 
					    "@semantic-release/git": "10.0.1",
 | 
				
			||||||
    "@tailwindcss/typography": "0.5.10",
 | 
					    "@tailwindcss/typography": "0.5.10",
 | 
				
			||||||
    "@tsconfig/strictest": "2.0.2",
 | 
					    "@tsconfig/strictest": "2.0.2",
 | 
				
			||||||
    "@types/negotiator": "0.6.1",
 | 
					    "@types/negotiator": "0.6.2",
 | 
				
			||||||
    "@types/node": "20.6.2",
 | 
					    "@types/node": "20.8.7",
 | 
				
			||||||
    "@types/react": "18.2.22",
 | 
					    "@types/react": "18.2.31",
 | 
				
			||||||
    "@types/unist": "3.0.0",
 | 
					    "@types/unist": "3.0.1",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "6.7.2",
 | 
					    "@typescript-eslint/eslint-plugin": "6.9.0",
 | 
				
			||||||
    "@typescript-eslint/parser": "6.7.2",
 | 
					    "@typescript-eslint/parser": "6.9.0",
 | 
				
			||||||
    "autoprefixer": "10.4.15",
 | 
					    "autoprefixer": "10.4.16",
 | 
				
			||||||
    "curriculum-vitae": "file:./curriculum-vitae",
 | 
					    "curriculum-vitae": "file:./curriculum-vitae",
 | 
				
			||||||
    "cypress": "13.2.0",
 | 
					    "cypress": "13.3.2",
 | 
				
			||||||
    "editorconfig-checker": "5.1.1",
 | 
					    "editorconfig-checker": "5.1.1",
 | 
				
			||||||
    "eslint": "8.49.0",
 | 
					    "eslint": "8.52.0",
 | 
				
			||||||
    "eslint-config-conventions": "11.0.1",
 | 
					    "eslint-config-conventions": "12.0.0",
 | 
				
			||||||
    "eslint-config-next": "13.4.19",
 | 
					    "eslint-config-next": "13.5.6",
 | 
				
			||||||
    "eslint-config-prettier": "9.0.0",
 | 
					    "eslint-config-prettier": "9.0.0",
 | 
				
			||||||
    "eslint-plugin-import": "2.28.1",
 | 
					    "eslint-plugin-import": "2.29.0",
 | 
				
			||||||
    "eslint-plugin-prettier": "5.0.0",
 | 
					    "eslint-plugin-prettier": "5.0.1",
 | 
				
			||||||
    "eslint-plugin-promise": "6.1.1",
 | 
					    "eslint-plugin-promise": "6.1.1",
 | 
				
			||||||
    "eslint-plugin-unicorn": "48.0.1",
 | 
					    "eslint-plugin-unicorn": "48.0.1",
 | 
				
			||||||
    "html-w3c-validator": "1.5.0",
 | 
					    "html-w3c-validator": "1.5.0",
 | 
				
			||||||
    "husky": "8.0.3",
 | 
					    "husky": "8.0.3",
 | 
				
			||||||
    "lint-staged": "14.0.1",
 | 
					    "lint-staged": "15.0.2",
 | 
				
			||||||
    "markdownlint-cli2": "0.10.0",
 | 
					    "markdownlint-cli2": "0.10.0",
 | 
				
			||||||
    "markdownlint-rule-relative-links": "2.1.0",
 | 
					    "markdownlint-rule-relative-links": "2.1.0",
 | 
				
			||||||
    "postcss": "8.4.29",
 | 
					    "postcss": "8.4.31",
 | 
				
			||||||
    "prettier": "3.0.3",
 | 
					    "prettier": "3.0.3",
 | 
				
			||||||
    "prettier-plugin-tailwindcss": "0.5.4",
 | 
					    "prettier-plugin-tailwindcss": "0.5.6",
 | 
				
			||||||
    "semantic-release": "21.1.2",
 | 
					    "semantic-release": "21.1.2",
 | 
				
			||||||
    "start-server-and-test": "2.0.0",
 | 
					    "start-server-and-test": "2.0.1",
 | 
				
			||||||
    "tailwindcss": "3.3.3",
 | 
					    "tailwindcss": "3.3.3",
 | 
				
			||||||
    "typescript": "5.2.2"
 | 
					    "typescript": "5.2.2"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  plugins: {
 | 
					  plugins: {
 | 
				
			||||||
    tailwindcss: {},
 | 
					    tailwindcss: {},
 | 
				
			||||||
    autoprefixer: {}
 | 
					    autoprefixer: {},
 | 
				
			||||||
  }
 | 
					  },
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user