mirror of
				https://github.com/theoludwig/theoludwig.git
				synced 2025-10-14 20:23:25 +02:00 
			
		
		
		
	chore: better Prettier config for easier reviews
This commit is contained in:
		| @@ -1,9 +1,9 @@ | ||||
| services: | ||||
|   workspace: | ||||
|     build: | ||||
|       context: './' | ||||
|       dockerfile: './Dockerfile' | ||||
|       context: "./" | ||||
|       dockerfile: "./Dockerfile" | ||||
|     volumes: | ||||
|       - '..:/workspace:cached' | ||||
|     command: 'sleep infinity' | ||||
|     network_mode: 'host' | ||||
|       - "..:/workspace:cached" | ||||
|     command: "sleep infinity" | ||||
|     network_mode: "host" | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/BUG.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/BUG.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| name: '🐛 Bug Report' | ||||
| about: 'Report an unexpected problem or unintended behavior.' | ||||
| title: '[Bug]' | ||||
| labels: 'bug' | ||||
| name: "🐛 Bug Report" | ||||
| about: "Report an unexpected problem or unintended behavior." | ||||
| title: "[Bug]" | ||||
| labels: "bug" | ||||
| --- | ||||
|  | ||||
| <!-- | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/DOCUMENTATION.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/DOCUMENTATION.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| name: '📜 Documentation' | ||||
| about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).' | ||||
| title: '[Documentation]' | ||||
| labels: 'documentation' | ||||
| name: "📜 Documentation" | ||||
| about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)." | ||||
| title: "[Documentation]" | ||||
| labels: "documentation" | ||||
| --- | ||||
|  | ||||
| <!-- 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' | ||||
| about: 'Suggest a new feature idea.' | ||||
| title: '[Feature]' | ||||
| labels: 'feature request' | ||||
| name: "✨ Feature Request" | ||||
| about: "Suggest a new feature idea." | ||||
| title: "[Feature]" | ||||
| labels: "feature request" | ||||
| --- | ||||
|  | ||||
| <!-- 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' | ||||
| about: 'Improve structure/format/performance/refactor/tests of the code.' | ||||
| title: '[Improvement]' | ||||
| labels: 'improvement' | ||||
| name: "🔧 Improvement" | ||||
| about: "Improve structure/format/performance/refactor/tests of the code." | ||||
| title: "[Improvement]" | ||||
| labels: "improvement" | ||||
| --- | ||||
|  | ||||
| <!-- 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' | ||||
| about: 'Further information is requested.' | ||||
| title: '[Question]' | ||||
| labels: 'question' | ||||
| name: "🙋 Question" | ||||
| about: "Further information is requested." | ||||
| title: "[Question]" | ||||
| labels: "question" | ||||
| --- | ||||
|  | ||||
| ### Question | ||||
|   | ||||
							
								
								
									
										22
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| name: 'Build' | ||||
| name: "Build" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
| @@ -8,18 +8,18 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v4.0.0' | ||||
|       - uses: "actions/checkout@v4.0.0" | ||||
|  | ||||
|       - name: 'Setup Node.js' | ||||
|         uses: 'actions/setup-node@v3.8.1' | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v3.8.1" | ||||
|         with: | ||||
|           node-version: '20.x' | ||||
|           cache: 'npm' | ||||
|           node-version: "20.x" | ||||
|           cache: "npm" | ||||
|  | ||||
|       - name: 'Install dependencies' | ||||
|         run: 'npm clean-install' | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|  | ||||
|       - name: 'Build' | ||||
|         run: 'npm run build' | ||||
|       - name: "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: | ||||
|   push: | ||||
| @@ -8,35 +8,35 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   lint: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v4.0.0' | ||||
|       - uses: "actions/checkout@v4.0.0" | ||||
|  | ||||
|       - name: 'Setup Node.js' | ||||
|         uses: 'actions/setup-node@v3.8.1' | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v3.8.1" | ||||
|         with: | ||||
|           node-version: '20.x' | ||||
|           cache: 'npm' | ||||
|           node-version: "20.x" | ||||
|           cache: "npm" | ||||
|  | ||||
|       - name: 'Install dependencies' | ||||
|         run: 'npm clean-install' | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|  | ||||
|       - name: 'lint:commit' | ||||
|       - name: "lint:commit" | ||||
|         run: 'npm run lint:commit -- --to "${{ github.sha }}"' | ||||
|  | ||||
|       - name: 'lint:editorconfig' | ||||
|         run: 'npm run lint:editorconfig' | ||||
|       - name: "lint:editorconfig" | ||||
|         run: "npm run lint:editorconfig" | ||||
|  | ||||
|       - name: 'lint:markdown' | ||||
|         run: 'npm run lint:markdown' | ||||
|       - name: "lint:markdown" | ||||
|         run: "npm run lint:markdown" | ||||
|  | ||||
|       - name: 'lint:eslint' | ||||
|         run: 'npm run lint:eslint' | ||||
|       - name: "lint:eslint" | ||||
|         run: "npm run lint:eslint" | ||||
|  | ||||
|       - name: 'lint:prettier' | ||||
|         run: 'npm run lint:prettier' | ||||
|       - name: "lint:prettier" | ||||
|         run: "npm run lint:prettier" | ||||
|  | ||||
|       - name: 'lint:dotenv' | ||||
|         uses: 'dotenv-linter/action-dotenv-linter@v2.18.0' | ||||
|       - name: "lint:dotenv" | ||||
|         uses: "dotenv-linter/action-dotenv-linter@v2.18.0" | ||||
|         with: | ||||
|           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: | ||||
|   push: | ||||
| @@ -6,31 +6,31 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   release: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v4.0.0' | ||||
|       - uses: "actions/checkout@v4.0.0" | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|           persist-credentials: false | ||||
|  | ||||
|       - name: 'Import GPG key' | ||||
|         uses: 'crazy-max/ghaction-import-gpg@v6.0.0' | ||||
|       - name: "Import GPG key" | ||||
|         uses: "crazy-max/ghaction-import-gpg@v6.0.0" | ||||
|         with: | ||||
|           gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} | ||||
|           git_user_signingkey: true | ||||
|           git_commit_gpgsign: true | ||||
|  | ||||
|       - name: 'Setup Node.js' | ||||
|         uses: 'actions/setup-node@v3.8.1' | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v3.8.1" | ||||
|         with: | ||||
|           node-version: '20.x' | ||||
|           cache: 'npm' | ||||
|           node-version: "20.x" | ||||
|           cache: "npm" | ||||
|  | ||||
|       - name: 'Install dependencies' | ||||
|         run: 'npm clean-install' | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|  | ||||
|       - name: 'Release' | ||||
|         run: 'npm run release' | ||||
|       - name: "Release" | ||||
|         run: "npm run release" | ||||
|         env: | ||||
|           GH_TOKEN: ${{ secrets.GH_TOKEN }} | ||||
|           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: | ||||
|   push: | ||||
| @@ -8,41 +8,41 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   test-unit: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v4.0.0' | ||||
|       - uses: "actions/checkout@v4.0.0" | ||||
|  | ||||
|       - name: 'Setup Node.js' | ||||
|         uses: 'actions/setup-node@v3.8.1' | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v3.8.1" | ||||
|         with: | ||||
|           node-version: '20.x' | ||||
|           cache: 'npm' | ||||
|           node-version: "20.x" | ||||
|           cache: "npm" | ||||
|  | ||||
|       - name: 'Install dependencies' | ||||
|         run: 'npm clean-install' | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|  | ||||
|       - name: 'Unit Test' | ||||
|         run: 'npm run test:unit' | ||||
|       - name: "Unit Test" | ||||
|         run: "npm run test:unit" | ||||
|  | ||||
|   test-e2e: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     runs-on: "ubuntu-latest" | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v4.0.0' | ||||
|       - uses: "actions/checkout@v4.0.0" | ||||
|  | ||||
|       - name: 'Setup Node.js' | ||||
|         uses: 'actions/setup-node@v3.8.1' | ||||
|       - name: "Setup Node.js" | ||||
|         uses: "actions/setup-node@v3.8.1" | ||||
|         with: | ||||
|           node-version: '20.x' | ||||
|           cache: 'npm' | ||||
|           node-version: "20.x" | ||||
|           cache: "npm" | ||||
|  | ||||
|       - name: 'Install dependencies' | ||||
|         run: 'npm clean-install' | ||||
|       - name: "Install dependencies" | ||||
|         run: "npm clean-install" | ||||
|  | ||||
|       - name: 'Build' | ||||
|         run: 'npm run build' | ||||
|       - name: "Build" | ||||
|         run: "npm run build" | ||||
|  | ||||
|       - name: 'html-w3c-validator' | ||||
|         run: 'npm run test:html-w3c-validator' | ||||
|       - name: "html-w3c-validator" | ||||
|         run: "npm run test:html-w3c-validator" | ||||
|  | ||||
|       - name: 'End To End (e2e) Test' | ||||
|         run: 'npm run test:e2e' | ||||
|       - name: "End To End (e2e) Test" | ||||
|         run: "npm run test:e2e" | ||||
|   | ||||
							
								
								
									
										10
									
								
								.gitpod.yml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.gitpod.yml
									
									
									
									
									
								
							| @@ -1,13 +1,13 @@ | ||||
