mirror of
https://github.com/theoludwig/theoludwig.git
synced 2025-05-29 22:37:44 +02:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
62222dbb0c
|
|||
ee0a02bc8b
|
|||
2e04053ec3
|
|||
45a9a69122
|
|||
e566ef6c38
|
|||
c7ad15a465
|
|||
f4a842efb5
|
|||
424c97019b
|
|||
c0508dc0b9
|
|||
f04d8a0c11
|
|||
d29064745c
|
|||
95febe2a99
|
|||
fdab2a7ea8
|
|||
35211fa279
|
|||
137cceffa1
|
@ -1,9 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
workspace:
|
workspace:
|
||||||
build:
|
build:
|
||||||
context: './'
|
context: "./"
|
||||||
dockerfile: './Dockerfile'
|
dockerfile: "./Dockerfile"
|
||||||
volumes:
|
volumes:
|
||||||
- '..:/workspace:cached'
|
- "..:/workspace:cached"
|
||||||
command: 'sleep infinity'
|
command: "sleep infinity"
|
||||||
network_mode: 'host'
|
network_mode: "host"
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
COMPOSE_PROJECT_NAME=theoludwig
|
COMPOSE_PROJECT_NAME=theoludwig
|
||||||
|
HOSTNAME=0.0.0.0
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
NEXT_TELEMETRY_DISABLED=1
|
||||||
|
8
.github/ISSUE_TEMPLATE/BUG.md
vendored
8
.github/ISSUE_TEMPLATE/BUG.md
vendored
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: '🐛 Bug Report'
|
name: "🐛 Bug Report"
|
||||||
about: 'Report an unexpected problem or unintended behavior.'
|
about: "Report an unexpected problem or unintended behavior."
|
||||||
title: '[Bug]'
|
title: "[Bug]"
|
||||||
labels: 'bug'
|
labels: "bug"
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
8
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
8
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: '📜 Documentation'
|
name: "📜 Documentation"
|
||||||
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).'
|
about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
|
||||||
title: '[Documentation]'
|
title: "[Documentation]"
|
||||||
labels: 'documentation'
|
labels: "documentation"
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Please make sure your issue has not already been fixed. -->
|
<!-- Please make sure your issue has not already been fixed. -->
|
||||||
|
8
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
8
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: '✨ Feature Request'
|
name: "✨ Feature Request"
|
||||||
about: 'Suggest a new feature idea.'
|
about: "Suggest a new feature idea."
|
||||||
title: '[Feature]'
|
title: "[Feature]"
|
||||||
labels: 'feature request'
|
labels: "feature request"
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Please make sure your issue has not already been fixed. -->
|
<!-- Please make sure your issue has not already been fixed. -->
|
||||||
|
8
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
8
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: '🔧 Improvement'
|
name: "🔧 Improvement"
|
||||||
about: 'Improve structure/format/performance/refactor/tests of the code.'
|
about: "Improve structure/format/performance/refactor/tests of the code."
|
||||||
title: '[Improvement]'
|
title: "[Improvement]"
|
||||||
labels: 'improvement'
|
labels: "improvement"
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Please make sure your issue has not already been fixed. -->
|
<!-- Please make sure your issue has not already been fixed. -->
|
||||||
|
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: '🙋 Question'
|
name: "🙋 Question"
|
||||||
about: 'Further information is requested.'
|
about: "Further information is requested."
|
||||||
title: '[Question]'
|
title: "[Question]"
|
||||||
labels: 'question'
|
labels: "question"
|
||||||
---
|
---
|
||||||
|
|
||||||
### Question
|
### Question
|
||||||
|
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: 'Build'
|
name: "Build"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -8,18 +8,18 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.5.3'
|
- uses: "actions/checkout@v4.1.1"
|
||||||
|
|
||||||
- name: 'Setup Node.js'
|
- name: "Setup Node.js"
|
||||||
uses: 'actions/setup-node@v3.7.0'
|
uses: "actions/setup-node@v4.0.0"
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: "20.x"
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
|
|
||||||
- name: 'Install dependencies'
|
- name: "Install dependencies"
|
||||||
run: 'npm clean-install'
|
run: "npm clean-install"
|
||||||
|
|
||||||
- name: 'Build'
|
- name: "Build"
|
||||||
run: 'npm run build'
|
run: "npm run build"
|
||||||
|
40
.github/workflows/lint.yml
vendored
40
.github/workflows/lint.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: 'Lint'
|
name: "Lint"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -8,35 +8,35 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.5.3'
|
- uses: "actions/checkout@v4.1.1"
|
||||||
|
|
||||||
- name: 'Setup Node.js'
|
- name: "Setup Node.js"
|
||||||
uses: 'actions/setup-node@v3.7.0'
|
uses: "actions/setup-node@v4.0.0"
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: "20.x"
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
|
|
||||||
- name: 'Install dependencies'
|
- name: "Install dependencies"
|
||||||
run: 'npm clean-install'
|
run: "npm clean-install"
|
||||||
|
|
||||||
- name: 'lint:commit'
|
- name: "lint:commit"
|
||||||
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||||
|
|
||||||
- name: 'lint:editorconfig'
|
- name: "lint:editorconfig"
|
||||||
run: 'npm run lint:editorconfig'
|
run: "npm run lint:editorconfig"
|
||||||
|
|
||||||
- name: 'lint:markdown'
|
- name: "lint:markdown"
|
||||||
run: 'npm run lint:markdown'
|
run: "npm run lint:markdown"
|
||||||
|
|
||||||
- name: 'lint:eslint'
|
- name: "lint:eslint"
|
||||||
run: 'npm run lint:eslint'
|
run: "npm run lint:eslint"
|
||||||
|
|
||||||
- name: 'lint:prettier'
|
- name: "lint:prettier"
|
||||||
run: 'npm run lint:prettier'
|
run: "npm run lint:prettier"
|
||||||
|
|
||||||
- name: 'lint:dotenv'
|
- name: "lint:dotenv"
|
||||||
uses: 'dotenv-linter/action-dotenv-linter@v2'
|
uses: "dotenv-linter/action-dotenv-linter@v2.18.0"
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.github_token }}
|
github_token: ${{ secrets.github_token }}
|
||||||
|
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: 'Release'
|
name: "Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -6,31 +6,31 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.5.3'
|
- uses: "actions/checkout@v4.1.1"
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: 'Import GPG key'
|
- name: "Import GPG key"
|
||||||
uses: 'crazy-max/ghaction-import-gpg@v5.3.0'
|
uses: "crazy-max/ghaction-import-gpg@v6.0.0"
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
git_user_signingkey: true
|
git_user_signingkey: true
|
||||||
git_commit_gpgsign: true
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
- name: 'Setup Node.js'
|
- name: "Setup Node.js"
|
||||||
uses: 'actions/setup-node@v3.7.0'
|
uses: "actions/setup-node@v4.0.0"
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: "20.x"
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
|
|
||||||
- name: 'Install dependencies'
|
- name: "Install dependencies"
|
||||||
run: 'npm clean-install'
|
run: "npm clean-install"
|
||||||
|
|
||||||
- name: 'Release'
|
- name: "Release"
|
||||||
run: 'npm run release'
|
run: "npm run release"
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
|
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
|
||||||
|
50
.github/workflows/test.yml
vendored
50
.github/workflows/test.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: 'Test'
|
name: "Test"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -8,41 +8,41 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-unit:
|
test-unit:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.5.3'
|
- uses: "actions/checkout@v4.1.1"
|
||||||
|
|
||||||
- name: 'Setup Node.js'
|
- name: "Setup Node.js"
|
||||||
uses: 'actions/setup-node@v3.7.0'
|
uses: "actions/setup-node@v4.0.0"
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: "20.x"
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
|
|
||||||
- name: 'Install dependencies'
|
- name: "Install dependencies"
|
||||||
run: 'npm clean-install'
|
run: "npm clean-install"
|
||||||
|
|
||||||
- name: 'Unit Test'
|
- name: "Unit Test"
|
||||||
run: 'npm run test:unit'
|
run: "npm run test:unit"
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: 'actions/checkout@v3.5.3'
|
- uses: "actions/checkout@v4.1.1"
|
||||||
|
|
||||||
- name: 'Setup Node.js'
|
- name: "Setup Node.js"
|
||||||
uses: 'actions/setup-node@v3.7.0'
|
uses: "actions/setup-node@v4.0.0"
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: "20.x"
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
|
|
||||||
- name: 'Install dependencies'
|
- name: "Install dependencies"
|
||||||
run: 'npm clean-install'
|
run: "npm clean-install"
|
||||||
|
|
||||||
- name: 'Build'
|
- name: "Build"
|
||||||
run: 'npm run build'
|
run: "npm run build"
|
||||||
|
|
||||||
- name: 'html-w3c-validator'
|
- name: "html-w3c-validator"
|
||||||
run: 'npm run test:html-w3c-validator'
|
run: "npm run test:html-w3c-validator"
|
||||||
|
|
||||||
- name: 'End To End (e2e) Test'
|
- name: "End To End (e2e) Test"
|
||||||
run: 'npm run test:e2e'
|
run: "npm run test:e2e"
|
||||||
|
10
.gitpod.yml
10
.gitpod.yml
@ -1,13 +1,13 @@
|
|||||||
image: 'gitpod/workspace-full'
|
image: "gitpod/workspace-full"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- before: 'cp .env.example .env'
|
- before: "cp .env.example .env"
|
||||||
init: 'npm clean-install'
|
init: "npm clean-install"
|
||||||
command: 'npm run dev'
|
command: "npm run dev"
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- port: 3000
|
- port: 3000
|
||||||
onOpen: 'open-preview'
|
onOpen: "open-preview"
|
||||||
|
|
||||||
github:
|
github:
|
||||||
prebuilds:
|
prebuilds:
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"semi": false
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"semi": false,
|
|
||||||
"trailingComma": "none"
|
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) >= 20.0.0
|
- [Node.js](https://nodejs.org/) >= 20.0.0
|
||||||
- [npm](https://www.npmjs.com/) >= 9.0.0
|
- [npm](https://www.npmjs.com/) >= 10.0.0
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
FROM node:20.5.0 AS builder-dependencies
|
FROM node:20.9.0 AS builder-dependencies
|
||||||
WORKDIR /usr/src/application
|
WORKDIR /usr/src/application
|
||||||
COPY ./package*.json ./
|
COPY ./package*.json ./
|
||||||
RUN npm clean-install
|
RUN npm clean-install
|
||||||
|
|
||||||
FROM node:20.5.0 AS builder
|
FROM node:20.9.0 AS builder
|
||||||
WORKDIR /usr/src/application
|
WORKDIR /usr/src/application
|
||||||
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
|
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM gcr.io/distroless/nodejs20-debian11:latest AS runner
|
FROM gcr.io/distroless/nodejs20-debian12:latest AS runner
|
||||||
WORKDIR /usr/src/application
|
WORKDIR /usr/src/application
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
|
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
|
||||||
COPY --from=builder /usr/src/application/.next/standalone ./
|
COPY --from=builder /usr/src/application/.next/standalone ./
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Loader } from '@/components/design/Loader'
|
import { Loader } from "@/components/design/Loader"
|
||||||
|
|
||||||
const Loading = (): JSX.Element => {
|
const Loading = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<main className='flex flex-col flex-1 items-center justify-center'>
|
<main className="flex flex-col flex-1 items-center justify-center">
|
||||||
<Loader />
|
<Loader />
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from "next"
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import 'katex/dist/katex.min.css'
|
import "katex/dist/katex.min.css"
|
||||||
|
|
||||||
import { getBlogPostBySlug } from '@/blog/blog'
|
import { getBlogPostBySlug } from "@/blog/blog"
|
||||||
import { BlogPost } from '@/blog/BlogPost'
|
import { BlogPost } from "@/blog/BlogPost"
|
||||||
|
|
||||||
interface BlogPostPageProps {
|
interface BlogPostPageProps {
|
||||||
params: {
|
params: {
|
||||||
@ -13,7 +13,7 @@ interface BlogPostPageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const generateMetadata = async (
|
export const generateMetadata = async (
|
||||||
props: BlogPostPageProps
|
props: BlogPostPageProps,
|
||||||
): Promise<Metadata> => {
|
): Promise<Metadata> => {
|
||||||
const blogPost = await getBlogPostBySlug(props.params.slug)
|
const blogPost = await getBlogPostBySlug(props.params.slug)
|
||||||
if (blogPost == null) {
|
if (blogPost == null) {
|
||||||
@ -26,12 +26,12 @@ export const generateMetadata = async (
|
|||||||
description,
|
description,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title,
|
title,
|
||||||
description
|
description,
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title,
|
title,
|
||||||
description
|
description,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Loader } from '@/components/design/Loader'
|
import { Loader } from "@/components/design/Loader"
|
||||||
|
|
||||||
const Loading = (): JSX.Element => {
|
const Loading = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<main className='flex flex-col flex-1 items-center justify-center'>
|
<main className="flex flex-col flex-1 items-center justify-center">
|
||||||
<Loader />
|
<Loader />
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
@ -1,36 +1,36 @@
|
|||||||
import { Suspense } from 'react'
|
import { Suspense } from "react"
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from "next"
|
||||||
|
|
||||||
import { BlogPosts } from '@/blog/BlogPosts'
|
import { BlogPosts } from "@/blog/BlogPosts"
|
||||||
import { Loader } from '@/components/design/Loader'
|
import { Loader } from "@/components/design/Loader"
|
||||||
|
|
||||||
const title = 'Blog | Théo LUDWIG'
|
const title = "Blog | Théo LUDWIG"
|
||||||
const description =
|
const description =
|
||||||
'The latest news about my journey of learning computer science.'
|
"The latest news about my journey of learning computer science."
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title,
|
title,
|
||||||
description
|
description,
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
title,
|
title,
|
||||||
description
|
description,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const BlogPage = async (): Promise<JSX.Element> => {
|
const BlogPage = async (): Promise<JSX.Element> => {
|
||||||
return (
|
return (
|
||||||
<main className='flex flex-1 flex-col flex-wrap items-center'>
|
<main className="flex flex-1 flex-col flex-wrap items-center">
|
||||||
<div className='mt-10 flex flex-col items-center'>
|
<div className="mt-10 flex flex-col items-center">
|
||||||
<h1 className='text-4xl font-semibold'>Blog</h1>
|
<h1 className="text-4xl font-semibold">Blog</h1>
|
||||||
<p className='mt-6 text-center' data-cy='blog-post-date'>
|
<p className="mt-6 text-center" data-cy="blog-post-date">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Suspense fallback={<Loader className='mt-8' />}>
|
<Suspense fallback={<Loader className="mt-8" />}>
|
||||||
<BlogPosts />
|
<BlogPosts />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</main>
|
</main>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
"use client"
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from "react"
|
||||||
|
|
||||||
export interface ErrorHandlingProps {
|
export interface ErrorHandlingProps {
|
||||||
error: Error
|
error: Error
|
||||||
@ -14,17 +14,17 @@ const ErrorHandling = (props: ErrorHandlingProps): JSX.Element => {
|
|||||||
}, [error])
|
}, [error])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className='flex flex-col flex-1 items-center justify-center'>
|
<main className="flex flex-col flex-1 items-center justify-center">
|
||||||
<h1 className='my-6 text-4xl font-semibold'>
|
<h1 className="my-6 text-4xl font-semibold">
|
||||||
Error{' '}
|
Error{" "}
|
||||||
<span
|
<span
|
||||||
className='text-yellow dark:text-yellow-dark'
|
className="text-yellow dark:text-yellow-dark"
|
||||||
data-cy='status-code'
|
data-cy="status-code"
|
||||||
>
|
>
|
||||||
500
|
500
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-center text-lg'>Server error</p>
|
<p className="text-center text-lg">Server error</p>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose [id]::before {
|
.prose [id]::before {
|
||||||
content: '';
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
margin-top: -90px;
|
margin-top: -90px;
|
||||||
@ -39,9 +39,9 @@
|
|||||||
.prose code {
|
.prose code {
|
||||||
color: #ce9178;
|
color: #ce9178;
|
||||||
}
|
}
|
||||||
.prose :where(code):not(:where([class~='not-prose'] *))::before,
|
.prose :where(code):not(:where([class~="not-prose"] *))::before,
|
||||||
.prose :where(code):not(:where([class~='not-prose'] *))::after {
|
.prose :where(code):not(:where([class~="not-prose"] *))::after {
|
||||||
content: '';
|
content: "";
|
||||||
}
|
}
|
||||||
.shiki {
|
.shiki {
|
||||||
white-space: pre-wrap !important;
|
white-space: pre-wrap !important;
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from "next"
|
||||||
import classNames from 'clsx'
|
import classNames from "clsx"
|
||||||
|
|
||||||
import '@fontsource/montserrat/400.css'
|
import "@fontsource/montserrat/400.css"
|
||||||
import '@fontsource/montserrat/600.css'
|
import "@fontsource/montserrat/600.css"
|
||||||
import './globals.css'
|
import "./globals.css"
|
||||||
|
|
||||||
import { Header } from '@/components/Header'
|
import { Header } from "@/components/Header"
|
||||||
import { Footer } from '@/components/Footer'
|
import { Footer } from "@/components/Footer"
|
||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
import { getTheme } from '@/theme/theme.server'
|
import { getTheme } from "@/theme/theme.server"
|
||||||
|
|
||||||
const title = 'Théo LUDWIG'
|
const title = "Théo LUDWIG"
|
||||||
const description =
|
const description =
|
||||||
'Théo LUDWIG - Developer Full Stack • Open-Source enthusiast'
|
"Théo LUDWIG - Developer Full Stack • Open-Source enthusiast"
|
||||||
const image = '/images/icon-96x96.png'
|
const image = "/images/icon-96x96.png"
|
||||||
const url = new URL('https://theoludwig.fr')
|
const url = new URL("https://theoludwig.fr")
|
||||||
const locale = 'fr-FR, en-US'
|
const locale = "fr-FR, en-US"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title,
|
title,
|
||||||
@ -30,21 +30,21 @@ export const metadata: Metadata = {
|
|||||||
{
|
{
|
||||||
url: image,
|
url: image,
|
||||||
width: 96,
|
width: 96,
|
||||||
height: 96
|
height: 96,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
locale,
|
locale,
|
||||||
type: 'website'
|
type: "website",
|
||||||
},
|
},
|
||||||
icons: {
|
icons: {
|
||||||
icon: '/images/icon-96x96.png'
|
icon: "/images/icon-96x96.png",
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary',
|
card: "summary",
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
images: [image]
|
images: [image],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RootLayoutProps {
|
interface RootLayoutProps {
|
||||||
@ -61,14 +61,14 @@ const RootLayout = (props: RootLayoutProps): JSX.Element => {
|
|||||||
<html
|
<html
|
||||||
lang={i18n.locale}
|
lang={i18n.locale}
|
||||||
className={classNames({
|
className={classNames({
|
||||||
dark: theme === 'dark',
|
dark: theme === "dark",
|
||||||
light: theme === 'light'
|
light: theme === "light",
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
colorScheme: theme
|
colorScheme: theme,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<body className='bg-white font-headline text-black dark:bg-black dark:text-white flex flex-col min-h-screen'>
|
<body className="bg-white font-headline text-black dark:bg-black dark:text-white flex flex-col min-h-screen">
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Loader } from '@/components/design/Loader'
|
import { Loader } from "@/components/design/Loader"
|
||||||
|
|
||||||
const Loading = (): JSX.Element => {
|
const Loading = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<main className='flex flex-col flex-1 items-center justify-center'>
|
<main className="flex flex-col flex-1 items-center justify-center">
|
||||||
<Loader />
|
<Loader />
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
import Link from 'next/link'
|
import Link from "next/link"
|
||||||
|
|
||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
|
|
||||||
const NotFound = (): JSX.Element => {
|
const NotFound = (): JSX.Element => {
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className='flex flex-col flex-1 items-center justify-center'>
|
<main className="flex flex-col flex-1 items-center justify-center">
|
||||||
<h1 className='my-6 text-4xl font-semibold'>
|
<h1 className="my-6 text-4xl font-semibold">
|
||||||
{i18n.translate('errors.error')}{' '}
|
{i18n.translate("errors.error")}{" "}
|
||||||
<span
|
<span
|
||||||
className='text-yellow dark:text-yellow-dark'
|
className="text-yellow dark:text-yellow-dark"
|
||||||
data-cy='status-code'
|
data-cy="status-code"
|
||||||
>
|
>
|
||||||
404
|
404
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-center text-lg'>
|
<p className="text-center text-lg">
|
||||||
{i18n.translate('errors.not-found')}{' '}
|
{i18n.translate("errors.not-found")}{" "}
|
||||||
<Link
|
<Link
|
||||||
href='/'
|
href="/"
|
||||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
className="text-yellow hover:underline dark:text-yellow-dark"
|
||||||
>
|
>
|
||||||
{i18n.translate('errors.return-to-home-page')}
|
{i18n.translate("errors.return-to-home-page")}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
|
36
app/page.tsx
36
app/page.tsx
@ -1,27 +1,27 @@
|
|||||||
import { RevealFade } from '@/components/design/RevealFade'
|
import { RevealFade } from "@/components/design/RevealFade"
|
||||||
import { Section } from '@/components/design/Section'
|
import { Section } from "@/components/design/Section"
|
||||||
import { Interests } from '@/components/Interests'
|
import { Interests } from "@/components/Interests"
|
||||||
import { Portfolio } from '@/components/Portfolio'
|
import { Portfolio } from "@/components/Portfolio"
|
||||||
import { Profile } from '@/components/Profile'
|
import { Profile } from "@/components/Profile"
|
||||||
import { SocialMediaList } from '@/components/Profile/SocialMediaList'
|
import { SocialMediaList } from "@/components/Profile/SocialMediaList"
|
||||||
import { Skills } from '@/components/Skills'
|
import { Skills } from "@/components/Skills"
|
||||||
import { OpenSource } from '@/components/OpenSource'
|
import { OpenSource } from "@/components/OpenSource"
|
||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
|
|
||||||
const HomePage = (): JSX.Element => {
|
const HomePage = (): JSX.Element => {
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
|
<main className="flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl">
|
||||||
<Section isMain id='about'>
|
<Section isMain id="about">
|
||||||
<Profile />
|
<Profile />
|
||||||
<SocialMediaList />
|
<SocialMediaList />
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<RevealFade>
|
<RevealFade>
|
||||||
<Section
|
<Section
|
||||||
id='interests'
|
id="interests"
|
||||||
heading={i18n.translate('home.interests.title')}
|
heading={i18n.translate("home.interests.title")}
|
||||||
>
|
>
|
||||||
<Interests />
|
<Interests />
|
||||||
</Section>
|
</Section>
|
||||||
@ -29,8 +29,8 @@ const HomePage = (): JSX.Element => {
|
|||||||
|
|
||||||
<RevealFade>
|
<RevealFade>
|
||||||
<Section
|
<Section
|
||||||
id='skills'
|
id="skills"
|
||||||
heading={i18n.translate('home.skills.title')}
|
heading={i18n.translate("home.skills.title")}
|
||||||
withoutShadowContainer
|
withoutShadowContainer
|
||||||
>
|
>
|
||||||
<Skills />
|
<Skills />
|
||||||
@ -39,8 +39,8 @@ const HomePage = (): JSX.Element => {
|
|||||||
|
|
||||||
<RevealFade>
|
<RevealFade>
|
||||||
<Section
|
<Section
|
||||||
id='portfolio'
|
id="portfolio"
|
||||||
heading={i18n.translate('home.portfolio.title')}
|
heading={i18n.translate("home.portfolio.title")}
|
||||||
withoutShadowContainer
|
withoutShadowContainer
|
||||||
>
|
>
|
||||||
<Portfolio />
|
<Portfolio />
|
||||||
@ -48,7 +48,7 @@ const HomePage = (): JSX.Element => {
|
|||||||
</RevealFade>
|
</RevealFade>
|
||||||
|
|
||||||
<RevealFade>
|
<RevealFade>
|
||||||
<Section id='open-source' heading='Open source' withoutShadowContainer>
|
<Section id="open-source" heading="Open source" withoutShadowContainer>
|
||||||
<OpenSource />
|
<OpenSource />
|
||||||
</Section>
|
</Section>
|
||||||
</RevealFade>
|
</RevealFade>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { notFound } from 'next/navigation'
|
import { notFound } from "next/navigation"
|
||||||
import date from 'date-and-time'
|
import date from "date-and-time"
|
||||||
|
|
||||||
import 'katex/dist/katex.min.css'
|
import "katex/dist/katex.min.css"
|
||||||
|
|
||||||
import { getBlogPostBySlug } from '@/blog/blog'
|
import { getBlogPostBySlug } from "@/blog/blog"
|
||||||
import { BlogPostContent } from '@/blog/BlogPostContent'
|
import { BlogPostContent } from "@/blog/BlogPostContent"
|
||||||
|
|
||||||
export interface BlogPostProps {
|
export interface BlogPostProps {
|
||||||
slug: string
|
slug: string
|
||||||
@ -19,13 +19,13 @@ export const BlogPost = async (props: BlogPostProps): Promise<JSX.Element> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className='break-wrap-words flex flex-1 flex-col flex-wrap items-center justify-center'>
|
<main className="break-wrap-words flex flex-1 flex-col flex-wrap items-center justify-center">
|
||||||
<div className='my-10 flex flex-col items-center text-center'>
|
<div className="my-10 flex flex-col items-center text-center">
|
||||||
<h1 className='text-3xl font-semibold'>{blogPost.frontmatter.title}</h1>
|
<h1 className="text-3xl font-semibold">{blogPost.frontmatter.title}</h1>
|
||||||
<p className='mt-2' data-cy='blog-post-date'>
|
<p className="mt-2" data-cy="blog-post-date">
|
||||||
{date.format(
|
{date.format(
|
||||||
new Date(blogPost.frontmatter.publishedOn),
|
new Date(blogPost.frontmatter.publishedOn),
|
||||||
'DD/MM/YYYY'
|
"DD/MM/YYYY",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
'use client'
|
"use client"
|
||||||
|
|
||||||
import Giscus from '@giscus/react'
|
import Giscus from "@giscus/react"
|
||||||
|
|
||||||
import { useTheme } from '@/theme/theme.client'
|
import { useTheme } from "@/theme/theme.client"
|
||||||
import type { CookiesStore } from '@/utils/constants'
|
import type { CookiesStore } from "@/utils/constants"
|
||||||
|
|
||||||
interface BlogPostCommentsProps {
|
interface BlogPostCommentsProps {
|
||||||
cookiesStore: CookiesStore
|
cookiesStore: CookiesStore
|
||||||
@ -16,18 +16,18 @@ export const BlogPostComments = (props: BlogPostCommentsProps): JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Giscus
|
<Giscus
|
||||||
id='comments'
|
id="comments"
|
||||||
repo='theoludwig/theoludwig'
|
repo="theoludwig/theoludwig"
|
||||||
repoId='MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ='
|
repoId="MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ="
|
||||||
category='General'
|
category="General"
|
||||||
categoryId='DIC_kwDOFWUewM4CQ_WK'
|
categoryId="DIC_kwDOFWUewM4CQ_WK"
|
||||||
mapping='pathname'
|
mapping="pathname"
|
||||||
reactionsEnabled='1'
|
reactionsEnabled="1"
|
||||||
emitMetadata='0'
|
emitMetadata="0"
|
||||||
inputPosition='top'
|
inputPosition="top"
|
||||||
theme={theme}
|
theme={theme}
|
||||||
lang='en'
|
lang="en"
|
||||||
loading='lazy'
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,37 @@
|
|||||||
import Image from 'next/image'
|
import Image from "next/image"
|
||||||
import Link from 'next/link'
|
import Link from "next/link"
|
||||||
import { cookies } from 'next/headers'
|
import { cookies } from "next/headers"
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
import { faLink } from '@fortawesome/free-solid-svg-icons'
|
import { faLink } from "@fortawesome/free-solid-svg-icons"
|
||||||
import { MDXRemote } from 'next-mdx-remote/rsc'
|
import { MDXRemote } from "next-mdx-remote/rsc"
|
||||||
import { nodeTypes } from '@mdx-js/mdx'
|
import { nodeTypes } from "@mdx-js/mdx"
|
||||||
import rehypeRaw from 'rehype-raw'
|
import rehypeRaw from "rehype-raw"
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from "remark-gfm"
|
||||||
import rehypeSlug from 'rehype-slug'
|
import rehypeSlug from "rehype-slug"
|
||||||
import remarkMath from 'remark-math'
|
import remarkMath from "remark-math"
|
||||||
import rehypeKatex from 'rehype-katex'
|
import rehypeKatex from "rehype-katex"
|
||||||
import { getHighlighter } from 'shiki'
|
import { getHighlighter } from "shiki"
|
||||||
|
|
||||||
import 'katex/dist/katex.min.css'
|
import "katex/dist/katex.min.css"
|
||||||
|
|
||||||
import { getTheme } from '@/theme/theme.server'
|
import { getTheme } from "@/theme/theme.server"
|
||||||
import { remarkSyntaxHighlightingPlugin } from '@/blog/remarkSyntaxHighlightingPlugin'
|
import { remarkSyntaxHighlightingPlugin } from "@/blog/remarkSyntaxHighlightingPlugin"
|
||||||
import { BlogPostComments } from '@/blog/BlogPostComments'
|
import { BlogPostComments } from "@/blog/BlogPostComments"
|
||||||
|
|
||||||
const Heading = (
|
const Heading = (
|
||||||
props: React.DetailedHTMLProps<
|
props: React.DetailedHTMLProps<
|
||||||
React.HTMLAttributes<HTMLHeadingElement>,
|
React.HTMLAttributes<HTMLHeadingElement>,
|
||||||
HTMLHeadingElement
|
HTMLHeadingElement
|
||||||
>
|
>,
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
const { children, id = '' } = props
|
const { children, id = "" } = props
|
||||||
return (
|
return (
|
||||||
<h2 {...props} className='group'>
|
<h2 {...props} className="group">
|
||||||
<Link
|
<Link
|
||||||
href={`#${id}`}
|
href={`#${id}`}
|
||||||
className='invisible !text-black group-hover:visible dark:!text-white'
|
className="invisible !text-black group-hover:visible dark:!text-white"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon className='mr-2 inline h-4 w-4' icon={faLink} />
|
<FontAwesomeIcon className="mr-2 inline h-4 w-4" icon={faLink} />
|
||||||
</Link>
|
</Link>
|
||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
@ -43,7 +43,7 @@ export interface BlogPostContentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BlogPostContent = async (
|
export const BlogPostContent = async (
|
||||||
props: BlogPostContentProps
|
props: BlogPostContentProps,
|
||||||
): Promise<JSX.Element> => {
|
): Promise<JSX.Element> => {
|
||||||
const { content } = props
|
const { content } = props
|
||||||
|
|
||||||
@ -51,12 +51,12 @@ export const BlogPostContent = async (
|
|||||||
const theme = getTheme()
|
const theme = getTheme()
|
||||||
|
|
||||||
const highlighter = await getHighlighter({
|
const highlighter = await getHighlighter({
|
||||||
theme: `${theme}-plus`
|
theme: `${theme}-plus`,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='prose mb-10'>
|
<div className="prose mb-10">
|
||||||
<div className='px-8'>
|
<div className="px-8">
|
||||||
<MDXRemote
|
<MDXRemote
|
||||||
source={content}
|
source={content}
|
||||||
options={{
|
options={{
|
||||||
@ -64,14 +64,14 @@ export const BlogPostContent = async (
|
|||||||
remarkPlugins: [
|
remarkPlugins: [
|
||||||
remarkGfm,
|
remarkGfm,
|
||||||
[remarkSyntaxHighlightingPlugin, { highlighter }],
|
[remarkSyntaxHighlightingPlugin, { highlighter }],
|
||||||
remarkMath
|
remarkMath,
|
||||||
],
|
],
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
rehypeSlug,
|
rehypeSlug,
|
||||||
[rehypeRaw, { passThrough: nodeTypes }],
|
[rehypeRaw, { passThrough: nodeTypes }],
|
||||||
rehypeKatex
|
rehypeKatex,
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
h1: Heading,
|
h1: Heading,
|
||||||
@ -81,27 +81,27 @@ export const BlogPostContent = async (
|
|||||||
h5: Heading,
|
h5: Heading,
|
||||||
h6: Heading,
|
h6: Heading,
|
||||||
img: (properties) => {
|
img: (properties) => {
|
||||||
const { src = '', alt = 'Blog Image' } = properties
|
const { src = "", alt = "Blog Image" } = properties
|
||||||
const source = src.replace('../../public/', '/')
|
const source = src.replace("../../public/", "/")
|
||||||
return (
|
return (
|
||||||
<span className='flex flex-col items-center justify-center'>
|
<span className="flex flex-col items-center justify-center">
|
||||||
<Image
|
<Image
|
||||||
src={source}
|
src={source}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
width={1000}
|
width={1000}
|
||||||
height={1000}
|
height={1000}
|
||||||
className='h-auto w-auto'
|
className="h-auto w-auto"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
a: (props) => {
|
a: (props) => {
|
||||||
const { href = '' } = props
|
const { href = "" } = props
|
||||||
if (href.startsWith('#')) {
|
if (href.startsWith("#")) {
|
||||||
return <a {...props} />
|
return <a {...props} />
|
||||||
}
|
}
|
||||||
return <a target='_blank' rel='noopener noreferrer' {...props} />
|
return <a target="_blank" rel="noopener noreferrer" {...props} />
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<BlogPostComments cookiesStore={cookiesStore.toString()} />
|
<BlogPostComments cookiesStore={cookiesStore.toString()} />
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
import Link from 'next/link'
|
import Link from "next/link"
|
||||||
import date from 'date-and-time'
|
import date from "date-and-time"
|
||||||
|
|
||||||
import { ShadowContainer } from '@/components/design/ShadowContainer'
|
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||||
import { getBlogPosts } from '@/blog/blog'
|
import { getBlogPosts } from "@/blog/blog"
|
||||||
|
|
||||||
export const BlogPosts = async (): Promise<JSX.Element> => {
|
export const BlogPosts = async (): Promise<JSX.Element> => {
|
||||||
const posts = await getBlogPosts()
|
const posts = await getBlogPosts()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex w-full items-center justify-center p-8'>
|
<div className="flex w-full items-center justify-center p-8">
|
||||||
<div className='w-[1600px]' data-cy='blog-posts'>
|
<div className="w-[1600px]" data-cy="blog-posts">
|
||||||
{posts.map((post, index) => {
|
{posts.map((post, index) => {
|
||||||
const postPublishedOn = date.format(
|
const postPublishedOn = date.format(
|
||||||
new Date(post.frontmatter.publishedOn),
|
new Date(post.frontmatter.publishedOn),
|
||||||
'DD/MM/YYYY'
|
"DD/MM/YYYY",
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/blog/${post.slug}`}
|
href={`/blog/${post.slug}`}
|
||||||
key={index}
|
key={index}
|
||||||
locale='en'
|
locale="en"
|
||||||
data-cy={post.slug}
|
data-cy={post.slug}
|
||||||
>
|
>
|
||||||
<ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'>
|
<ShadowContainer className="cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2">
|
||||||
<h2 data-cy='blog-post-title' className='text-xl font-semibold'>
|
<h2 data-cy="blog-post-title" className="text-xl font-semibold">
|
||||||
{post.frontmatter.title}
|
{post.frontmatter.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p data-cy='blog-post-date' className='mt-2'>
|
<p data-cy="blog-post-date" className="mt-2">
|
||||||
{postPublishedOn}
|
{postPublishedOn}
|
||||||
</p>
|
</p>
|
||||||
<p data-cy='blog-post-description' className='mt-3'>
|
<p data-cy="blog-post-description" className="mt-3">
|
||||||
{post.frontmatter.description}
|
{post.frontmatter.description}
|
||||||
</p>
|
</p>
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
|
22
blog/blog.ts
22
blog/blog.ts
@ -1,10 +1,10 @@
|
|||||||
import fs from 'node:fs'
|
import fs from "node:fs"
|
||||||
import path from 'node:path'
|
import path from "node:path"
|
||||||
|
|
||||||
import { cache } from 'react'
|
import { cache } from "react"
|
||||||
import matter from 'gray-matter'
|
import matter from "gray-matter"
|
||||||
|
|
||||||
export const BLOG_POSTS_PATH = path.join(process.cwd(), 'blog', 'posts')
|
export const BLOG_POSTS_PATH = path.join(process.cwd(), "blog", "posts")
|
||||||
|
|
||||||
export interface FrontMatter {
|
export interface FrontMatter {
|
||||||
title: string
|
title: string
|
||||||
@ -23,13 +23,13 @@ export const getBlogPosts = cache(async (): Promise<BlogPost[]> => {
|
|||||||
const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH)
|
const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH)
|
||||||
const blogPostsWithTime = await Promise.all(
|
const blogPostsWithTime = await Promise.all(
|
||||||
blogPosts.map(async (blogPostFilename) => {
|
blogPosts.map(async (blogPostFilename) => {
|
||||||
const [slug, extension] = blogPostFilename.split('.')
|
const [slug, extension] = blogPostFilename.split(".")
|
||||||
if (slug == null || extension == null) {
|
if (slug == null || extension == null) {
|
||||||
throw new Error('Invalid blog post filename.')
|
throw new Error("Invalid blog post filename.")
|
||||||
}
|
}
|
||||||
const blogPostPath = path.join(BLOG_POSTS_PATH, `${slug}.${extension}`)
|
const blogPostPath = path.join(BLOG_POSTS_PATH, `${slug}.${extension}`)
|
||||||
const blogPostContent = await fs.promises.readFile(blogPostPath, {
|
const blogPostContent = await fs.promises.readFile(blogPostPath, {
|
||||||
encoding: 'utf8'
|
encoding: "utf8",
|
||||||
})
|
})
|
||||||
const { data, content } = matter(blogPostContent) as unknown as {
|
const { data, content } = matter(blogPostContent) as unknown as {
|
||||||
data: FrontMatter
|
data: FrontMatter
|
||||||
@ -40,9 +40,9 @@ export const getBlogPosts = cache(async (): Promise<BlogPost[]> => {
|
|||||||
slug,
|
slug,
|
||||||
content,
|
content,
|
||||||
frontmatter: data,
|
frontmatter: data,
|
||||||
time: date.getTime()
|
time: date.getTime(),
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
const blogPostsSortedByPublicationDate = blogPostsWithTime
|
const blogPostsSortedByPublicationDate = blogPostsWithTime
|
||||||
.filter((post) => {
|
.filter((post) => {
|
||||||
@ -61,5 +61,5 @@ export const getBlogPostBySlug = cache(
|
|||||||
return blogPost.slug === slug && blogPost.frontmatter.isPublished
|
return blogPost.slug === slug && blogPost.frontmatter.isPublished
|
||||||
})
|
})
|
||||||
return blogPost
|
return blogPost
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: '🧼 Clean Code'
|
title: "🧼 Clean Code"
|
||||||
description: 'What is "Clean Code", what are "Design Patterns", and why is it so important today? Tips and tricks to make your code more readable and maintainable in the long term.'
|
description: 'What is "Clean Code", what are "Design Patterns", and why is it so important today? Tips and tricks to make your code more readable and maintainable in the long term.'
|
||||||
isPublished: true
|
isPublished: true
|
||||||
publishedOn: '2022-02-23T08:00:18.758Z'
|
publishedOn: "2022-02-23T08:00:18.758Z"
|
||||||
---
|
---
|
||||||
|
|
||||||
Hello! 👋
|
Hello! 👋
|
||||||
@ -110,7 +110,7 @@ const transaction = charge(user, subscription)
|
|||||||
```typescript
|
```typescript
|
||||||
interface Car {
|
interface Car {
|
||||||
carModel: string
|
carModel: string
|
||||||
carColor: 'red' | 'blue' | 'yellow'
|
carColor: "red" | "blue" | "yellow"
|
||||||
}
|
}
|
||||||
const printCar = (car: Car): void => {
|
const printCar = (car: Car): void => {
|
||||||
console.log(`${car.carModel} (${car.carColor})`)
|
console.log(`${car.carModel} (${car.carColor})`)
|
||||||
@ -122,7 +122,7 @@ const printCar = (car: Car): void => {
|
|||||||
```typescript
|
```typescript
|
||||||
interface Car {
|
interface Car {
|
||||||
model: string
|
model: string
|
||||||
color: 'red' | 'blue' | 'yellow'
|
color: "red" | "blue" | "yellow"
|
||||||
}
|
}
|
||||||
const printCar = (car: Car): void => {
|
const printCar = (car: Car): void => {
|
||||||
console.log(`${car.model} (${car.color})`)
|
console.log(`${car.model} (${car.color})`)
|
||||||
@ -170,17 +170,17 @@ We have to keep it as simple as possible, not to implement features that are not
|
|||||||
### Example (bad way)
|
### Example (bad way)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import fs from 'node:fs'
|
import fs from "node:fs"
|
||||||
import path from 'node:path'
|
import path from "node:path"
|
||||||
|
|
||||||
const createFile = async (
|
const createFile = async (
|
||||||
name: string,
|
name: string,
|
||||||
isTemporary: boolean = false
|
isTemporary: boolean = false,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (isTemporary) {
|
if (isTemporary) {
|
||||||
return await fs.promises.writeFile(path.join('temporary', name), '')
|
return await fs.promises.writeFile(path.join("temporary", name), "")
|
||||||
}
|
}
|
||||||
return await fs.promises.writeFile(name, '')
|
return await fs.promises.writeFile(name, "")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -189,15 +189,15 @@ const createFile = async (
|
|||||||
### Example (good way)
|
### Example (good way)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import fs from 'node:fs'
|
import fs from "node:fs"
|
||||||
import path from 'node:path'
|
import path from "node:path"
|
||||||
|
|
||||||
const createFile = async (name: string): Promise<void> => {
|
const createFile = async (name: string): Promise<void> => {
|
||||||
await fs.promises.writeFile(name, '')
|
await fs.promises.writeFile(name, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
const createTemporaryFile = async (name: string): Promise<void> => {
|
const createTemporaryFile = async (name: string): Promise<void> => {
|
||||||
await createFile(path.join('temporary', name))
|
await createFile(path.join("temporary", name))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: '🗓️ Git version control: Ultimate Guide'
|
title: "🗓️ Git version control: Ultimate Guide"
|
||||||
description: 'What is `git`, what are the most used commands, best practices, and tips and tricks. The Ultimate guide to master `git` in your daily workflow.'
|
description: "What is `git`, what are the most used commands, best practices, and tips and tricks. The Ultimate guide to master `git` in your daily workflow."
|
||||||
isPublished: true
|
isPublished: true
|
||||||
publishedOn: '2022-10-27T14:33:07.465Z'
|
publishedOn: "2022-10-27T14:33:07.465Z"
|
||||||
---
|
---
|
||||||
|
|
||||||
Hello! 👋
|
Hello! 👋
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: '👋 Hello, world!'
|
title: "👋 Hello, world!"
|
||||||
description: 'First post of the blog, introduction and explanation of how this blog is made.'
|
description: "First post of the blog, introduction and explanation of how this blog is made."
|
||||||
isPublished: true
|
isPublished: true
|
||||||
publishedOn: '2022-02-20T08:00:18.758Z'
|
publishedOn: "2022-02-20T08:00:18.758Z"
|
||||||
---
|
---
|
||||||
|
|
||||||
Hello, world! 👋
|
Hello, world! 👋
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: '❌ Mistakes I made as a junior developer'
|
title: "❌ Mistakes I made as a junior developer"
|
||||||
description: 'Here are mistakes I made when I started, to prevent you from making the same mistakes.'
|
description: "Here are mistakes I made when I started, to prevent you from making the same mistakes."
|
||||||
isPublished: true
|
isPublished: true
|
||||||
publishedOn: '2022-03-14T07:42:52.989Z'
|
publishedOn: "2022-03-14T07:42:52.989Z"
|
||||||
---
|
---
|
||||||
|
|
||||||
Hello! 👋
|
Hello! 👋
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: '🧠 Programming Challenges'
|
title: "🧠 Programming Challenges"
|
||||||
description: 'What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation.'
|
description: "What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation."
|
||||||
isPublished: true
|
isPublished: true
|
||||||
publishedOn: '2023-05-21T10:20:18.837Z'
|
publishedOn: "2023-05-21T10:20:18.837Z"
|
||||||
---
|
---
|
||||||
|
|
||||||
Hello! 👋
|
Hello! 👋
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: '🟢 Thream v1.0.0'
|
title: "🟢 Thream v1.0.0"
|
||||||
description: 'Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.'
|
description: "Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun."
|
||||||
isPublished: true
|
isPublished: true
|
||||||
publishedOn: '2022-04-11T10:24:55.206Z'
|
publishedOn: "2022-04-11T10:24:55.206Z"
|
||||||
---
|
---
|
||||||
|
|
||||||
Hello! 👋
|
Hello! 👋
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Plugin, Transformer } from 'unified'
|
import type { Plugin, Transformer } from "unified"
|
||||||
import type { Literal, Node } from 'unist'
|
import type { Literal, Node } from "unist"
|
||||||
import { visit } from 'unist-util-visit'
|
import { visit } from "unist-util-visit"
|
||||||
import type { Highlighter } from 'shiki'
|
import type { Highlighter } from "shiki"
|
||||||
|
|
||||||
export interface RemarkSyntaxHighlightingPluginOptions {
|
export interface RemarkSyntaxHighlightingPluginOptions {
|
||||||
highlighter: Highlighter
|
highlighter: Highlighter
|
||||||
@ -20,11 +20,11 @@ export const remarkSyntaxHighlightingPlugin: Plugin<
|
|||||||
Literal
|
Literal
|
||||||
> = (options) => {
|
> = (options) => {
|
||||||
const transformer: Transformer<RemarkSyntaxHighlightingNode> = (tree) => {
|
const transformer: Transformer<RemarkSyntaxHighlightingNode> = (tree) => {
|
||||||
visit<RemarkSyntaxHighlightingNode, string>(tree, 'code', (node) => {
|
visit<RemarkSyntaxHighlightingNode, string>(tree, "code", (node) => {
|
||||||
node.type = 'html'
|
node.type = "html"
|
||||||
node.children = undefined
|
node.children = undefined
|
||||||
node.value = options.highlighter.codeToHtml(node.value, {
|
node.value = options.highlighter.codeToHtml(node.value, {
|
||||||
lang: node.lang
|
lang: node.lang,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Link from 'next/link'
|
import Link from "next/link"
|
||||||
|
|
||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
|
|
||||||
export const FooterText = (): JSX.Element => {
|
export const FooterText = (): JSX.Element => {
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
@ -8,12 +8,12 @@ export const FooterText = (): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
<Link
|
<Link
|
||||||
href='/'
|
href="/"
|
||||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
className="text-yellow hover:underline dark:text-yellow-dark"
|
||||||
>
|
>
|
||||||
Théo LUDWIG
|
Théo LUDWIG
|
||||||
</Link>{' '}
|
</Link>{" "}
|
||||||
| {i18n.translate('common.all-rights-reserved')}
|
| {i18n.translate("common.all-rights-reserved")}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from "react"
|
||||||
|
|
||||||
interface FooterVersionProps {
|
interface FooterVersionProps {
|
||||||
version: string
|
version: string
|
||||||
@ -12,14 +12,14 @@ export const FooterVersion = (props: FooterVersionProps): JSX.Element => {
|
|||||||
}, [version])
|
}, [version])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className='mt-1'>
|
<p className="mt-1">
|
||||||
Version{' '}
|
Version{" "}
|
||||||
<a
|
<a
|
||||||
data-cy='version-link'
|
data-cy="version-link"
|
||||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
className="text-yellow hover:underline dark:text-yellow-dark"
|
||||||
href={versionLink}
|
href={versionLink}
|
||||||
target='_blank'
|
target="_blank"
|
||||||
rel='noopener noreferrer'
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{version}
|
{version}
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { FooterText } from './FooterText'
|
import { FooterText } from "./FooterText"
|
||||||
import { FooterVersion } from './FooterVersion'
|
import { FooterVersion } from "./FooterVersion"
|
||||||
|
|
||||||
export const Footer = async (): Promise<JSX.Element> => {
|
export const Footer = async (): Promise<JSX.Element> => {
|
||||||
const { readPackage } = await import('read-pkg')
|
const { readPackage } = await import("read-pkg")
|
||||||
const { version } = await readPackage()
|
const { version } = await readPackage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'>
|
<footer className="flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black">
|
||||||
<FooterText />
|
<FooterText />
|
||||||
<FooterVersion version={version} />
|
<FooterVersion version={version} />
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
export const Arrow = (): JSX.Element => {
|
export const Arrow = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width='12'
|
width="12"
|
||||||
height='8'
|
height="8"
|
||||||
viewBox='0 0 12 8'
|
viewBox="0 0 12 8"
|
||||||
fill='none'
|
fill="none"
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
className='fill-current text-black dark:text-white'
|
className="fill-current text-black dark:text-white"
|
||||||
d='M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z'
|
d="M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Image from 'next/image'
|
import Image from "next/image"
|
||||||
|
|
||||||
import type { CookiesStore } from '@/utils/constants'
|
import type { CookiesStore } from "@/utils/constants"
|
||||||
import { useI18n } from '@/i18n/i18n.client'
|
import { useI18n } from "@/i18n/i18n.client"
|
||||||
|
|
||||||
export interface LocaleFlagProps {
|
export interface LocaleFlagProps {
|
||||||
locale: string
|
locale: string
|
||||||
@ -22,7 +22,7 @@ export const LocaleFlag = (props: LocaleFlagProps): JSX.Element => {
|
|||||||
src={`/images/locales/${locale}.svg`}
|
src={`/images/locales/${locale}.svg`}
|
||||||
alt={locale}
|
alt={locale}
|
||||||
/>
|
/>
|
||||||
<p data-cy='locale-flag-text' className='mx-2 text-base'>
|
<p data-cy="locale-flag-text" className="mx-2 text-base">
|
||||||
{i18n.translate(`common.${locale}`)}
|
{i18n.translate(`common.${locale}`)}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
'use client'
|
"use client"
|
||||||
|
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from "next/navigation"
|
||||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
import { useCallback, useEffect, useState, useRef } from "react"
|
||||||
import classNames from 'clsx'
|
import classNames from "clsx"
|
||||||
|
|
||||||
import type { Locale as LocaleType, CookiesStore } from '@/utils/constants'
|
import type { Locale as LocaleType, CookiesStore } from "@/utils/constants"
|
||||||
import { LOCALES } from '@/utils/constants'
|
import { LOCALES } from "@/utils/constants"
|
||||||
|
|
||||||
import { Arrow } from './Arrow'
|
import { Arrow } from "./Arrow"
|
||||||
import { LocaleFlag } from './LocaleFlag'
|
import { LocaleFlag } from "./LocaleFlag"
|
||||||
|
|
||||||
export interface LocalesProps {
|
export interface LocalesProps {
|
||||||
currentLocale: string
|
currentLocale: string
|
||||||
@ -38,28 +38,28 @@ export const Locales = (props: LocalesProps): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.document.addEventListener('click', handleClickEvent)
|
window.document.addEventListener("click", handleClickEvent)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
return window.removeEventListener('click', handleClickEvent)
|
return window.removeEventListener("click", handleClickEvent)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleLocale = async (locale: LocaleType): Promise<void> => {
|
const handleLocale = async (locale: LocaleType): Promise<void> => {
|
||||||
const { setLocale } = await import('@/i18n/i18n.server')
|
const { setLocale } = await import("@/i18n/i18n.server")
|
||||||
setLocale(locale)
|
setLocale(locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith('/blog')) {
|
if (pathname.startsWith("/blog")) {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
<div className="flex cursor-pointer flex-col items-center justify-center">
|
||||||
<div
|
<div
|
||||||
ref={languageClickRef}
|
ref={languageClickRef}
|
||||||
data-cy='locale-click'
|
data-cy="locale-click"
|
||||||
className='mr-5 flex items-center'
|
className="mr-5 flex items-center"
|
||||||
onClick={handleHiddenMenu}
|
onClick={handleHiddenMenu}
|
||||||
>
|
>
|
||||||
<LocaleFlag
|
<LocaleFlag
|
||||||
@ -70,10 +70,10 @@ export const Locales = (props: LocalesProps): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
data-cy='locales-list'
|
data-cy="locales-list"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute top-14 z-10 mr-4 mt-3 flex w-32 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
"absolute top-14 z-10 mr-4 mt-3 flex w-32 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag",
|
||||||
{ hidden: hiddenMenu }
|
{ hidden: hiddenMenu },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{LOCALES.filter((locale) => {
|
{LOCALES.filter((locale) => {
|
||||||
@ -82,7 +82,7 @@ export const Locales = (props: LocalesProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={locale}
|
key={locale}
|
||||||
className='flex h-12 w-full items-center justify-center hover:bg-[#4f545c] hover:bg-opacity-20'
|
className="flex h-12 w-full items-center justify-center hover:bg-[#4f545c] hover:bg-opacity-20"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
return await handleLocale(locale)
|
return await handleLocale(locale)
|
||||||
}}
|
}}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
'use client'
|
"use client"
|
||||||
|
|
||||||
import classNames from 'clsx'
|
import classNames from "clsx"
|
||||||
|
|
||||||
import { useTheme } from '@/theme/theme.client'
|
import { useTheme } from "@/theme/theme.client"
|
||||||
import type { CookiesStore } from '@/utils/constants'
|
import type { CookiesStore } from "@/utils/constants"
|
||||||
|
|
||||||
export interface SwitchThemeProps {
|
export interface SwitchThemeProps {
|
||||||
cookiesStore: CookiesStore
|
cookiesStore: CookiesStore
|
||||||
@ -14,63 +14,63 @@ export const SwitchTheme = (props: SwitchThemeProps): JSX.Element => {
|
|||||||
const theme = useTheme(cookiesStore)
|
const theme = useTheme(cookiesStore)
|
||||||
|
|
||||||
const handleClick = async (): Promise<void> => {
|
const handleClick = async (): Promise<void> => {
|
||||||
const { setTheme } = await import('@/theme/theme.server')
|
const { setTheme } = await import("@/theme/theme.server")
|
||||||
const newTheme = theme === 'dark' ? 'light' : 'dark'
|
const newTheme = theme === "dark" ? "light" : "dark"
|
||||||
setTheme(newTheme)
|
setTheme(newTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='flex items-center'
|
className="flex items-center"
|
||||||
data-cy='switch-theme-click'
|
data-cy="switch-theme-click"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<div className='relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0'>
|
<div className="relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0">
|
||||||
<div className='h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out'>
|
<div className="h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out">
|
||||||
<div
|
<div
|
||||||
data-cy='switch-theme-dark'
|
data-cy="switch-theme-dark"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute bottom-0 left-[8px] top-0 mb-auto mt-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out',
|
"absolute bottom-0 left-[8px] top-0 mb-auto mt-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out",
|
||||||
{
|
{
|
||||||
'opacity-100': theme === 'dark',
|
"opacity-100": theme === "dark",
|
||||||
'opacity-0': theme === 'light'
|
"opacity-0": theme === "light",
|
||||||
}
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
|
<span className="relative flex h-[10px] w-[10px] items-center justify-center">
|
||||||
🌜
|
🌜
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
data-cy='switch-theme-light'
|
data-cy="switch-theme-light"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute bottom-0 right-[10px] top-0 mb-auto mt-auto h-[10px] w-[10px] leading-[0]',
|
"absolute bottom-0 right-[10px] top-0 mb-auto mt-auto h-[10px] w-[10px] leading-[0]",
|
||||||
{
|
{
|
||||||
'opacity-100': theme === 'light',
|
"opacity-100": theme === "light",
|
||||||
'opacity-0': theme === 'dark'
|
"opacity-0": theme === "dark",
|
||||||
}
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
|
<span className="relative flex h-[10px] w-[10px] items-center justify-center">
|
||||||
🌞
|
🌞
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out',
|
"absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out",
|
||||||
{
|
{
|
||||||
'left-[27px]': theme === 'dark',
|
"left-[27px]": theme === "dark",
|
||||||
'left-0': theme === 'light'
|
"left-0": theme === "light",
|
||||||
}
|
},
|
||||||
)}
|
)}
|
||||||
style={{ border: '1px solid #4d4d4d' }}
|
style={{ border: "1px solid #4d4d4d" }}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
data-cy='switch-theme-input'
|
data-cy="switch-theme-input"
|
||||||
type='checkbox'
|
type="checkbox"
|
||||||
aria-label='Dark mode toggle'
|
aria-label="Dark mode toggle"
|
||||||
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0 hidden'
|
className="absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0 hidden"
|
||||||
defaultChecked
|
defaultChecked
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,39 +1,39 @@
|
|||||||
import { cookies } from 'next/headers'
|
import { cookies } from "next/headers"
|
||||||
import Link from 'next/link'
|
import Link from "next/link"
|
||||||
import Image from 'next/image'
|
import Image from "next/image"
|
||||||
|
|
||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
|
|
||||||
import { Locales } from './Locales'
|
import { Locales } from "./Locales"
|
||||||
import { SwitchTheme } from './SwitchTheme'
|
import { SwitchTheme } from "./SwitchTheme"
|
||||||
|
|
||||||
export const Header = (): JSX.Element => {
|
export const Header = (): JSX.Element => {
|
||||||
const cookiesStore = cookies()
|
const cookiesStore = cookies()
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
|
<header className="sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black">
|
||||||
<Link href='/'>
|
<Link href="/">
|
||||||
<div className='flex items-center justify-center'>
|
<div className="flex items-center justify-center">
|
||||||
<Image
|
<Image
|
||||||
quality={100}
|
quality={100}
|
||||||
width={60}
|
width={60}
|
||||||
height={60}
|
height={60}
|
||||||
src='/images/icon_small.png'
|
src="/images/icon_small.png"
|
||||||
alt='Théo LUDWIG'
|
alt="Théo LUDWIG"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
|
<strong className="ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block">
|
||||||
Théo LUDWIG
|
Théo LUDWIG
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div className='flex justify-between'>
|
<div className="flex justify-between">
|
||||||
<div className='flex flex-col items-center justify-center px-6'>
|
<div className="flex flex-col items-center justify-center px-6">
|
||||||
<Link
|
<Link
|
||||||
href='/blog'
|
href="/blog"
|
||||||
data-cy='header-blog-link'
|
data-cy="header-blog-link"
|
||||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
className="text-yellow hover:underline dark:text-yellow-dark"
|
||||||
>
|
>
|
||||||
Blog
|
Blog
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import htmlParser from 'html-react-parser'
|
import htmlParser from "html-react-parser"
|
||||||
|
|
||||||
export interface InterestParagraphProps {
|
export interface InterestParagraphProps {
|
||||||
title: string
|
title: string
|
||||||
@ -6,14 +6,14 @@ export interface InterestParagraphProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const InterestParagraph = (
|
export const InterestParagraph = (
|
||||||
props: InterestParagraphProps
|
props: InterestParagraphProps,
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
const { title, description } = props
|
const { title, description } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className='my-6 text-center text-gray dark:text-gray-dark'>
|
<p className="my-6 text-center text-gray dark:text-gray-dark">
|
||||||
<strong className='text-lg font-semibold text-yellow dark:text-yellow-dark'>
|
<strong className="text-lg font-semibold text-yellow dark:text-yellow-dark">
|
||||||
{title}
|
{title}
|
||||||
</strong>
|
</strong>
|
||||||
<br />
|
<br />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
import type { IconDefinition } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
|
||||||
interface InterestItemProps {
|
interface InterestItemProps {
|
||||||
title: string
|
title: string
|
||||||
@ -10,9 +10,9 @@ export const InterestItem = (props: InterestItemProps): JSX.Element => {
|
|||||||
const { fontAwesomeIcon, title } = props
|
const { fontAwesomeIcon, title } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className='interest-item mx-2 my-2 h-8 w-8' title={title}>
|
<li className="interest-item mx-2 my-2 h-8 w-8" title={title}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className='block h-full w-full text-yellow dark:text-yellow-dark'
|
className="block h-full w-full text-yellow dark:text-yellow-dark"
|
||||||
icon={fontAwesomeIcon}
|
icon={fontAwesomeIcon}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { faCode, faMicrochip } from '@fortawesome/free-solid-svg-icons'
|
import { faCode, faMicrochip } from "@fortawesome/free-solid-svg-icons"
|
||||||
import { faGit } from '@fortawesome/free-brands-svg-icons'
|
import { faGit } from "@fortawesome/free-brands-svg-icons"
|
||||||
|
|
||||||
import { InterestItem } from './InterestItem'
|
import { InterestItem } from "./InterestItem"
|
||||||
|
|
||||||
export const InterestsList = (): JSX.Element => {
|
export const InterestsList = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className='my-4 flex justify-center'>
|
<div className="my-4 flex justify-center">
|
||||||
<ul className='m-0 flex w-96 list-none justify-around p-0'>
|
<ul className="m-0 flex w-96 list-none justify-around p-0">
|
||||||
<InterestItem title='Developer Full Stack' fontAwesomeIcon={faCode} />
|
<InterestItem title="Developer Full Stack" fontAwesomeIcon={faCode} />
|
||||||
<InterestItem
|
<InterestItem
|
||||||
title='Passionate about High-Tech'
|
title="Passionate about High-Tech"
|
||||||
fontAwesomeIcon={faMicrochip}
|
fontAwesomeIcon={faMicrochip}
|
||||||
/>
|
/>
|
||||||
<InterestItem title='Open-Source enthusiast' fontAwesomeIcon={faGit} />
|
<InterestItem title="Open-Source enthusiast" fontAwesomeIcon={faGit} />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
|
|
||||||
import type { InterestParagraphProps } from './InterestParagraph'
|
import type { InterestParagraphProps } from "./InterestParagraph"
|
||||||
import { InterestParagraph } from './InterestParagraph'
|
import { InterestParagraph } from "./InterestParagraph"
|
||||||
import { InterestsList } from './InterestsList'
|
import { InterestsList } from "./InterestsList"
|
||||||
|
|
||||||
export const Interests = (): JSX.Element => {
|
export const Interests = (): JSX.Element => {
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
|
|
||||||
let paragraphs = i18n.translate<InterestParagraphProps[]>(
|
let paragraphs = i18n.translate<InterestParagraphProps[]>(
|
||||||
'home.interests.paragraphs'
|
"home.interests.paragraphs",
|
||||||
)
|
)
|
||||||
if (!Array.isArray(paragraphs)) {
|
if (!Array.isArray(paragraphs)) {
|
||||||
paragraphs = []
|
paragraphs = []
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-full'>
|
<div className="max-w-full">
|
||||||
{paragraphs.map((paragraph, index) => {
|
{paragraphs.map((paragraph, index) => {
|
||||||
return <InterestParagraph key={index} {...paragraph} />
|
return <InterestParagraph key={index} {...paragraph} />
|
||||||
})}
|
})}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ShadowContainer } from '@/components/design/ShadowContainer'
|
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||||
import { GitHubIcon } from '@/components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon'
|
import { GitHubIcon } from "@/components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon"
|
||||||
|
|
||||||
export interface RepositoryProps {
|
export interface RepositoryProps {
|
||||||
name: string
|
name: string
|
||||||
@ -11,13 +11,13 @@ export const Repository = (props: RepositoryProps): JSX.Element => {
|
|||||||
const { name, description, href } = props
|
const { name, description, href } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShadowContainer className='relative !mb-4 max-h-32 cursor-pointer p-6 transition-transform duration-200 ease-in-out hover:-translate-y-2'>
|
<ShadowContainer className="relative !mb-4 max-h-32 cursor-pointer p-6 transition-transform duration-200 ease-in-out hover:-translate-y-2">
|
||||||
<a href={href} target='_blank' rel='noopener noreferrer'>
|
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||||
<div className='flex'>
|
<div className="flex">
|
||||||
<GitHubIcon className='mr-2 h-6' />
|
<GitHubIcon className="mr-2 h-6" />
|
||||||
<span className='text-yellow dark:text-yellow-dark'>{name}</span>
|
<span className="text-yellow dark:text-yellow-dark">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className='my-4'>{description}</p>
|
<p className="my-4">{description}</p>
|
||||||
</a>
|
</a>
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
)
|
)
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
|
|
||||||
import { Repository } from './Repository'
|
import { Repository } from "./Repository"
|
||||||
|
|
||||||
export const OpenSource = (): JSX.Element => {
|
export const OpenSource = (): JSX.Element => {
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-0 flex max-w-full flex-col items-center'>
|
<div className="mt-0 flex max-w-full flex-col items-center">
|
||||||
<p className='text-center'>
|
<p className="text-center">
|
||||||
{i18n.translate('home.open-source.description')}
|
{i18n.translate("home.open-source.description")}
|
||||||
</p>
|
</p>
|
||||||
<div className='my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2'>
|
<div className="my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2">
|
||||||
<Repository
|
<Repository
|
||||||
name='nodejs/node'
|
name="nodejs/node"
|
||||||
description='Node.js JavaScript runtime ✨🐢🚀✨'
|
description="Node.js JavaScript runtime ✨🐢🚀✨"
|
||||||
href='https://github.com/nodejs/node/commits?author=theoludwig'
|
href="https://github.com/nodejs/node/commits?author=theoludwig"
|
||||||
/>
|
/>
|
||||||
<Repository
|
<Repository
|
||||||
name='standard/standard'
|
name="standard/standard"
|
||||||
description='🌟 JavaScript Style Guide, with linter & automatic code fixer'
|
description="🌟 JavaScript Style Guide, with linter & automatic code fixer"
|
||||||
href='https://github.com/standard/standard/commits?author=theoludwig'
|
href="https://github.com/standard/standard/commits?author=theoludwig"
|
||||||
/>
|
/>
|
||||||
<Repository
|
<Repository
|
||||||
name='nrwl/nx'
|
name="nrwl/nx"
|
||||||
description='Smart, Fast and Extensible Build System'
|
description="Smart, Fast and Extensible Build System"
|
||||||
href='https://github.com/nrwl/nx/commits?author=theoludwig'
|
href="https://github.com/nrwl/nx/commits?author=theoludwig"
|
||||||
/>
|
/>
|
||||||
<Repository
|
<Repository
|
||||||
name='vercel/next.js'
|
name="vercel/next.js"
|
||||||
description='The React Framework'
|
description="The React Framework"
|
||||||
href='https://github.com/vercel/next.js/commits?author=theoludwig'
|
href="https://github.com/vercel/next.js/commits?author=theoludwig"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Image from 'next/image'
|
import Image from "next/image"
|
||||||
|
|
||||||
import { ShadowContainer } from '@/components/design/ShadowContainer'
|
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||||
|
|
||||||
export interface PortfolioItemProps {
|
export interface PortfolioItemProps {
|
||||||
title: string
|
title: string
|
||||||
@ -13,29 +13,29 @@ export const PortfolioItem = (props: PortfolioItemProps): JSX.Element => {
|
|||||||
const { title, description, link, image } = props
|
const { title, description, link, image } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShadowContainer className='relative cursor-pointer items-center sm:ml-10'>
|
<ShadowContainer className="relative cursor-pointer items-center sm:ml-10">
|
||||||
<a
|
<a
|
||||||
className='group inline-flex justify-center'
|
className="group inline-flex justify-center"
|
||||||
target='_blank'
|
target="_blank"
|
||||||
rel='noopener noreferrer'
|
rel="noopener noreferrer"
|
||||||
href={link}
|
href={link}
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
>
|
>
|
||||||
<div className='flex justify-center'>
|
<div className="flex justify-center">
|
||||||
<Image
|
<Image
|
||||||
quality={100}
|
quality={100}
|
||||||
className='h-auto w-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5'
|
className="h-auto w-auto transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5"
|
||||||
width={300}
|
width={300}
|
||||||
height={300}
|
height={300}
|
||||||
src={image}
|
src={image}
|
||||||
alt={title}
|
alt={title}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100'>
|
<div className="absolute bottom-0 h-auto overflow-hidden text-center opacity-0 transition-opacity duration-500 group-hover:opacity-100">
|
||||||
<h3 className='my-6 text-xl font-semibold text-yellow dark:text-yellow-dark'>
|
<h3 className="my-6 text-xl font-semibold text-yellow dark:text-yellow-dark">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className='my-6'>{description}</p>
|
<p className="my-6">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
|
|
||||||
import type { PortfolioItemProps } from './PortfolioItem'
|
import type { PortfolioItemProps } from "./PortfolioItem"
|
||||||
import { PortfolioItem } from './PortfolioItem'
|
import { PortfolioItem } from "./PortfolioItem"
|
||||||
|
|
||||||
export const Portfolio = (): JSX.Element => {
|
export const Portfolio = (): JSX.Element => {
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
|
|
||||||
let items = i18n.translate<PortfolioItemProps[]>('home.portfolio.items')
|
let items = i18n.translate<PortfolioItemProps[]>("home.portfolio.items")
|
||||||
if (!Array.isArray(items)) {
|
if (!Array.isArray(items)) {
|
||||||
items = []
|
items = []
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex w-full flex-wrap justify-center px-3'>
|
<div className="flex w-full flex-wrap justify-center px-3">
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
return <PortfolioItem key={index} {...item} />
|
return <PortfolioItem key={index} {...item} />
|
||||||
})}
|
})}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
|
|
||||||
export const ProfileDescriptionBottom = (): JSX.Element => {
|
export const ProfileDescriptionBottom = (): JSX.Element => {
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'>
|
<p className="mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark">
|
||||||
{i18n.translate('home.about.description-bottom')}
|
{i18n.translate("home.about.description-bottom")}
|
||||||
{i18n.locale === 'fr-FR' ? (
|
{i18n.locale === "fr-FR" ? (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<a
|
<a
|
||||||
href='/curriculum-vitae/index.html'
|
href="/curriculum-vitae/index.html"
|
||||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
className="text-yellow hover:underline dark:text-yellow-dark"
|
||||||
>
|
>
|
||||||
Curriculum vitæ
|
Curriculum vitæ
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
|
|
||||||
export const ProfileInformation = (): JSX.Element => {
|
export const ProfileInformation = (): JSX.Element => {
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'>
|
<div className="mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400">
|
||||||
<h1 className='mb-2 text-4xl font-semibold text-yellow dark:text-yellow-dark'>
|
<h1 className="mb-2 text-4xl font-semibold text-yellow dark:text-yellow-dark">
|
||||||
Théo LUDWIG
|
Théo LUDWIG
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className='mb-3 text-base'>
|
<h2 className="mb-3 text-base">
|
||||||
{i18n.translate('home.about.description')}
|
{i18n.translate("home.about.description")}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -8,14 +8,14 @@ export const ProfileItem = (props: ProfileItemProps): JSX.Element => {
|
|||||||
const { title, value, link } = props
|
const { title, value, link } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className='mb-3 before:table after:clear-both after:table'>
|
<li className="mb-3 before:table after:clear-both after:table">
|
||||||
<strong className='float-left block w-28 text-sm font-bold text-black dark:text-white'>
|
<strong className="float-left block w-28 text-sm font-bold text-black dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
</strong>
|
</strong>
|
||||||
<span className='mb-4 ml-0 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32'>
|
<span className="mb-4 ml-0 block text-sm font-normal text-gray dark:text-gray-dark sm:mb-0 sm:ml-32">
|
||||||
{link != null ? (
|
{link != null ? (
|
||||||
<a
|
<a
|
||||||
className='text-gray hover:underline dark:text-gray-dark'
|
className="text-gray hover:underline dark:text-gray-dark"
|
||||||
href={link}
|
href={link}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
'use client'
|
"use client"
|
||||||
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from "react"
|
||||||
|
|
||||||
import { useI18n } from '@/i18n/i18n.client'
|
import { useI18n } from "@/i18n/i18n.client"
|
||||||
import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from '@/utils/getAge'
|
import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from "@/utils/getAge"
|
||||||
import type { CookiesStore } from '@/utils/constants'
|
import type { CookiesStore } from "@/utils/constants"
|
||||||
|
|
||||||
import { ProfileItem } from './ProfileItem'
|
import { ProfileItem } from "./ProfileItem"
|
||||||
|
|
||||||
export interface ProfileListProps {
|
export interface ProfileListProps {
|
||||||
cookiesStore: CookiesStore
|
cookiesStore: CookiesStore
|
||||||
@ -22,25 +22,25 @@ export const ProfileList = (props: ProfileListProps): JSX.Element => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className='m-0 list-none p-0'>
|
<ul className="m-0 list-none p-0">
|
||||||
<ProfileItem
|
<ProfileItem
|
||||||
title={i18n.translate('home.about.pronouns')}
|
title={i18n.translate("home.about.pronouns")}
|
||||||
value={i18n.translate('home.about.pronouns-value')}
|
value={i18n.translate("home.about.pronouns-value")}
|
||||||
/>
|
/>
|
||||||
<ProfileItem
|
<ProfileItem
|
||||||
title={i18n.translate('home.about.birth-date')}
|
title={i18n.translate("home.about.birth-date")}
|
||||||
value={`${BIRTH_DATE_STRING} (${age} ${i18n.translate(
|
value={`${BIRTH_DATE_STRING} (${age} ${i18n.translate(
|
||||||
'home.about.years-old'
|
"home.about.years-old",
|
||||||
)})`}
|
)})`}
|
||||||
/>
|
/>
|
||||||
<ProfileItem
|
<ProfileItem
|
||||||
title={i18n.translate('home.about.nationality')}
|
title={i18n.translate("home.about.nationality")}
|
||||||
value='Alsace, France'
|
value="Alsace, France"
|
||||||
/>
|
/>
|
||||||
<ProfileItem
|
<ProfileItem
|
||||||
title='Email'
|
title="Email"
|
||||||
value='contact@theoludwig.fr'
|
value="contact@theoludwig.fr"
|
||||||
link='mailto:contact@theoludwig.fr'
|
link="mailto:contact@theoludwig.fr"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import Image from 'next/image'
|
import Image from "next/image"
|
||||||
|
|
||||||
import Logo from 'public/images/logo.png'
|
import Logo from "public/images/logo.png"
|
||||||
|
|
||||||
export const ProfileLogo = (): JSX.Element => {
|
export const ProfileLogo = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className='max-h-[370px] max-w-[370px] px-2 py-6'>
|
<div className="max-h-[370px] max-w-[370px] px-2 py-6">
|
||||||
<Image quality={100} src={Logo} alt='Théo LUDWIG' priority />
|
<Image quality={100} src={Logo} alt="Théo LUDWIG" priority />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Icon } from './Icon'
|
import { Icon } from "./Icon"
|
||||||
|
|
||||||
export const EmailIcon = (
|
export const EmailIcon = (
|
||||||
props: React.SVGProps<SVGSVGElement>
|
props: React.SVGProps<SVGSVGElement>,
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<title>Email</title>
|
<title>Email</title>
|
||||||
<path d='M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12' />
|
<path d="M15.61 12c0 1.99-1.62 3.61-3.61 3.61-1.99 0-3.61-1.62-3.61-3.61 0-1.99 1.62-3.61 3.61-3.61 1.99 0 3.61 1.62 3.61 3.61M12 0C5.383 0 0 5.383 0 12s5.383 12 12 12c2.424 0 4.761-.722 6.76-2.087l.034-.024-1.617-1.879-.027.017A9.494 9.494 0 0112 21.54c-5.26 0-9.54-4.28-9.54-9.54 0-5.26 4.28-9.54 9.54-9.54 5.26 0 9.54 4.28 9.54 9.54a9.63 9.63 0 01-.225 2.05c-.301 1.239-1.169 1.618-1.82 1.568-.654-.053-1.42-.52-1.426-1.661V12A6.076 6.076 0 0012 5.93 6.076 6.076 0 005.93 12 6.076 6.076 0 0012 18.07a6.02 6.02 0 004.3-1.792 3.9 3.9 0 003.32 1.805c.874 0 1.74-.292 2.437-.821.719-.547 1.256-1.336 1.553-2.285.047-.154.135-.504.135-.507l.002-.013c.175-.76.253-1.52.253-2.457 0-6.617-5.383-12-12-12" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Icon } from './Icon'
|
import { Icon } from "./Icon"
|
||||||
|
|
||||||
export const GitHubIcon = (
|
export const GitHubIcon = (
|
||||||
props: React.SVGProps<SVGSVGElement>
|
props: React.SVGProps<SVGSVGElement>,
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<title>GitHub</title>
|
<title>GitHub</title>
|
||||||
<path d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12' />
|
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Icon } from './Icon'
|
import { Icon } from "./Icon"
|
||||||
|
|
||||||
export const GitLabIcon = (
|
export const GitLabIcon = (
|
||||||
props: React.SVGProps<SVGSVGElement>
|
props: React.SVGProps<SVGSVGElement>,
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<title>GitLab</title>
|
<title>GitLab</title>
|
||||||
<path d='M4.845.904c-.435 0-.82.28-.955.692C2.639 5.449 1.246 9.728.07 13.335a1.437 1.437 0 00.522 1.607l11.071 8.045c.2.145.472.144.67-.004l11.073-8.04a1.436 1.436 0 00.522-1.61c-1.285-3.942-2.683-8.256-3.817-11.746a1.004 1.004 0 00-.957-.684.987.987 0 00-.949.69l-2.405 7.408H8.203l-2.41-7.408a.987.987 0 00-.942-.69h-.006zm-.006 1.42l2.173 6.678H2.675zm14.326 0l2.168 6.678h-4.341zm-10.593 7.81h6.862c-1.142 3.52-2.288 7.04-3.434 10.559L8.572 10.135zm-5.514.005h4.321l3.086 9.5zm13.567 0h4.325c-2.467 3.17-4.95 6.328-7.411 9.502 1.028-3.167 2.059-6.334 3.086-9.502zM2.1 10.762l6.977 8.947-7.817-5.682a.305.305 0 01-.112-.341zm19.798 0l.952 2.922a.305.305 0 01-.11.341v.002l-7.82 5.68.026-.035z' />
|
<path d="M4.845.904c-.435 0-.82.28-.955.692C2.639 5.449 1.246 9.728.07 13.335a1.437 1.437 0 00.522 1.607l11.071 8.045c.2.145.472.144.67-.004l11.073-8.04a1.436 1.436 0 00.522-1.61c-1.285-3.942-2.683-8.256-3.817-11.746a1.004 1.004 0 00-.957-.684.987.987 0 00-.949.69l-2.405 7.408H8.203l-2.41-7.408a.987.987 0 00-.942-.69h-.006zm-.006 1.42l2.173 6.678H2.675zm14.326 0l2.168 6.678h-4.341zm-10.593 7.81h6.862c-1.142 3.52-2.288 7.04-3.434 10.559L8.572 10.135zm-5.514.005h4.321l3.086 9.5zm13.567 0h4.325c-2.467 3.17-4.95 6.328-7.411 9.502 1.028-3.167 2.059-6.334 3.086-9.502zM2.1 10.762l6.977 8.947-7.817-5.682a.305.305 0 01-.112-.341zm19.798 0l.952 2.922a.305.305 0 01-.11.341v.002l-7.82 5.68.026-.035z" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import classNames from 'clsx'
|
import classNames from "clsx"
|
||||||
|
|
||||||
export const Icon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
|
export const Icon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
|
||||||
const { children, className, ...rest } = props
|
const { children, className, ...rest } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox='0 0 24 24'
|
viewBox="0 0 24 24"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'h-8 w-8 fill-current text-black dark:text-white',
|
"h-8 w-8 fill-current text-black dark:text-white",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Icon } from './Icon'
|
import { Icon } from "./Icon"
|
||||||
|
|
||||||
export const NPMIcon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
|
export const NPMIcon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<title>npm</title>
|
<title>npm</title>
|
||||||
<path d='M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z' />
|
<path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Icon } from './Icon'
|
import { Icon } from "./Icon"
|
||||||
|
|
||||||
export const TwitchIcon = (
|
export const TwitchIcon = (
|
||||||
props: React.SVGProps<SVGSVGElement>
|
props: React.SVGProps<SVGSVGElement>,
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<title>Twitch</title>
|
<title>Twitch</title>
|
||||||
<path d='M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z' />
|
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714z" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Icon } from './Icon'
|
import { Icon } from "./Icon"
|
||||||
|
|
||||||
export const TwitterIcon = (
|
export const TwitterIcon = (
|
||||||
props: React.SVGProps<SVGSVGElement>
|
props: React.SVGProps<SVGSVGElement>,
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<title>Twitter</title>
|
<title>Twitter</title>
|
||||||
<path d='M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z' />
|
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Icon } from './Icon'
|
import { Icon } from "./Icon"
|
||||||
|
|
||||||
export const YouTubeIcon = (
|
export const YouTubeIcon = (
|
||||||
props: React.SVGProps<SVGSVGElement>
|
props: React.SVGProps<SVGSVGElement>,
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<title>YouTube</title>
|
<title>YouTube</title>
|
||||||
<path d='M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z' />
|
<path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,13 @@ export const SocialMediaItem = (props: SocialMediaItemProps): JSX.Element => {
|
|||||||
const { link, ariaLabel, children } = props
|
const { link, ariaLabel, children } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className='mx-4 my-1 inline-block'>
|
<li className="mx-4 my-1 inline-block">
|
||||||
<a
|
<a
|
||||||
href={link}
|
href={link}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
target='_blank'
|
target="_blank"
|
||||||
rel='noopener noreferrer'
|
rel="noopener noreferrer"
|
||||||
className='relative inline-block bg-transparent'
|
className="relative inline-block bg-transparent"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,43 +1,43 @@
|
|||||||
import { SocialMediaItem } from './SocialMediaItem'
|
import { SocialMediaItem } from "./SocialMediaItem"
|
||||||
import { TwitterIcon } from './SocialMediaIcons/TwitterIcon'
|
import { TwitterIcon } from "./SocialMediaIcons/TwitterIcon"
|
||||||
import { GitHubIcon } from './SocialMediaIcons/GitHubIcon'
|
import { GitHubIcon } from "./SocialMediaIcons/GitHubIcon"
|
||||||
import { GitLabIcon } from './SocialMediaIcons/GitLabIcon'
|
import { GitLabIcon } from "./SocialMediaIcons/GitLabIcon"
|
||||||
import { YouTubeIcon } from './SocialMediaIcons/YouTubeIcon'
|
import { YouTubeIcon } from "./SocialMediaIcons/YouTubeIcon"
|
||||||
import { TwitchIcon } from './SocialMediaIcons/TwitchIcon'
|
import { TwitchIcon } from "./SocialMediaIcons/TwitchIcon"
|
||||||
import { EmailIcon } from './SocialMediaIcons/EmailIcon'
|
import { EmailIcon } from "./SocialMediaIcons/EmailIcon"
|
||||||
import { NPMIcon } from './SocialMediaIcons/NPMIcon'
|
import { NPMIcon } from "./SocialMediaIcons/NPMIcon"
|
||||||
|
|
||||||
export const SocialMediaList = (): JSX.Element => {
|
export const SocialMediaList = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<ul className='social-media-list m-0 mt-2 list-none py-4 text-center'>
|
<ul className="social-media-list m-0 mt-2 list-none py-4 text-center">
|
||||||
<SocialMediaItem link='https://github.com/theoludwig' ariaLabel='GitHub'>
|
<SocialMediaItem link="https://github.com/theoludwig" ariaLabel="GitHub">
|
||||||
<GitHubIcon />
|
<GitHubIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem link='https://gitlab.com/theoludwig' ariaLabel='GitLab'>
|
<SocialMediaItem link="https://gitlab.com/theoludwig" ariaLabel="GitLab">
|
||||||
<GitLabIcon />
|
<GitLabIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem link='https://www.npmjs.com/~theoludwig' ariaLabel='npm'>
|
<SocialMediaItem link="https://www.npmjs.com/~theoludwig" ariaLabel="npm">
|
||||||
<NPMIcon />
|
<NPMIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem
|
<SocialMediaItem
|
||||||
link='https://twitter.com/theoludwig_'
|
link="https://twitter.com/theoludwig_"
|
||||||
ariaLabel='Twitter'
|
ariaLabel="Twitter"
|
||||||
>
|
>
|
||||||
<TwitterIcon />
|
<TwitterIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem
|
<SocialMediaItem
|
||||||
link='https://www.youtube.com/@theo_ludwig'
|
link="https://www.youtube.com/@theo_ludwig"
|
||||||
ariaLabel='YouTube'
|
ariaLabel="YouTube"
|
||||||
>
|
>
|
||||||
<YouTubeIcon />
|
<YouTubeIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem
|
<SocialMediaItem
|
||||||
link='https://www.twitch.tv/theoludwig'
|
link="https://www.twitch.tv/theoludwig"
|
||||||
ariaLabel='Twitch'
|
ariaLabel="Twitch"
|
||||||
>
|
>
|
||||||
<TwitchIcon />
|
<TwitchIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
<SocialMediaItem link='mailto:contact@theoludwig.fr' ariaLabel='Email'>
|
<SocialMediaItem link="mailto:contact@theoludwig.fr" ariaLabel="Email">
|
||||||
<EmailIcon />
|
<EmailIcon />
|
||||||
</SocialMediaItem>
|
</SocialMediaItem>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { cookies } from 'next/headers'
|
import { cookies } from "next/headers"
|
||||||
|
|
||||||
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
|
import { ProfileDescriptionBottom } from "./ProfileDescriptionBottom"
|
||||||
import { ProfileInformation } from './ProfileInfo'
|
import { ProfileInformation } from "./ProfileInfo"
|
||||||
import { ProfileList } from './ProfileList'
|
import { ProfileList } from "./ProfileList"
|
||||||
import { ProfileLogo } from './ProfileLogo'
|
import { ProfileLogo } from "./ProfileLogo"
|
||||||
|
|
||||||
export const Profile = (): JSX.Element => {
|
export const Profile = (): JSX.Element => {
|
||||||
const cookiesStore = cookies()
|
const cookiesStore = cookies()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10'>
|
<div className="flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10">
|
||||||
<ProfileLogo />
|
<ProfileLogo />
|
||||||
<div>
|
<div>
|
||||||
<ProfileInformation />
|
<ProfileInformation />
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import Image from 'next/image'
|
import Image from "next/image"
|
||||||
|
|
||||||
import { getTheme } from '@/theme/theme.server'
|
import { getTheme } from "@/theme/theme.server"
|
||||||
|
|
||||||
import type { SkillName } from './skills'
|
import type { SkillName } from "./skills"
|
||||||
import { skills } from './skills'
|
import { skills } from "./skills"
|
||||||
|
|
||||||
export interface SkillComponentProps {
|
export interface SkillComponentProps {
|
||||||
skill: SkillName
|
skill: SkillName
|
||||||
@ -17,10 +17,10 @@ export const SkillComponent = (props: SkillComponentProps): JSX.Element => {
|
|||||||
const theme = getTheme()
|
const theme = getTheme()
|
||||||
|
|
||||||
const getImage = (): string => {
|
const getImage = (): string => {
|
||||||
if (typeof skillProperties.image === 'string') {
|
if (typeof skillProperties.image === "string") {
|
||||||
return skillProperties.image
|
return skillProperties.image
|
||||||
}
|
}
|
||||||
if (theme === 'light') {
|
if (theme === "light") {
|
||||||
return skillProperties.image.light
|
return skillProperties.image.light
|
||||||
}
|
}
|
||||||
return skillProperties.image.dark
|
return skillProperties.image.dark
|
||||||
@ -29,20 +29,20 @@ export const SkillComponent = (props: SkillComponentProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={skillProperties.link}
|
href={skillProperties.link}
|
||||||
className='mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark'
|
className="mx-2 max-w-xl text-yellow hover:underline dark:text-yellow-dark"
|
||||||
target='_blank'
|
target="_blank"
|
||||||
rel='noopener noreferrer'
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<div className='text-center'>
|
<div className="text-center">
|
||||||
<Image
|
<Image
|
||||||
className='inline h-16 w-16'
|
className="inline h-16 w-16"
|
||||||
quality={100}
|
quality={100}
|
||||||
width={64}
|
width={64}
|
||||||
height={64}
|
height={64}
|
||||||
alt={skill}
|
alt={skill}
|
||||||
src={getImage()}
|
src={getImage()}
|
||||||
/>
|
/>
|
||||||
<p className='mt-1'>{skill}</p>
|
<p className="mt-1">{skill}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ShadowContainer } from '@/components/design/ShadowContainer'
|
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||||
|
|
||||||
export interface SkillsSectionProps {
|
export interface SkillsSectionProps {
|
||||||
title: string
|
title: string
|
||||||
@ -10,15 +10,15 @@ export const SkillsSection = (props: SkillsSectionProps): JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ShadowContainer>
|
<ShadowContainer>
|
||||||
<div className='mx-auto w-full px-4'>
|
<div className="mx-auto w-full px-4">
|
||||||
<div className='flex flex-wrap px-4 py-6'>
|
<div className="flex flex-wrap px-4 py-6">
|
||||||
<div className='flex-1'>
|
<div className="flex-1">
|
||||||
<div className='mb-8 border-b border-gray-600 dark:border-white dark:border-opacity-10'>
|
<div className="mb-8 border-b border-gray-600 dark:border-white dark:border-opacity-10">
|
||||||
<h3 className='my-3 text-xl font-semibold text-yellow dark:text-yellow-dark'>
|
<h3 className="my-3 text-xl font-semibold text-yellow dark:text-yellow-dark">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-wrap justify-around'>{children}</div>
|
<div className="flex flex-wrap justify-around">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,40 +1,40 @@
|
|||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from "@/i18n/i18n.server"
|
||||||
|
|
||||||
import { SkillComponent } from './Skill'
|
import { SkillComponent } from "./Skill"
|
||||||
import { SkillsSection } from './SkillsSection'
|
import { SkillsSection } from "./SkillsSection"
|
||||||
|
|
||||||
export const Skills = (): JSX.Element => {
|
export const Skills = (): JSX.Element => {
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SkillsSection title={i18n.translate('home.skills.languages')}>
|
<SkillsSection title={i18n.translate("home.skills.languages")}>
|
||||||
<SkillComponent skill='TypeScript' />
|
<SkillComponent skill="TypeScript" />
|
||||||
<SkillComponent skill='Python' />
|
<SkillComponent skill="Python" />
|
||||||
<SkillComponent skill='C/C++' />
|
<SkillComponent skill="C/C++" />
|
||||||
<SkillComponent skill='PHP' />
|
<SkillComponent skill="PHP" />
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title='Frontend'>
|
<SkillsSection title="Frontend">
|
||||||
<SkillComponent skill='HTML' />
|
<SkillComponent skill="HTML" />
|
||||||
<SkillComponent skill='CSS' />
|
<SkillComponent skill="CSS" />
|
||||||
<SkillComponent skill='Tailwind CSS' />
|
<SkillComponent skill="Tailwind CSS" />
|
||||||
<SkillComponent skill='React.js (+ Next.js)' />
|
<SkillComponent skill="React.js (+ Next.js)" />
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title='Backend'>
|
<SkillsSection title="Backend">
|
||||||
<SkillComponent skill='Laravel' />
|
<SkillComponent skill="Laravel" />
|
||||||
<SkillComponent skill='Node.js' />
|
<SkillComponent skill="Node.js" />
|
||||||
<SkillComponent skill='Fastify' />
|
<SkillComponent skill="Fastify" />
|
||||||
<SkillComponent skill='PostgreSQL' />
|
<SkillComponent skill="PostgreSQL" />
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
|
|
||||||
<SkillsSection title={i18n.translate('home.skills.software-tools')}>
|
<SkillsSection title={i18n.translate("home.skills.software-tools")}>
|
||||||
<SkillComponent skill='GNU/Linux' />
|
<SkillComponent skill="GNU/Linux" />
|
||||||
<SkillComponent skill='Arch Linux' />
|
<SkillComponent skill="Arch Linux" />
|
||||||
<SkillComponent skill='Visual Studio Code' />
|
<SkillComponent skill="Visual Studio Code" />
|
||||||
<SkillComponent skill='Git' />
|
<SkillComponent skill="Git" />
|
||||||
<SkillComponent skill='Docker' />
|
<SkillComponent skill="Docker" />
|
||||||
</SkillsSection>
|
</SkillsSection>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -5,111 +5,111 @@ export interface Skill {
|
|||||||
|
|
||||||
export const skills = {
|
export const skills = {
|
||||||
JavaScript: {
|
JavaScript: {
|
||||||
link: 'https://developer.mozilla.org/docs/Web/JavaScript',
|
link: "https://developer.mozilla.org/docs/Web/JavaScript",
|
||||||
image: '/images/skills/JavaScript.png'
|
image: "/images/skills/JavaScript.png",
|
||||||
},
|
},
|
||||||
TypeScript: {
|
TypeScript: {
|
||||||
link: 'https://www.typescriptlang.org/',
|
link: "https://www.typescriptlang.org/",
|
||||||
image: '/images/skills/TypeScript.png'
|
image: "/images/skills/TypeScript.png",
|
||||||
},
|
},
|
||||||
Python: {
|
Python: {
|
||||||
link: 'https://www.python.org/',
|
link: "https://www.python.org/",
|
||||||
image: '/images/skills/Python.png'
|
image: "/images/skills/Python.png",
|
||||||
},
|
},
|
||||||
'C/C++': {
|
"C/C++": {
|
||||||
link: 'https://isocpp.org/',
|
link: "https://isocpp.org/",
|
||||||
image: '/images/skills/C-Cpp.png'
|
image: "/images/skills/C-Cpp.png",
|
||||||
},
|
},
|
||||||
PHP: {
|
PHP: {
|
||||||
link: 'https://www.php.net/',
|
link: "https://www.php.net/",
|
||||||
image: '/images/skills/PHP.png'
|
image: "/images/skills/PHP.png",
|
||||||
},
|
},
|
||||||
Laravel: {
|
Laravel: {
|
||||||
link: 'https://laravel.com/',
|
link: "https://laravel.com/",
|
||||||
image: '/images/skills/Laravel.png'
|
image: "/images/skills/Laravel.png",
|
||||||
},
|
},
|
||||||
Dart: {
|
Dart: {
|
||||||
link: 'https://dart.dev/',
|
link: "https://dart.dev/",
|
||||||
image: '/images/skills/Dart.png'
|
image: "/images/skills/Dart.png",
|
||||||
},
|
},
|
||||||
Flutter: {
|
Flutter: {
|
||||||
link: 'https://flutter.dev/',
|
link: "https://flutter.dev/",
|
||||||
image: '/images/skills/Flutter.webp'
|
image: "/images/skills/Flutter.webp",
|
||||||
},
|
},
|
||||||
HTML: {
|
HTML: {
|
||||||
link: 'https://developer.mozilla.org/docs/Web/HTML',
|
link: "https://developer.mozilla.org/docs/Web/HTML",
|
||||||
image: '/images/skills/HTML.png'
|
image: "/images/skills/HTML.png",
|
||||||
},
|
},
|
||||||
CSS: {
|
CSS: {
|
||||||
link: 'https://developer.mozilla.org/docs/Web/CSS',
|
link: "https://developer.mozilla.org/docs/Web/CSS",
|
||||||
image: '/images/skills/CSS.png'
|
image: "/images/skills/CSS.png",
|
||||||
},
|
},
|
||||||
'Tailwind CSS': {
|
"Tailwind CSS": {
|
||||||
link: 'https://tailwindcss.com/',
|
link: "https://tailwindcss.com/",
|
||||||
image: '/images/skills/TailwindCSS.png'
|
image: "/images/skills/TailwindCSS.png",
|
||||||
},
|
},
|
||||||
SASS: {
|
SASS: {
|
||||||
link: 'https://sass-lang.com/',
|
link: "https://sass-lang.com/",
|
||||||
image: '/images/skills/SASS.svg'
|
image: "/images/skills/SASS.svg",
|
||||||
},
|
},
|
||||||
'React.js (+ Next.js)': {
|
"React.js (+ Next.js)": {
|
||||||
link: 'https://reactjs.org/',
|
link: "https://reactjs.org/",
|
||||||
image: '/images/skills/ReactJS.png'
|
image: "/images/skills/ReactJS.png",
|
||||||
},
|
},
|
||||||
'Node.js': {
|
"Node.js": {
|
||||||
link: 'https://nodejs.org/',
|
link: "https://nodejs.org/",
|
||||||
image: '/images/skills/NodeJS.png'
|
image: "/images/skills/NodeJS.png",
|
||||||
},
|
},
|
||||||
Fastify: {
|
Fastify: {
|
||||||
link: 'https://www.fastify.io/',
|
link: "https://www.fastify.io/",
|
||||||
image: {
|
image: {
|
||||||
light: '/images/skills/Fastify-light.png',
|
light: "/images/skills/Fastify-light.png",
|
||||||
dark: '/images/skills/Fastify-dark.png'
|
dark: "/images/skills/Fastify-dark.png",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
Prisma: {
|
Prisma: {
|
||||||
link: 'https://www.prisma.io/',
|
link: "https://www.prisma.io/",
|
||||||
image: {
|
image: {
|
||||||
light: '/images/skills/Prisma-light.png',
|
light: "/images/skills/Prisma-light.png",
|
||||||
dark: '/images/skills/Prisma-dark.png'
|
dark: "/images/skills/Prisma-dark.png",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
PostgreSQL: {
|
PostgreSQL: {
|
||||||
link: 'https://www.postgresql.org/',
|
link: "https://www.postgresql.org/",
|
||||||
image: '/images/skills/PostgreSQL.png'
|
image: "/images/skills/PostgreSQL.png",
|
||||||
},
|
},
|
||||||
MySQL: {
|
MySQL: {
|
||||||
link: 'https://www.mysql.com/',
|
link: "https://www.mysql.com/",
|
||||||
image: '/images/skills/MySQL.png'
|
image: "/images/skills/MySQL.png",
|
||||||
},
|
},
|
||||||
Strapi: {
|
Strapi: {
|
||||||
link: 'https://strapi.io/',
|
link: "https://strapi.io/",
|
||||||
image: '/images/skills/Strapi.png'
|
image: "/images/skills/Strapi.png",
|
||||||
},
|
},
|
||||||
'Visual Studio Code': {
|
"Visual Studio Code": {
|
||||||
link: 'https://code.visualstudio.com/',
|
link: "https://code.visualstudio.com/",
|
||||||
image: '/images/skills/VisualStudioCode.png'
|
image: "/images/skills/VisualStudioCode.png",
|
||||||
},
|
},
|
||||||
Git: {
|
Git: {
|
||||||
link: 'https://git-scm.com/',
|
link: "https://git-scm.com/",
|
||||||
image: '/images/skills/Git.png'
|
image: "/images/skills/Git.png",
|
||||||
},
|
},
|
||||||
Ubuntu: {
|
Ubuntu: {
|
||||||
link: 'https://ubuntu.com/',
|
link: "https://ubuntu.com/",
|
||||||
image: '/images/skills/Ubuntu.png'
|
image: "/images/skills/Ubuntu.png",
|
||||||
},
|
},
|
||||||
'Arch Linux': {
|
"Arch Linux": {
|
||||||
link: 'https://archlinux.org/',
|
link: "https://archlinux.org/",
|
||||||
image: '/images/skills/ArchLinux.png'
|
image: "/images/skills/ArchLinux.png",
|
||||||
},
|
},
|
||||||
'GNU/Linux': {
|
"GNU/Linux": {
|
||||||
link: 'https://www.gnu.org/',
|
link: "https://www.gnu.org/",
|
||||||
image: '/images/skills/GNU-Linux.png'
|
image: "/images/skills/GNU-Linux.png",
|
||||||
},
|
},
|
||||||
Docker: {
|
Docker: {
|
||||||
link: 'https://www.docker.com/',
|
link: "https://www.docker.com/",
|
||||||
image: '/images/skills/Docker.png'
|
image: "/images/skills/Docker.png",
|
||||||
}
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type SkillName = keyof typeof skills
|
export type SkillName = keyof typeof skills
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import classNames from 'clsx'
|
import classNames from "clsx"
|
||||||
|
|
||||||
export interface LoaderProps {
|
export interface LoaderProps {
|
||||||
width?: number
|
width?: number
|
||||||
@ -13,16 +13,16 @@ export const Loader = (props: LoaderProps): JSX.Element => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width,
|
width,
|
||||||
height
|
height,
|
||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'animate-spin inline-block border-[3px] border-current border-t-transparent text-yellow dark:text-yellow-dark rounded-full',
|
"animate-spin inline-block border-[3px] border-current border-t-transparent text-yellow dark:text-yellow-dark rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
role='status'
|
role="status"
|
||||||
aria-label='loading'
|
aria-label="loading"
|
||||||
>
|
>
|
||||||
<span className='sr-only'>Loading...</span>
|
<span className="sr-only">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from "react"
|
||||||
|
|
||||||
export type RevealFadeProps = React.PropsWithChildren
|
export type RevealFadeProps = React.PropsWithChildren
|
||||||
|
|
||||||
@ -15,22 +15,22 @@ export const RevealFade = (props: RevealFadeProps): JSX.Element => {
|
|||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
entry.target.className =
|
entry.target.className =
|
||||||
'opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out'
|
"opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out"
|
||||||
observer.unobserve(entry.target)
|
observer.unobserve(entry.target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '0px',
|
rootMargin: "0px",
|
||||||
threshold: 0.28
|
threshold: 0.28,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
observer.observe(htmlElement.current as HTMLDivElement)
|
observer.observe(htmlElement.current as HTMLDivElement)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={htmlElement} className='invisible -translate-y-7 opacity-0'>
|
<div ref={htmlElement} className="invisible -translate-y-7 opacity-0">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'>
|
type SectionHeadingProps = React.ComponentPropsWithRef<"h2">
|
||||||
|
|
||||||
export const SectionHeading = (props: SectionHeadingProps): JSX.Element => {
|
export const SectionHeading = (props: SectionHeadingProps): JSX.Element => {
|
||||||
const { children, ...rest } = props
|
const { children, ...rest } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h2 {...rest} className='mb-3 mt-1 text-center text-4xl font-semibold'>
|
<h2 {...rest} className="mb-3 mt-1 text-center text-4xl font-semibold">
|
||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ShadowContainer } from '@/components/design/ShadowContainer'
|
import { ShadowContainer } from "@/components/design/ShadowContainer"
|
||||||
import { SectionHeading } from '@/components/design/Section/SectionHeading'
|
import { SectionHeading } from "@/components/design/Section/SectionHeading"
|
||||||
|
|
||||||
type SectionProps = React.ComponentPropsWithRef<'section'> & {
|
type SectionProps = React.ComponentPropsWithRef<"section"> & {
|
||||||
heading?: string
|
heading?: string
|
||||||
description?: string
|
description?: string
|
||||||
isMain?: boolean
|
isMain?: boolean
|
||||||
@ -20,13 +20,13 @@ export const Section = (props: SectionProps): JSX.Element => {
|
|||||||
|
|
||||||
if (isMain) {
|
if (isMain) {
|
||||||
return (
|
return (
|
||||||
<div className='w-full px-3'>
|
<div className="w-full px-3">
|
||||||
<ShadowContainer style={{ marginTop: 50 }}>
|
<ShadowContainer style={{ marginTop: 50 }}>
|
||||||
<section {...rest}>
|
<section {...rest}>
|
||||||
{heading != null ? (
|
{heading != null ? (
|
||||||
<SectionHeading>{heading}</SectionHeading>
|
<SectionHeading>{heading}</SectionHeading>
|
||||||
) : null}
|
) : null}
|
||||||
<div className='w-full px-3'>{children}</div>
|
<div className="w-full px-3">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
</div>
|
</div>
|
||||||
@ -37,7 +37,7 @@ export const Section = (props: SectionProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<section {...rest}>
|
<section {...rest}>
|
||||||
{heading != null ? <SectionHeading>{heading}</SectionHeading> : null}
|
{heading != null ? <SectionHeading>{heading}</SectionHeading> : null}
|
||||||
<div className='w-full px-3'>{children}</div>
|
<div className="w-full px-3">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -52,13 +52,13 @@ export const Section = (props: SectionProps): JSX.Element => {
|
|||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
) : null}
|
) : null}
|
||||||
{description != null ? (
|
{description != null ? (
|
||||||
<p style={{ marginTop: 7 }} className='text-center'>
|
<p style={{ marginTop: 7 }} className="text-center">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<div className='w-full px-3'>
|
<div className="w-full px-3">
|
||||||
<ShadowContainer>
|
<ShadowContainer>
|
||||||
<div className='w-full px-16 py-4 leading-8'>{children}</div>
|
<div className="w-full px-16 py-4 leading-8">{children}</div>
|
||||||
</ShadowContainer>
|
</ShadowContainer>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import classNames from 'clsx'
|
import classNames from "clsx"
|
||||||
|
|
||||||
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
type ShadowContainerProps = React.ComponentPropsWithRef<"div">
|
||||||
|
|
||||||
export const ShadowContainer = (props: ShadowContainerProps): JSX.Element => {
|
export const ShadowContainer = (props: ShadowContainerProps): JSX.Element => {
|
||||||
const { children, className, ...rest } = props
|
const { children, className, ...rest } = props
|
||||||
@ -8,8 +8,8 @@ export const ShadowContainer = (props: ShadowContainerProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'mb-12 h-full max-w-full break-words rounded-2xl border border-solid border-[#000] shadow-light dark:shadow-dark ',
|
"mb-12 h-full max-w-full break-words rounded-2xl border border-solid border-[#000] shadow-light dark:shadow-dark ",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
|
10
compose.yaml
10
compose.yaml
@ -1,11 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
theoludwig:
|
theoludwig:
|
||||||
container_name: ${COMPOSE_PROJECT_NAME}
|
container_name: ${COMPOSE_PROJECT_NAME}
|
||||||
image: 'theoludwig'
|
image: "theoludwig"
|
||||||
restart: 'unless-stopped'
|
restart: "unless-stopped"
|
||||||
build:
|
build:
|
||||||
context: './'
|
context: "./"
|
||||||
network_mode: 'host'
|
network_mode: "host"
|
||||||
environment:
|
environment:
|
||||||
PORT: ${PORT-3000}
|
PORT: ${PORT-3000}
|
||||||
env_file: '.env'
|
env_file: ".env"
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from "node:url"
|
||||||
import fs from 'node:fs'
|
import fs from "node:fs"
|
||||||
|
|
||||||
import { build } from 'vite'
|
import { build } from "vite"
|
||||||
|
|
||||||
const curriculumVitae = new URL('./', import.meta.url)
|
const curriculumVitae = new URL("./", import.meta.url)
|
||||||
const curriculumVitaeDist = new URL('./dist', curriculumVitae)
|
const curriculumVitaeDist = new URL("./dist", curriculumVitae)
|
||||||
const publicCurriculumVitaeOutputURL = new URL(
|
const publicCurriculumVitaeOutputURL = new URL(
|
||||||
'../public/curriculum-vitae',
|
"../public/curriculum-vitae",
|
||||||
import.meta.url
|
import.meta.url,
|
||||||
)
|
)
|
||||||
|
|
||||||
await build({
|
await build({
|
||||||
root: fileURLToPath(curriculumVitae),
|
root: fileURLToPath(curriculumVitae),
|
||||||
base: '/curriculum-vitae/'
|
base: "/curriculum-vitae/",
|
||||||
})
|
})
|
||||||
|
|
||||||
await fs.promises.cp(curriculumVitaeDist, publicCurriculumVitaeOutputURL, {
|
await fs.promises.cp(curriculumVitaeDist, publicCurriculumVitaeOutputURL, {
|
||||||
recursive: true
|
recursive: true,
|
||||||
})
|
})
|
||||||
|
@ -13,6 +13,20 @@
|
|||||||
"summary": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne. <br/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets (disponible sur <a href=\"https://theoludwig.fr\">theoludwig.fr</a>)."
|
"summary": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne. <br/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets (disponible sur <a href=\"https://theoludwig.fr\">theoludwig.fr</a>)."
|
||||||
},
|
},
|
||||||
"education": [
|
"education": [
|
||||||
|
{
|
||||||
|
"startDate": "2023",
|
||||||
|
"endDate": "2024",
|
||||||
|
"studyType": "Bachelor Universitaire de Technologie (BUT) Informatique",
|
||||||
|
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
|
||||||
|
"score": "3ème année",
|
||||||
|
"courses": [
|
||||||
|
"Développement Web en Node.js et React.js",
|
||||||
|
"Intégration/Déploiement Continue et Docker",
|
||||||
|
"Complexité Algorithmique Théorique et Pratique en C++",
|
||||||
|
// "Projet développement LLM (Large Language Model) et NLP (Natural Language Processing)",
|
||||||
|
"Base de données NoSQL (Redis, MongoDB, Cassandra)"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"startDate": "2022",
|
"startDate": "2022",
|
||||||
"endDate": "2023",
|
"endDate": "2023",
|
||||||
@ -21,10 +35,10 @@
|
|||||||
"score": "2ème année",
|
"score": "2ème année",
|
||||||
"courses": [
|
"courses": [
|
||||||
"Développement Web avec le framework Laravel en PHP",
|
"Développement Web avec le framework Laravel en PHP",
|
||||||
|
"Qualité de développement et Tests automatisés",
|
||||||
"Patrons et Principes de conceptions (Code maintenable et réutilisable) en UML",
|
"Patrons et Principes de conceptions (Code maintenable et réutilisable) en UML",
|
||||||
"Programmation systèmes en C (Multi-Thread, Serveur/Client UDP/TCP)",
|
"Programmation systèmes en C (Multi-Thread, Serveur/Client UDP/TCP)",
|
||||||
"Sécurisation des accès à la base de données et PL/SQL",
|
"Sécurisation des accès à la base de données et PL/SQL"
|
||||||
"Projet développement d'une application web en React.js en équipe de 3 personnes pendant 3 mois"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -56,29 +70,39 @@
|
|||||||
// }
|
// }
|
||||||
],
|
],
|
||||||
"work": [
|
"work": [
|
||||||
|
{
|
||||||
|
"summary": "Développement de WebSurg, une université virtuelle consacrée à la formation médico-chirurgicale, en React.js/Next.js et API Platform avec Symfony.",
|
||||||
|
"website": "https://ircad.fr/",
|
||||||
|
"name": "IRCAD",
|
||||||
|
"location": "1 Place de l'Hôpital, 67000 Strasbourg",
|
||||||
|
"position": "Alternant Développeur Web Full Stack",
|
||||||
|
"startDate": "2023-08-28",
|
||||||
|
"endDate": "2024-09-02",
|
||||||
|
"duration": "1 an"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"summary": "Développement d'un outil GED (Gestion Électronique de Documents) en React.js, Laravel et GraphQL.",
|
"summary": "Développement d'un outil GED (Gestion Électronique de Documents) en React.js, Laravel et GraphQL.",
|
||||||
"website": "https://numerize.com/",
|
"website": "https://numerize.com/",
|
||||||
"name": "Numerize",
|
"name": "Numerize",
|
||||||
"location": "4 Rue Sophie Germain, 67720 Hœrdt",
|
"location": "4 Rue Sophie Germain, 67720 Hœrdt",
|
||||||
"position": "Stagiaire Développeur Web",
|
"position": "Stagiaire Développeur Web Full Stack",
|
||||||
"startDate": "2023-04-11",
|
"startDate": "2023-04-11",
|
||||||
"endDate": "2023-07-26",
|
"endDate": "2023-07-26",
|
||||||
"duration": "4 mois"
|
"duration": "4 mois"
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
"summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.",
|
// "summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.",
|
||||||
"website": "https://www.es.fr/",
|
// "website": "https://www.es.fr/",
|
||||||
"name": "ÉS (Électricité de Strasbourg)",
|
// "name": "ÉS (Électricité de Strasbourg)",
|
||||||
"location": "5 Rue André Marie Ampère, 67450 Mundolsheim",
|
// "location": "5 Rue André Marie Ampère, 67450 Mundolsheim",
|
||||||
"position": "Emploi d'été en qualité d'agent administratif",
|
// "position": "Emploi d'été en qualité d'agent administratif",
|
||||||
"startDate": "2021-07-07",
|
// "startDate": "2021-07-07",
|
||||||
"endDate": "2021-07-30",
|
// "endDate": "2021-07-30",
|
||||||
"duration": "1 mois"
|
// "duration": "1 mois"
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
"summary": "Développement d'un site web pour trouver un restaurant à la pause repas.",
|
"summary": "Développement d'un site web pour trouver un restaurant à la pause repas.",
|
||||||
"website": "https://www.itpartners.fr/",
|
"website": "https://itpartners.fr/",
|
||||||
"name": "Tribe | IT Partners",
|
"name": "Tribe | IT Partners",
|
||||||
"location": "16 Rue du Parc, 67205 Oberhausbergen",
|
"location": "16 Rue du Parc, 67205 Oberhausbergen",
|
||||||
"position": "Stage initiation métier développeur web",
|
"position": "Stage initiation métier développeur web",
|
||||||
@ -88,8 +112,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "interests",
|
"description": "interests",
|
||||||
"summary": "Développement site web en React.js et Strapi.<br /> Classé n°1 en France sur le Défi de l'entreprise <a href=\"https://www.toolpad.fr/\">ToolPad</a>.",
|
"summary": "Développement site web en React.js et Strapi.<br /> Classé n°1 en France sur le Défi de l'entreprise <a href=\"https://toolpad.fr/\">ToolPad</a>.",
|
||||||
"website": "https://www.nuitdelinfo.com/",
|
"website": "https://nuitdelinfo.com/",
|
||||||
"name": "La Nuit de l'info 2021",
|
"name": "La Nuit de l'info 2021",
|
||||||
"position": "Participation en équipe de 5 personnes",
|
"position": "Participation en équipe de 5 personnes",
|
||||||
"startDate": "2021-12-02",
|
"startDate": "2021-12-02",
|
||||||
@ -99,7 +123,7 @@
|
|||||||
{
|
{
|
||||||
"description": "interests",
|
"description": "interests",
|
||||||
"summary": "Hackathon développement d'une landing page et web scraping.",
|
"summary": "Hackathon développement d'une landing page et web scraping.",
|
||||||
"website": "https://www.wildcodeschool.fr/",
|
"website": "https://wildcodeschool.fr/",
|
||||||
"name": "Wild Code School",
|
"name": "Wild Code School",
|
||||||
"location": "32 Rue du Bass. d'Austerlitz, 67100 Strasbourg",
|
"location": "32 Rue du Bass. d'Austerlitz, 67100 Strasbourg",
|
||||||
"position": "Initiation métier Développeur web",
|
"position": "Initiation métier Développeur web",
|
||||||
@ -109,7 +133,7 @@
|
|||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
// "summary": "Apprentissage du métier \"Chargé de communication\" et des logiciels de graphisme tels que \"Adobe Photoshop\".",
|
// "summary": "Apprentissage du métier \"Chargé de communication\" et des logiciels de graphisme tels que \"Adobe Photoshop\".",
|
||||||
// "website": "https://www.es.fr/",
|
// "website": "https://es.fr/",
|
||||||
// "name": "ÉS (Électricité de Strasbourg)",
|
// "name": "ÉS (Électricité de Strasbourg)",
|
||||||
// "location": "26 Bd du Président-Wilson, 67000 Strasbourg",
|
// "location": "26 Bd du Président-Wilson, 67000 Strasbourg",
|
||||||
// "position": "Stage de découverte (3ème)",
|
// "position": "Stage de découverte (3ème)",
|
||||||
|
521
curriculum-vitae/package-lock.json
generated
521
curriculum-vitae/package-lock.json
generated
@ -12,16 +12,256 @@
|
|||||||
"modern-normalize": "2.0.0"
|
"modern-normalize": "2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.4.5",
|
"@types/node": "20.8.10",
|
||||||
"date-and-time": "3.0.2",
|
"date-and-time": "3.0.3",
|
||||||
"vite": "4.4.7",
|
"vite": "4.5.0",
|
||||||
"vite-plugin-html": "3.2.0"
|
"vite-plugin-html": "3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.18.17",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||||
"integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==",
|
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -34,6 +274,102 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.18.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||||
|
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
||||||
@ -49,9 +385,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
|
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@ -83,21 +419,15 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.18",
|
"version": "0.3.20",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
|
||||||
"integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
|
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
"@jridgewell/sourcemap-codec": "1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
|
|
||||||
"version": "1.4.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
|
||||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -147,15 +477,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.4.5",
|
"version": "20.8.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
|
||||||
"integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==",
|
"integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.10.0",
|
"version": "8.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
||||||
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
|
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@ -180,9 +513,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
|
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
@ -346,9 +679,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/date-and-time": {
|
"node_modules/date-and-time": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.0.3.tgz",
|
||||||
"integrity": "sha512-MOqlRertOQmQI7ySbz6dKLM7Rxm9dgcPuBI9IL7NVe0UGqHPK+6hWSKVhLrVHxlSgQQtocE2R7+HFOf5aMz8vw==",
|
"integrity": "sha512-CmHCeTixc3KA5pcLTVs9JCFhmJMFTBsmSsgHnNed4YDNw9yUOrjjRn3zALy8eMgqmTO+4U8k5jl1peC7IoezfA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
@ -462,9 +795,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.18.17",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||||
"integrity": "sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==",
|
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -474,28 +807,28 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/android-arm": "0.18.17",
|
"@esbuild/android-arm": "0.18.20",
|
||||||
"@esbuild/android-arm64": "0.18.17",
|
"@esbuild/android-arm64": "0.18.20",
|
||||||
"@esbuild/android-x64": "0.18.17",
|
"@esbuild/android-x64": "0.18.20",
|
||||||
"@esbuild/darwin-arm64": "0.18.17",
|
"@esbuild/darwin-arm64": "0.18.20",
|
||||||
"@esbuild/darwin-x64": "0.18.17",
|
"@esbuild/darwin-x64": "0.18.20",
|
||||||
"@esbuild/freebsd-arm64": "0.18.17",
|
"@esbuild/freebsd-arm64": "0.18.20",
|
||||||
"@esbuild/freebsd-x64": "0.18.17",
|
"@esbuild/freebsd-x64": "0.18.20",
|
||||||
"@esbuild/linux-arm": "0.18.17",
|
"@esbuild/linux-arm": "0.18.20",
|
||||||
"@esbuild/linux-arm64": "0.18.17",
|
"@esbuild/linux-arm64": "0.18.20",
|
||||||
"@esbuild/linux-ia32": "0.18.17",
|
"@esbuild/linux-ia32": "0.18.20",
|
||||||
"@esbuild/linux-loong64": "0.18.17",
|
"@esbuild/linux-loong64": "0.18.20",
|
||||||
"@esbuild/linux-mips64el": "0.18.17",
|
"@esbuild/linux-mips64el": "0.18.20",
|
||||||
"@esbuild/linux-ppc64": "0.18.17",
|
"@esbuild/linux-ppc64": "0.18.20",
|
||||||
"@esbuild/linux-riscv64": "0.18.17",
|
"@esbuild/linux-riscv64": "0.18.20",
|
||||||
"@esbuild/linux-s390x": "0.18.17",
|
"@esbuild/linux-s390x": "0.18.20",
|
||||||
"@esbuild/linux-x64": "0.18.17",
|
"@esbuild/linux-x64": "0.18.20",
|
||||||
"@esbuild/netbsd-x64": "0.18.17",
|
"@esbuild/netbsd-x64": "0.18.20",
|
||||||
"@esbuild/openbsd-x64": "0.18.17",
|
"@esbuild/openbsd-x64": "0.18.20",
|
||||||
"@esbuild/sunos-x64": "0.18.17",
|
"@esbuild/sunos-x64": "0.18.20",
|
||||||
"@esbuild/win32-arm64": "0.18.17",
|
"@esbuild/win32-arm64": "0.18.20",
|
||||||
"@esbuild/win32-ia32": "0.18.17",
|
"@esbuild/win32-ia32": "0.18.20",
|
||||||
"@esbuild/win32-x64": "0.18.17"
|
"@esbuild/win32-x64": "0.18.20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
@ -505,9 +838,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||||
"integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
|
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "^2.0.2",
|
"@nodelib/fs.stat": "^2.0.2",
|
||||||
@ -585,6 +918,20 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
@ -762,9 +1109,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.6",
|
"version": "3.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
|
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -856,9 +1203,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.27",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
|
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -923,9 +1270,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "3.26.3",
|
"version": "3.29.4",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.3.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
|
||||||
"integrity": "sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==",
|
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
@ -1002,9 +1349,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.19.2",
|
"version": "5.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz",
|
||||||
"integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==",
|
"integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
@ -1038,29 +1385,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||||
"integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==",
|
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.4.7",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||||
"integrity": "sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==",
|
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.18.10",
|
"esbuild": "^0.18.10",
|
||||||
"postcss": "^8.4.26",
|
"postcss": "^8.4.27",
|
||||||
"rollup": "^3.25.2"
|
"rollup": "^3.27.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
@ -13,9 +13,9 @@
|
|||||||
"modern-normalize": "2.0.0"
|
"modern-normalize": "2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.4.5",
|
"@types/node": "20.8.10",
|
||||||
"date-and-time": "3.0.2",
|
"date-and-time": "3.0.3",
|
||||||
"vite": "4.4.7",
|
"vite": "4.5.0",
|
||||||
"vite-plugin-html": "3.2.0"
|
"vite-plugin-html": "3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { BIRTH_DATE, getAge } from '../../utils/getAge.ts'
|
import { BIRTH_DATE, getAge } from "../../utils/getAge.ts"
|
||||||
|
|
||||||
const yearOld = document.getElementById('year-old')
|
const yearOld = document.getElementById("year-old")
|
||||||
|
|
||||||
yearOld.textContent = getAge(BIRTH_DATE).toString()
|
yearOld.textContent = getAge(BIRTH_DATE).toString()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
@import 'modern-normalize/modern-normalize.css';
|
@import "modern-normalize/modern-normalize.css";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Montserrat', 'Arial', 'sans-serif';
|
font-family: "Montserrat", "Arial", "sans-serif";
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
color: #333;
|
color: #333;
|
||||||
line-height: 1.42857143;
|
line-height: 1.42857143;
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import fs from 'node:fs'
|
import fs from "node:fs"
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite"
|
||||||
import { parse as JSONCParser } from 'jsonc-parser'
|
import { parse as JSONCParser } from "jsonc-parser"
|
||||||
import { createHtmlPlugin } from 'vite-plugin-html'
|
import { createHtmlPlugin } from "vite-plugin-html"
|
||||||
import date from 'date-and-time'
|
import date from "date-and-time"
|
||||||
|
|
||||||
const jsonCurriculumVitaeURL = new URL(
|
const jsonCurriculumVitaeURL = new URL(
|
||||||
'./curriculum-vitae.jsonc',
|
"./curriculum-vitae.jsonc",
|
||||||
import.meta.url
|
import.meta.url,
|
||||||
)
|
)
|
||||||
const dataCurriculumVitaeStringJSON = await fs.promises.readFile(
|
const dataCurriculumVitaeStringJSON = await fs.promises.readFile(
|
||||||
jsonCurriculumVitaeURL,
|
jsonCurriculumVitaeURL,
|
||||||
{
|
{
|
||||||
encoding: 'utf-8'
|
encoding: "utf-8",
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
const curriculumVitae = JSONCParser(dataCurriculumVitaeStringJSON)
|
const curriculumVitae = JSONCParser(dataCurriculumVitaeStringJSON)
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ const curriculumVitae = JSONCParser(dataCurriculumVitaeStringJSON)
|
|||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
assetsDir: './'
|
assetsDir: "./",
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
createHtmlPlugin({
|
createHtmlPlugin({
|
||||||
@ -30,13 +30,13 @@ export default defineConfig({
|
|||||||
data: {
|
data: {
|
||||||
date,
|
date,
|
||||||
locals: {
|
locals: {
|
||||||
...curriculumVitae
|
...curriculumVitae,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
],
|
],
|
||||||
css: {
|
css: {
|
||||||
postcss: {}
|
postcss: {},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { defineConfig } from 'cypress'
|
import { defineConfig } from "cypress"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
fixturesFolder: false,
|
fixturesFolder: false,
|
||||||
video: false,
|
video: false,
|
||||||
screenshotOnRunFailure: false,
|
screenshotOnRunFailure: false,
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: 'http://127.0.0.1:3000',
|
baseUrl: "http://127.0.0.1:3000",
|
||||||
supportFile: false
|
supportFile: false,
|
||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
devServer: {
|
devServer: {
|
||||||
framework: 'next',
|
framework: "next",
|
||||||
bundler: 'webpack'
|
bundler: "webpack",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { getAge } from '@/utils/getAge'
|
import { getAge } from "@/utils/getAge"
|
||||||
|
|
||||||
describe('utils/getAge', () => {
|
describe("utils/getAge", () => {
|
||||||
it('should calculate the right age of a person', () => {
|
it("should calculate the right age of a person", () => {
|
||||||
cy.clock(new Date('2018-03-20')).then(() => {
|
cy.clock(new Date("2018-03-20")).then(() => {
|
||||||
const birthDate = new Date('1980-02-20')
|
const birthDate = new Date("1980-02-20")
|
||||||
expect(getAge(birthDate)).equal(38)
|
expect(getAge(birthDate)).equal(38)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should calculate the right age of a person (taking into account the months)', () => {
|
it("should calculate the right age of a person (taking into account the months)", () => {
|
||||||
cy.clock(new Date('2018-03-20')).then(() => {
|
cy.clock(new Date("2018-03-20")).then(() => {
|
||||||
const birthDate = new Date('1980-07-20')
|
const birthDate = new Date("1980-07-20")
|
||||||
expect(getAge(birthDate)).equal(37)
|
expect(getAge(birthDate)).equal(37)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,62 +1,62 @@
|
|||||||
describe('Common > Header', () => {
|
describe("Common > Header", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
return cy.visit('/')
|
return cy.visit("/")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should redirect to /blog on click of the blog link', () => {
|
it("should redirect to /blog on click of the blog link", () => {
|
||||||
cy.get('[data-cy=header-blog-link]')
|
cy.get("[data-cy=header-blog-link]")
|
||||||
.click()
|
.click()
|
||||||
.location('pathname')
|
.location("pathname")
|
||||||
.should('eq', '/blog')
|
.should("eq", "/blog")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should always be visible (sticky header)', () => {
|
it("should always be visible (sticky header)", () => {
|
||||||
cy.scrollTo('bottom').get('header').should('be.visible')
|
cy.scrollTo("bottom").get("header").should("be.visible")
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Switch theme color (dark/light)', () => {
|
describe("Switch theme color (dark/light)", () => {
|
||||||
it('should switch theme from `dark` (default) to `light`', () => {
|
it("should switch theme from `dark` (default) to `light`", () => {
|
||||||
cy.get('[data-cy=switch-theme-dark]').should('be.visible')
|
cy.get("[data-cy=switch-theme-dark]").should("be.visible")
|
||||||
cy.get('[data-cy=switch-theme-light]').should('not.be.visible')
|
cy.get("[data-cy=switch-theme-light]").should("not.be.visible")
|
||||||
cy.get('body').should(
|
cy.get("body").should(
|
||||||
'not.have.css',
|
"not.have.css",
|
||||||
'background-color',
|
"background-color",
|
||||||
'rgb(255, 255, 255)'
|
"rgb(255, 255, 255)",
|
||||||
)
|
)
|
||||||
|
|
||||||
cy.get('[data-cy=switch-theme-click]').click()
|
cy.get("[data-cy=switch-theme-click]").click()
|
||||||
|
|
||||||
cy.get('[data-cy=switch-theme-dark]').should('not.be.visible')
|
cy.get("[data-cy=switch-theme-dark]").should("not.be.visible")
|
||||||
cy.get('[data-cy=switch-theme-light]').should('be.visible')
|
cy.get("[data-cy=switch-theme-light]").should("be.visible")
|
||||||
cy.get('body').should(
|
cy.get("body").should(
|
||||||
'have.css',
|
"have.css",
|
||||||
'background-color',
|
"background-color",
|
||||||
'rgb(255, 255, 255)'
|
"rgb(255, 255, 255)",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Switch Language', () => {
|
describe("Switch Language", () => {
|
||||||
it('should switch locale from English (default) to French', () => {
|
it("should switch locale from English (default) to French", () => {
|
||||||
cy.get('h1').contains('Théo LUDWIG')
|
cy.get("h1").contains("Théo LUDWIG")
|
||||||
cy.get('[data-cy=locale-flag-text]').contains('English')
|
cy.get("[data-cy=locale-flag-text]").contains("English")
|
||||||
cy.get('[data-cy=locales-list]').should('not.be.visible')
|
cy.get("[data-cy=locales-list]").should("not.be.visible")
|
||||||
cy.get('[data-cy=locale-click]').click()
|
cy.get("[data-cy=locale-click]").click()
|
||||||
cy.get('[data-cy=locales-list]').should('be.visible')
|
cy.get("[data-cy=locales-list]").should("be.visible")
|
||||||
cy.get('[data-cy=locales-list] > li:first-child')
|
cy.get("[data-cy=locales-list] > li:first-child")
|
||||||
.contains('French')
|
.contains("French")
|
||||||
.click()
|
.click()
|
||||||
cy.get('[data-cy=locales-list]').should('not.be.visible')
|
cy.get("[data-cy=locales-list]").should("not.be.visible")
|
||||||
cy.get('[data-cy=locale-flag-text]').contains('French')
|
cy.get("[data-cy=locale-flag-text]").contains("French")
|
||||||
cy.get('h1').contains('Théo LUDWIG')
|
cy.get("h1").contains("Théo LUDWIG")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should close the locale list menu when clicking outside', () => {
|
it("should close the locale list menu when clicking outside", () => {
|
||||||
cy.get('[data-cy=locales-list]').should('not.be.visible')
|
cy.get("[data-cy=locales-list]").should("not.be.visible")
|
||||||
cy.get('[data-cy=locale-click]').click()
|
cy.get("[data-cy=locale-click]").click()
|
||||||
cy.get('[data-cy=locales-list]').should('be.visible')
|
cy.get("[data-cy=locales-list]").should("be.visible")
|
||||||
cy.get('h1').click()
|
cy.get("h1").click()
|
||||||
cy.get('[data-cy=locales-list]').should('not.be.visible')
|
cy.get("[data-cy=locales-list]").should("not.be.visible")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
describe('Page /404', () => {
|
describe("Page /404", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
return cy.visit('/404', { failOnStatusCode: false })
|
return cy.visit("/404", { failOnStatusCode: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display the statusCode of 404', () => {
|
it("should display the statusCode of 404", () => {
|
||||||
cy.get('[data-cy=status-code]').contains('404')
|
cy.get("[data-cy=status-code]").contains("404")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
describe('Page /blog/[slug]', () => {
|
describe("Page /blog/[slug]", () => {
|
||||||
it('should displays the first blog post (`hello-world`)', () => {
|
it("should displays the first blog post (`hello-world`)", () => {
|
||||||
cy.visit('/blog/hello-world')
|
cy.visit("/blog/hello-world")
|
||||||
cy.get('[data-cy=locale-flag-text]').should('not.exist')
|
cy.get("[data-cy=locale-flag-text]").should("not.exist")
|
||||||
cy.get('h1').should('have.text', '👋 Hello, world!')
|
cy.get("h1").should("have.text", "👋 Hello, world!")
|
||||||
cy.get('.prose a:visible').should('have.attr', 'target', '_blank')
|
cy.get(".prose a:visible").should("have.attr", "target", "_blank")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should redirect to /404 if the blog post doesn't exist", () => {
|
it("should redirect to /404 if the blog post doesn't exist", () => {
|
||||||
cy.visit('/blog/random-blog-post-not-found', { failOnStatusCode: false })
|
cy.visit("/blog/random-blog-post-not-found", { failOnStatusCode: false })
|
||||||
cy.get('[data-cy=status-code]').contains('404')
|
cy.get("[data-cy=status-code]").contains("404")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
describe('Page /blog', () => {
|
describe("Page /blog", () => {
|
||||||
it('should displays the blog posts sorted from newest to oldest', () => {
|
it("should displays the blog posts sorted from newest to oldest", () => {
|
||||||
cy.visit('/blog')
|
cy.visit("/blog")
|
||||||
cy.get('[data-cy=blog-posts] [data-cy=blog-post-title]')
|
cy.get("[data-cy=blog-posts] [data-cy=blog-post-title]")
|
||||||
.last()
|
.last()
|
||||||
.should('have.text', '👋 Hello, world!')
|
.should("have.text", "👋 Hello, world!")
|
||||||
cy.get('[data-cy=blog-posts] [data-cy=blog-post-description]')
|
cy.get("[data-cy=blog-posts] [data-cy=blog-post-description]")
|
||||||
.last()
|
.last()
|
||||||
.should(
|
.should(
|
||||||
'have.text',
|
"have.text",
|
||||||
'First post of the blog, introduction and explanation of how this blog is made.'
|
"First post of the blog, introduction and explanation of how this blog is made.",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should redirect the user to the right blog post', () => {
|
it("should redirect the user to the right blog post", () => {
|
||||||
cy.visit('/blog')
|
cy.visit("/blog")
|
||||||
cy.get('[data-cy=hello-world]')
|
cy.get("[data-cy=hello-world]")
|
||||||
.click()
|
.click()
|
||||||
.location('pathname')
|
.location("pathname")
|
||||||
.should('eq', '/blog/hello-world')
|
.should("eq", "/blog/hello-world")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
describe('Page /', () => {
|
describe("Page /", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
return cy.visit('/')
|
return cy.visit("/")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reveals the sections while scrolling except the about section', () => {
|
it("should reveals the sections while scrolling except the about section", () => {
|
||||||
const sectionsReveals = ['#interests', '#skills', '#portfolio']
|
const sectionsReveals = ["#interests", "#skills", "#portfolio"]
|
||||||
cy.get('#about').should('be.visible')
|
cy.get("#about").should("be.visible")
|
||||||
for (const section of sectionsReveals) {
|
for (const section of sectionsReveals) {
|
||||||
cy.get(section)
|
cy.get(section)
|
||||||
.should('not.be.visible')
|
.should("not.be.visible")
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should('be.visible')
|
.should("be.visible")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { mount } from 'cypress/react'
|
import { mount } from "cypress/react"
|
||||||
|
|
||||||
import './commands'
|
import "./commands"
|
||||||
import '../../app/globals.css'
|
import "../../app/globals.css"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
@ -11,4 +11,4 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add('mount', mount)
|
Cypress.Commands.add("mount", mount)
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import UniversalCookie from 'universal-cookie'
|
import UniversalCookie from "universal-cookie"
|
||||||
import type { I18n } from 'i18n-js'
|
import type { I18n } from "i18n-js"
|
||||||
|
|
||||||
import type { CookiesStore } from '@/utils/constants'
|
import type { CookiesStore } from "@/utils/constants"
|
||||||
|
|
||||||
import { i18n } from './i18n'
|
import { i18n } from "./i18n"
|
||||||
|
|
||||||
export const useI18n = (cookiesStore: CookiesStore): I18n => {
|
export const useI18n = (cookiesStore: CookiesStore): I18n => {
|
||||||
const universalCookie = new UniversalCookie(cookiesStore)
|
const universalCookie = new UniversalCookie(cookiesStore)
|
||||||
i18n.locale = universalCookie.get('locale') ?? i18n.defaultLocale
|
i18n.locale = universalCookie.get("locale") ?? i18n.defaultLocale
|
||||||
return i18n
|
return i18n
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
'use server'
|
"use server"
|
||||||
|
|
||||||
import { cookies } from 'next/headers'
|
import { cookies } from "next/headers"
|
||||||
import type { I18n } from 'i18n-js'
|
import type { I18n } from "i18n-js"
|
||||||
|
|
||||||
import type { Locale } from '@/utils/constants'
|
import type { Locale } from "@/utils/constants"
|
||||||
import { COOKIE_MAX_AGE } from '@/utils/constants'
|
import { COOKIE_MAX_AGE } from "@/utils/constants"
|
||||||
|
|
||||||
import { i18n } from './i18n'
|
import { i18n } from "./i18n"
|
||||||
|
|
||||||
export const setLocale = (locale: Locale): void => {
|
export const setLocale = (locale: Locale): void => {
|
||||||
cookies().set('locale', locale, {
|
cookies().set("locale", locale, {
|
||||||
path: '/',
|
path: "/",
|
||||||
maxAge: COOKIE_MAX_AGE
|
maxAge: COOKIE_MAX_AGE,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getI18n = (): I18n => {
|
export const getI18n = (): I18n => {
|
||||||
i18n.locale = cookies().get('locale')?.value ?? i18n.defaultLocale
|
i18n.locale = cookies().get("locale")?.value ?? i18n.defaultLocale
|
||||||
return i18n
|
return i18n
|
||||||
}
|
}
|
||||||
|
30
i18n/i18n.ts
30
i18n/i18n.ts
@ -1,30 +1,30 @@
|
|||||||
import { I18n } from 'i18n-js'
|
import { I18n } from "i18n-js"
|
||||||
|
|
||||||
import type { Locale } from '@/utils/constants'
|
import type { Locale } from "@/utils/constants"
|
||||||
import { DEFAULT_LOCALE, LOCALES } from '@/utils/constants'
|
import { DEFAULT_LOCALE, LOCALES } from "@/utils/constants"
|
||||||
|
|
||||||
import commonEnglish from './translations/en-US/common.json'
|
import commonEnglish from "./translations/en-US/common.json"
|
||||||
import errorsEnglish from './translations/en-US/errors.json'
|
import errorsEnglish from "./translations/en-US/errors.json"
|
||||||
import homeEnglish from './translations/en-US/home.json'
|
import homeEnglish from "./translations/en-US/home.json"
|
||||||
import commonFrench from './translations/fr-FR/common.json'
|
import commonFrench from "./translations/fr-FR/common.json"
|
||||||
import errorsFrench from './translations/fr-FR/errors.json'
|
import errorsFrench from "./translations/fr-FR/errors.json"
|
||||||
import homeFrench from './translations/fr-FR/home.json'
|
import homeFrench from "./translations/fr-FR/home.json"
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
'en-US': {
|
"en-US": {
|
||||||
common: commonEnglish,
|
common: commonEnglish,
|
||||||
errors: errorsEnglish,
|
errors: errorsEnglish,
|
||||||
home: homeEnglish
|
home: homeEnglish,
|
||||||
},
|
},
|
||||||
'fr-FR': {
|
"fr-FR": {
|
||||||
common: commonFrench,
|
common: commonFrench,
|
||||||
errors: errorsFrench,
|
errors: errorsFrench,
|
||||||
home: homeFrench
|
home: homeFrench,
|
||||||
}
|
},
|
||||||
} satisfies Record<Locale, Record<string, unknown>>
|
} satisfies Record<Locale, Record<string, unknown>>
|
||||||
|
|
||||||
export const i18n = new I18n(translations, {
|
export const i18n = new I18n(translations, {
|
||||||
defaultLocale: DEFAULT_LOCALE,
|
defaultLocale: DEFAULT_LOCALE,
|
||||||
availableLocales: LOCALES.slice(),
|
availableLocales: LOCALES.slice(),
|
||||||
enableFallback: true
|
enableFallback: true,
|
||||||
})
|
})
|
||||||
|
@ -1,39 +1,43 @@
|
|||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from "next/server"
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from "next/server"
|
||||||
import { match } from '@formatjs/intl-localematcher'
|
import { match } from "@formatjs/intl-localematcher"
|
||||||
import Negotiator from 'negotiator'
|
import Negotiator from "negotiator"
|
||||||
|
|
||||||
import type { Locale, Theme } from '@/utils/constants'
|
import type { Locale, Theme } from "@/utils/constants"
|
||||||
import {
|
import {
|
||||||
COOKIE_MAX_AGE,
|
COOKIE_MAX_AGE,
|
||||||
DEFAULT_LOCALE,
|
DEFAULT_LOCALE,
|
||||||
DEFAULT_THEME,
|
DEFAULT_THEME,
|
||||||
LOCALES,
|
LOCALES,
|
||||||
THEMES
|
THEMES,
|
||||||
} from '@/utils/constants'
|
} from "@/utils/constants"
|
||||||
|
|
||||||
export const middleware = (request: NextRequest): NextResponse => {
|
export const middleware = (request: NextRequest): NextResponse => {
|
||||||
const response = NextResponse.next()
|
const response = NextResponse.next()
|
||||||
|
|
||||||
let locale = request.cookies.get('locale')?.value
|
let locale = request.cookies.get("locale")?.value
|
||||||
if (locale == null || !LOCALES.includes(locale as Locale)) {
|
if (locale == null || !LOCALES.includes(locale as Locale)) {
|
||||||
const headers = {
|
try {
|
||||||
'accept-language':
|
const headers = {
|
||||||
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
|
||||||
}
|
}
|
||||||
const languages = new Negotiator({ headers }).languages()
|
response.cookies.set("locale", locale, {
|
||||||
locale = match(languages, LOCALES.slice(), DEFAULT_LOCALE)
|
path: "/",
|
||||||
response.cookies.set('locale', locale, {
|
maxAge: COOKIE_MAX_AGE,
|
||||||
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)) {
|
if (theme == null || !THEMES.includes(theme as Theme)) {
|
||||||
response.cookies.set('theme', DEFAULT_THEME, {
|
response.cookies.set("theme", DEFAULT_THEME, {
|
||||||
path: '/',
|
path: "/",
|
||||||
maxAge: COOKIE_MAX_AGE
|
maxAge: COOKIE_MAX_AGE,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +53,6 @@ export const config = {
|
|||||||
* - _next/image (image optimization files)
|
* - _next/image (image optimization files)
|
||||||
* - favicon.ico (favicon file)
|
* - favicon.ico (favicon file)
|
||||||
*/
|
*/
|
||||||
'/((?!api|_next/static|_next/image|favicon.ico).*)'
|
"/((?!api|_next/static|_next/image|favicon.ico).*)",
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
output: 'standalone',
|
output: "standalone",
|
||||||
experimental: {
|
eslint: {
|
||||||
serverActions: true
|
ignoreDuringBuilds: true,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig
|
||||||
|
5007
package-lock.json
generated
5007
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user