1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2024-10-05 04:56:10 +02:00

chore: better Prettier config for easier reviews

This commit is contained in:
Théo LUDWIG 2023-10-23 23:11:59 +02:00
parent c7ad15a465
commit e566ef6c38
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
105 changed files with 2138 additions and 2080 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
image: 'gitpod/workspace-full'
image: "gitpod/workspace-full"
tasks:
- before: 'cp .env.example .env'
init: 'npm clean-install'
command: 'npm run dev'
- before: "cp .env.example .env"
init: "npm clean-install"
command: "npm run dev"
ports:
- port: 3000
onOpen: 'open-preview'
onOpen: "open-preview"
github:
prebuilds:

View File

@ -1,6 +1,3 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"semi": false,
"trailingComma": "none"
"semi": false
}

View File

@ -34,7 +34,7 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
### Prerequisites
- [Node.js](https://nodejs.org/) >= 20.0.0
- [npm](https://www.npmjs.com/) >= 9.0.0
- [npm](https://www.npmjs.com/) >= 10.0.0
### Installation

View File

@ -1,15 +1,15 @@
FROM node:20.6.1 AS builder-dependencies
FROM node:20.9.0 AS builder-dependencies
WORKDIR /usr/src/application
COPY ./package*.json ./
RUN npm clean-install
FROM node:20.6.1 AS builder
FROM node:20.9.0 AS builder
WORKDIR /usr/src/application
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
COPY ./ ./
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian11:latest AS runner
FROM gcr.io/distroless/nodejs20-debian12:latest AS runner
WORKDIR /usr/src/application
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@
}
.prose [id]::before {
content: '';
content: "";
display: block;
height: 90px;
margin-top: -90px;
@ -39,9 +39,9 @@
.prose code {
color: #ce9178;
}
.prose :where(code):not(:where([class~='not-prose'] *))::before,
.prose :where(code):not(:where([class~='not-prose'] *))::after {
content: '';
.prose :where(code):not(:where([class~="not-prose"] *))::before,
.prose :where(code):not(:where([class~="not-prose"] *))::after {
content: "";
}
.shiki {
white-space: pre-wrap !important;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
---
title: '🧼 Clean Code'
title: "🧼 Clean Code"
description: 'What is "Clean Code", what are "Design Patterns", and why is it so important today? Tips and tricks to make your code more readable and maintainable in the long term.'
isPublished: true
publishedOn: '2022-02-23T08:00:18.758Z'
publishedOn: "2022-02-23T08:00:18.758Z"
---
Hello! 👋
@ -110,7 +110,7 @@ const transaction = charge(user, subscription)
```typescript
interface Car {
carModel: string
carColor: 'red' | 'blue' | 'yellow'
carColor: "red" | "blue" | "yellow"
}
const printCar = (car: Car): void => {
console.log(`${car.carModel} (${car.carColor})`)
@ -122,7 +122,7 @@ const printCar = (car: Car): void => {
```typescript
interface Car {
model: string
color: 'red' | 'blue' | 'yellow'
color: "red" | "blue" | "yellow"
}
const printCar = (car: Car): void => {
console.log(`${car.model} (${car.color})`)
@ -170,17 +170,17 @@ We have to keep it as simple as possible, not to implement features that are not
### Example (bad way)
```typescript
import fs from 'node:fs'
import path from 'node:path'
import fs from "node:fs"
import path from "node:path"
const createFile = async (
name: string,
isTemporary: boolean = false
isTemporary: boolean = false,
): Promise<void> => {
if (isTemporary) {
return await fs.promises.writeFile(path.join('temporary', name), '')
return await fs.promises.writeFile(path.join("temporary", name), "")
}
return await fs.promises.writeFile(name, '')
return await fs.promises.writeFile(name, "")
}
```
@ -189,15 +189,15 @@ const createFile = async (
### Example (good way)
```typescript
import fs from 'node:fs'
import path from 'node:path'
import fs from "node:fs"
import path from "node:path"
const createFile = async (name: string): Promise<void> => {
await fs.promises.writeFile(name, '')
await fs.promises.writeFile(name, "")
}
const createTemporaryFile = async (name: string): Promise<void> => {
await createFile(path.join('temporary', name))
await createFile(path.join("temporary", name))
}
```

View File

@ -1,8 +1,8 @@
---
title: '🗓️ Git version control: Ultimate Guide'
description: 'What is `git`, what are the most used commands, best practices, and tips and tricks. The Ultimate guide to master `git` in your daily workflow.'
title: "🗓️ Git version control: Ultimate Guide"
description: "What is `git`, what are the most used commands, best practices, and tips and tricks. The Ultimate guide to master `git` in your daily workflow."
isPublished: true
publishedOn: '2022-10-27T14:33:07.465Z'
publishedOn: "2022-10-27T14:33:07.465Z"
---
Hello! 👋

View File

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

View File

@ -1,8 +1,8 @@
---
title: '❌ Mistakes I made as a junior developer'
description: 'Here are mistakes I made when I started, to prevent you from making the same mistakes.'
title: "❌ Mistakes I made as a junior developer"
description: "Here are mistakes I made when I started, to prevent you from making the same mistakes."
isPublished: true
publishedOn: '2022-03-14T07:42:52.989Z'
publishedOn: "2022-03-14T07:42:52.989Z"
---
Hello! 👋

View File

@ -1,8 +1,8 @@
---
title: '🧠 Programming Challenges'
description: 'What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation.'
title: "🧠 Programming Challenges"
description: "What are Programming Challenges and Competitive Programming and an introduction to Time/Space Complexity with Big O Notation."
isPublished: true
publishedOn: '2023-05-21T10:20:18.837Z'
publishedOn: "2023-05-21T10:20:18.837Z"
---
Hello! 👋

View File

@ -1,8 +1,8 @@
---
title: '🟢 Thream v1.0.0'
description: 'Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.'
title: "🟢 Thream v1.0.0"
description: "Your open source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun."
isPublished: true
publishedOn: '2022-04-11T10:24:55.206Z'
publishedOn: "2022-04-11T10:24:55.206Z"
---
Hello! 👋

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import htmlParser from 'html-react-parser'
import htmlParser from "html-react-parser"
export interface InterestParagraphProps {
title: string
@ -6,14 +6,14 @@ export interface InterestParagraphProps {
}
export const InterestParagraph = (
props: InterestParagraphProps
props: InterestParagraphProps,
): JSX.Element => {
const { title, description } = props
return (
<>
<p className='my-6 text-center text-gray dark:text-gray-dark'>
<strong className='text-lg font-semibold text-yellow dark:text-yellow-dark'>
<p className="my-6 text-center text-gray dark:text-gray-dark">
<strong className="text-lg font-semibold text-yellow dark:text-yellow-dark">
{title}
</strong>
<br />

View File

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

View File

@ -1,18 +1,18 @@
import { faCode, faMicrochip } from '@fortawesome/free-solid-svg-icons'
import { faGit } from '@fortawesome/free-brands-svg-icons'
import { faCode, faMicrochip } from "@fortawesome/free-solid-svg-icons"
import { faGit } from "@fortawesome/free-brands-svg-icons"
import { InterestItem } from './InterestItem'
import { InterestItem } from "./InterestItem"
export const InterestsList = (): JSX.Element => {
return (
<div className='my-4 flex justify-center'>
<ul className='m-0 flex w-96 list-none justify-around p-0'>
<InterestItem title='Developer Full Stack' fontAwesomeIcon={faCode} />
<div className="my-4 flex justify-center">
<ul className="m-0 flex w-96 list-none justify-around p-0">
<InterestItem title="Developer Full Stack" fontAwesomeIcon={faCode} />
<InterestItem
title='Passionate about High-Tech'
title="Passionate about High-Tech"
fontAwesomeIcon={faMicrochip}
/>
<InterestItem title='Open-Source enthusiast' fontAwesomeIcon={faGit} />
<InterestItem title="Open-Source enthusiast" fontAwesomeIcon={faGit} />
</ul>
</div>
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,18 @@
import { getI18n } from '@/i18n/i18n.server'
import { getI18n } from "@/i18n/i18n.server"
export const ProfileDescriptionBottom = (): JSX.Element => {
const i18n = getI18n()
return (
<p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'>
{i18n.translate('home.about.description-bottom')}
{i18n.locale === 'fr-FR' ? (
<p className="mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark">
{i18n.translate("home.about.description-bottom")}
{i18n.locale === "fr-FR" ? (
<>
<br />
<br />
<a
href='/curriculum-vitae/index.html'
className='text-yellow hover:underline dark:text-yellow-dark'
href="/curriculum-vitae/index.html"
className="text-yellow hover:underline dark:text-yellow-dark"
>
Curriculum vitæ
</a>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,9 +12,9 @@
"modern-normalize": "2.0.0"
},
"devDependencies": {
"@types/node": "20.6.2",
"@types/node": "20.8.7",
"date-and-time": "3.0.3",
"vite": "4.4.9",
"vite": "4.5.0",
"vite-plugin-html": "3.2.0"
}
},
@ -419,9 +419,9 @@
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@ -477,10 +477,13 @@
}
},
"node_modules/@types/node": {
"version": "20.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz",
"integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==",
"dev": true
"version": "20.8.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz",
"integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.25.1"
}
},
"node_modules/acorn": {
"version": "8.10.0",
@ -1200,9 +1203,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.29",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
"integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"funding": [
{
@ -1267,9 +1270,9 @@
}
},
"node_modules/rollup": {
"version": "3.29.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.2.tgz",
"integrity": "sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==",
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@ -1346,9 +1349,9 @@
}
},
"node_modules/terser": {
"version": "5.19.4",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz",
"integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==",
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.22.0.tgz",
"integrity": "sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@ -1387,6 +1390,12 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
},
"node_modules/undici-types": {
"version": "5.25.3",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==",
"dev": true
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@ -1397,9 +1406,9 @@
}
},
"node_modules/vite": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",

View File

@ -13,9 +13,9 @@
"modern-normalize": "2.0.0"
},
"devDependencies": {
"@types/node": "20.6.2",
"@types/node": "20.8.7",
"date-and-time": "3.0.3",
"vite": "4.4.9",
"vite": "4.5.0",
"vite-plugin-html": "3.2.0"
}
}

View File

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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
import { defineConfig } from 'cypress'
import { defineConfig } from "cypress"
export default defineConfig({
fixturesFolder: false,
video: false,
screenshotOnRunFailure: false,
e2e: {
baseUrl: 'http://127.0.0.1:3000',
supportFile: false
baseUrl: "http://127.0.0.1:3000",
supportFile: false,
},
component: {
devServer: {
framework: 'next',
bundler: 'webpack'
}
}
framework: "next",
bundler: "webpack",
},
},
})

View File

@ -1,16 +1,16 @@
import { getAge } from '@/utils/getAge'
import { getAge } from "@/utils/getAge"
describe('utils/getAge', () => {
it('should calculate the right age of a person', () => {
cy.clock(new Date('2018-03-20')).then(() => {
const birthDate = new Date('1980-02-20')
describe("utils/getAge", () => {
it("should calculate the right age of a person", () => {
cy.clock(new Date("2018-03-20")).then(() => {
const birthDate = new Date("1980-02-20")
expect(getAge(birthDate)).equal(38)
})
})
it('should calculate the right age of a person (taking into account the months)', () => {
cy.clock(new Date('2018-03-20')).then(() => {
const birthDate = new Date('1980-07-20')
it("should calculate the right age of a person (taking into account the months)", () => {
cy.clock(new Date("2018-03-20")).then(() => {
const birthDate = new Date("1980-07-20")
expect(getAge(birthDate)).equal(37)
})
})

View File

@ -1,62 +1,62 @@
describe('Common > Header', () => {
describe("Common > Header", () => {
beforeEach(() => {
return cy.visit('/')
return cy.visit("/")
})
it('should redirect to /blog on click of the blog link', () => {
cy.get('[data-cy=header-blog-link]')
it("should redirect to /blog on click of the blog link", () => {
cy.get("[data-cy=header-blog-link]")
.click()
.location('pathname')
.should('eq', '/blog')
.location("pathname")
.should("eq", "/blog")
})
it('should always be visible (sticky header)', () => {
cy.scrollTo('bottom').get('header').should('be.visible')
it("should always be visible (sticky header)", () => {
cy.scrollTo("bottom").get("header").should("be.visible")
})
describe('Switch theme color (dark/light)', () => {
it('should switch theme from `dark` (default) to `light`', () => {
cy.get('[data-cy=switch-theme-dark]').should('be.visible')
cy.get('[data-cy=switch-theme-light]').should('not.be.visible')
cy.get('body').should(
'not.have.css',
'background-color',
'rgb(255, 255, 255)'
describe("Switch theme color (dark/light)", () => {
it("should switch theme from `dark` (default) to `light`", () => {
cy.get("[data-cy=switch-theme-dark]").should("be.visible")
cy.get("[data-cy=switch-theme-light]").should("not.be.visible")
cy.get("body").should(
"not.have.css",
"background-color",
"rgb(255, 255, 255)",
)
cy.get('[data-cy=switch-theme-click]').click()
cy.get("[data-cy=switch-theme-click]").click()
cy.get('[data-cy=switch-theme-dark]').should('not.be.visible')
cy.get('[data-cy=switch-theme-light]').should('be.visible')
cy.get('body').should(
'have.css',
'background-color',
'rgb(255, 255, 255)'
cy.get("[data-cy=switch-theme-dark]").should("not.be.visible")
cy.get("[data-cy=switch-theme-light]").should("be.visible")
cy.get("body").should(
"have.css",
"background-color",
"rgb(255, 255, 255)",
)
})
})
describe('Switch Language', () => {
it('should switch locale from English (default) to French', () => {
cy.get('h1').contains('Théo LUDWIG')
cy.get('[data-cy=locale-flag-text]').contains('English')
cy.get('[data-cy=locales-list]').should('not.be.visible')
cy.get('[data-cy=locale-click]').click()
cy.get('[data-cy=locales-list]').should('be.visible')
cy.get('[data-cy=locales-list] > li:first-child')
.contains('French')
describe("Switch Language", () => {
it("should switch locale from English (default) to French", () => {
cy.get("h1").contains("Théo LUDWIG")
cy.get("[data-cy=locale-flag-text]").contains("English")
cy.get("[data-cy=locales-list]").should("not.be.visible")
cy.get("[data-cy=locale-click]").click()
cy.get("[data-cy=locales-list]").should("be.visible")
cy.get("[data-cy=locales-list] > li:first-child")
.contains("French")
.click()
cy.get('[data-cy=locales-list]').should('not.be.visible')
cy.get('[data-cy=locale-flag-text]').contains('French')
cy.get('h1').contains('Théo LUDWIG')
cy.get("[data-cy=locales-list]").should("not.be.visible")
cy.get("[data-cy=locale-flag-text]").contains("French")
cy.get("h1").contains("Théo LUDWIG")
})
it('should close the locale list menu when clicking outside', () => {
cy.get('[data-cy=locales-list]').should('not.be.visible')
cy.get('[data-cy=locale-click]').click()
cy.get('[data-cy=locales-list]').should('be.visible')
cy.get('h1').click()
cy.get('[data-cy=locales-list]').should('not.be.visible')
it("should close the locale list menu when clicking outside", () => {
cy.get("[data-cy=locales-list]").should("not.be.visible")
cy.get("[data-cy=locale-click]").click()
cy.get("[data-cy=locales-list]").should("be.visible")
cy.get("h1").click()
cy.get("[data-cy=locales-list]").should("not.be.visible")
})
})
})

View File

@ -1,10 +1,10 @@
describe('Page /404', () => {
describe("Page /404", () => {
beforeEach(() => {
return cy.visit('/404', { failOnStatusCode: false })
return cy.visit("/404", { failOnStatusCode: false })
})
it('should display the statusCode of 404', () => {
cy.get('[data-cy=status-code]').contains('404')
it("should display the statusCode of 404", () => {
cy.get("[data-cy=status-code]").contains("404")
})
})

View File

@ -1,14 +1,14 @@
describe('Page /blog/[slug]', () => {
it('should displays the first blog post (`hello-world`)', () => {
cy.visit('/blog/hello-world')
cy.get('[data-cy=locale-flag-text]').should('not.exist')
cy.get('h1').should('have.text', '👋 Hello, world!')
cy.get('.prose a:visible').should('have.attr', 'target', '_blank')
describe("Page /blog/[slug]", () => {
it("should displays the first blog post (`hello-world`)", () => {
cy.visit("/blog/hello-world")
cy.get("[data-cy=locale-flag-text]").should("not.exist")
cy.get("h1").should("have.text", "👋 Hello, world!")
cy.get(".prose a:visible").should("have.attr", "target", "_blank")
})
it("should redirect to /404 if the blog post doesn't exist", () => {
cy.visit('/blog/random-blog-post-not-found', { failOnStatusCode: false })
cy.get('[data-cy=status-code]').contains('404')
cy.visit("/blog/random-blog-post-not-found", { failOnStatusCode: false })
cy.get("[data-cy=status-code]").contains("404")
})
})

View File

@ -1,23 +1,23 @@
describe('Page /blog', () => {
it('should displays the blog posts sorted from newest to oldest', () => {
cy.visit('/blog')
cy.get('[data-cy=blog-posts] [data-cy=blog-post-title]')
describe("Page /blog", () => {
it("should displays the blog posts sorted from newest to oldest", () => {
cy.visit("/blog")
cy.get("[data-cy=blog-posts] [data-cy=blog-post-title]")
.last()
.should('have.text', '👋 Hello, world!')
cy.get('[data-cy=blog-posts] [data-cy=blog-post-description]')
.should("have.text", "👋 Hello, world!")
cy.get("[data-cy=blog-posts] [data-cy=blog-post-description]")
.last()
.should(
'have.text',
'First post of the blog, introduction and explanation of how this blog is made.'
"have.text",
"First post of the blog, introduction and explanation of how this blog is made.",
)
})
it('should redirect the user to the right blog post', () => {
cy.visit('/blog')
cy.get('[data-cy=hello-world]')
it("should redirect the user to the right blog post", () => {
cy.visit("/blog")
cy.get("[data-cy=hello-world]")
.click()
.location('pathname')
.should('eq', '/blog/hello-world')
.location("pathname")
.should("eq", "/blog/hello-world")
})
})

View File

@ -1,16 +1,16 @@
describe('Page /', () => {
describe("Page /", () => {
beforeEach(() => {
return cy.visit('/')
return cy.visit("/")
})
it('should reveals the sections while scrolling except the about section', () => {
const sectionsReveals = ['#interests', '#skills', '#portfolio']
cy.get('#about').should('be.visible')
it("should reveals the sections while scrolling except the about section", () => {
const sectionsReveals = ["#interests", "#skills", "#portfolio"]
cy.get("#about").should("be.visible")
for (const section of sectionsReveals) {
cy.get(section)
.should('not.be.visible')
.should("not.be.visible")
.scrollIntoView()
.should('be.visible')
.should("be.visible")
}
})
})

View File

@ -1,7 +1,7 @@
import { mount } from 'cypress/react'
import { mount } from "cypress/react"
import './commands'
import '../../app/globals.css'
import "./commands"
import "../../app/globals.css"
declare global {
namespace Cypress {
@ -11,4 +11,4 @@ declare global {
}
}
Cypress.Commands.add('mount', mount)
Cypress.Commands.add("mount", mount)

View File

@ -1,12 +1,12 @@
import UniversalCookie from 'universal-cookie'
import type { I18n } from 'i18n-js'
import UniversalCookie from "universal-cookie"
import type { I18n } from "i18n-js"
import type { CookiesStore } from '@/utils/constants'
import type { CookiesStore } from "@/utils/constants"
import { i18n } from './i18n'
import { i18n } from "./i18n"
export const useI18n = (cookiesStore: CookiesStore): I18n => {
const universalCookie = new UniversalCookie(cookiesStore)
i18n.locale = universalCookie.get('locale') ?? i18n.defaultLocale
i18n.locale = universalCookie.get("locale") ?? i18n.defaultLocale
return i18n
}

View File

@ -1,21 +1,21 @@
'use server'
"use server"
import { cookies } from 'next/headers'
import type { I18n } from 'i18n-js'
import { cookies } from "next/headers"
import type { I18n } from "i18n-js"
import type { Locale } from '@/utils/constants'
import { COOKIE_MAX_AGE } from '@/utils/constants'
import type { Locale } from "@/utils/constants"
import { COOKIE_MAX_AGE } from "@/utils/constants"
import { i18n } from './i18n'
import { i18n } from "./i18n"
export const setLocale = (locale: Locale): void => {
cookies().set('locale', locale, {
path: '/',
maxAge: COOKIE_MAX_AGE
cookies().set("locale", locale, {
path: "/",
maxAge: COOKIE_MAX_AGE,
})
}
export const getI18n = (): I18n => {
i18n.locale = cookies().get('locale')?.value ?? i18n.defaultLocale
i18n.locale = cookies().get("locale")?.value ?? i18n.defaultLocale
return i18n
}

View File

@ -1,30 +1,30 @@
import { I18n } from 'i18n-js'
import { I18n } from "i18n-js"
import type { Locale } from '@/utils/constants'
import { DEFAULT_LOCALE, LOCALES } from '@/utils/constants'
import type { Locale } from "@/utils/constants"
import { DEFAULT_LOCALE, LOCALES } from "@/utils/constants"
import commonEnglish from './translations/en-US/common.json'
import errorsEnglish from './translations/en-US/errors.json'
import homeEnglish from './translations/en-US/home.json'
import commonFrench from './translations/fr-FR/common.json'
import errorsFrench from './translations/fr-FR/errors.json'
import homeFrench from './translations/fr-FR/home.json'
import commonEnglish from "./translations/en-US/common.json"
import errorsEnglish from "./translations/en-US/errors.json"
import homeEnglish from "./translations/en-US/home.json"
import commonFrench from "./translations/fr-FR/common.json"
import errorsFrench from "./translations/fr-FR/errors.json"
import homeFrench from "./translations/fr-FR/home.json"
const translations = {
'en-US': {
"en-US": {
common: commonEnglish,
errors: errorsEnglish,
home: homeEnglish
home: homeEnglish,
},
'fr-FR': {
"fr-FR": {
common: commonFrench,
errors: errorsFrench,
home: homeFrench
}
home: homeFrench,
},
} satisfies Record<Locale, Record<string, unknown>>
export const i18n = new I18n(translations, {
defaultLocale: DEFAULT_LOCALE,
availableLocales: LOCALES.slice(),
enableFallback: true
enableFallback: true,
})

View File

@ -1,43 +1,43 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { match } from "@formatjs/intl-localematcher"
import Negotiator from "negotiator"
import type { Locale, Theme } from '@/utils/constants'
import type { Locale, Theme } from "@/utils/constants"
import {
COOKIE_MAX_AGE,
DEFAULT_LOCALE,
DEFAULT_THEME,
LOCALES,
THEMES
} from '@/utils/constants'
THEMES,
} from "@/utils/constants"
export const middleware = (request: NextRequest): NextResponse => {
const response = NextResponse.next()
let locale = request.cookies.get('locale')?.value
let locale = request.cookies.get("locale")?.value
if (locale == null || !LOCALES.includes(locale as Locale)) {
try {
const headers = {
'accept-language':
request.headers.get('accept-language') ?? DEFAULT_LOCALE
"accept-language":
request.headers.get("accept-language") ?? DEFAULT_LOCALE,
}
const languages = new Negotiator({ headers }).languages()
locale = match(languages, LOCALES.slice(), DEFAULT_LOCALE)
} catch {
locale = DEFAULT_LOCALE
}
response.cookies.set('locale', locale, {
path: '/',
maxAge: COOKIE_MAX_AGE
response.cookies.set("locale", locale, {
path: "/",
maxAge: COOKIE_MAX_AGE,
})
}
const theme = request.cookies.get('theme')?.value
const theme = request.cookies.get("theme")?.value
if (theme == null || !THEMES.includes(theme as Theme)) {
response.cookies.set('theme', DEFAULT_THEME, {
path: '/',
maxAge: COOKIE_MAX_AGE
response.cookies.set("theme", DEFAULT_THEME, {
path: "/",
maxAge: COOKIE_MAX_AGE,
})
}
@ -53,6 +53,6 @@ export const config = {
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)'
]
"/((?!api|_next/static|_next/image|favicon.ico).*)",
],
}

View File

@ -1,13 +1,13 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
output: 'standalone',
output: "standalone",
eslint: {
ignoreDuringBuilds: true
ignoreDuringBuilds: true,
},
experimental: {
serverActions: true
}
serverActions: true,
},
}
module.exports = nextConfig

2244
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
},
"engines": {
"node": ">=20.0.0",
"npm": ">=9.0.0"
"npm": ">=10.0.0"
},
"scripts": {
"dev": "next dev",
@ -29,7 +29,7 @@
"postinstall": "husky install"
},
"dependencies": {
"@fontsource/montserrat": "5.0.8",
"@fontsource/montserrat": "5.0.15",
"@formatjs/intl-localematcher": "0.4.2",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",
@ -39,11 +39,11 @@
"clsx": "2.0.0",
"date-and-time": "3.0.3",
"gray-matter": "4.0.3",
"html-react-parser": "4.2.2",
"html-react-parser": "4.2.9",
"i18n-js": "4.3.2",
"katex": "0.16.8",
"katex": "0.16.9",
"negotiator": "0.6.3",
"next": "13.4.19",
"next": "13.5.6",
"next-mdx-remote": "4.4.1",
"react": "18.2.0",
"react-dom": "18.2.0",
@ -53,47 +53,47 @@
"rehype-slug": "5.1.0",
"remark-gfm": "3.0.1",
"remark-math": "5.1.1",
"sharp": "0.32.5",
"shiki": "0.14.4",
"sharp": "0.32.6",
"shiki": "0.14.5",
"unified": "10.1.2",
"unist-util-visit": "5.0.0",
"universal-cookie": "6.1.1"
},
"devDependencies": {
"@commitlint/cli": "17.7.1",
"@commitlint/config-conventional": "17.7.0",
"@saithodev/semantic-release-backmerge": "3.2.0",
"@commitlint/cli": "18.0.0",
"@commitlint/config-conventional": "18.0.0",
"@saithodev/semantic-release-backmerge": "3.2.1",
"@semantic-release/git": "10.0.1",
"@tailwindcss/typography": "0.5.10",
"@tsconfig/strictest": "2.0.2",
"@types/negotiator": "0.6.1",
"@types/node": "20.6.2",
"@types/react": "18.2.22",
"@types/unist": "3.0.0",
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"autoprefixer": "10.4.15",
"@types/negotiator": "0.6.2",
"@types/node": "20.8.7",
"@types/react": "18.2.31",
"@types/unist": "3.0.1",
"@typescript-eslint/eslint-plugin": "6.9.0",
"@typescript-eslint/parser": "6.9.0",
"autoprefixer": "10.4.16",
"curriculum-vitae": "file:./curriculum-vitae",
"cypress": "13.2.0",
"cypress": "13.3.2",
"editorconfig-checker": "5.1.1",
"eslint": "8.49.0",
"eslint-config-conventions": "11.0.1",
"eslint-config-next": "13.4.19",
"eslint": "8.52.0",
"eslint-config-conventions": "12.0.0",
"eslint-config-next": "13.5.6",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-prettier": "5.0.1",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "48.0.1",
"html-w3c-validator": "1.5.0",
"husky": "8.0.3",
"lint-staged": "14.0.1",
"lint-staged": "15.0.2",
"markdownlint-cli2": "0.10.0",
"markdownlint-rule-relative-links": "2.1.0",
"postcss": "8.4.29",
"postcss": "8.4.31",
"prettier": "3.0.3",
"prettier-plugin-tailwindcss": "0.5.4",
"prettier-plugin-tailwindcss": "0.5.6",
"semantic-release": "21.1.2",
"start-server-and-test": "2.0.0",
"start-server-and-test": "2.0.1",
"tailwindcss": "3.3.3",
"typescript": "5.2.2"
}

View File

@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
autoprefixer: {},
},
}

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