| image: 'gitpod/workspace-full' | ||||
| image: "gitpod/workspace-full" | ||||
|  | ||||
| tasks: | ||||
|   - before: 'cp .env.example .env' | ||||
|     init: 'npm clean-install' | ||||
|     command: 'npm run dev' | ||||
|   - before: "cp .env.example .env" | ||||
|     init: "npm clean-install" | ||||
|     command: "npm run dev" | ||||
|  | ||||
| ports: | ||||
|   - port: 3000 | ||||
|     onOpen: 'open-preview' | ||||
|     onOpen: "open-preview" | ||||
|  | ||||
| github: | ||||
|   prebuilds: | ||||
|   | ||||
| @@ -1,6 +1,3 @@ | ||||
| { | ||||
|   "singleQuote": true, | ||||
|   "jsxSingleQuote": true, | ||||
|   "semi": false, | ||||
|   "trailingComma": "none" | ||||
|   "semi": false | ||||
| } | ||||
|   | ||||
| @@ -34,7 +34,7 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv | ||||
| ### Prerequisites | ||||
|  | ||||
| - [Node.js](https://nodejs.org/) >= 20.0.0 | ||||
| - [npm](https://www.npmjs.com/) >= 9.0.0 | ||||
| - [npm](https://www.npmjs.com/) >= 10.0.0 | ||||
|  | ||||
| ### 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 | ||||
| COPY ./package*.json ./ | ||||
| RUN npm clean-install | ||||
|  | ||||
| FROM node:20.6.1 AS builder | ||||
| FROM node:20.9.0 AS builder | ||||
| WORKDIR /usr/src/application | ||||
| COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules | ||||
| COPY ./ ./ | ||||
| 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 | ||||
| ENV NODE_ENV=production | ||||
| 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 => { | ||||
|   return ( | ||||
|     <main className='flex flex-col flex-1 items-center justify-center'> | ||||
|     <main className="flex flex-col flex-1 items-center justify-center"> | ||||
|       <Loader /> | ||||
|     </main> | ||||
|   ) | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import type { Metadata } from 'next' | ||||
| import { notFound } from 'next/navigation' | ||||
| import type { Metadata } from "next" | ||||
| import { notFound } from "next/navigation" | ||||
|  | ||||
| import 'katex/dist/katex.min.css' | ||||
| import "katex/dist/katex.min.css" | ||||
|  | ||||
| import { getBlogPostBySlug } from '@/blog/blog' | ||||
| import { BlogPost } from '@/blog/BlogPost' | ||||
| import { getBlogPostBySlug } from "@/blog/blog" | ||||
| import { BlogPost } from "@/blog/BlogPost" | ||||
|  | ||||
| interface BlogPostPageProps { | ||||
|   params: { | ||||
| @@ -13,7 +13,7 @@ interface BlogPostPageProps { | ||||
| } | ||||
|  | ||||
| export const generateMetadata = async ( | ||||
|   props: BlogPostPageProps | ||||
|   props: BlogPostPageProps, | ||||
| ): Promise<Metadata> => { | ||||
|   const blogPost = await getBlogPostBySlug(props.params.slug) | ||||
|   if (blogPost == null) { | ||||
| @@ -26,12 +26,12 @@ export const generateMetadata = async ( | ||||
|     description, | ||||
|     openGraph: { | ||||
|       title, | ||||
|       description | ||||
|       description, | ||||
|     }, | ||||
|     twitter: { | ||||
|       title, | ||||
|       description | ||||
|     } | ||||
|       description, | ||||
|     }, | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import { Loader } from '@/components/design/Loader' | ||||
| import { Loader } from "@/components/design/Loader" | ||||
|  | ||||
| const Loading = (): JSX.Element => { | ||||
|   return ( | ||||
|     <main className='flex flex-col flex-1 items-center justify-center'> | ||||
|     <main className="flex flex-col flex-1 items-center justify-center"> | ||||
|       <Loader /> | ||||
|     </main> | ||||
|   ) | ||||
|   | ||||
| @@ -1,36 +1,36 @@ | ||||
| import { Suspense } from 'react' | ||||
| import type { Metadata } from 'next' | ||||
| import { Suspense } from "react" | ||||
| import type { Metadata } from "next" | ||||
|  | ||||
| import { BlogPosts } from '@/blog/BlogPosts' | ||||
| import { Loader } from '@/components/design/Loader' | ||||
| import { BlogPosts } from "@/blog/BlogPosts" | ||||
| import { Loader } from "@/components/design/Loader" | ||||
|  | ||||
| const title = 'Blog | Théo LUDWIG' | ||||
| const title = "Blog | Théo LUDWIG" | ||||
| 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 = { | ||||
|   title, | ||||
|   description, | ||||
|   openGraph: { | ||||
|     title, | ||||
|     description | ||||
|     description, | ||||
|   }, | ||||
|   twitter: { | ||||
|     title, | ||||
|     description | ||||
|   } | ||||
|     description, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| const BlogPage = async (): Promise<JSX.Element> => { | ||||
|   return ( | ||||
|     <main className='flex flex-1 flex-col flex-wrap items-center'> | ||||
|       <div className='mt-10 flex flex-col items-center'> | ||||
|         <h1 className='text-4xl font-semibold'>Blog</h1> | ||||
|         <p className='mt-6 text-center' data-cy='blog-post-date'> | ||||
|     <main className="flex flex-1 flex-col flex-wrap items-center"> | ||||
|       <div className="mt-10 flex flex-col items-center"> | ||||
|         <h1 className="text-4xl font-semibold">Blog</h1> | ||||
|         <p className="mt-6 text-center" data-cy="blog-post-date"> | ||||
|           {description} | ||||
|         </p> | ||||
|       </div> | ||||
|       <Suspense fallback={<Loader className='mt-8' />}> | ||||
|       <Suspense fallback={<Loader className="mt-8" />}> | ||||
|         <BlogPosts /> | ||||
|       </Suspense> | ||||
|     </main> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| 'use client' | ||||
| "use client" | ||||
|  | ||||
| import { useEffect } from 'react' | ||||
| import { useEffect } from "react" | ||||
|  | ||||
| export interface ErrorHandlingProps { | ||||
|   error: Error | ||||
| @@ -14,17 +14,17 @@ const ErrorHandling = (props: ErrorHandlingProps): JSX.Element => { | ||||
|   }, [error]) | ||||
|  | ||||
|   return ( | ||||
|     <main className='flex flex-col flex-1 items-center justify-center'> | ||||
|       <h1 className='my-6 text-4xl font-semibold'> | ||||
|         Error{' '} | ||||
|     <main className="flex flex-col flex-1 items-center justify-center"> | ||||
|       <h1 className="my-6 text-4xl font-semibold"> | ||||
|         Error{" "} | ||||
|         <span | ||||
|           className='text-yellow dark:text-yellow-dark' | ||||
|           data-cy='status-code' | ||||
|           className="text-yellow dark:text-yellow-dark" | ||||
|           data-cy="status-code" | ||||
|         > | ||||
|           500 | ||||
|         </span> | ||||
|       </h1> | ||||
|       <p className='text-center text-lg'>Server error</p> | ||||
|       <p className="text-center text-lg">Server error</p> | ||||
|     </main> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
| } | ||||
|  | ||||
| .prose [id]::before { | ||||
|   content: ''; | ||||
|   content: ""; | ||||
|   display: block; | ||||
|   height: 90px; | ||||
|   margin-top: -90px; | ||||
| @@ -39,9 +39,9 @@ | ||||
| .prose code { | ||||
|   color: #ce9178; | ||||
| } | ||||
| .prose :where(code):not(:where([class~='not-prose'] *))::before, | ||||
| .prose :where(code):not(:where([class~='not-prose'] *))::after { | ||||
|   content: ''; | ||||
| .prose :where(code):not(:where([class~="not-prose"] *))::before, | ||||
| .prose :where(code):not(:where([class~="not-prose"] *))::after { | ||||
|   content: ""; | ||||
| } | ||||
| .shiki { | ||||
|   white-space: pre-wrap !important; | ||||
|   | ||||
| @@ -1,21 +1,21 @@ | ||||
| import type { Metadata } from 'next' | ||||
| import classNames from 'clsx' | ||||
| import type { Metadata } from "next" | ||||
| import classNames from "clsx" | ||||
|  | ||||
| import '@fontsource/montserrat/400.css' | ||||
| import '@fontsource/montserrat/600.css' | ||||
| import './globals.css' | ||||
| import "@fontsource/montserrat/400.css" | ||||
| import "@fontsource/montserrat/600.css" | ||||
| import "./globals.css" | ||||
|  | ||||
| import { Header } from '@/components/Header' | ||||
| import { Footer } from '@/components/Footer' | ||||
| import { getI18n } from '@/i18n/i18n.server' | ||||
| import { getTheme } from '@/theme/theme.server' | ||||
| import { Header } from "@/components/Header" | ||||
| import { Footer } from "@/components/Footer" | ||||
| import { getI18n } from "@/i18n/i18n.server" | ||||
| import { getTheme } from "@/theme/theme.server" | ||||
|  | ||||
| const title = 'Théo LUDWIG' | ||||
| const title = "Théo LUDWIG" | ||||
| const description = | ||||
|   'Théo LUDWIG - Developer Full Stack • Open-Source enthusiast' | ||||
| const image = '/images/icon-96x96.png' | ||||
| const url = new URL('https://theoludwig.fr') | ||||
| const locale = 'fr-FR, en-US' | ||||
|   "Théo LUDWIG - Developer Full Stack • Open-Source enthusiast" | ||||
| const image = "/images/icon-96x96.png" | ||||
| const url = new URL("https://theoludwig.fr") | ||||
| const locale = "fr-FR, en-US" | ||||
|  | ||||
| export const metadata: Metadata = { | ||||
|   title, | ||||
| @@ -30,21 +30,21 @@ export const metadata: Metadata = { | ||||
|       { | ||||
|         url: image, | ||||
|         width: 96, | ||||
|         height: 96 | ||||
|       } | ||||
|         height: 96, | ||||
|       }, | ||||
|     ], | ||||
|     locale, | ||||
|     type: 'website' | ||||
|     type: "website", | ||||
|   }, | ||||
|   icons: { | ||||
|     icon: '/images/icon-96x96.png' | ||||
|     icon: "/images/icon-96x96.png", | ||||
|   }, | ||||
|   twitter: { | ||||
|     card: 'summary', | ||||
|     card: "summary", | ||||
|     title, | ||||
|     description, | ||||
|     images: [image] | ||||
|   } | ||||
|     images: [image], | ||||
|   }, | ||||
| } | ||||
|  | ||||
| interface RootLayoutProps { | ||||
| @@ -61,14 +61,14 @@ const RootLayout = (props: RootLayoutProps): JSX.Element => { | ||||
|     <html | ||||
|       lang={i18n.locale} | ||||
|       className={classNames({ | ||||
|         dark: theme === 'dark', | ||||
|         light: theme === 'light' | ||||
|         dark: theme === "dark", | ||||
|         light: theme === "light", | ||||
|       })} | ||||
|       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 /> | ||||
|         {children} | ||||
|         <Footer /> | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import { Loader } from '@/components/design/Loader' | ||||
| import { Loader } from "@/components/design/Loader" | ||||
|  | ||||
| const Loading = (): JSX.Element => { | ||||
|   return ( | ||||
|     <main className='flex flex-col flex-1 items-center justify-center'> | ||||
|     <main className="flex flex-col flex-1 items-center justify-center"> | ||||
|       <Loader /> | ||||
|     </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 i18n = getI18n() | ||||
|  | ||||
|   return ( | ||||
|     <main className='flex flex-col flex-1 items-center justify-center'> | ||||
|       <h1 className='my-6 text-4xl font-semibold'> | ||||
|         {i18n.translate('errors.error')}{' '} | ||||
|     <main className="flex flex-col flex-1 items-center justify-center"> | ||||
|       <h1 className="my-6 text-4xl font-semibold"> | ||||
|         {i18n.translate("errors.error")}{" "} | ||||
|         <span | ||||
|           className='text-yellow dark:text-yellow-dark' | ||||
|           data-cy='status-code' | ||||
|           className="text-yellow dark:text-yellow-dark" | ||||
|           data-cy="status-code" | ||||
|         > | ||||
|           404 | ||||
|         </span> | ||||
|       </h1> | ||||
|       <p className='text-center text-lg'> | ||||
|         {i18n.translate('errors.not-found')}{' '} | ||||
|       <p className="text-center text-lg"> | ||||
|         {i18n.translate("errors.not-found")}{" "} | ||||
|         <Link | ||||
|           href='/' | ||||
|           className='text-yellow hover:underline dark:text-yellow-dark' | ||||
|           href="/" | ||||
|           className="text-yellow hover:underline dark:text-yellow-dark" | ||||
|         > | ||||
|           {i18n.translate('errors.return-to-home-page')} | ||||
|           {i18n.translate("errors.return-to-home-page")} | ||||
|         </Link> | ||||
|       </p> | ||||
|     </main> | ||||
|   | ||||
							
								
								
									
										36
									
								
								app/page.tsx
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								app/page.tsx
									
									
									
									
									
								
							| @@ -1,27 +1,27 @@ | ||||
| import { RevealFade } from '@/components/design/RevealFade' | ||||
| import { Section } from '@/components/design/Section' | ||||
| import { Interests } from '@/components/Interests' | ||||
| import { Portfolio } from '@/components/Portfolio' | ||||
| import { Profile } from '@/components/Profile' | ||||
| import { SocialMediaList } from '@/components/Profile/SocialMediaList' | ||||
| import { Skills } from '@/components/Skills' | ||||
| import { OpenSource } from '@/components/OpenSource' | ||||
| import { getI18n } from '@/i18n/i18n.server' | ||||
| import { RevealFade } from "@/components/design/RevealFade" | ||||
| import { Section } from "@/components/design/Section" | ||||
| import { Interests } from "@/components/Interests" | ||||
| import { Portfolio } from "@/components/Portfolio" | ||||
| import { Profile } from "@/components/Profile" | ||||
| import { SocialMediaList } from "@/components/Profile/SocialMediaList" | ||||
| import { Skills } from "@/components/Skills" | ||||
| import { OpenSource } from "@/components/OpenSource" | ||||
| import { getI18n } from "@/i18n/i18n.server" | ||||
|  | ||||
| const HomePage = (): JSX.Element => { | ||||
|   const i18n = getI18n() | ||||
|  | ||||
|   return ( | ||||
|     <main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'> | ||||
|       <Section isMain id='about'> | ||||
|     <main className="flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl"> | ||||
|       <Section isMain id="about"> | ||||
|         <Profile /> | ||||
|         <SocialMediaList /> | ||||
|       </Section> | ||||
|  | ||||
|       <RevealFade> | ||||
|         <Section | ||||
|           id='interests' | ||||
|           heading={i18n.translate('home.interests.title')} | ||||
|           id="interests" | ||||
|           heading={i18n.translate("home.interests.title")} | ||||
|         > | ||||
|           <Interests /> | ||||
|         </Section> | ||||
| @@ -29,8 +29,8 @@ const HomePage = (): JSX.Element => { | ||||
|  | ||||
|       <RevealFade> | ||||
|         <Section | ||||
|           id='skills' | ||||
|           heading={i18n.translate('home.skills.title')} | ||||
|           id="skills" | ||||
|           heading={i18n.translate("home.skills.title")} | ||||
|           withoutShadowContainer | ||||
|         > | ||||
|           <Skills /> | ||||
| @@ -39,8 +39,8 @@ const HomePage = (): JSX.Element => { | ||||
|  | ||||
|       <RevealFade> | ||||
|         <Section | ||||
|           id='portfolio' | ||||
|           heading={i18n.translate('home.portfolio.title')} | ||||
|           id="portfolio" | ||||
|           heading={i18n.translate("home.portfolio.title")} | ||||
|           withoutShadowContainer | ||||
|         > | ||||
|           <Portfolio /> | ||||
| @@ -48,7 +48,7 @@ const HomePage = (): JSX.Element => { | ||||
|       </RevealFade> | ||||
|  | ||||
|       <RevealFade> | ||||
|         <Section id='open-source' heading='Open source' withoutShadowContainer> | ||||
|         <Section id="open-source" heading="Open source" withoutShadowContainer> | ||||
|           <OpenSource /> | ||||
|         </Section> | ||||
|       </RevealFade> | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { notFound } from 'next/navigation' | ||||
| import date from 'date-and-time' | ||||
| import { notFound } from "next/navigation" | ||||
| import date from "date-and-time" | ||||
|  | ||||
| import 'katex/dist/katex.min.css' | ||||
| import "katex/dist/katex.min.css" | ||||
|  | ||||
| import { getBlogPostBySlug } from '@/blog/blog' | ||||
| import { BlogPostContent } from '@/blog/BlogPostContent' | ||||
| import { getBlogPostBySlug } from "@/blog/blog" | ||||
| import { BlogPostContent } from "@/blog/BlogPostContent" | ||||
|  | ||||
| export interface BlogPostProps { | ||||
|   slug: string | ||||
| @@ -19,13 +19,13 @@ export const BlogPost = async (props: BlogPostProps): Promise<JSX.Element> => { | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <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'> | ||||
|         <h1 className='text-3xl font-semibold'>{blogPost.frontmatter.title}</h1> | ||||
|         <p className='mt-2' data-cy='blog-post-date'> | ||||
|     <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"> | ||||
|         <h1 className="text-3xl font-semibold">{blogPost.frontmatter.title}</h1> | ||||
|         <p className="mt-2" data-cy="blog-post-date"> | ||||
|           {date.format( | ||||
|             new Date(blogPost.frontmatter.publishedOn), | ||||
|             'DD/MM/YYYY' | ||||
|             "DD/MM/YYYY", | ||||
|           )} | ||||
|         </p> | ||||
|       </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 type { CookiesStore } from '@/utils/constants' | ||||
| import { useTheme } from "@/theme/theme.client" | ||||
| import type { CookiesStore } from "@/utils/constants" | ||||
|  | ||||
| interface BlogPostCommentsProps { | ||||
|   cookiesStore: CookiesStore | ||||
| @@ -16,18 +16,18 @@ export const BlogPostComments = (props: BlogPostCommentsProps): JSX.Element => { | ||||
|  | ||||
|   return ( | ||||
|     <Giscus | ||||
|       id='comments' | ||||
|       repo='theoludwig/theoludwig' | ||||
|       repoId='MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ=' | ||||
|       category='General' | ||||
|       categoryId='DIC_kwDOFWUewM4CQ_WK' | ||||
|       mapping='pathname' | ||||
|       reactionsEnabled='1' | ||||
|       emitMetadata='0' | ||||
|       inputPosition='top' | ||||
|       id="comments" | ||||
|       repo="theoludwig/theoludwig" | ||||
|       repoId="MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ=" | ||||
|       category="General" | ||||
|       categoryId="DIC_kwDOFWUewM4CQ_WK" | ||||
|       mapping="pathname" | ||||
|       reactionsEnabled="1" | ||||
|       emitMetadata="0" | ||||
|       inputPosition="top" | ||||
|       theme={theme} | ||||
|       lang='en' | ||||
|       loading='lazy' | ||||
|       lang="en" | ||||
|       loading="lazy" | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,37 +1,37 @@ | ||||
| import Image from 'next/image' | ||||
| import Link from 'next/link' | ||||
| import { cookies } from 'next/headers' | ||||
| import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' | ||||
| import { faLink } from '@fortawesome/free-solid-svg-icons' | ||||
| import { MDXRemote } from 'next-mdx-remote/rsc' | ||||
| import { nodeTypes } from '@mdx-js/mdx' | ||||
| import rehypeRaw from 'rehype-raw' | ||||
| import remarkGfm from 'remark-gfm' | ||||
| import rehypeSlug from 'rehype-slug' | ||||
| import remarkMath from 'remark-math' | ||||
| import rehypeKatex from 'rehype-katex' | ||||
| import { getHighlighter } from 'shiki' | ||||
| import Image from "next/image" | ||||
| import Link from "next/link" | ||||
| import { cookies } from "next/headers" | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" | ||||
| import { faLink } from "@fortawesome/free-solid-svg-icons" | ||||
| import { MDXRemote } from "next-mdx-remote/rsc" | ||||
| import { nodeTypes } from "@mdx-js/mdx" | ||||
| import rehypeRaw from "rehype-raw" | ||||
| import remarkGfm from "remark-gfm" | ||||
| import rehypeSlug from "rehype-slug" | ||||
| import remarkMath from "remark-math" | ||||
| import rehypeKatex from "rehype-katex" | ||||
| import { getHighlighter } from "shiki" | ||||
|  | ||||
| import 'katex/dist/katex.min.css' | ||||
| import "katex/dist/katex.min.css" | ||||
|  | ||||
| import { getTheme } from '@/theme/theme.server' | ||||
| import { remarkSyntaxHighlightingPlugin } from '@/blog/remarkSyntaxHighlightingPlugin' | ||||
| import { BlogPostComments } from '@/blog/BlogPostComments' | ||||
| import { getTheme } from "@/theme/theme.server" | ||||
| import { remarkSyntaxHighlightingPlugin } from "@/blog/remarkSyntaxHighlightingPlugin" | ||||
| import { BlogPostComments } from "@/blog/BlogPostComments" | ||||
|  | ||||
| const Heading = ( | ||||
|   props: React.DetailedHTMLProps< | ||||
|     React.HTMLAttributes<HTMLHeadingElement>, | ||||
|     HTMLHeadingElement | ||||
|   > | ||||
|   >, | ||||
| ): JSX.Element => { | ||||
|   const { children, id = '' } = props | ||||
|   const { children, id = "" } = props | ||||
|   return ( | ||||
|     <h2 {...props} className='group'> | ||||
|     <h2 {...props} className="group"> | ||||
|       <Link | ||||
|         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> | ||||
|       {children} | ||||
|     </h2> | ||||
| @@ -43,7 +43,7 @@ export interface BlogPostContentProps { | ||||
| } | ||||
|  | ||||
| export const BlogPostContent = async ( | ||||
|   props: BlogPostContentProps | ||||
|   props: BlogPostContentProps, | ||||
| ): Promise<JSX.Element> => { | ||||
|   const { content } = props | ||||
|  | ||||
| @@ -51,12 +51,12 @@ export const BlogPostContent = async ( | ||||
|   const theme = getTheme() | ||||
|  | ||||
|   const highlighter = await getHighlighter({ | ||||
|     theme: `${theme}-plus` | ||||
|     theme: `${theme}-plus`, | ||||
|   }) | ||||
|  | ||||
|   return ( | ||||
|     <div className='prose mb-10'> | ||||
|       <div className='px-8'> | ||||
|     <div className="prose mb-10"> | ||||
|       <div className="px-8"> | ||||
|         <MDXRemote | ||||
|           source={content} | ||||
|           options={{ | ||||
| @@ -64,14 +64,14 @@ export const BlogPostContent = async ( | ||||
|               remarkPlugins: [ | ||||
|                 remarkGfm, | ||||
|                 [remarkSyntaxHighlightingPlugin, { highlighter }], | ||||
|                 remarkMath | ||||
|                 remarkMath, | ||||
|               ], | ||||
|               rehypePlugins: [ | ||||
|                 rehypeSlug, | ||||
|                 [rehypeRaw, { passThrough: nodeTypes }], | ||||
|                 rehypeKatex | ||||
|               ] | ||||
|             } | ||||
|                 rehypeKatex, | ||||
|               ], | ||||
|             }, | ||||
|           }} | ||||
|           components={{ | ||||
|             h1: Heading, | ||||
| @@ -81,27 +81,27 @@ export const BlogPostContent = async ( | ||||
|             h5: Heading, | ||||
|             h6: Heading, | ||||
|             img: (properties) => { | ||||
|               const { src = '', alt = 'Blog Image' } = properties | ||||
|               const source = src.replace('../../public/', '/') | ||||
|               const { src = "", alt = "Blog Image" } = properties | ||||
|               const source = src.replace("../../public/", "/") | ||||
|               return ( | ||||
|                 <span className='flex flex-col items-center justify-center'> | ||||
|                 <span className="flex flex-col items-center justify-center"> | ||||
|                   <Image | ||||
|                     src={source} | ||||
|                     alt={alt} | ||||
|                     width={1000} | ||||
|                     height={1000} | ||||
|                     className='h-auto w-auto' | ||||
|                     className="h-auto w-auto" | ||||
|                   /> | ||||
|                 </span> | ||||
|               ) | ||||
|             }, | ||||
|             a: (props) => { | ||||
|               const { href = '' } = props | ||||
|               if (href.startsWith('#')) { | ||||
|               const { href = "" } = props | ||||
|               if (href.startsWith("#")) { | ||||
|                 return <a {...props} /> | ||||
|               } | ||||
|               return <a target='_blank' rel='noopener noreferrer' {...props} /> | ||||
|             } | ||||
|               return <a target="_blank" rel="noopener noreferrer" {...props} /> | ||||
|             }, | ||||
|           }} | ||||
|         /> | ||||
|         <BlogPostComments cookiesStore={cookiesStore.toString()} /> | ||||
|   | ||||
| @@ -1,35 +1,35 @@ | ||||
| import Link from 'next/link' | ||||
| import date from 'date-and-time' | ||||
| import Link from "next/link" | ||||
| import date from "date-and-time" | ||||
|  | ||||
| import { ShadowContainer } from '@/components/design/ShadowContainer' | ||||
| import { getBlogPosts } from '@/blog/blog' | ||||
| import { ShadowContainer } from "@/components/design/ShadowContainer" | ||||
| import { getBlogPosts } from "@/blog/blog" | ||||
|  | ||||
| export const BlogPosts = async (): Promise<JSX.Element> => { | ||||
|   const posts = await getBlogPosts() | ||||
|  | ||||
|   return ( | ||||
|     <div className='flex w-full items-center justify-center p-8'> | ||||
|       <div className='w-[1600px]' data-cy='blog-posts'> | ||||
|     <div className="flex w-full items-center justify-center p-8"> | ||||
|       <div className="w-[1600px]" data-cy="blog-posts"> | ||||
|         {posts.map((post, index) => { | ||||
|           const postPublishedOn = date.format( | ||||
|             new Date(post.frontmatter.publishedOn), | ||||
|             'DD/MM/YYYY' | ||||
|             "DD/MM/YYYY", | ||||
|           ) | ||||
|           return ( | ||||
|             <Link | ||||
|               href={`/blog/${post.slug}`} | ||||
|               key={index} | ||||
|               locale='en' | ||||
|               locale="en" | ||||
|               data-cy={post.slug} | ||||
|             > | ||||
|               <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'> | ||||
|               <ShadowContainer className="cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2"> | ||||
|                 <h2 data-cy="blog-post-title" className="text-xl font-semibold"> | ||||
|                   {post.frontmatter.title} | ||||
|                 </h2> | ||||
|                 <p data-cy='blog-post-date' className='mt-2'> | ||||
|                 <p data-cy="blog-post-date" className="mt-2"> | ||||
|                   {postPublishedOn} | ||||
|                 </p> | ||||
|                 <p data-cy='blog-post-description' className='mt-3'> | ||||
|                 <p data-cy="blog-post-description" className="mt-3"> | ||||
|                   {post.frontmatter.description} | ||||
|                 </p> | ||||
|               </ShadowContainer> | ||||
|   | ||||
							
								
								
									
										22
									
								
								blog/blog.ts
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								blog/blog.ts
									
									
									
									
									
								
							| @@ -1,10 +1,10 @@ | ||||
| import fs from 'node:fs' | ||||
| import path from 'node:path' | ||||
| import fs from "node:fs" | ||||
| import path from "node:path" | ||||
|  | ||||
| import { cache } from 'react' | ||||
| import matter from 'gray-matter' | ||||
| import { cache } from "react" | ||||
| 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 { | ||||
|   title: string | ||||
| @@ -23,13 +23,13 @@ export const getBlogPosts = cache(async (): Promise<BlogPost[]> => { | ||||
|   const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH) | ||||
|   const blogPostsWithTime = await Promise.all( | ||||
|     blogPosts.map(async (blogPostFilename) => { | ||||
|       const [slug, extension] = blogPostFilename.split('.') | ||||
|       const [slug, extension] = blogPostFilename.split(".") | ||||
|       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 blogPostContent = await fs.promises.readFile(blogPostPath, { | ||||
|         encoding: 'utf8' | ||||
|         encoding: "utf8", | ||||
|       }) | ||||
|       const { data, content } = matter(blogPostContent) as unknown as { | ||||
|         data: FrontMatter | ||||
| @@ -40,9 +40,9 @@ export const getBlogPosts = cache(async (): Promise<BlogPost[]> => { | ||||
|         slug, | ||||
|         content, | ||||
|         frontmatter: data, | ||||
|         time: date.getTime() | ||||
|         time: date.getTime(), | ||||
|       } | ||||
|     }) | ||||
|     }), | ||||
|   ) | ||||
|   const blogPostsSortedByPublicationDate = blogPostsWithTime | ||||
|     .filter((post) => { | ||||
| @@ -61,5 +61,5 @@ export const getBlogPostBySlug = cache( | ||||
|       return blogPost.slug === slug && blogPost.frontmatter.isPublished | ||||
|     }) | ||||
|     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.' | ||||
| isPublished: true | ||||
| publishedOn: '2022-02-23T08:00:18.758Z' | ||||
| publishedOn: "2022-02-23T08:00:18.758Z" | ||||
| --- | ||||
|  | ||||
| Hello! 👋 | ||||
| @@ -110,7 +110,7 @@ const transaction = charge(user, subscription) | ||||
| ```typescript | ||||
| interface Car { | ||||
|   carModel: string | ||||
|   carColor: 'red' | 'blue' | 'yellow' | ||||
|   carColor: "red" | "blue" | "yellow" | ||||
| } | ||||
| const printCar = (car: Car): void => { | ||||
|   console.log(`${car.carModel} (${car.carColor})`) | ||||
| @@ -122,7 +122,7 @@ const printCar = (car: Car): void => { | ||||
| ```typescript | ||||
| interface Car { | ||||
|   model: string | ||||
|   color: 'red' | 'blue' | 'yellow' | ||||
|   color: "red" | "blue" | "yellow" | ||||
| } | ||||
| const printCar = (car: Car): void => { | ||||
|   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) | ||||
|  | ||||
| ```typescript | ||||
| import fs from 'node:fs' | ||||
| import path from 'node:path' | ||||
| import fs from "node:fs" | ||||
| import path from "node:path" | ||||
|  | ||||
| const createFile = async ( | ||||
|   name: string, | ||||
|   isTemporary: boolean = false | ||||
|   isTemporary: boolean = false, | ||||
| ): Promise<void> => { | ||||
|   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) | ||||
|  | ||||
| ```typescript | ||||
| import fs from 'node:fs' | ||||
| import path from 'node:path' | ||||
| import fs from "node:fs" | ||||
| import path from "node:path" | ||||
|  | ||||
| const createFile = async (name: string): Promise<void> => { | ||||
|   await fs.promises.writeFile(name, '') | ||||
|   await fs.promises.writeFile(name, "") | ||||
| } | ||||
|  | ||||
| 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' | ||||
| 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.' | ||||
| 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." | ||||
| isPublished: true | ||||
| publishedOn: '2022-10-27T14:33:07.465Z' | ||||
| publishedOn: "2022-10-27T14:33:07.465Z" | ||||
| --- | ||||
|  | ||||
| Hello! 👋 | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| title: '👋 Hello, world!' | ||||
| description: 'First post of the blog, introduction and explanation of how this blog is made.' | ||||
| title: "👋 Hello, world!" | ||||
| description: "First post of the blog, introduction and explanation of how this blog is made." | ||||
| isPublished: true | ||||
| publishedOn: '2022-02-20T08:00:18.758Z' | ||||
| publishedOn: "2022-02-20T08:00:18.758Z" | ||||
| --- | ||||
|  | ||||
| Hello, world! 👋 | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| 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.' | ||||
| 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." | ||||
| isPublished: true | ||||
| publishedOn: '2022-03-14T07:42:52.989Z' | ||||
| publishedOn: "2022-03-14T07:42:52.989Z" | ||||
| --- | ||||
|  | ||||
| Hello! 👋 | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| title: '🧠 Programming Challenges' | ||||
| description: 'What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation.' | ||||
| title: "🧠 Programming Challenges" | ||||
| description: "What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation." | ||||
| isPublished: true | ||||
| publishedOn: '2023-05-21T10:20:18.837Z' | ||||
| publishedOn: "2023-05-21T10:20:18.837Z" | ||||
| --- | ||||
|  | ||||
| Hello! 👋 | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| --- | ||||
| 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.' | ||||
| 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." | ||||
| isPublished: true | ||||
| publishedOn: '2022-04-11T10:24:55.206Z' | ||||
| publishedOn: "2022-04-11T10:24:55.206Z" | ||||
| --- | ||||
|  | ||||
| Hello! 👋 | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import type { Plugin, Transformer } from 'unified' | ||||
| import type { Literal, Node } from 'unist' | ||||
| import { visit } from 'unist-util-visit' | ||||
| import type { Highlighter } from 'shiki' | ||||
| import type { Plugin, Transformer } from "unified" | ||||
| import type { Literal, Node } from "unist" | ||||
| import { visit } from "unist-util-visit" | ||||
| import type { Highlighter } from "shiki" | ||||
|  | ||||
| export interface RemarkSyntaxHighlightingPluginOptions { | ||||
|   highlighter: Highlighter | ||||
| @@ -20,11 +20,11 @@ export const remarkSyntaxHighlightingPlugin: Plugin< | ||||
|   Literal | ||||
| > = (options) => { | ||||
|   const transformer: Transformer<RemarkSyntaxHighlightingNode> = (tree) => { | ||||
|     visit<RemarkSyntaxHighlightingNode, string>(tree, 'code', (node) => { | ||||
|       node.type = 'html' | ||||
|     visit<RemarkSyntaxHighlightingNode, string>(tree, "code", (node) => { | ||||
|       node.type = "html" | ||||
|       node.children = undefined | ||||
|       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 => { | ||||
|   const i18n = getI18n() | ||||
| @@ -8,12 +8,12 @@ export const FooterText = (): JSX.Element => { | ||||
|   return ( | ||||
|     <p> | ||||
|       <Link | ||||
|         href='/' | ||||
|         className='text-yellow hover:underline dark:text-yellow-dark' | ||||
|         href="/" | ||||
|         className="text-yellow hover:underline dark:text-yellow-dark" | ||||
|       > | ||||
|         Théo LUDWIG | ||||
|       </Link>{' '} | ||||
|       | {i18n.translate('common.all-rights-reserved')} | ||||
|       </Link>{" "} | ||||
|       | {i18n.translate("common.all-rights-reserved")} | ||||
|     </p> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { useMemo } from 'react' | ||||
| import { useMemo } from "react" | ||||
|  | ||||
| interface FooterVersionProps { | ||||
|   version: string | ||||
| @@ -12,14 +12,14 @@ export const FooterVersion = (props: FooterVersionProps): JSX.Element => { | ||||
|   }, [version]) | ||||
|  | ||||
|   return ( | ||||
|     <p className='mt-1'> | ||||
|       Version{' '} | ||||
|     <p className="mt-1"> | ||||
|       Version{" "} | ||||
|       <a | ||||
|         data-cy='version-link' | ||||
|         className='text-yellow hover:underline dark:text-yellow-dark' | ||||
|         data-cy="version-link" | ||||
|         className="text-yellow hover:underline dark:text-yellow-dark" | ||||
|         href={versionLink} | ||||
|         target='_blank' | ||||
|         rel='noopener noreferrer' | ||||
|         target="_blank" | ||||
|         rel="noopener noreferrer" | ||||
|       > | ||||
|         {version} | ||||
|       </a> | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { FooterText } from './FooterText' | ||||
| import { FooterVersion } from './FooterVersion' | ||||
| import { FooterText } from "./FooterText" | ||||
| import { FooterVersion } from "./FooterVersion" | ||||
|  | ||||
| export const Footer = async (): Promise<JSX.Element> => { | ||||
|   const { readPackage } = await import('read-pkg') | ||||
|   const { readPackage } = await import("read-pkg") | ||||
|   const { version } = await readPackage() | ||||
|  | ||||
|   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 /> | ||||
|       <FooterVersion version={version} /> | ||||
|     </footer> | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| export const Arrow = (): JSX.Element => { | ||||
|   return ( | ||||
|     <svg | ||||
|       width='12' | ||||
|       height='8' | ||||
|       viewBox='0 0 12 8' | ||||
|       fill='none' | ||||
|       xmlns='http://www.w3.org/2000/svg' | ||||
|       width="12" | ||||
|       height="8" | ||||
|       viewBox="0 0 12 8" | ||||
|       fill="none" | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|     > | ||||
|       <path | ||||
|         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' | ||||
|         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" | ||||
|       /> | ||||
|     </svg> | ||||
|   ) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import Image from 'next/image' | ||||
| import Image from "next/image" | ||||
|  | ||||
| import type { CookiesStore } from '@/utils/constants' | ||||
| import { useI18n } from '@/i18n/i18n.client' | ||||
| import type { CookiesStore } from "@/utils/constants" | ||||
| import { useI18n } from "@/i18n/i18n.client" | ||||
|  | ||||
| export interface LocaleFlagProps { | ||||
|   locale: string | ||||
| @@ -22,7 +22,7 @@ export const LocaleFlag = (props: LocaleFlagProps): JSX.Element => { | ||||
|         src={`/images/locales/${locale}.svg`} | ||||
|         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}`)} | ||||
|       </p> | ||||
|     </> | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| 'use client' | ||||
| "use client" | ||||
|  | ||||
| import { usePathname } from 'next/navigation' | ||||
| import { useCallback, useEffect, useState, useRef } from 'react' | ||||
| import classNames from 'clsx' | ||||
| import { usePathname } from "next/navigation" | ||||
| import { useCallback, useEffect, useState, useRef } from "react" | ||||
| import classNames from "clsx" | ||||
|  | ||||
| import type { Locale as LocaleType, CookiesStore } from '@/utils/constants' | ||||
| import { LOCALES } from '@/utils/constants' | ||||
| import type { Locale as LocaleType, CookiesStore } from "@/utils/constants" | ||||
| import { LOCALES } from "@/utils/constants" | ||||
|  | ||||
| import { Arrow } from './Arrow' | ||||
| import { LocaleFlag } from './LocaleFlag' | ||||
| import { Arrow } from "./Arrow" | ||||
| import { LocaleFlag } from "./LocaleFlag" | ||||
|  | ||||
| export interface LocalesProps { | ||||
|   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 window.removeEventListener('click', handleClickEvent) | ||||
|       return window.removeEventListener("click", handleClickEvent) | ||||
|     } | ||||
|   }, []) | ||||
|  | ||||
|   const handleLocale = async (locale: LocaleType): Promise<void> => { | ||||
|     const { setLocale } = await import('@/i18n/i18n.server') | ||||
|     const { setLocale } = await import("@/i18n/i18n.server") | ||||
|     setLocale(locale) | ||||
|   } | ||||
|  | ||||
|   if (pathname.startsWith('/blog')) { | ||||
|   if (pathname.startsWith("/blog")) { | ||||
|     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 | ||||
|         ref={languageClickRef} | ||||
|         data-cy='locale-click' | ||||
|         className='mr-5 flex items-center' | ||||
|         data-cy="locale-click" | ||||
|         className="mr-5 flex items-center" | ||||
|         onClick={handleHiddenMenu} | ||||
|       > | ||||
|         <LocaleFlag | ||||
| @@ -70,10 +70,10 @@ export const Locales = (props: LocalesProps): JSX.Element => { | ||||
|       </div> | ||||
|  | ||||
|       <ul | ||||
|         data-cy='locales-list' | ||||
|         data-cy="locales-list" | ||||
|         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', | ||||
|           { hidden: hiddenMenu } | ||||
|           "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 }, | ||||
|         )} | ||||
|       > | ||||
|         {LOCALES.filter((locale) => { | ||||
| @@ -82,7 +82,7 @@ export const Locales = (props: LocalesProps): JSX.Element => { | ||||
|           return ( | ||||
|             <li | ||||
|               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 () => { | ||||
|                 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 type { CookiesStore } from '@/utils/constants' | ||||
| import { useTheme } from "@/theme/theme.client" | ||||
| import type { CookiesStore } from "@/utils/constants" | ||||
|  | ||||
| export interface SwitchThemeProps { | ||||
|   cookiesStore: CookiesStore | ||||
| @@ -14,63 +14,63 @@ export const SwitchTheme = (props: SwitchThemeProps): JSX.Element => { | ||||
|   const theme = useTheme(cookiesStore) | ||||
|  | ||||
|   const handleClick = async (): Promise<void> => { | ||||
|     const { setTheme } = await import('@/theme/theme.server') | ||||
|     const newTheme = theme === 'dark' ? 'light' : 'dark' | ||||
|     const { setTheme } = await import("@/theme/theme.server") | ||||
|     const newTheme = theme === "dark" ? "light" : "dark" | ||||
|     setTheme(newTheme) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       className='flex items-center' | ||||
|       data-cy='switch-theme-click' | ||||
|       className="flex items-center" | ||||
|       data-cy="switch-theme-click" | ||||
|       onClick={handleClick} | ||||
|     > | ||||
|       <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="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 | ||||
|             data-cy='switch-theme-dark' | ||||
|             data-cy="switch-theme-dark" | ||||
|             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-0': theme === 'light' | ||||
|               } | ||||
|                 "opacity-100": theme === "dark", | ||||
|                 "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> | ||||
|           </div> | ||||
|           <div | ||||
|             data-cy='switch-theme-light' | ||||
|             data-cy="switch-theme-light" | ||||
|             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-0': theme === 'dark' | ||||
|               } | ||||
|                 "opacity-100": theme === "light", | ||||
|                 "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> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div | ||||
|           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-0': theme === 'light' | ||||
|             } | ||||
|               "left-[27px]": theme === "dark", | ||||
|               "left-0": theme === "light", | ||||
|             }, | ||||
|           )} | ||||
|           style={{ border: '1px solid #4d4d4d' }} | ||||
|           style={{ border: "1px solid #4d4d4d" }} | ||||
|         /> | ||||
|         <input | ||||
|           data-cy='switch-theme-input' | ||||
|           type='checkbox' | ||||
|           aria-label='Dark mode toggle' | ||||
|           className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0 hidden' | ||||
|           data-cy="switch-theme-input" | ||||
|           type="checkbox" | ||||
|           aria-label="Dark mode toggle" | ||||
|           className="absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0 hidden" | ||||
|           defaultChecked | ||||
|         /> | ||||
|       </div> | ||||
|   | ||||
| @@ -1,39 +1,39 @@ | ||||
| import { cookies } from 'next/headers' | ||||
| import Link from 'next/link' | ||||
| import Image from 'next/image' | ||||
| import { cookies } from "next/headers" | ||||
| import Link from "next/link" | ||||
| import Image from "next/image" | ||||
|  | ||||
| import { getI18n } from '@/i18n/i18n.server' | ||||
| import { getI18n } from "@/i18n/i18n.server" | ||||
|  | ||||
| import { Locales } from './Locales' | ||||
| import { SwitchTheme } from './SwitchTheme' | ||||
| import { Locales } from "./Locales" | ||||
| import { SwitchTheme } from "./SwitchTheme" | ||||
|  | ||||
| export const Header = (): JSX.Element => { | ||||
|   const cookiesStore = cookies() | ||||
|   const i18n = getI18n() | ||||
|  | ||||
|   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'> | ||||
|       <Link href='/'> | ||||
|         <div className='flex items-center justify-center'> | ||||
|     <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="/"> | ||||
|         <div className="flex items-center justify-center"> | ||||
|           <Image | ||||
|             quality={100} | ||||
|             width={60} | ||||
|             height={60} | ||||
|             src='/images/icon_small.png' | ||||
|             alt='Théo LUDWIG' | ||||
|             src="/images/icon_small.png" | ||||
|             alt="Théo LUDWIG" | ||||
|             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 | ||||
|           </strong> | ||||
|         </div> | ||||
|       </Link> | ||||
|       <div className='flex justify-between'> | ||||
|         <div className='flex flex-col items-center justify-center px-6'> | ||||
|       <div className="flex justify-between"> | ||||
|         <div className="flex flex-col items-center justify-center px-6"> | ||||
|           <Link | ||||
|             href='/blog' | ||||
|             data-cy='header-blog-link' | ||||
|             className='text-yellow hover:underline dark:text-yellow-dark' | ||||
|             href="/blog" | ||||
|             data-cy="header-blog-link" | ||||
|             className="text-yellow hover:underline dark:text-yellow-dark" | ||||
|           > | ||||
|             Blog | ||||
|           </Link> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import htmlParser from 'html-react-parser' | ||||
| import htmlParser from "html-react-parser" | ||||
|  | ||||
| export interface InterestParagraphProps { | ||||
|   title: string | ||||
| @@ -6,14 +6,14 @@ export interface InterestParagraphProps { | ||||
| } | ||||
|  | ||||
| export const InterestParagraph = ( | ||||
|   props: InterestParagraphProps | ||||
|   props: InterestParagraphProps, | ||||
| ): JSX.Element => { | ||||
|   const { title, description } = props | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <p className='my-6 text-center text-gray dark:text-gray-dark'> | ||||
|         <strong className='text-lg font-semibold text-yellow dark:text-yellow-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"> | ||||
|           {title} | ||||
|         </strong> | ||||
|         <br /> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' | ||||
| import type { IconDefinition } from '@fortawesome/free-solid-svg-icons' | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" | ||||
| import type { IconDefinition } from "@fortawesome/free-solid-svg-icons" | ||||
|  | ||||
| interface InterestItemProps { | ||||
|   title: string | ||||
| @@ -10,9 +10,9 @@ export const InterestItem = (props: InterestItemProps): JSX.Element => { | ||||
|   const { fontAwesomeIcon, title } = props | ||||
|  | ||||
|   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 | ||||
|         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} | ||||
|       /> | ||||
|     </li> | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| import { faCode, faMicrochip } from '@fortawesome/free-solid-svg-icons' | ||||
| import { faGit } from '@fortawesome/free-brands-svg-icons' | ||||
| import { faCode, faMicrochip } from "@fortawesome/free-solid-svg-icons" | ||||
| import { faGit } from "@fortawesome/free-brands-svg-icons" | ||||
|  | ||||
| import { InterestItem } from './InterestItem' | ||||
| import { InterestItem } from "./InterestItem" | ||||
|  | ||||
| export const InterestsList = (): JSX.Element => { | ||||
|   return ( | ||||
|     <div className='my-4 flex justify-center'> | ||||
|       <ul className='m-0 flex w-96 list-none justify-around p-0'> | ||||
|         <InterestItem title='Developer Full Stack' fontAwesomeIcon={faCode} /> | ||||
|     <div className="my-4 flex justify-center"> | ||||
|       <ul className="m-0 flex w-96 list-none justify-around p-0"> | ||||
|         <InterestItem title="Developer Full Stack" fontAwesomeIcon={faCode} /> | ||||
|         <InterestItem | ||||
|           title='Passionate about High-Tech' | ||||
|           title="Passionate about High-Tech" | ||||
|           fontAwesomeIcon={faMicrochip} | ||||
|         /> | ||||
|         <InterestItem title='Open-Source enthusiast' fontAwesomeIcon={faGit} /> | ||||
|         <InterestItem title="Open-Source enthusiast" fontAwesomeIcon={faGit} /> | ||||
|       </ul> | ||||
|     </div> | ||||
|   ) | ||||
|   | ||||
| @@ -1,21 +1,21 @@ | ||||
| import { getI18n } from '@/i18n/i18n.server' | ||||
| import { getI18n } from "@/i18n/i18n.server" | ||||
|  | ||||
| import type { InterestParagraphProps } from './InterestParagraph' | ||||
| import { InterestParagraph } from './InterestParagraph' | ||||
| import { InterestsList } from './InterestsList' | ||||
| import type { InterestParagraphProps } from "./InterestParagraph" | ||||
| import { InterestParagraph } from "./InterestParagraph" | ||||
| import { InterestsList } from "./InterestsList" | ||||
|  | ||||
| export const Interests = (): JSX.Element => { | ||||
|   const i18n = getI18n() | ||||
|  | ||||
|   let paragraphs = i18n.translate<InterestParagraphProps[]>( | ||||
|     'home.interests.paragraphs' | ||||
|     "home.interests.paragraphs", | ||||
|   ) | ||||
|   if (!Array.isArray(paragraphs)) { | ||||
|     paragraphs = [] | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div className='max-w-full'> | ||||
|     <div className="max-w-full"> | ||||
|       {paragraphs.map((paragraph, index) => { | ||||
|         return <InterestParagraph key={index} {...paragraph} /> | ||||
|       })} | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { ShadowContainer } from '@/components/design/ShadowContainer' | ||||
| import { GitHubIcon } from '@/components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon' | ||||
| import { ShadowContainer } from "@/components/design/ShadowContainer" | ||||
| import { GitHubIcon } from "@/components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon" | ||||
|  | ||||
| export interface RepositoryProps { | ||||
|   name: string | ||||
| @@ -11,13 +11,13 @@ export const Repository = (props: RepositoryProps): JSX.Element => { | ||||
|   const { name, description, href } = props | ||||
|  | ||||
|   return ( | ||||
|     <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'> | ||||
|         <div className='flex'> | ||||
|           <GitHubIcon className='mr-2 h-6' /> | ||||
|           <span className='text-yellow dark:text-yellow-dark'>{name}</span> | ||||
|     <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"> | ||||
|         <div className="flex"> | ||||
|           <GitHubIcon className="mr-2 h-6" /> | ||||
|           <span className="text-yellow dark:text-yellow-dark">{name}</span> | ||||
|         </div> | ||||
|         <p className='my-4'>{description}</p> | ||||
|         <p className="my-4">{description}</p> | ||||
|       </a> | ||||
|     </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 => { | ||||
|   const i18n = getI18n() | ||||
|  | ||||
|   return ( | ||||
|     <div className='mt-0 flex max-w-full flex-col items-center'> | ||||
|       <p className='text-center'> | ||||
|         {i18n.translate('home.open-source.description')} | ||||
|     <div className="mt-0 flex max-w-full flex-col items-center"> | ||||
|       <p className="text-center"> | ||||
|         {i18n.translate("home.open-source.description")} | ||||
|       </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 | ||||
|           name='nodejs/node' | ||||
|           description='Node.js JavaScript runtime ✨🐢🚀✨' | ||||
|           href='https://github.com/nodejs/node/commits?author=theoludwig' | ||||
|           name="nodejs/node" | ||||
|           description="Node.js JavaScript runtime ✨🐢🚀✨" | ||||
|           href="https://github.com/nodejs/node/commits?author=theoludwig" | ||||
|         /> | ||||
|         <Repository | ||||
|           name='standard/standard' | ||||
|           description='🌟 JavaScript Style Guide, with linter & automatic code fixer' | ||||
|           href='https://github.com/standard/standard/commits?author=theoludwig' | ||||
|           name="standard/standard" | ||||
|           description="🌟 JavaScript Style Guide, with linter & automatic code fixer" | ||||
|           href="https://github.com/standard/standard/commits?author=theoludwig" | ||||
|         /> | ||||
|         <Repository | ||||
|           name='nrwl/nx' | ||||
|           description='Smart, Fast and Extensible Build System' | ||||
|           href='https://github.com/nrwl/nx/commits?author=theoludwig' | ||||
|           name="nrwl/nx" | ||||
|           description="Smart, Fast and Extensible Build System" | ||||
|           href="https://github.com/nrwl/nx/commits?author=theoludwig" | ||||
|         /> | ||||
|         <Repository | ||||
|           name='vercel/next.js' | ||||
|           description='The React Framework' | ||||
|           href='https://github.com/vercel/next.js/commits?author=theoludwig' | ||||
|           name="vercel/next.js" | ||||
|           description="The React Framework" | ||||
|           href="https://github.com/vercel/next.js/commits?author=theoludwig" | ||||
|         /> | ||||
|       </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 { | ||||
|   title: string | ||||
| @@ -13,29 +13,29 @@ export const PortfolioItem = (props: PortfolioItemProps): JSX.Element => { | ||||
|   const { title, description, link, image } = props | ||||
|  | ||||
|   return ( | ||||
|     <ShadowContainer className='relative cursor-pointer items-center sm:ml-10'> | ||||
|     <ShadowContainer className="relative cursor-pointer items-center sm:ml-10"> | ||||
|       <a | ||||
|         className='group inline-flex justify-center' | ||||
|         target='_blank' | ||||
|         rel='noopener noreferrer' | ||||
|         className="group inline-flex justify-center" | ||||
|         target="_blank" | ||||
|         rel="noopener noreferrer" | ||||
|         href={link} | ||||
|         aria-label={title} | ||||
|       > | ||||
|         <div className='flex justify-center'> | ||||
|         <div className="flex justify-center"> | ||||
|           <Image | ||||
|             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} | ||||
|             height={300} | ||||
|             src={image} | ||||
|             alt={title} | ||||
|           /> | ||||
|         </div> | ||||
|         <div className='absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100'> | ||||
|           <h3 className='my-6 text-xl font-semibold text-yellow dark:text-yellow-dark'> | ||||
|         <div className="absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100"> | ||||
|           <h3 className="my-6 text-xl font-semibold text-yellow dark:text-yellow-dark"> | ||||
|             {title} | ||||
|           </h3> | ||||
|           <p className='my-6'>{description}</p> | ||||
|           <p className="my-6">{description}</p> | ||||
|         </div> | ||||
|       </a> | ||||
|     </ShadowContainer> | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| import { getI18n } from '@/i18n/i18n.server' | ||||
| import { getI18n } from "@/i18n/i18n.server" | ||||
|  | ||||
| import type { PortfolioItemProps } from './PortfolioItem' | ||||
| import { PortfolioItem } from './PortfolioItem' | ||||
| import type { PortfolioItemProps } from "./PortfolioItem" | ||||
| import { PortfolioItem } from "./PortfolioItem" | ||||
|  | ||||
| export const Portfolio = (): JSX.Element => { | ||||
|   const i18n = getI18n() | ||||
|  | ||||
|   let items = i18n.translate<PortfolioItemProps[]>('home.portfolio.items') | ||||
|   let items = i18n.translate<PortfolioItemProps[]>("home.portfolio.items") | ||||
|   if (!Array.isArray(items)) { | ||||
|     items = [] | ||||
|   } | ||||
|  | ||||
|   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) => { | ||||
|         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 => { | ||||
|   const i18n = getI18n() | ||||
|  | ||||
|   return ( | ||||
|     <p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'> | ||||
|       {i18n.translate('home.about.description-bottom')} | ||||
|       {i18n.locale === 'fr-FR' ? ( | ||||
|     <p className="mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark"> | ||||
|       {i18n.translate("home.about.description-bottom")} | ||||
|       {i18n.locale === "fr-FR" ? ( | ||||
|         <> | ||||
|           <br /> | ||||
|           <br /> | ||||
|           <a | ||||
|             href='/curriculum-vitae/index.html' | ||||
|             className='text-yellow hover:underline dark:text-yellow-dark' | ||||
|             href="/curriculum-vitae/index.html" | ||||
|             className="text-yellow hover:underline dark:text-yellow-dark" | ||||
|           > | ||||
|             Curriculum vitæ | ||||
|           </a> | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| import { getI18n } from '@/i18n/i18n.server' | ||||
| import { getI18n } from "@/i18n/i18n.server" | ||||
|  | ||||
| export const ProfileInformation = (): JSX.Element => { | ||||
|   const i18n = getI18n() | ||||
|  | ||||
|   return ( | ||||
|     <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'> | ||||
|     <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"> | ||||
|         Théo LUDWIG | ||||
|       </h1> | ||||
|       <h2 className='mb-3 text-base'> | ||||
|         {i18n.translate('home.about.description')} | ||||
|       <h2 className="mb-3 text-base"> | ||||
|         {i18n.translate("home.about.description")} | ||||
|       </h2> | ||||
|     </div> | ||||
|   ) | ||||
|   | ||||
| @@ -8,14 +8,14 @@ export const ProfileItem = (props: ProfileItemProps): JSX.Element => { | ||||
|   const { title, value, link } = props | ||||
|  | ||||
|   return ( | ||||
|     <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'> | ||||
|     <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"> | ||||
|         {title} | ||||
|       </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 ? ( | ||||
|           <a | ||||
|             className='text-gray hover:underline dark:text-gray-dark' | ||||
|             className="text-gray hover:underline dark:text-gray-dark" | ||||
|             href={link} | ||||
|           > | ||||
|             {value} | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| 'use client' | ||||
| "use client" | ||||
|  | ||||
| import { useMemo } from 'react' | ||||
| import { useMemo } from "react" | ||||
|  | ||||
| import { useI18n } from '@/i18n/i18n.client' | ||||
| import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from '@/utils/getAge' | ||||
| import type { CookiesStore } from '@/utils/constants' | ||||
| import { useI18n } from "@/i18n/i18n.client" | ||||
| import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from "@/utils/getAge" | ||||
| import type { CookiesStore } from "@/utils/constants" | ||||
|  | ||||
| import { ProfileItem } from './ProfileItem' | ||||
| import { ProfileItem } from "./ProfileItem" | ||||
|  | ||||
| export interface ProfileListProps { | ||||
|   cookiesStore: CookiesStore | ||||
| @@ -22,25 +22,25 @@ export const ProfileList = (props: ProfileListProps): JSX.Element => { | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <ul className='m-0 list-none p-0'> | ||||
|     <ul className="m-0 list-none p-0"> | ||||
|       <ProfileItem | ||||
|         title={i18n.translate('home.about.pronouns')} | ||||
|         value={i18n.translate('home.about.pronouns-value')} | ||||
|         title={i18n.translate("home.about.pronouns")} | ||||
|         value={i18n.translate("home.about.pronouns-value")} | ||||
|       /> | ||||
|       <ProfileItem | ||||
|         title={i18n.translate('home.about.birth-date')} | ||||
|         title={i18n.translate("home.about.birth-date")} | ||||
|         value={`${BIRTH_DATE_STRING} (${age} ${i18n.translate( | ||||
|           'home.about.years-old' | ||||
|           "home.about.years-old", | ||||
|         )})`} | ||||
|       /> | ||||
|       <ProfileItem | ||||
|         title={i18n.translate('home.about.nationality')} | ||||
|         value='Alsace, France' | ||||
|         title={i18n.translate("home.about.nationality")} | ||||
|         value="Alsace, France" | ||||
|       /> | ||||
|       <ProfileItem | ||||
|         title='Email' | ||||
|         value='contact@theoludwig.fr' | ||||
|         link='mailto:contact@theoludwig.fr' | ||||
|         title="Email" | ||||
|         value="contact@theoludwig.fr" | ||||
|         link="mailto:contact@theoludwig.fr" | ||||
|       /> | ||||
|     </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 => { | ||||
|   return ( | ||||
|     <div className='max-h-[370px] max-w-[370px] px-2 py-6'> | ||||
|       <Image quality={100} src={Logo} alt='Théo LUDWIG' priority /> | ||||
|     <div className="max-h-[370px] max-w-[370px] px-2 py-6"> | ||||
|       <Image quality={100} src={Logo} alt="Théo LUDWIG" priority /> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { Icon } from './Icon' | ||||
| import { Icon } from "./Icon" | ||||
|  | ||||
| export const EmailIcon = ( | ||||
|   props: React.SVGProps<SVGSVGElement> | ||||
|   props: React.SVGProps<SVGSVGElement>, | ||||
| ): JSX.Element => { | ||||
|   return ( | ||||
|     <Icon {...props}> | ||||
|       <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> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { Icon } from './Icon' | ||||
| import { Icon } from "./Icon" | ||||
|  | ||||
| export const GitHubIcon = ( | ||||
|   props: React.SVGProps<SVGSVGElement> | ||||
|   props: React.SVGProps<SVGSVGElement>, | ||||
| ): JSX.Element => { | ||||
|   return ( | ||||
|     <Icon {...props}> | ||||
|       <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> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { Icon } from './Icon' | ||||
| import { Icon } from "./Icon" | ||||
|  | ||||
| export const GitLabIcon = ( | ||||
|   props: React.SVGProps<SVGSVGElement> | ||||
|   props: React.SVGProps<SVGSVGElement>, | ||||
| ): JSX.Element => { | ||||
|   return ( | ||||
|     <Icon {...props}> | ||||
|       <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> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| import classNames from 'clsx' | ||||
| import classNames from "clsx" | ||||
|  | ||||
| export const Icon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => { | ||||
|   const { children, className, ...rest } = props | ||||
|  | ||||
|   return ( | ||||
|     <svg | ||||
|       xmlns='http://www.w3.org/2000/svg' | ||||
|       viewBox='0 0 24 24' | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       viewBox="0 0 24 24" | ||||
|       className={classNames( | ||||
|         'h-8 w-8 fill-current text-black dark:text-white', | ||||
|         className | ||||
|         "h-8 w-8 fill-current text-black dark:text-white", | ||||
|         className, | ||||
|       )} | ||||
|       {...rest} | ||||
|     > | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { Icon } from './Icon' | ||||
| import { Icon } from "./Icon" | ||||
|  | ||||
| export const NPMIcon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => { | ||||
|   return ( | ||||
|     <Icon {...props}> | ||||
|       <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> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { Icon } from './Icon' | ||||
| import { Icon } from "./Icon" | ||||
|  | ||||
| export const TwitchIcon = ( | ||||
|   props: React.SVGProps<SVGSVGElement> | ||||
|   props: React.SVGProps<SVGSVGElement>, | ||||
| ): JSX.Element => { | ||||
|   return ( | ||||
|     <Icon {...props}> | ||||
|       <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> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { Icon } from './Icon' | ||||
| import { Icon } from "./Icon" | ||||
|  | ||||
| export const TwitterIcon = ( | ||||
|   props: React.SVGProps<SVGSVGElement> | ||||
|   props: React.SVGProps<SVGSVGElement>, | ||||
| ): JSX.Element => { | ||||
|   return ( | ||||
|     <Icon {...props}> | ||||
|       <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> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { Icon } from './Icon' | ||||
| import { Icon } from "./Icon" | ||||
|  | ||||
| export const YouTubeIcon = ( | ||||
|   props: React.SVGProps<SVGSVGElement> | ||||
|   props: React.SVGProps<SVGSVGElement>, | ||||
| ): JSX.Element => { | ||||
|   return ( | ||||
|     <Icon {...props}> | ||||
|       <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> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -7,13 +7,13 @@ export const SocialMediaItem = (props: SocialMediaItemProps): JSX.Element => { | ||||
|   const { link, ariaLabel, children } = props | ||||
|  | ||||
|   return ( | ||||
|     <li className='mx-4 my-1 inline-block'> | ||||
|     <li className="mx-4 my-1 inline-block"> | ||||
|       <a | ||||
|         href={link} | ||||
|         aria-label={ariaLabel} | ||||
|         target='_blank' | ||||
|         rel='noopener noreferrer' | ||||
|         className='relative inline-block bg-transparent' | ||||
|         target="_blank" | ||||
|         rel="noopener noreferrer" | ||||
|         className="relative inline-block bg-transparent" | ||||
|       > | ||||
|         {children} | ||||
|       </a> | ||||
|   | ||||
| @@ -1,43 +1,43 @@ | ||||
| import { SocialMediaItem } from './SocialMediaItem' | ||||
| import { TwitterIcon } from './SocialMediaIcons/TwitterIcon' | ||||
| import { GitHubIcon } from './SocialMediaIcons/GitHubIcon' | ||||
| import { GitLabIcon } from './SocialMediaIcons/GitLabIcon' | ||||
| import { YouTubeIcon } from './SocialMediaIcons/YouTubeIcon' | ||||
| import { TwitchIcon } from './SocialMediaIcons/TwitchIcon' | ||||
| import { EmailIcon } from './SocialMediaIcons/EmailIcon' | ||||
| import { NPMIcon } from './SocialMediaIcons/NPMIcon' | ||||
| import { SocialMediaItem } from "./SocialMediaItem" | ||||
| import { TwitterIcon } from "./SocialMediaIcons/TwitterIcon" | ||||
| import { GitHubIcon } from "./SocialMediaIcons/GitHubIcon" | ||||
| import { GitLabIcon } from "./SocialMediaIcons/GitLabIcon" | ||||
| import { YouTubeIcon } from "./SocialMediaIcons/YouTubeIcon" | ||||
| import { TwitchIcon } from "./SocialMediaIcons/TwitchIcon" | ||||
| import { EmailIcon } from "./SocialMediaIcons/EmailIcon" | ||||
| import { NPMIcon } from "./SocialMediaIcons/NPMIcon" | ||||
|  | ||||
| export const SocialMediaList = (): JSX.Element => { | ||||
|   return ( | ||||
|     <ul className='social-media-list m-0 mt-2 list-none py-4 text-center'> | ||||
|       <SocialMediaItem link='https://github.com/theoludwig' ariaLabel='GitHub'> | ||||
|     <ul className="social-media-list m-0 mt-2 list-none py-4 text-center"> | ||||
|       <SocialMediaItem link="https://github.com/theoludwig" ariaLabel="GitHub"> | ||||
|         <GitHubIcon /> | ||||
|       </SocialMediaItem> | ||||
|       <SocialMediaItem link='https://gitlab.com/theoludwig' ariaLabel='GitLab'> | ||||
|       <SocialMediaItem link="https://gitlab.com/theoludwig" ariaLabel="GitLab"> | ||||
|         <GitLabIcon /> | ||||
|       </SocialMediaItem> | ||||
|       <SocialMediaItem link='https://www.npmjs.com/~theoludwig' ariaLabel='npm'> | ||||
|       <SocialMediaItem link="https://www.npmjs.com/~theoludwig" ariaLabel="npm"> | ||||
|         <NPMIcon /> | ||||
|       </SocialMediaItem> | ||||
|       <SocialMediaItem | ||||
|         link='https://twitter.com/theoludwig_' | ||||
|         ariaLabel='Twitter' | ||||
|         link="https://twitter.com/theoludwig_" | ||||
|         ariaLabel="Twitter" | ||||
|       > | ||||
|         <TwitterIcon /> | ||||
|       </SocialMediaItem> | ||||
|       <SocialMediaItem | ||||
|         link='https://www.youtube.com/@theo_ludwig' | ||||
|         ariaLabel='YouTube' | ||||
|         link="https://www.youtube.com/@theo_ludwig" | ||||
|         ariaLabel="YouTube" | ||||
|       > | ||||
|         <YouTubeIcon /> | ||||
|       </SocialMediaItem> | ||||
|       <SocialMediaItem | ||||
|         link='https://www.twitch.tv/theoludwig' | ||||
|         ariaLabel='Twitch' | ||||
|         link="https://www.twitch.tv/theoludwig" | ||||
|         ariaLabel="Twitch" | ||||
|       > | ||||
|         <TwitchIcon /> | ||||
|       </SocialMediaItem> | ||||
|       <SocialMediaItem link='mailto:contact@theoludwig.fr' ariaLabel='Email'> | ||||
|       <SocialMediaItem link="mailto:contact@theoludwig.fr" ariaLabel="Email"> | ||||
|         <EmailIcon /> | ||||
|       </SocialMediaItem> | ||||
|     </ul> | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| import { cookies } from 'next/headers' | ||||
| import { cookies } from "next/headers" | ||||
|  | ||||
| import { ProfileDescriptionBottom } from './ProfileDescriptionBottom' | ||||
| import { ProfileInformation } from './ProfileInfo' | ||||
| import { ProfileList } from './ProfileList' | ||||
| import { ProfileLogo } from './ProfileLogo' | ||||
| import { ProfileDescriptionBottom } from "./ProfileDescriptionBottom" | ||||
| import { ProfileInformation } from "./ProfileInfo" | ||||
| import { ProfileList } from "./ProfileList" | ||||
| import { ProfileLogo } from "./ProfileLogo" | ||||
|  | ||||
| export const Profile = (): JSX.Element => { | ||||
|   const cookiesStore = cookies() | ||||
|  | ||||
|   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 /> | ||||
|       <div> | ||||
|         <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 { skills } from './skills' | ||||
| import type { SkillName } from "./skills" | ||||
| import { skills } from "./skills" | ||||
|  | ||||
| export interface SkillComponentProps { | ||||
|   skill: SkillName | ||||
| @@ -17,10 +17,10 @@ export const SkillComponent = (props: SkillComponentProps): JSX.Element => { | ||||
|   const theme = getTheme() | ||||
|  | ||||
|   const getImage = (): string => { | ||||
|     if (typeof skillProperties.image === 'string') { | ||||
|     if (typeof skillProperties.image === "string") { | ||||
|       return skillProperties.image | ||||
|     } | ||||
|     if (theme === 'light') { | ||||
|     if (theme === "light") { | ||||
|       return skillProperties.image.light | ||||
|     } | ||||
|     return skillProperties.image.dark | ||||
| @@ -29,20 +29,20 @@ export const SkillComponent = (props: SkillComponentProps): JSX.Element => { | ||||
|   return ( | ||||
|     <a | ||||
|       href={skillProperties.link} | ||||
|       className='mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark' | ||||
|       target='_blank' | ||||
|       rel='noopener noreferrer' | ||||
|       className="mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark" | ||||
|       target="_blank" | ||||
|       rel="noopener noreferrer" | ||||
|     > | ||||
|       <div className='text-center'> | ||||
|       <div className="text-center"> | ||||
|         <Image | ||||
|           className='inline h-16 w-16' | ||||
|           className="inline h-16 w-16" | ||||
|           quality={100} | ||||
|           width={64} | ||||
|           height={64} | ||||
|           alt={skill} | ||||
|           src={getImage()} | ||||
|         /> | ||||
|         <p className='mt-1'>{skill}</p> | ||||
|         <p className="mt-1">{skill}</p> | ||||
|       </div> | ||||
|     </a> | ||||
|   ) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { ShadowContainer } from '@/components/design/ShadowContainer' | ||||
| import { ShadowContainer } from "@/components/design/ShadowContainer" | ||||
|  | ||||
| export interface SkillsSectionProps { | ||||
|   title: string | ||||
| @@ -10,15 +10,15 @@ export const SkillsSection = (props: SkillsSectionProps): JSX.Element => { | ||||
|  | ||||
|   return ( | ||||
|     <ShadowContainer> | ||||
|       <div className='mx-auto w-full px-4'> | ||||
|         <div className='flex flex-wrap px-4 py-6'> | ||||
|           <div className='flex-1'> | ||||
|             <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'> | ||||
|       <div className="mx-auto w-full px-4"> | ||||
|         <div className="flex flex-wrap px-4 py-6"> | ||||
|           <div className="flex-1"> | ||||
|             <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"> | ||||
|                 {title} | ||||
|               </h3> | ||||
|             </div> | ||||
|             <div className='flex flex-wrap justify-around'>{children}</div> | ||||
|             <div className="flex flex-wrap justify-around">{children}</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 { SkillsSection } from './SkillsSection' | ||||
| import { SkillComponent } from "./Skill" | ||||
| import { SkillsSection } from "./SkillsSection" | ||||
|  | ||||
| export const Skills = (): JSX.Element => { | ||||
|   const i18n = getI18n() | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <SkillsSection title={i18n.translate('home.skills.languages')}> | ||||
|         <SkillComponent skill='TypeScript' /> | ||||
|         <SkillComponent skill='Python' /> | ||||
|         <SkillComponent skill='C/C++' /> | ||||
|         <SkillComponent skill='PHP' /> | ||||
|       <SkillsSection title={i18n.translate("home.skills.languages")}> | ||||
|         <SkillComponent skill="TypeScript" /> | ||||
|         <SkillComponent skill="Python" /> | ||||
|         <SkillComponent skill="C/C++" /> | ||||
|         <SkillComponent skill="PHP" /> | ||||
|       </SkillsSection> | ||||
|  | ||||
|       <SkillsSection title='Frontend'> | ||||
|         <SkillComponent skill='HTML' /> | ||||
|         <SkillComponent skill='CSS' /> | ||||
|         <SkillComponent skill='Tailwind CSS' /> | ||||
|         <SkillComponent skill='React.js (+ Next.js)' /> | ||||
|       <SkillsSection title="Frontend"> | ||||
|         <SkillComponent skill="HTML" /> | ||||
|         <SkillComponent skill="CSS" /> | ||||
|         <SkillComponent skill="Tailwind CSS" /> | ||||
|         <SkillComponent skill="React.js (+ Next.js)" /> | ||||
|       </SkillsSection> | ||||
|  | ||||
|       <SkillsSection title='Backend'> | ||||
|         <SkillComponent skill='Laravel' /> | ||||
|         <SkillComponent skill='Node.js' /> | ||||
|         <SkillComponent skill='Fastify' /> | ||||
|         <SkillComponent skill='PostgreSQL' /> | ||||
|       <SkillsSection title="Backend"> | ||||
|         <SkillComponent skill="Laravel" /> | ||||
|         <SkillComponent skill="Node.js" /> | ||||
|         <SkillComponent skill="Fastify" /> | ||||
|         <SkillComponent skill="PostgreSQL" /> | ||||
|       </SkillsSection> | ||||
|  | ||||
|       <SkillsSection title={i18n.translate('home.skills.software-tools')}> | ||||
|         <SkillComponent skill='GNU/Linux' /> | ||||
|         <SkillComponent skill='Arch Linux' /> | ||||
|         <SkillComponent skill='Visual Studio Code' /> | ||||
|         <SkillComponent skill='Git' /> | ||||
|         <SkillComponent skill='Docker' /> | ||||
|       <SkillsSection title={i18n.translate("home.skills.software-tools")}> | ||||
|         <SkillComponent skill="GNU/Linux" /> | ||||
|         <SkillComponent skill="Arch Linux" /> | ||||
|         <SkillComponent skill="Visual Studio Code" /> | ||||
|         <SkillComponent skill="Git" /> | ||||
|         <SkillComponent skill="Docker" /> | ||||
|       </SkillsSection> | ||||
|     </> | ||||
|   ) | ||||
|   | ||||
| @@ -5,111 +5,111 @@ export interface Skill { | ||||
|  | ||||
| export const skills = { | ||||
|   JavaScript: { | ||||
|     link: 'https://developer.mozilla.org/docs/Web/JavaScript', | ||||
|     image: '/images/skills/JavaScript.png' | ||||
|     link: "https://developer.mozilla.org/docs/Web/JavaScript", | ||||
|     image: "/images/skills/JavaScript.png", | ||||
|   }, | ||||
|   TypeScript: { | ||||
|     link: 'https://www.typescriptlang.org/', | ||||
|     image: '/images/skills/TypeScript.png' | ||||
|     link: "https://www.typescriptlang.org/", | ||||
|     image: "/images/skills/TypeScript.png", | ||||
|   }, | ||||
|   Python: { | ||||
|     link: 'https://www.python.org/', | ||||
|     image: '/images/skills/Python.png' | ||||
|     link: "https://www.python.org/", | ||||
|     image: "/images/skills/Python.png", | ||||
|   }, | ||||
|   'C/C++': { | ||||
|     link: 'https://isocpp.org/', | ||||
|     image: '/images/skills/C-Cpp.png' | ||||
|   "C/C++": { | ||||
|     link: "https://isocpp.org/", | ||||
|     image: "/images/skills/C-Cpp.png", | ||||
|   }, | ||||
|   PHP: { | ||||
|     link: 'https://www.php.net/', | ||||
|     image: '/images/skills/PHP.png' | ||||
|     link: "https://www.php.net/", | ||||
|     image: "/images/skills/PHP.png", | ||||
|   }, | ||||
|   Laravel: { | ||||
|     link: 'https://laravel.com/', | ||||
|     image: '/images/skills/Laravel.png' | ||||
|     link: "https://laravel.com/", | ||||
|     image: "/images/skills/Laravel.png", | ||||
|   }, | ||||
|   Dart: { | ||||
|     link: 'https://dart.dev/', | ||||
|     image: '/images/skills/Dart.png' | ||||
|     link: "https://dart.dev/", | ||||
|     image: "/images/skills/Dart.png", | ||||
|   }, | ||||
|   Flutter: { | ||||
|     link: 'https://flutter.dev/', | ||||
|     image: '/images/skills/Flutter.webp' | ||||
|     link: "https://flutter.dev/", | ||||
|     image: "/images/skills/Flutter.webp", | ||||
|   }, | ||||
|   HTML: { | ||||
|     link: 'https://developer.mozilla.org/docs/Web/HTML', | ||||
|     image: '/images/skills/HTML.png' | ||||
|     link: "https://developer.mozilla.org/docs/Web/HTML", | ||||
|     image: "/images/skills/HTML.png", | ||||
|   }, | ||||
|   CSS: { | ||||
|     link: 'https://developer.mozilla.org/docs/Web/CSS', | ||||
|     image: '/images/skills/CSS.png' | ||||
|     link: "https://developer.mozilla.org/docs/Web/CSS", | ||||
|     image: "/images/skills/CSS.png", | ||||
|   }, | ||||
|   'Tailwind CSS': { | ||||
|     link: 'https://tailwindcss.com/', | ||||
|     image: '/images/skills/TailwindCSS.png' | ||||
|   "Tailwind CSS": { | ||||
|     link: "https://tailwindcss.com/", | ||||
|     image: "/images/skills/TailwindCSS.png", | ||||
|   }, | ||||
|   SASS: { | ||||
|     link: 'https://sass-lang.com/', | ||||
|     image: '/images/skills/SASS.svg' | ||||
|     link: "https://sass-lang.com/", | ||||
|     image: "/images/skills/SASS.svg", | ||||
|   }, | ||||
|   'React.js (+ Next.js)': { | ||||
|     link: 'https://reactjs.org/', | ||||
|     image: '/images/skills/ReactJS.png' | ||||
|   "React.js (+ Next.js)": { | ||||
|     link: "https://reactjs.org/", | ||||
|     image: "/images/skills/ReactJS.png", | ||||
|   }, | ||||
|   'Node.js': { | ||||
|     link: 'https://nodejs.org/', | ||||
|     image: '/images/skills/NodeJS.png' | ||||
|   "Node.js": { | ||||
|     link: "https://nodejs.org/", | ||||
|     image: "/images/skills/NodeJS.png", | ||||
|   }, | ||||
|   Fastify: { | ||||
|     link: 'https://www.fastify.io/', | ||||
|     link: "https://www.fastify.io/", | ||||
|     image: { | ||||
|       light: '/images/skills/Fastify-light.png', | ||||
|       dark: '/images/skills/Fastify-dark.png' | ||||
|     } | ||||
|       light: "/images/skills/Fastify-light.png", | ||||
|       dark: "/images/skills/Fastify-dark.png", | ||||
|     }, | ||||
|   }, | ||||
|   Prisma: { | ||||
|     link: 'https://www.prisma.io/', | ||||
|     link: "https://www.prisma.io/", | ||||
|     image: { | ||||
|       light: '/images/skills/Prisma-light.png', | ||||
|       dark: '/images/skills/Prisma-dark.png' | ||||
|     } | ||||
|       light: "/images/skills/Prisma-light.png", | ||||
|       dark: "/images/skills/Prisma-dark.png", | ||||
|     }, | ||||
|   }, | ||||
|   PostgreSQL: { | ||||
|     link: 'https://www.postgresql.org/', | ||||
|     image: '/images/skills/PostgreSQL.png' | ||||
|     link: "https://www.postgresql.org/", | ||||
|     image: "/images/skills/PostgreSQL.png", | ||||
|   }, | ||||
|   MySQL: { | ||||
|     link: 'https://www.mysql.com/', | ||||
|     image: '/images/skills/MySQL.png' | ||||
|     link: "https://www.mysql.com/", | ||||
|     image: "/images/skills/MySQL.png", | ||||
|   }, | ||||
|   Strapi: { | ||||
|     link: 'https://strapi.io/', | ||||
|     image: '/images/skills/Strapi.png' | ||||
|     link: "https://strapi.io/", | ||||
|     image: "/images/skills/Strapi.png", | ||||
|   }, | ||||
|   'Visual Studio Code': { | ||||
|     link: 'https://code.visualstudio.com/', | ||||
|     image: '/images/skills/VisualStudioCode.png' | ||||
|   "Visual Studio Code": { | ||||
|     link: "https://code.visualstudio.com/", | ||||
|     image: "/images/skills/VisualStudioCode.png", | ||||
|   }, | ||||
|   Git: { | ||||
|     link: 'https://git-scm.com/', | ||||
|     image: '/images/skills/Git.png' | ||||
|     link: "https://git-scm.com/", | ||||
|     image: "/images/skills/Git.png", | ||||
|   }, | ||||
|   Ubuntu: { | ||||
|     link: 'https://ubuntu.com/', | ||||
|     image: '/images/skills/Ubuntu.png' | ||||
|     link: "https://ubuntu.com/", | ||||
|     image: "/images/skills/Ubuntu.png", | ||||
|   }, | ||||
|   'Arch Linux': { | ||||
|     link: 'https://archlinux.org/', | ||||
|     image: '/images/skills/ArchLinux.png' | ||||
|   "Arch Linux": { | ||||
|     link: "https://archlinux.org/", | ||||
|     image: "/images/skills/ArchLinux.png", | ||||
|   }, | ||||
|   'GNU/Linux': { | ||||
|     link: 'https://www.gnu.org/', | ||||
|     image: '/images/skills/GNU-Linux.png' | ||||
|   "GNU/Linux": { | ||||
|     link: "https://www.gnu.org/", | ||||
|     image: "/images/skills/GNU-Linux.png", | ||||
|   }, | ||||
|   Docker: { | ||||
|     link: 'https://www.docker.com/', | ||||
|     image: '/images/skills/Docker.png' | ||||
|   } | ||||
|     link: "https://www.docker.com/", | ||||
|     image: "/images/skills/Docker.png", | ||||
|   }, | ||||
| } as const | ||||
|  | ||||
| export type SkillName = keyof typeof skills | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import classNames from 'clsx' | ||||
| import classNames from "clsx" | ||||
|  | ||||
| export interface LoaderProps { | ||||
|   width?: number | ||||
| @@ -13,16 +13,16 @@ export const Loader = (props: LoaderProps): JSX.Element => { | ||||
|     <div | ||||
|       style={{ | ||||
|         width, | ||||
|         height | ||||
|         height, | ||||
|       }} | ||||
|       className={classNames( | ||||
|         'animate-spin inline-block border-[3px] border-current border-t-transparent text-yellow dark:text-yellow-dark rounded-full', | ||||
|         className | ||||
|         "animate-spin inline-block border-[3px] border-current border-t-transparent text-yellow dark:text-yellow-dark rounded-full", | ||||
|         className, | ||||
|       )} | ||||
|       role='status' | ||||
|       aria-label='loading' | ||||
|       role="status" | ||||
|       aria-label="loading" | ||||
|     > | ||||
|       <span className='sr-only'>Loading...</span> | ||||
|       <span className="sr-only">Loading...</span> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| 'use client' | ||||
| "use client" | ||||
|  | ||||
| import { useEffect, useRef } from 'react' | ||||
| import { useEffect, useRef } from "react" | ||||
|  | ||||
| export type RevealFadeProps = React.PropsWithChildren | ||||
|  | ||||
| @@ -15,22 +15,22 @@ export const RevealFade = (props: RevealFadeProps): JSX.Element => { | ||||
|         for (const entry of entries) { | ||||
|           if (entry.isIntersecting) { | ||||
|             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) | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         root: null, | ||||
|         rootMargin: '0px', | ||||
|         threshold: 0.28 | ||||
|       } | ||||
|         rootMargin: "0px", | ||||
|         threshold: 0.28, | ||||
|       }, | ||||
|     ) | ||||
|     observer.observe(htmlElement.current as HTMLDivElement) | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <div ref={htmlElement} className='invisible -translate-y-7 opacity-0'> | ||||
|     <div ref={htmlElement} className="invisible -translate-y-7 opacity-0"> | ||||
|       {children} | ||||
|     </div> | ||||
|   ) | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| type SectionHeadingProps = React.ComponentPropsWithRef<'h2'> | ||||
| type SectionHeadingProps = React.ComponentPropsWithRef<"h2"> | ||||
|  | ||||
| export const SectionHeading = (props: SectionHeadingProps): JSX.Element => { | ||||
|   const { children, ...rest } = props | ||||
|  | ||||
|   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} | ||||
|     </h2> | ||||
|   ) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { ShadowContainer } from '@/components/design/ShadowContainer' | ||||
| import { SectionHeading } from '@/components/design/Section/SectionHeading' | ||||
| import { ShadowContainer } from "@/components/design/ShadowContainer" | ||||
| import { SectionHeading } from "@/components/design/Section/SectionHeading" | ||||
|  | ||||
| type SectionProps = React.ComponentPropsWithRef<'section'> & { | ||||
| type SectionProps = React.ComponentPropsWithRef<"section"> & { | ||||
|   heading?: string | ||||
|   description?: string | ||||
|   isMain?: boolean | ||||
| @@ -20,13 +20,13 @@ export const Section = (props: SectionProps): JSX.Element => { | ||||
|  | ||||
|   if (isMain) { | ||||
|     return ( | ||||
|       <div className='w-full px-3'> | ||||
|       <div className="w-full px-3"> | ||||
|         <ShadowContainer style={{ marginTop: 50 }}> | ||||
|           <section {...rest}> | ||||
|             {heading != null ? ( | ||||
|               <SectionHeading>{heading}</SectionHeading> | ||||
|             ) : null} | ||||
|             <div className='w-full px-3'>{children}</div> | ||||
|             <div className="w-full px-3">{children}</div> | ||||
|           </section> | ||||
|         </ShadowContainer> | ||||
|       </div> | ||||
| @@ -37,7 +37,7 @@ export const Section = (props: SectionProps): JSX.Element => { | ||||
|     return ( | ||||
|       <section {...rest}> | ||||
|         {heading != null ? <SectionHeading>{heading}</SectionHeading> : null} | ||||
|         <div className='w-full px-3'>{children}</div> | ||||
|         <div className="w-full px-3">{children}</div> | ||||
|       </section> | ||||
|     ) | ||||
|   } | ||||
| @@ -52,13 +52,13 @@ export const Section = (props: SectionProps): JSX.Element => { | ||||
|         </SectionHeading> | ||||
|       ) : null} | ||||
|       {description != null ? ( | ||||
|         <p style={{ marginTop: 7 }} className='text-center'> | ||||
|         <p style={{ marginTop: 7 }} className="text-center"> | ||||
|           {description} | ||||
|         </p> | ||||
|       ) : null} | ||||
|       <div className='w-full px-3'> | ||||
|       <div className="w-full px-3"> | ||||
|         <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> | ||||
|       </div> | ||||
|     </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 => { | ||||
|   const { children, className, ...rest } = props | ||||
| @@ -8,8 +8,8 @@ export const ShadowContainer = (props: ShadowContainerProps): JSX.Element => { | ||||
|   return ( | ||||
|     <div | ||||
|       className={classNames( | ||||
|         'mb-12 h-full max-w-full break-words rounded-2xl border border-solid border-[#000] shadow-light dark:shadow-dark ', | ||||
|         className | ||||
|         "mb-12 h-full max-w-full break-words rounded-2xl border border-solid border-[#000] shadow-light dark:shadow-dark ", | ||||
|         className, | ||||
|       )} | ||||
|       {...rest} | ||||
|     > | ||||
|   | ||||
							
								
								
									
										10
									
								
								compose.yaml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								compose.yaml
									
									
									
									
									
								
							| @@ -1,11 +1,11 @@ | ||||
| services: | ||||
|   theoludwig: | ||||
|     container_name: ${COMPOSE_PROJECT_NAME} | ||||
|     image: 'theoludwig' | ||||
|     restart: 'unless-stopped' | ||||
|     image: "theoludwig" | ||||
|     restart: "unless-stopped" | ||||
|     build: | ||||
|       context: './' | ||||
|     network_mode: 'host' | ||||
|       context: "./" | ||||
|     network_mode: "host" | ||||
|     environment: | ||||
|       PORT: ${PORT-3000} | ||||
|     env_file: '.env' | ||||
|     env_file: ".env" | ||||
|   | ||||
| @@ -1,20 +1,20 @@ | ||||
| import { fileURLToPath } from 'node:url' | ||||
| import fs from 'node:fs' | ||||
| import { fileURLToPath } from "node:url" | ||||
| import fs from "node:fs" | ||||
|  | ||||
| import { build } from 'vite' | ||||
| import { build } from "vite" | ||||
|  | ||||
| const curriculumVitae = new URL('./', import.meta.url) | ||||
| const curriculumVitaeDist = new URL('./dist', curriculumVitae) | ||||
| const curriculumVitae = new URL("./", import.meta.url) | ||||
| const curriculumVitaeDist = new URL("./dist", curriculumVitae) | ||||
| const publicCurriculumVitaeOutputURL = new URL( | ||||
|   '../public/curriculum-vitae', | ||||
|   import.meta.url | ||||
|   "../public/curriculum-vitae", | ||||
|   import.meta.url, | ||||
| ) | ||||
|  | ||||
| await build({ | ||||
|   root: fileURLToPath(curriculumVitae), | ||||
|   base: '/curriculum-vitae/' | ||||
|   base: "/curriculum-vitae/", | ||||
| }) | ||||
|  | ||||
| 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" | ||||
|       }, | ||||
|       "devDependencies": { | ||||
|         "@types/node": "20.6.2", | ||||
|         "@types/node": "20.8.7", | ||||
|         "date-and-time": "3.0.3", | ||||
|         "vite": "4.4.9", | ||||
|         "vite": "4.5.0", | ||||
|         "vite-plugin-html": "3.2.0" | ||||
|       } | ||||
|     }, | ||||
| @@ -419,9 +419,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/@jridgewell/trace-mapping": { | ||||
|       "version": "0.3.19", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", | ||||
|       "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", | ||||
|       "version": "0.3.20", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", | ||||
|       "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "@jridgewell/resolve-uri": "^3.1.0", | ||||
| @@ -477,10 +477,13 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/node": { | ||||
|       "version": "20.6.2", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", | ||||
|       "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", | ||||
|       "dev": true | ||||
|       "version": "20.8.7", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", | ||||
|       "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "undici-types": "~5.25.1" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/acorn": { | ||||
|       "version": "8.10.0", | ||||
| @@ -1200,9 +1203,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/postcss": { | ||||
|       "version": "8.4.29", | ||||
|       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", | ||||
|       "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", | ||||
|       "version": "8.4.31", | ||||
|       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", | ||||
|       "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", | ||||
|       "dev": true, | ||||
|       "funding": [ | ||||
|         { | ||||
| @@ -1267,9 +1270,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/rollup": { | ||||
|       "version": "3.29.2", | ||||
|       "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.2.tgz", | ||||
|       "integrity": "sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==", | ||||
|       "version": "3.29.4", | ||||
|       "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", | ||||
|       "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", | ||||
|       "dev": true, | ||||
|       "bin": { | ||||
|         "rollup": "dist/bin/rollup" | ||||
| @@ -1346,9 +1349,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/terser": { | ||||
|       "version": "5.19.4", | ||||
|       "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz", | ||||
|       "integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==", | ||||
|       "version": "5.22.0", | ||||
|       "resolved": "https://registry.npmjs.org/terser/-/terser-5.22.0.tgz", | ||||
|       "integrity": "sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "@jridgewell/source-map": "^0.3.3", | ||||
| @@ -1387,6 +1390,12 @@ | ||||
|       "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", | ||||
|       "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": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", | ||||
| @@ -1397,9 +1406,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/vite": { | ||||
|       "version": "4.4.9", | ||||
|       "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", | ||||
|       "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", | ||||
|       "version": "4.5.0", | ||||
|       "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", | ||||
|       "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "esbuild": "^0.18.10", | ||||
|   | ||||
| @@ -13,9 +13,9 @@ | ||||
|     "modern-normalize": "2.0.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@types/node": "20.6.2", | ||||
|     "@types/node": "20.8.7", | ||||
|     "date-and-time": "3.0.3", | ||||
|     "vite": "4.4.9", | ||||
|     "vite": "4.5.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() | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| @import 'modern-normalize/modern-normalize.css'; | ||||
| @import "modern-normalize/modern-normalize.css"; | ||||
|  | ||||
| body { | ||||
|   font-family: 'Montserrat', 'Arial', 'sans-serif'; | ||||
|   font-family: "Montserrat", "Arial", "sans-serif"; | ||||
|   background: #f0f0f0; | ||||
|   color: #333; | ||||
|   line-height: 1.42857143; | ||||
|   | ||||
| @@ -1,19 +1,19 @@ | ||||
| import fs from 'node:fs' | ||||
| import fs from "node:fs" | ||||
|  | ||||
| import { defineConfig } from 'vite' | ||||
| import { parse as JSONCParser } from 'jsonc-parser' | ||||
| import { createHtmlPlugin } from 'vite-plugin-html' | ||||
| import date from 'date-and-time' | ||||
| import { defineConfig } from "vite" | ||||
| import { parse as JSONCParser } from "jsonc-parser" | ||||
| import { createHtmlPlugin } from "vite-plugin-html" | ||||
| import date from "date-and-time" | ||||
|  | ||||
| const jsonCurriculumVitaeURL = new URL( | ||||
|   './curriculum-vitae.jsonc', | ||||
|   import.meta.url | ||||
|   "./curriculum-vitae.jsonc", | ||||
|   import.meta.url, | ||||
| ) | ||||
| const dataCurriculumVitaeStringJSON = await fs.promises.readFile( | ||||
|   jsonCurriculumVitaeURL, | ||||
|   { | ||||
|     encoding: 'utf-8' | ||||
|   } | ||||
|     encoding: "utf-8", | ||||
|   }, | ||||
| ) | ||||
| const curriculumVitae = JSONCParser(dataCurriculumVitaeStringJSON) | ||||
|  | ||||
| @@ -22,7 +22,7 @@ const curriculumVitae = JSONCParser(dataCurriculumVitaeStringJSON) | ||||
|  */ | ||||
| export default defineConfig({ | ||||
|   build: { | ||||
|     assetsDir: './' | ||||
|     assetsDir: "./", | ||||
|   }, | ||||
|   plugins: [ | ||||
|     createHtmlPlugin({ | ||||
| @@ -30,13 +30,13 @@ export default defineConfig({ | ||||
|         data: { | ||||
|           date, | ||||
|           locals: { | ||||
|             ...curriculumVitae | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|             ...curriculumVitae, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }), | ||||
|   ], | ||||
|   css: { | ||||
|     postcss: {} | ||||
|   } | ||||
|     postcss: {}, | ||||
|   }, | ||||
| }) | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| import { defineConfig } from 'cypress' | ||||
| import { defineConfig } from "cypress" | ||||
|  | ||||
| export default defineConfig({ | ||||
|   fixturesFolder: false, | ||||
|   video: false, | ||||
|   screenshotOnRunFailure: false, | ||||
|   e2e: { | ||||
|     baseUrl: 'http://127.0.0.1:3000', | ||||
|     supportFile: false | ||||
|     baseUrl: "http://127.0.0.1:3000", | ||||
|     supportFile: false, | ||||
|   }, | ||||
|   component: { | ||||
|     devServer: { | ||||
|       framework: 'next', | ||||
|       bundler: 'webpack' | ||||
|     } | ||||
|   } | ||||
|       framework: "next", | ||||
|       bundler: "webpack", | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
| import { getAge } from '@/utils/getAge' | ||||
| import { getAge } from "@/utils/getAge" | ||||
|  | ||||
| describe('utils/getAge', () => { | ||||
|   it('should calculate the right age of a person', () => { | ||||
|     cy.clock(new Date('2018-03-20')).then(() => { | ||||
|       const birthDate = new Date('1980-02-20') | ||||
| describe("utils/getAge", () => { | ||||
|   it("should calculate the right age of a person", () => { | ||||
|     cy.clock(new Date("2018-03-20")).then(() => { | ||||
|       const birthDate = new Date("1980-02-20") | ||||
|       expect(getAge(birthDate)).equal(38) | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   it('should calculate the right age of a person (taking into account the months)', () => { | ||||
|     cy.clock(new Date('2018-03-20')).then(() => { | ||||
|       const birthDate = new Date('1980-07-20') | ||||
|   it("should calculate the right age of a person (taking into account the months)", () => { | ||||
|     cy.clock(new Date("2018-03-20")).then(() => { | ||||
|       const birthDate = new Date("1980-07-20") | ||||
|       expect(getAge(birthDate)).equal(37) | ||||
|     }) | ||||
|   }) | ||||
|   | ||||
| @@ -1,62 +1,62 @@ | ||||
| describe('Common > Header', () => { | ||||
| describe("Common > Header", () => { | ||||
|   beforeEach(() => { | ||||
|     return cy.visit('/') | ||||
|     return cy.visit("/") | ||||
|   }) | ||||
|  | ||||
|   it('should redirect to /blog on click of the blog link', () => { | ||||
|     cy.get('[data-cy=header-blog-link]') | ||||
|   it("should redirect to /blog on click of the blog link", () => { | ||||
|     cy.get("[data-cy=header-blog-link]") | ||||
|       .click() | ||||
|       .location('pathname') | ||||
|       .should('eq', '/blog') | ||||
|       .location("pathname") | ||||
|       .should("eq", "/blog") | ||||
|   }) | ||||
|  | ||||
|   it('should always be visible (sticky header)', () => { | ||||
|     cy.scrollTo('bottom').get('header').should('be.visible') | ||||
|   it("should always be visible (sticky header)", () => { | ||||
|     cy.scrollTo("bottom").get("header").should("be.visible") | ||||
|   }) | ||||
|  | ||||
|   describe('Switch theme color (dark/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-light]').should('not.be.visible') | ||||
|       cy.get('body').should( | ||||
|         'not.have.css', | ||||
|         'background-color', | ||||
|         'rgb(255, 255, 255)' | ||||
|   describe("Switch theme color (dark/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-light]").should("not.be.visible") | ||||
|       cy.get("body").should( | ||||
|         "not.have.css", | ||||
|         "background-color", | ||||
|         "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-light]').should('be.visible') | ||||
|       cy.get('body').should( | ||||
|         'have.css', | ||||
|         'background-color', | ||||
|         'rgb(255, 255, 255)' | ||||
|       cy.get("[data-cy=switch-theme-dark]").should("not.be.visible") | ||||
|       cy.get("[data-cy=switch-theme-light]").should("be.visible") | ||||
|       cy.get("body").should( | ||||
|         "have.css", | ||||
|         "background-color", | ||||
|         "rgb(255, 255, 255)", | ||||
|       ) | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   describe('Switch Language', () => { | ||||
|     it('should switch locale from English (default) to French', () => { | ||||
|       cy.get('h1').contains('Théo LUDWIG') | ||||
|       cy.get('[data-cy=locale-flag-text]').contains('English') | ||||
|       cy.get('[data-cy=locales-list]').should('not.be.visible') | ||||
|       cy.get('[data-cy=locale-click]').click() | ||||
|       cy.get('[data-cy=locales-list]').should('be.visible') | ||||
|       cy.get('[data-cy=locales-list] > li:first-child') | ||||
|         .contains('French') | ||||
|   describe("Switch Language", () => { | ||||
|     it("should switch locale from English (default) to French", () => { | ||||
|       cy.get("h1").contains("Théo LUDWIG") | ||||
|       cy.get("[data-cy=locale-flag-text]").contains("English") | ||||
|       cy.get("[data-cy=locales-list]").should("not.be.visible") | ||||
|       cy.get("[data-cy=locale-click]").click() | ||||
|       cy.get("[data-cy=locales-list]").should("be.visible") | ||||
|       cy.get("[data-cy=locales-list] > li:first-child") | ||||
|         .contains("French") | ||||
|         .click() | ||||
|       cy.get('[data-cy=locales-list]').should('not.be.visible') | ||||
|       cy.get('[data-cy=locale-flag-text]').contains('French') | ||||
|       cy.get('h1').contains('Théo LUDWIG') | ||||
|       cy.get("[data-cy=locales-list]").should("not.be.visible") | ||||
|       cy.get("[data-cy=locale-flag-text]").contains("French") | ||||
|       cy.get("h1").contains("Théo LUDWIG") | ||||
|     }) | ||||
|  | ||||
|     it('should close the locale list menu when clicking outside', () => { | ||||
|       cy.get('[data-cy=locales-list]').should('not.be.visible') | ||||
|       cy.get('[data-cy=locale-click]').click() | ||||
|       cy.get('[data-cy=locales-list]').should('be.visible') | ||||
|       cy.get('h1').click() | ||||
|       cy.get('[data-cy=locales-list]').should('not.be.visible') | ||||
|     it("should close the locale list menu when clicking outside", () => { | ||||
|       cy.get("[data-cy=locales-list]").should("not.be.visible") | ||||
|       cy.get("[data-cy=locale-click]").click() | ||||
|       cy.get("[data-cy=locales-list]").should("be.visible") | ||||
|       cy.get("h1").click() | ||||
|       cy.get("[data-cy=locales-list]").should("not.be.visible") | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| describe('Page /404', () => { | ||||
| describe("Page /404", () => { | ||||
|   beforeEach(() => { | ||||
|     return cy.visit('/404', { failOnStatusCode: false }) | ||||
|     return cy.visit("/404", { failOnStatusCode: false }) | ||||
|   }) | ||||
|  | ||||
|   it('should display the statusCode of 404', () => { | ||||
|     cy.get('[data-cy=status-code]').contains('404') | ||||
|   it("should display the statusCode of 404", () => { | ||||
|     cy.get("[data-cy=status-code]").contains("404") | ||||
|   }) | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| describe('Page /blog/[slug]', () => { | ||||
|   it('should displays the first blog post (`hello-world`)', () => { | ||||
|     cy.visit('/blog/hello-world') | ||||
|     cy.get('[data-cy=locale-flag-text]').should('not.exist') | ||||
|     cy.get('h1').should('have.text', '👋 Hello, world!') | ||||
|     cy.get('.prose a:visible').should('have.attr', 'target', '_blank') | ||||
| describe("Page /blog/[slug]", () => { | ||||
|   it("should displays the first blog post (`hello-world`)", () => { | ||||
|     cy.visit("/blog/hello-world") | ||||
|     cy.get("[data-cy=locale-flag-text]").should("not.exist") | ||||
|     cy.get("h1").should("have.text", "👋 Hello, world!") | ||||
|     cy.get(".prose a:visible").should("have.attr", "target", "_blank") | ||||
|   }) | ||||
|  | ||||
|   it("should redirect to /404 if the blog post doesn't exist", () => { | ||||
|     cy.visit('/blog/random-blog-post-not-found', { failOnStatusCode: false }) | ||||
|     cy.get('[data-cy=status-code]').contains('404') | ||||
|     cy.visit("/blog/random-blog-post-not-found", { failOnStatusCode: false }) | ||||
|     cy.get("[data-cy=status-code]").contains("404") | ||||
|   }) | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -1,23 +1,23 @@ | ||||
| describe('Page /blog', () => { | ||||
|   it('should displays the blog posts sorted from newest to oldest', () => { | ||||
|     cy.visit('/blog') | ||||
|     cy.get('[data-cy=blog-posts] [data-cy=blog-post-title]') | ||||
| describe("Page /blog", () => { | ||||
|   it("should displays the blog posts sorted from newest to oldest", () => { | ||||
|     cy.visit("/blog") | ||||
|     cy.get("[data-cy=blog-posts] [data-cy=blog-post-title]") | ||||
|       .last() | ||||
|       .should('have.text', '👋 Hello, world!') | ||||
|     cy.get('[data-cy=blog-posts] [data-cy=blog-post-description]') | ||||
|       .should("have.text", "👋 Hello, world!") | ||||
|     cy.get("[data-cy=blog-posts] [data-cy=blog-post-description]") | ||||
|       .last() | ||||
|       .should( | ||||
|         'have.text', | ||||
|         'First post of the blog, introduction and explanation of how this blog is made.' | ||||
|         "have.text", | ||||
|         "First post of the blog, introduction and explanation of how this blog is made.", | ||||
|       ) | ||||
|   }) | ||||
|  | ||||
|   it('should redirect the user to the right blog post', () => { | ||||
|     cy.visit('/blog') | ||||
|     cy.get('[data-cy=hello-world]') | ||||
|   it("should redirect the user to the right blog post", () => { | ||||
|     cy.visit("/blog") | ||||
|     cy.get("[data-cy=hello-world]") | ||||
|       .click() | ||||
|       .location('pathname') | ||||
|       .should('eq', '/blog/hello-world') | ||||
|       .location("pathname") | ||||
|       .should("eq", "/blog/hello-world") | ||||
|   }) | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
| describe('Page /', () => { | ||||
| describe("Page /", () => { | ||||
|   beforeEach(() => { | ||||
|     return cy.visit('/') | ||||
|     return cy.visit("/") | ||||
|   }) | ||||
|  | ||||
|   it('should reveals the sections while scrolling except the about section', () => { | ||||
|     const sectionsReveals = ['#interests', '#skills', '#portfolio'] | ||||
|     cy.get('#about').should('be.visible') | ||||
|   it("should reveals the sections while scrolling except the about section", () => { | ||||
|     const sectionsReveals = ["#interests", "#skills", "#portfolio"] | ||||
|     cy.get("#about").should("be.visible") | ||||
|     for (const section of sectionsReveals) { | ||||
|       cy.get(section) | ||||
|         .should('not.be.visible') | ||||
|         .should("not.be.visible") | ||||
|         .scrollIntoView() | ||||
|         .should('be.visible') | ||||
|         .should("be.visible") | ||||
|     } | ||||
|   }) | ||||
| }) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { mount } from 'cypress/react' | ||||
| import { mount } from "cypress/react" | ||||
|  | ||||
| import './commands' | ||||
| import '../../app/globals.css' | ||||
| import "./commands" | ||||
| import "../../app/globals.css" | ||||
|  | ||||
| declare global { | ||||
|   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 type { I18n } from 'i18n-js' | ||||
| import UniversalCookie from "universal-cookie" | ||||
| 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 => { | ||||
|   const universalCookie = new UniversalCookie(cookiesStore) | ||||
|   i18n.locale = universalCookie.get('locale') ?? i18n.defaultLocale | ||||
|   i18n.locale = universalCookie.get("locale") ?? i18n.defaultLocale | ||||
|   return i18n | ||||
| } | ||||
|   | ||||
| @@ -1,21 +1,21 @@ | ||||
| 'use server' | ||||
| "use server" | ||||
|  | ||||
| import { cookies } from 'next/headers' | ||||
| import type { I18n } from 'i18n-js' | ||||
| import { cookies } from "next/headers" | ||||
| import type { I18n } from "i18n-js" | ||||
|  | ||||
| import type { Locale } from '@/utils/constants' | ||||
| import { COOKIE_MAX_AGE } from '@/utils/constants' | ||||
| import type { Locale } from "@/utils/constants" | ||||
| import { COOKIE_MAX_AGE } from "@/utils/constants" | ||||
|  | ||||
| import { i18n } from './i18n' | ||||
| import { i18n } from "./i18n" | ||||
|  | ||||
| export const setLocale = (locale: Locale): void => { | ||||
|   cookies().set('locale', locale, { | ||||
|     path: '/', | ||||
|     maxAge: COOKIE_MAX_AGE | ||||
|   cookies().set("locale", locale, { | ||||
|     path: "/", | ||||
|     maxAge: COOKIE_MAX_AGE, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export const getI18n = (): I18n => { | ||||
|   i18n.locale = cookies().get('locale')?.value ?? i18n.defaultLocale | ||||
|   i18n.locale = cookies().get("locale")?.value ?? i18n.defaultLocale | ||||
|   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 { DEFAULT_LOCALE, LOCALES } from '@/utils/constants' | ||||
| import type { Locale } from "@/utils/constants" | ||||
| import { DEFAULT_LOCALE, LOCALES } from "@/utils/constants" | ||||
|  | ||||
| import commonEnglish from './translations/en-US/common.json' | ||||
| import errorsEnglish from './translations/en-US/errors.json' | ||||
| import homeEnglish from './translations/en-US/home.json' | ||||
| import commonFrench from './translations/fr-FR/common.json' | ||||
| import errorsFrench from './translations/fr-FR/errors.json' | ||||
| import homeFrench from './translations/fr-FR/home.json' | ||||
| import commonEnglish from "./translations/en-US/common.json" | ||||
| import errorsEnglish from "./translations/en-US/errors.json" | ||||
| import homeEnglish from "./translations/en-US/home.json" | ||||
| import commonFrench from "./translations/fr-FR/common.json" | ||||
| import errorsFrench from "./translations/fr-FR/errors.json" | ||||
| import homeFrench from "./translations/fr-FR/home.json" | ||||
|  | ||||
| const translations = { | ||||
|   'en-US': { | ||||
|   "en-US": { | ||||
|     common: commonEnglish, | ||||
|     errors: errorsEnglish, | ||||
|     home: homeEnglish | ||||
|     home: homeEnglish, | ||||
|   }, | ||||
|   'fr-FR': { | ||||
|   "fr-FR": { | ||||
|     common: commonFrench, | ||||
|     errors: errorsFrench, | ||||
|     home: homeFrench | ||||
|   } | ||||
|     home: homeFrench, | ||||
|   }, | ||||
| } satisfies Record<Locale, Record<string, unknown>> | ||||
|  | ||||
| export const i18n = new I18n(translations, { | ||||
|   defaultLocale: DEFAULT_LOCALE, | ||||
|   availableLocales: LOCALES.slice(), | ||||
|   enableFallback: true | ||||
|   enableFallback: true, | ||||
| }) | ||||
|   | ||||
| @@ -1,43 +1,43 @@ | ||||
| import { NextResponse } from 'next/server' | ||||
| import type { NextRequest } from 'next/server' | ||||
| import { match } from '@formatjs/intl-localematcher' | ||||
| import Negotiator from 'negotiator' | ||||
| import { NextResponse } from "next/server" | ||||
| import type { NextRequest } from "next/server" | ||||
| import { match } from "@formatjs/intl-localematcher" | ||||
| import Negotiator from "negotiator" | ||||
|  | ||||
| import type { Locale, Theme } from '@/utils/constants' | ||||
| import type { Locale, Theme } from "@/utils/constants" | ||||
| import { | ||||
|   COOKIE_MAX_AGE, | ||||
|   DEFAULT_LOCALE, | ||||
|   DEFAULT_THEME, | ||||
|   LOCALES, | ||||
|   THEMES | ||||
| } from '@/utils/constants' | ||||
|   THEMES, | ||||
| } from "@/utils/constants" | ||||
|  | ||||
| export const middleware = (request: NextRequest): NextResponse => { | ||||
|   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)) { | ||||
|     try { | ||||
|       const headers = { | ||||
|         'accept-language': | ||||
|           request.headers.get('accept-language') ?? DEFAULT_LOCALE | ||||
|         "accept-language": | ||||
|           request.headers.get("accept-language") ?? DEFAULT_LOCALE, | ||||
|       } | ||||
|       const languages = new Negotiator({ headers }).languages() | ||||
|       locale = match(languages, LOCALES.slice(), DEFAULT_LOCALE) | ||||
|     } catch { | ||||
|       locale = DEFAULT_LOCALE | ||||
|     } | ||||
|     response.cookies.set('locale', locale, { | ||||
|       path: '/', | ||||
|       maxAge: COOKIE_MAX_AGE | ||||
|     response.cookies.set("locale", locale, { | ||||
|       path: "/", | ||||
|       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)) { | ||||
|     response.cookies.set('theme', DEFAULT_THEME, { | ||||
|       path: '/', | ||||
|       maxAge: COOKIE_MAX_AGE | ||||
|     response.cookies.set("theme", DEFAULT_THEME, { | ||||
|       path: "/", | ||||
|       maxAge: COOKIE_MAX_AGE, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
| @@ -53,6 +53,6 @@ export const config = { | ||||
|      * - _next/image (image optimization files) | ||||
|      * - 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} */ | ||||
| const nextConfig = { | ||||
|   reactStrictMode: true, | ||||
|   output: 'standalone', | ||||
|   output: "standalone", | ||||
|   eslint: { | ||||
|     ignoreDuringBuilds: true | ||||
|     ignoreDuringBuilds: true, | ||||
|   }, | ||||
|   experimental: { | ||||
|     serverActions: true | ||||
|   } | ||||
|     serverActions: true, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| 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": { | ||||
|     "node": ">=20.0.0", | ||||
|     "npm": ">=9.0.0" | ||||
|     "npm": ">=10.0.0" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "dev": "next dev", | ||||
| @@ -29,7 +29,7 @@ | ||||
|     "postinstall": "husky install" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@fontsource/montserrat": "5.0.8", | ||||
|     "@fontsource/montserrat": "5.0.15", | ||||
|     "@formatjs/intl-localematcher": "0.4.2", | ||||
|     "@fortawesome/fontawesome-svg-core": "6.4.2", | ||||
|     "@fortawesome/free-brands-svg-icons": "6.4.2", | ||||
| @@ -39,11 +39,11 @@ | ||||
|     "clsx": "2.0.0", | ||||
|     "date-and-time": "3.0.3", | ||||
|     "gray-matter": "4.0.3", | ||||
|     "html-react-parser": "4.2.2", | ||||
|     "html-react-parser": "4.2.9", | ||||
|     "i18n-js": "4.3.2", | ||||
|     "katex": "0.16.8", | ||||
|     "katex": "0.16.9", | ||||
|     "negotiator": "0.6.3", | ||||
|     "next": "13.4.19", | ||||
|     "next": "13.5.6", | ||||
|     "next-mdx-remote": "4.4.1", | ||||
|     "react": "18.2.0", | ||||
|     "react-dom": "18.2.0", | ||||
| @@ -53,47 +53,47 @@ | ||||
|     "rehype-slug": "5.1.0", | ||||
|     "remark-gfm": "3.0.1", | ||||
|     "remark-math": "5.1.1", | ||||
|     "sharp": "0.32.5", | ||||
|     "shiki": "0.14.4", | ||||
|     "sharp": "0.32.6", | ||||
|     "shiki": "0.14.5", | ||||
|     "unified": "10.1.2", | ||||
|     "unist-util-visit": "5.0.0", | ||||
|     "universal-cookie": "6.1.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@commitlint/cli": "17.7.1", | ||||
|     "@commitlint/config-conventional": "17.7.0", | ||||
|     "@saithodev/semantic-release-backmerge": "3.2.0", | ||||
|     "@commitlint/cli": "18.0.0", | ||||
|     "@commitlint/config-conventional": "18.0.0", | ||||
|     "@saithodev/semantic-release-backmerge": "3.2.1", | ||||
|     "@semantic-release/git": "10.0.1", | ||||
|     "@tailwindcss/typography": "0.5.10", | ||||
|     "@tsconfig/strictest": "2.0.2", | ||||
|     "@types/negotiator": "0.6.1", | ||||
|     "@types/node": "20.6.2", | ||||
|     "@types/react": "18.2.22", | ||||
|     "@types/unist": "3.0.0", | ||||
|     "@typescript-eslint/eslint-plugin": "6.7.2", | ||||
|     "@typescript-eslint/parser": "6.7.2", | ||||
|     "autoprefixer": "10.4.15", | ||||
|     "@types/negotiator": "0.6.2", | ||||
|     "@types/node": "20.8.7", | ||||
|     "@types/react": "18.2.31", | ||||
|     "@types/unist": "3.0.1", | ||||
|     "@typescript-eslint/eslint-plugin": "6.9.0", | ||||
|     "@typescript-eslint/parser": "6.9.0", | ||||
|     "autoprefixer": "10.4.16", | ||||
|     "curriculum-vitae": "file:./curriculum-vitae", | ||||
|     "cypress": "13.2.0", | ||||
|     "cypress": "13.3.2", | ||||
|     "editorconfig-checker": "5.1.1", | ||||
|     "eslint": "8.49.0", | ||||
|     "eslint-config-conventions": "11.0.1", | ||||
|     "eslint-config-next": "13.4.19", | ||||
|     "eslint": "8.52.0", | ||||
|     "eslint-config-conventions": "12.0.0", | ||||
|     "eslint-config-next": "13.5.6", | ||||
|     "eslint-config-prettier": "9.0.0", | ||||
|     "eslint-plugin-import": "2.28.1", | ||||
|     "eslint-plugin-prettier": "5.0.0", | ||||
|     "eslint-plugin-import": "2.29.0", | ||||
|     "eslint-plugin-prettier": "5.0.1", | ||||
|     "eslint-plugin-promise": "6.1.1", | ||||
|     "eslint-plugin-unicorn": "48.0.1", | ||||
|     "html-w3c-validator": "1.5.0", | ||||
|     "husky": "8.0.3", | ||||
|     "lint-staged": "14.0.1", | ||||
|     "lint-staged": "15.0.2", | ||||
|     "markdownlint-cli2": "0.10.0", | ||||
|     "markdownlint-rule-relative-links": "2.1.0", | ||||
|     "postcss": "8.4.29", | ||||
|     "postcss": "8.4.31", | ||||
|     "prettier": "3.0.3", | ||||
|     "prettier-plugin-tailwindcss": "0.5.4", | ||||
|     "prettier-plugin-tailwindcss": "0.5.6", | ||||
|     "semantic-release": "21.1.2", | ||||
|     "start-server-and-test": "2.0.0", | ||||
|     "start-server-and-test": "2.0.1", | ||||
|     "tailwindcss": "3.3.3", | ||||
|     "typescript": "5.2.2" | ||||
|   } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| module.exports = { | ||||
|   plugins: { | ||||
|     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