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

Compare commits

...

27 Commits

Author SHA1 Message Date
c7ad15a465 chore(release): 3.1.0 [skip ci] 2023-09-18 20:09:29 +00:00
f4a842efb5 build: downgrade semantic-release to v21.1.2
Ref: https://github.com/semantic-release/semantic-release/issues/2968
2023-09-18 22:08:04 +02:00
424c97019b fix: update dependencies to latest 2023-09-18 22:00:11 +02:00
c0508dc0b9 build: ignore ESLint errors for Production build
It improves performance when extra checking is not necessary.
2023-09-14 12:26:19 +02:00
f04d8a0c11 Revert "build: ignore ESLint and TypeScript errors for Production build"
This reverts commit fdab2a7ea8.
2023-09-14 12:19:26 +02:00
d29064745c feat: add IRCAD as work experience 2023-09-14 12:11:18 +02:00
95febe2a99 feat: add 3rd year of BUT Informatique as education 2023-09-14 12:01:20 +02:00
fdab2a7ea8 build: ignore ESLint and TypeScript errors for Production build
It improves performance when extra checking is not necessary.
2023-09-14 11:49:52 +02:00
35211fa279 fix: try/catch inside middleware when checking locale 2023-08-24 23:09:33 +02:00
137cceffa1 build(deps): update latest 2023-08-24 22:38:09 +02:00
f6bfc466de chore(release): 3.0.0 [skip ci] 2023-08-01 17:41:12 +00:00
e4cf714d95 test: fix styles import for unit tests 2023-08-01 19:39:09 +02:00
d3c86b2a26 chore: update Dockerfile 2023-08-01 19:34:58 +02:00
d2578abeec fix: loader improvements 2023-08-01 18:59:45 +02:00
e51e3bdc19 test: fix e2e tests + 500 error page 2023-08-01 18:18:16 +02:00
56520830e9 refactor: blog directory 2023-08-01 17:44:08 +02:00
2e0138194c refactor: avoid usage of React.FC to use JSX.Element (to stay consistent) 2023-08-01 17:22:09 +02:00
4b2e7bae90 feat: rewrite blog to Next.js v13 app directory
Improvement: Support light theme in code block
2023-08-01 17:07:19 +02:00
caa6a90418 refactor: implement light/dark themes using cookies 2023-08-01 14:11:46 +02:00
e82db952db docs: update interests 2023-08-01 13:15:03 +02:00
6b29ce9b15 feat: rewrite to Next.js v13 app directory
Improvements:
- Hide switch theme input (ugly little white square)
- i18n without subpath (e.g: /fr or /en), same url whatever the locale used
2023-07-31 19:06:46 +02:00
5640f1b434 build(deps): bump Node.js to 20.0.0 and npm to 9.0.0
BREAKING CHANGE: minimum supported Node.js >= 20.0.0 and npm >= 9.0.0
2023-07-30 19:03:36 +02:00
6d0dcb50a7 refactor: 'use client' when appropriate 2023-07-30 18:50:14 +02:00
70603f1444 chore: remove build error with Docker copy wrong node_modules 2023-07-30 18:27:15 +02:00
f42fdbfd0c chore: rename jsonresume-theme-custom to curriculum-vitae 2023-07-28 11:53:04 +02:00
6a3f335f9f fix(posts): update git blog post 2023-07-28 11:40:19 +02:00
f1509d0af1 chore: rename docker-compose.yml to compose.yaml
Ref: https://docs.docker.com/compose/compose-file/03-compose-file/
2023-07-28 11:38:34 +02:00
131 changed files with 6329 additions and 4506 deletions

View File

@ -1,6 +1,6 @@
{
"name": "theoludwig",
"dockerComposeFile": "./docker-compose.yml",
"dockerComposeFile": "./compose.yaml",
"service": "workspace",
"workspaceFolder": "/workspace",
"customizations": {

View File

@ -1,6 +1,6 @@
{
"extends": ["conventions", "next/core-web-vitals", "prettier"],
"plugins": ["prettier", "unicorn"],
"plugins": ["prettier"],
"parserOptions": {
"project": "./tsconfig.json"
},

View File

@ -10,12 +10,12 @@ jobs:
build:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.5.3'
- uses: 'actions/checkout@v4.0.0'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
uses: 'actions/setup-node@v3.8.1'
with:
node-version: '18.x'
node-version: '20.x'
cache: 'npm'
- name: 'Install dependencies'

View File

@ -10,12 +10,12 @@ jobs:
lint:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.5.3'
- uses: 'actions/checkout@v4.0.0'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
uses: 'actions/setup-node@v3.8.1'
with:
node-version: '18.x'
node-version: '20.x'
cache: 'npm'
- name: 'Install dependencies'
@ -37,6 +37,6 @@ jobs:
run: 'npm run lint:prettier'
- name: 'lint:dotenv'
uses: 'dotenv-linter/action-dotenv-linter@v2'
uses: 'dotenv-linter/action-dotenv-linter@v2.18.0'
with:
github_token: ${{ secrets.github_token }}

View File

@ -8,22 +8,22 @@ jobs:
release:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.5.3'
- uses: 'actions/checkout@v4.0.0'
with:
fetch-depth: 0
persist-credentials: false
- name: 'Import GPG key'
uses: 'crazy-max/ghaction-import-gpg@v5.3.0'
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.6.0'
uses: 'actions/setup-node@v3.8.1'
with:
node-version: '18.x'
node-version: '20.x'
cache: 'npm'
- name: 'Install dependencies'

View File

@ -10,12 +10,12 @@ jobs:
test-unit:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.5.3'
- uses: 'actions/checkout@v4.0.0'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
uses: 'actions/setup-node@v3.8.1'
with:
node-version: '18.x'
node-version: '20.x'
cache: 'npm'
- name: 'Install dependencies'
@ -27,12 +27,12 @@ jobs:
test-e2e:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.5.3'
- uses: 'actions/checkout@v4.0.0'
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3.6.0'
uses: 'actions/setup-node@v3.8.1'
with:
node-version: '18.x'
node-version: '20.x'
cache: 'npm'
- name: 'Install dependencies'

3
.gitignore vendored
View File

@ -12,9 +12,6 @@ out
build
dist
public/curriculum-vitae
# PWA
public/workbox-*.js
public/sw.js
# testing
coverage

View File

@ -2,7 +2,7 @@ image: 'gitpod/workspace-full'
tasks:
- before: 'cp .env.example .env'
init: 'npm install'
init: 'npm clean-install'
command: 'npm run dev'
ports:

View File

@ -1,8 +1,4 @@
{
"urls": [
"http://127.0.0.1:3000/",
"http://127.0.0.1:3000/blog",
"http://127.0.0.1:3000/blog/hello-world"
],
"urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/blog"],
"files": ["./public/curriculum-vitae/index.html"]
}

View File

@ -33,8 +33,8 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
### Prerequisites
- [Node.js](https://nodejs.org/) >= 16.0.0
- [npm](https://www.npmjs.com/) >= 8.0.0
- [Node.js](https://nodejs.org/) >= 20.0.0
- [npm](https://www.npmjs.com/) >= 9.0.0
### Installation
@ -49,7 +49,7 @@ cd theoludwig
cp .env.example .env
# Install
npm install
npm clean-install
```
### Local Development environment

View File

@ -1,21 +1,23 @@
FROM node:20.5.0 AS builder-dependencies
FROM node:20.6.1 AS builder-dependencies
WORKDIR /usr/src/application
COPY ./package*.json ./
RUN npm clean-install
FROM node:20.5.0 AS builder
FROM node:20.6.1 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/nodejs18-debian11:latest AS runner
FROM gcr.io/distroless/nodejs20-debian11:latest AS runner
WORKDIR /usr/src/application
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
COPY --from=builder /usr/src/application/.next/standalone ./
COPY --from=builder /usr/src/application/.next/static ./.next/static
COPY --from=builder /usr/src/application/public ./public
COPY --from=builder /usr/src/application/locales ./locales
COPY --from=builder /usr/src/application/i18n/translations ./i18n/translations
COPY --from=builder /usr/src/application/next.config.js ./next.config.js
CMD ["./server.js"]

View File

@ -25,7 +25,7 @@
"pronouns": "He/Him",
"birthDate": "31/03/2003",
"nationality": "Alsace, France",
"interests": ["Open-Source enthusiast", "Passionate about High-Tech"],
"interests": ["Developer Full Stack", "Open-Source enthusiast"],
"skills": {
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
"frontend": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"],

View File

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

44
app/blog/[slug]/page.tsx Normal file
View File

@ -0,0 +1,44 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import 'katex/dist/katex.min.css'
import { getBlogPostBySlug } from '@/blog/blog'
import { BlogPost } from '@/blog/BlogPost'
interface BlogPostPageProps {
params: {
slug: string
}
}
export const generateMetadata = async (
props: BlogPostPageProps
): Promise<Metadata> => {
const blogPost = await getBlogPostBySlug(props.params.slug)
if (blogPost == null) {
return notFound()
}
const title = `${blogPost.frontmatter.title} | Théo LUDWIG`
const description = blogPost.frontmatter.description
return {
title,
description,
openGraph: {
title,
description
},
twitter: {
title,
description
}
}
}
const BlogPostPage = async (props: BlogPostPageProps): Promise<JSX.Element> => {
const { params } = props
return <BlogPost slug={params.slug} />
}
export default BlogPostPage

11
app/blog/loading.tsx Normal file
View File

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

40
app/blog/page.tsx Normal file
View File

@ -0,0 +1,40 @@
import { Suspense } from 'react'
import type { Metadata } from 'next'
import { BlogPosts } from '@/blog/BlogPosts'
import { Loader } from '@/components/design/Loader'
const title = 'Blog | Théo LUDWIG'
const description =
'The latest news about my journey of learning computer science.'
export const metadata: Metadata = {
title,
description,
openGraph: {
title,
description
},
twitter: {
title,
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'>
{description}
</p>
</div>
<Suspense fallback={<Loader className='mt-8' />}>
<BlogPosts />
</Suspense>
</main>
)
}
export default BlogPage

32
app/error.tsx Normal file
View File

@ -0,0 +1,32 @@
'use client'
import { useEffect } from 'react'
export interface ErrorHandlingProps {
error: Error
}
const ErrorHandling = (props: ErrorHandlingProps): JSX.Element => {
const { error } = props
useEffect(() => {
console.error(error)
}, [error])
return (
<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'
>
500
</span>
</h1>
<p className='text-center text-lg'>Server error</p>
</main>
)
}
export default ErrorHandling

80
app/layout.tsx Normal file
View File

@ -0,0 +1,80 @@
import type { Metadata } from 'next'
import classNames from 'clsx'
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'
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'
export const metadata: Metadata = {
title,
description,
metadataBase: url,
openGraph: {
title,
description,
url,
siteName: title,
images: [
{
url: image,
width: 96,
height: 96
}
],
locale,
type: 'website'
},
icons: {
icon: '/images/icon-96x96.png'
},
twitter: {
card: 'summary',
title,
description,
images: [image]
}
}
interface RootLayoutProps {
children: React.ReactNode
}
const RootLayout = (props: RootLayoutProps): JSX.Element => {
const { children } = props
const i18n = getI18n()
const theme = getTheme()
return (
<html
lang={i18n.locale}
className={classNames({
dark: theme === 'dark',
light: theme === 'light'
})}
style={{
colorScheme: theme
}}
>
<body className='bg-white font-headline text-black dark:bg-black dark:text-white flex flex-col min-h-screen'>
<Header />
{children}
<Footer />
</body>
</html>
)
}
export default RootLayout

11
app/loading.tsx Normal file
View File

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

32
app/not-found.tsx Normal file
View File

@ -0,0 +1,32 @@
import Link from 'next/link'
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')}{' '}
<span
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')}{' '}
<Link
href='/'
className='text-yellow hover:underline dark:text-yellow-dark'
>
{i18n.translate('errors.return-to-home-page')}
</Link>
</p>
</main>
)
}
export default NotFound

59
app/page.tsx Normal file
View File

@ -0,0 +1,59 @@
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'>
<Profile />
<SocialMediaList />
</Section>
<RevealFade>
<Section
id='interests'
heading={i18n.translate('home.interests.title')}
>
<Interests />
</Section>
</RevealFade>
<RevealFade>
<Section
id='skills'
heading={i18n.translate('home.skills.title')}
withoutShadowContainer
>
<Skills />
</Section>
</RevealFade>
<RevealFade>
<Section
id='portfolio'
heading={i18n.translate('home.portfolio.title')}
withoutShadowContainer
>
<Portfolio />
</Section>
</RevealFade>
<RevealFade>
<Section id='open-source' heading='Open source' withoutShadowContainer>
<OpenSource />
</Section>
</RevealFade>
</main>
)
}
export default HomePage

35
blog/BlogPost.tsx Normal file
View File

@ -0,0 +1,35 @@
import { notFound } from 'next/navigation'
import date from 'date-and-time'
import 'katex/dist/katex.min.css'
import { getBlogPostBySlug } from '@/blog/blog'
import { BlogPostContent } from '@/blog/BlogPostContent'
export interface BlogPostProps {
slug: string
}
export const BlogPost = async (props: BlogPostProps): Promise<JSX.Element> => {
const { slug } = props
const blogPost = await getBlogPostBySlug(slug)
if (blogPost == null) {
return notFound()
}
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'>
{date.format(
new Date(blogPost.frontmatter.publishedOn),
'DD/MM/YYYY'
)}
</p>
</div>
<BlogPostContent content={blogPost.content} />
</main>
)
}

33
blog/BlogPostComments.tsx Normal file
View File

@ -0,0 +1,33 @@
'use client'
import Giscus from '@giscus/react'
import { useTheme } from '@/theme/theme.client'
import type { CookiesStore } from '@/utils/constants'
interface BlogPostCommentsProps {
cookiesStore: CookiesStore
}
export const BlogPostComments = (props: BlogPostCommentsProps): JSX.Element => {
const { cookiesStore } = props
const theme = useTheme(cookiesStore)
return (
<Giscus
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'
/>
)
}

111
blog/BlogPostContent.tsx Normal file
View File

@ -0,0 +1,111 @@
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 { 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
return (
<h2 {...props} className='group'>
<Link
href={`#${id}`}
className='invisible !text-black group-hover:visible dark:!text-white'
>
<FontAwesomeIcon className='mr-2 inline h-4 w-4' icon={faLink} />
</Link>
{children}
</h2>
)
}
export interface BlogPostContentProps {
content: string
}
export const BlogPostContent = async (
props: BlogPostContentProps
): Promise<JSX.Element> => {
const { content } = props
const cookiesStore = cookies()
const theme = getTheme()
const highlighter = await getHighlighter({
theme: `${theme}-plus`
})
return (
<div className='prose mb-10'>
<div className='px-8'>
<MDXRemote
source={content}
options={{
mdxOptions: {
remarkPlugins: [
remarkGfm,
[remarkSyntaxHighlightingPlugin, { highlighter }],
remarkMath
],
rehypePlugins: [
rehypeSlug,
[rehypeRaw, { passThrough: nodeTypes }],
rehypeKatex
]
}
}}
components={{
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
img: (properties) => {
const { src = '', alt = 'Blog Image' } = properties
const source = src.replace('../../public/', '/')
return (
<span className='flex flex-col items-center justify-center'>
<Image
src={source}
alt={alt}
width={1000}
height={1000}
className='h-auto w-auto'
/>
</span>
)
},
a: (props) => {
const { href = '' } = props
if (href.startsWith('#')) {
return <a {...props} />
}
return <a target='_blank' rel='noopener noreferrer' {...props} />
}
}}
/>
<BlogPostComments cookiesStore={cookiesStore.toString()} />
</div>
</div>
)
}

42
blog/BlogPosts.tsx Normal file
View File

@ -0,0 +1,42 @@
import Link from 'next/link'
import date from 'date-and-time'
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'>
{posts.map((post, index) => {
const postPublishedOn = date.format(
new Date(post.frontmatter.publishedOn),
'DD/MM/YYYY'
)
return (
<Link
href={`/blog/${post.slug}`}
key={index}
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'>
{post.frontmatter.title}
</h2>
<p data-cy='blog-post-date' className='mt-2'>
{postPublishedOn}
</p>
<p data-cy='blog-post-description' className='mt-3'>
{post.frontmatter.description}
</p>
</ShadowContainer>
</Link>
)
})}
</div>
</div>
)
}

65
blog/blog.ts Normal file
View File

@ -0,0 +1,65 @@
import fs from 'node:fs'
import path from 'node:path'
import { cache } from 'react'
import matter from 'gray-matter'
export const BLOG_POSTS_PATH = path.join(process.cwd(), 'blog', 'posts')
export interface FrontMatter {
title: string
description: string
isPublished: boolean
publishedOn: string
}
export interface BlogPost {
frontmatter: FrontMatter
slug: string
content: string
}
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('.')
if (slug == null || extension == null) {
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'
})
const { data, content } = matter(blogPostContent) as unknown as {
data: FrontMatter
content: string
}
const date = new Date(data.publishedOn)
return {
slug,
content,
frontmatter: data,
time: date.getTime()
}
})
)
const blogPostsSortedByPublicationDate = blogPostsWithTime
.filter((post) => {
return post.frontmatter.isPublished
})
.sort((a, b) => {
return b.time - a.time
})
return blogPostsSortedByPublicationDate
})
export const getBlogPostBySlug = cache(
async (slug: string): Promise<BlogPost | undefined> => {
const blogPosts = await getBlogPosts()
const blogPost = blogPosts.find((blogPost) => {
return blogPost.slug === slug && blogPost.frontmatter.isPublished
})
return blogPost
}
)

View File

@ -122,6 +122,11 @@ git checkout <branch>
# Merge a branch into the current branch
git merge <branch>
# Note: Merge creates a "Merge commit" when the base branch and the branch to merge have diverged (they have different commits).
# To avoid creating a "Merge commit", we can use rebase instead of merge.
git rebase --interactive <branch-to-rebase-on>
# Combine multiple commits of a branch into one for a merge
git merge --squash <branch>
@ -145,6 +150,13 @@ git reset --soft <branch>
# Apply the changes introduced by some existing commits
# (by first being on the branch where you want to apply the commit)
git cherry-pick <commit>
# To list all commits that differ between two branches
git log <branch1>..<branch2> # commits in branch2 that are not in branch1 (branch2 ahead of branch1, branch2 behind branch1)
git log <branch2>..<branch1> # commits in branch1 that are not in branch2 (branch1 ahead of branch2, branch1 behind branch2)
# Summary of commit authors across all branches, excluding merge commits.
git shortlog --summary --numbered --all --no-merges
```
## `.gitignore` file
@ -194,7 +206,7 @@ As we have seen in the [Get started with `git` and `.gitconfig` config file](#ge
That means that **anyone can create a commit with any name and email address and claim to be whoever they want** when they create a commit.
To avoid this, you can sign your commits with a <abbr title="GNU Privacy Guard">[GPG](https://gnupg.org/)</abbr> key.
To avoid this, you can sign your commits with a [GNU Privacy Guard](https://gnupg.org/) (<abbr>gpg</abbr>) key.
You can find more information about signing commits in the [official documentation](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work).

View File

@ -216,7 +216,7 @@ $$
#### Complexity Classes (from fastest to slowest)
![Big O Notation](../public/images/posts/programming-challenges/big-o-chart-notations.webp)
![Big O Notation](../../public/images/posts/programming-challenges/big-o-chart-notations.webp)
Here is a list of classes of functions that are commonly encountered when analyzing the running time of an algorithm.

View File

@ -21,7 +21,7 @@ The source code is available on [GitHub](https://github.com/Thream).
The idea is that a user can create an account to authenticate with an email address, and a password, or directly use an account from another platform (currently supported: Google, GitHub, Discord). Once the user is authenticated, he/she can create and join "guilds", in other words communities, in order to discuss with other people in several channels to group discussions talking about the same subject.
![The Thream app on a community page](../public/images/posts/thream-v1-0-0/thream-ui.png)
![The Thream app on a community page](../../public/images/posts/thream-v1-0-0/thream-ui.png)
[**Thream**](https://thream.theoludwig.fr/) is a website that works on any recent browser, accessible on [thream.theoludwig.fr](https://thream.theoludwig.fr/).
@ -33,7 +33,7 @@ The main goal is to put into **practice knowledge in web development** and compu
The development of the project begins under the name of **SocialProject**, on August 20, 2020.
![SocialProject](../public/images/posts/thream-v1-0-0/social-project.jpg)
![SocialProject](../../public/images/posts/thream-v1-0-0/social-project.jpg)
When I started the project, I had little knowledge of database design, real-time management or the architecture of such a large <abbr title="Information Technology">IT</abbr> project, so this will be accompanied by many technical problems, to which we will need to find appropriate solutions.
@ -53,7 +53,7 @@ Since the project is mainly developed during free time (mainly on weekends), the
- The **client** part, called **frontend**, what **the user sees on the screen**, such as forms, buttons and all the **graphic elements** with which the user can interact from a browser.
![HTTP Communication Schema](../public/images/posts/thream-v1-0-0/http-communication.png)
![HTTP Communication Schema](../../public/images/posts/thream-v1-0-0/http-communication.png)
This design allows the separation between the client and the server, as long as they both structure their communication according to the <abbr title="Representational state transfer">REST</abbr> architectural guidelines, using the <abbr title="Hypertext Transfer Protocol">HTTP</abbr> protocol, they will be able to communicate with each other, which makes it possible to work independently on the backend and on the frontend using different technologies and skills, really useful in teamwork.

View File

@ -1,45 +0,0 @@
import useTranslation from 'next-translate/useTranslation'
import Link from 'next/link'
import type { FooterProps } from './Footer'
import { Footer } from './Footer'
import { Header } from './Header'
export interface ErrorPageProps extends FooterProps {
statusCode: number
message: string
}
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
const { message, statusCode, version } = props
const { t } = useTranslation()
return (
<>
<div className='flex h-screen flex-col pt-0'>
<Header showLanguage />
<main className='flex min-w-full flex-1 flex-col items-center justify-center'>
<h1 className='my-6 text-4xl font-semibold'>
{t('errors:error')}{' '}
<span
className='text-yellow dark:text-yellow-dark'
data-cy='status-code'
>
{statusCode}
</span>
</h1>
<p className='text-center text-lg'>
{message}{' '}
<Link
href='/'
className='text-yellow hover:underline dark:text-yellow-dark'
>
{t('errors:return-to-home-page')}
</Link>
</p>
</main>
<Footer version={version} />
</div>
</>
)
}

View File

@ -1,42 +0,0 @@
import { useMemo } from 'react'
import Link from 'next/link'
import useTranslation from 'next-translate/useTranslation'
export interface FooterProps {
version: string
}
export const Footer: React.FC<FooterProps> = (props) => {
const { t } = useTranslation()
const { version } = props
const versionLink = useMemo(() => {
return `https://github.com/theoludwig/theoludwig/releases/tag/v${version}`
}, [version])
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'>
<p>
<Link
href='/'
className='text-yellow hover:underline dark:text-yellow-dark'
>
Théo LUDWIG
</Link>{' '}
| {t('common:all-rights-reserved')}
</p>
<p className='mt-1'>
Version{' '}
<a
data-cy='version-link'
className='text-yellow hover:underline dark:text-yellow-dark'
href={versionLink}
target='_blank'
rel='noopener noreferrer'
>
{version}
</a>
</p>
</footer>
)
}

View File

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

View File

@ -0,0 +1,28 @@
import { useMemo } from 'react'
interface FooterVersionProps {
version: string
}
export const FooterVersion = (props: FooterVersionProps): JSX.Element => {
const { version } = props
const versionLink = useMemo(() => {
return `https://github.com/theoludwig/theoludwig/releases/tag/v${version}`
}, [version])
return (
<p className='mt-1'>
Version{' '}
<a
data-cy='version-link'
className='text-yellow hover:underline dark:text-yellow-dark'
href={versionLink}
target='_blank'
rel='noopener noreferrer'
>
{version}
</a>
</p>
)
}

View File

@ -0,0 +1,14 @@
import { FooterText } from './FooterText'
import { FooterVersion } from './FooterVersion'
export const Footer = async (): Promise<JSX.Element> => {
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'>
<FooterText />
<FooterVersion version={version} />
</footer>
)
}

View File

@ -1,45 +0,0 @@
import NextHead from 'next/head'
interface HeadProps {
title?: string
image?: string
description?: string
url?: string
}
export const Head: React.FC<HeadProps> = (props) => {
const {
title = 'Théo LUDWIG',
image = 'https://theoludwig.fr/images/icon-96x96.png',
description = 'Théo LUDWIG - Developer Full Stack • Passionate about High-Tech',
url = 'https://theoludwig.fr/'
} = props
return (
<NextHead>
<title>{title}</title>
<link rel='icon' type='image/png' href={image} />
{/* Meta Tag */}
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<meta name='description' content={description} />
<meta name='Language' content='fr-FR, en-US' />
<meta name='theme-color' content='#ffd800' />
{/* Open Graph Metadata */}
<meta property='og:title' content={title} />
<meta property='og:type' content='website' />
<meta property='og:url' content={url} />
<meta property='og:image' content={image} />
<meta property='og:description' content={description} />
<meta property='og:locale' content='fr-FR, en-US' />
<meta property='og:site_name' content={title} />
{/* Twitter card Metadata */}
<meta name='twitter:card' content='summary' />
<meta name='twitter:description' content={description} />
<meta name='twitter:title' content={title} />
<meta name='twitter:image' content={image} />
</NextHead>
)
}

View File

@ -1,24 +0,0 @@
import Image from 'next/image'
export interface LanguageFlagProps {
language: string
}
export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
const { language } = props
return (
<>
<Image
quality={100}
width={35}
height={35}
src={`/images/languages/${language}.svg`}
alt={language}
/>
<p data-cy='language-flag-text' className='mx-2 text-base'>
{language.toUpperCase()}
</p>
</>
)
}

View File

@ -1,4 +1,4 @@
export const Arrow: React.FC = () => {
export const Arrow = (): JSX.Element => {
return (
<svg
width='12'

View File

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

View File

@ -1,15 +1,24 @@
'use client'
import { usePathname } from 'next/navigation'
import { useCallback, useEffect, useState, useRef } from 'react'
import useTranslation from 'next-translate/useTranslation'
import setLanguage from 'next-translate/setLanguage'
import classNames from 'clsx'
import i18n from 'i18n.json'
import type { Locale as LocaleType, CookiesStore } from '@/utils/constants'
import { LOCALES } from '@/utils/constants'
import { Arrow } from './Arrow'
import { LanguageFlag } from './LanguageFlag'
import { LocaleFlag } from './LocaleFlag'
export interface LocalesProps {
currentLocale: string
cookiesStore: CookiesStore
}
export const Locales = (props: LocalesProps): JSX.Element => {
const { currentLocale, cookiesStore } = props
const pathname = usePathname()
export const Language: React.FC = () => {
const { lang: currentLanguage } = useTranslation()
const [hiddenMenu, setHiddenMenu] = useState(true)
const languageClickRef = useRef<HTMLDivElement | null>(null)
@ -36,42 +45,52 @@ export const Language: React.FC = () => {
}
}, [])
const handleLanguage = async (language: string): Promise<void> => {
await setLanguage(language, false)
const handleLocale = async (locale: LocaleType): Promise<void> => {
const { setLocale } = await import('@/i18n/i18n.server')
setLocale(locale)
}
if (pathname.startsWith('/blog')) {
return <></>
}
return (
<div className='flex cursor-pointer flex-col items-center justify-center'>
<div
ref={languageClickRef}
data-cy='language-click'
data-cy='locale-click'
className='mr-5 flex items-center'
onClick={handleHiddenMenu}
>
<LanguageFlag language={currentLanguage} />
<LocaleFlag
locale={currentLocale}
cookiesStore={cookiesStore?.toString()}
/>
<Arrow />
</div>
<ul
data-cy='languages-list'
data-cy='locales-list'
className={classNames(
'absolute top-14 z-10 mr-4 mt-3 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
'absolute top-14 z-10 mr-4 mt-3 flex w-32 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
{ hidden: hiddenMenu }
)}
>
{i18n.locales.map((language, index) => {
if (language === currentLanguage) {
return null
}
{LOCALES.filter((locale) => {
return locale !== currentLocale
}).map((locale) => {
return (
<li
key={index}
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
key={locale}
className='flex h-12 w-full items-center justify-center hover:bg-[#4f545c] hover:bg-opacity-20'
onClick={async () => {
return await handleLanguage(language)
return await handleLocale(locale)
}}
>
<LanguageFlag language={language} />
<LocaleFlag
locale={locale}
cookiesStore={cookiesStore?.toString()}
/>
</li>
)
})}

View File

@ -1,21 +1,22 @@
import { useEffect, useState } from 'react'
'use client'
import classNames from 'clsx'
import { useTheme } from 'next-themes'
export const SwitchTheme: React.FC = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
import { useTheme } from '@/theme/theme.client'
import type { CookiesStore } from '@/utils/constants'
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
export interface SwitchThemeProps {
cookiesStore: CookiesStore
}
const handleClick = (): void => {
setTheme(theme === 'dark' ? 'light' : 'dark')
export const SwitchTheme = (props: SwitchThemeProps): JSX.Element => {
const { cookiesStore } = props
const theme = useTheme(cookiesStore)
const handleClick = async (): Promise<void> => {
const { setTheme } = await import('@/theme/theme.server')
const newTheme = theme === 'dark' ? 'light' : 'dark'
setTheme(newTheme)
}
return (
@ -69,7 +70,7 @@ export const SwitchTheme: React.FC = () => {
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'
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0 hidden'
defaultChecked
/>
</div>

View File

@ -1,15 +1,15 @@
import { cookies } from 'next/headers'
import Link from 'next/link'
import Image from 'next/image'
import { Language } from './Language'
import { getI18n } from '@/i18n/i18n.server'
import { Locales } from './Locales'
import { SwitchTheme } from './SwitchTheme'
export interface HeaderProps {
showLanguage?: boolean
}
export const Header: React.FC<HeaderProps> = (props) => {
const { showLanguage = false } = props
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'>
@ -38,8 +38,11 @@ export const Header: React.FC<HeaderProps> = (props) => {
Blog
</Link>
</div>
{showLanguage ? <Language /> : null}
<SwitchTheme />
<Locales
currentLocale={i18n.locale}
cookiesStore={cookiesStore.toString()}
/>
<SwitchTheme cookiesStore={cookiesStore.toString()} />
</div>
</header>
)

View File

@ -5,7 +5,9 @@ export interface InterestParagraphProps {
description: string
}
export const InterestParagraph: React.FC<InterestParagraphProps> = (props) => {
export const InterestParagraph = (
props: InterestParagraphProps
): JSX.Element => {
const { title, description } = props
return (

View File

@ -6,7 +6,7 @@ interface InterestItemProps {
fontAwesomeIcon: IconDefinition
}
export const InterestItem: React.FC<InterestItemProps> = (props) => {
export const InterestItem = (props: InterestItemProps): JSX.Element => {
const { fontAwesomeIcon, title } = props
return (

View File

@ -3,7 +3,7 @@ import { faGit } from '@fortawesome/free-brands-svg-icons'
import { InterestItem } from './InterestItem'
export const InterestsList: React.FC = () => {
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'>

View File

@ -1,19 +1,18 @@
import useTranslation from 'next-translate/useTranslation'
import { getI18n } from '@/i18n/i18n.server'
import type { InterestParagraphProps } from './InterestParagraph'
import { InterestParagraph } from './InterestParagraph'
import { InterestsList } from './InterestsList'
export const Interests: React.FC = () => {
const { t } = useTranslation()
export const Interests = (): JSX.Element => {
const i18n = getI18n()
const paragraphs: InterestParagraphProps[] = t(
'home:interests.paragraphs',
{},
{
returnObjects: true
}
let paragraphs = i18n.translate<InterestParagraphProps[]>(
'home.interests.paragraphs'
)
if (!Array.isArray(paragraphs)) {
paragraphs = []
}
return (
<div className='max-w-full'>

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
@ -7,7 +7,7 @@ export interface RepositoryProps {
href: string
}
export const Repository: React.FC<RepositoryProps> = (props) => {
export const Repository = (props: RepositoryProps): JSX.Element => {
const { name, description, href } = props
return (

View File

@ -1,17 +1,19 @@
import useTranslation from 'next-translate/useTranslation'
import { getI18n } from '@/i18n/i18n.server'
import { Repository } from './Repository'
export const OpenSource: React.FC = () => {
const { t } = useTranslation()
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'>{t('home:open-source.description')}</p>
<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'>
<Repository
name='nodejs/node'
description='Node.js JavaScript runtime 🐢🚀'
description='Node.js JavaScript runtime 🐢🚀'
href='https://github.com/nodejs/node/commits?author=theoludwig'
/>
<Repository
@ -21,12 +23,12 @@ export const OpenSource: React.FC = () => {
/>
<Repository
name='nrwl/nx'
description='Smart, Extensible Build Framework'
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 for Production'
description='The React Framework'
href='https://github.com/vercel/next.js/commits?author=theoludwig'
/>
</div>

View File

@ -1,6 +1,6 @@
import Image from 'next/image'
import { ShadowContainer } from 'components/design/ShadowContainer'
import { ShadowContainer } from '@/components/design/ShadowContainer'
export interface PortfolioItemProps {
title: string
@ -9,7 +9,7 @@ export interface PortfolioItemProps {
image: string
}
export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
export const PortfolioItem = (props: PortfolioItemProps): JSX.Element => {
const { title, description, link, image } = props
return (

View File

@ -1,18 +1,15 @@
import useTranslation from 'next-translate/useTranslation'
import { getI18n } from '@/i18n/i18n.server'
import type { PortfolioItemProps } from './PortfolioItem'
import { PortfolioItem } from './PortfolioItem'
export const Portfolio: React.FC = () => {
const { t } = useTranslation('home')
export const Portfolio = (): JSX.Element => {
const i18n = getI18n()
const items: PortfolioItemProps[] = t(
'home:portfolio.items',
{},
{
returnObjects: true
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'>

View File

@ -1,12 +1,12 @@
import useTranslation from 'next-translate/useTranslation'
import { getI18n } from '@/i18n/i18n.server'
export const ProfileDescriptionBottom: React.FC = () => {
const { t, lang } = useTranslation()
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'>
{t('home:about.description-bottom')}
{lang === 'fr' ? (
{i18n.translate('home.about.description-bottom')}
{i18n.locale === 'fr-FR' ? (
<>
<br />
<br />

View File

@ -1,14 +1,16 @@
import useTranslation from 'next-translate/useTranslation'
import { getI18n } from '@/i18n/i18n.server'
export const ProfileInformation: React.FC = () => {
const { t } = useTranslation()
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'>
Théo LUDWIG
</h1>
<h2 className='mb-3 text-base'>{t('home:about.description')}</h2>
<h2 className='mb-3 text-base'>
{i18n.translate('home.about.description')}
</h2>
</div>
)
}

View File

@ -4,7 +4,7 @@ interface ProfileItemProps {
link?: string
}
export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
export const ProfileItem = (props: ProfileItemProps): JSX.Element => {
const { title, value, link } = props
return (

View File

@ -1,12 +1,21 @@
import useTranslation from 'next-translate/useTranslation'
'use client'
import { useMemo } from 'react'
import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from 'utils/getAge'
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'
export const ProfileList: React.FC = () => {
const { t } = useTranslation('home')
export interface ProfileListProps {
cookiesStore: CookiesStore
}
export const ProfileList = (props: ProfileListProps): JSX.Element => {
const { cookiesStore } = props
const i18n = useI18n(cookiesStore)
const age = useMemo(() => {
return getAge(BIRTH_DATE)
@ -15,14 +24,19 @@ export const ProfileList: React.FC = () => {
return (
<ul className='m-0 list-none p-0'>
<ProfileItem
title={t('home:about.pronouns')}
value={t('home:about.pronouns-value')}
title={i18n.translate('home.about.pronouns')}
value={i18n.translate('home.about.pronouns-value')}
/>
<ProfileItem
title={t('home:about.birth-date')}
value={`${BIRTH_DATE_STRING} (${age} ${t('home:about.years-old')})`}
title={i18n.translate('home.about.birth-date')}
value={`${BIRTH_DATE_STRING} (${age} ${i18n.translate(
'home.about.years-old'
)})`}
/>
<ProfileItem
title={i18n.translate('home.about.nationality')}
value='Alsace, France'
/>
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
<ProfileItem
title='Email'
value='contact@theoludwig.fr'

View File

@ -2,7 +2,7 @@ import Image from 'next/image'
import Logo from 'public/images/logo.png'
export const ProfileLogo: React.FC = () => {
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 />

View File

@ -1,6 +1,8 @@
import { Icon } from './Icon'
export const EmailIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
export const EmailIcon = (
props: React.SVGProps<SVGSVGElement>
): JSX.Element => {
return (
<Icon {...props}>
<title>Email</title>

View File

@ -1,6 +1,8 @@
import { Icon } from './Icon'
export const GitHubIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
export const GitHubIcon = (
props: React.SVGProps<SVGSVGElement>
): JSX.Element => {
return (
<Icon {...props}>
<title>GitHub</title>

View File

@ -1,6 +1,8 @@
import { Icon } from './Icon'
export const GitLabIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
export const GitLabIcon = (
props: React.SVGProps<SVGSVGElement>
): JSX.Element => {
return (
<Icon {...props}>
<title>GitLab</title>

View File

@ -1,6 +1,6 @@
import classNames from 'clsx'
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
export const Icon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
const { children, className, ...rest } = props
return (

View File

@ -1,6 +1,6 @@
import { Icon } from './Icon'
export const NPMIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
export const NPMIcon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
return (
<Icon {...props}>
<title>npm</title>

View File

@ -1,6 +1,8 @@
import { Icon } from './Icon'
export const TwitchIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
export const TwitchIcon = (
props: React.SVGProps<SVGSVGElement>
): JSX.Element => {
return (
<Icon {...props}>
<title>Twitch</title>

View File

@ -1,6 +1,8 @@
import { Icon } from './Icon'
export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
export const TwitterIcon = (
props: React.SVGProps<SVGSVGElement>
): JSX.Element => {
return (
<Icon {...props}>
<title>Twitter</title>

View File

@ -1,6 +1,8 @@
import { Icon } from './Icon'
export const YouTubeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
export const YouTubeIcon = (
props: React.SVGProps<SVGSVGElement>
): JSX.Element => {
return (
<Icon {...props}>
<title>YouTube</title>

View File

@ -1,11 +1,9 @@
interface SocialMediaItemProps {
interface SocialMediaItemProps extends React.PropsWithChildren {
link: string
ariaLabel: string
}
export const SocialMediaItem: React.FC<
React.PropsWithChildren<SocialMediaItemProps>
> = (props) => {
export const SocialMediaItem = (props: SocialMediaItemProps): JSX.Element => {
const { link, ariaLabel, children } = props
return (

View File

@ -7,7 +7,7 @@ import { TwitchIcon } from './SocialMediaIcons/TwitchIcon'
import { EmailIcon } from './SocialMediaIcons/EmailIcon'
import { NPMIcon } from './SocialMediaIcons/NPMIcon'
export const SocialMediaList: React.FC = () => {
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'>
@ -16,7 +16,7 @@ export const SocialMediaList: React.FC = () => {
<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

View File

@ -1,15 +1,19 @@
import { cookies } from 'next/headers'
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
import { ProfileInformation } from './ProfileInfo'
import { ProfileList } from './ProfileList'
import { ProfileLogo } from './ProfileLogo'
export const Profile: React.FC = () => {
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'>
<ProfileLogo />
<div>
<ProfileInformation />
<ProfileList />
<ProfileList cookiesStore={cookiesStore.toString()} />
<ProfileDescriptionBottom />
</div>
</div>

View File

@ -1,6 +1,6 @@
import { useTheme } from 'next-themes'
import Image from 'next/image'
import { useMemo } from 'react'
import { getTheme } from '@/theme/theme.server'
import type { SkillName } from './skills'
import { skills } from './skills'
@ -9,12 +9,14 @@ export interface SkillComponentProps {
skill: SkillName
}
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
export const SkillComponent = (props: SkillComponentProps): JSX.Element => {
const { skill } = props
const skillProperties = skills[skill]
const { theme } = useTheme()
const image = useMemo(() => {
const skillProperties = skills[skill]
const theme = getTheme()
const getImage = (): string => {
if (typeof skillProperties.image === 'string') {
return skillProperties.image
}
@ -22,7 +24,7 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
return skillProperties.image.light
}
return skillProperties.image.dark
}, [skillProperties, theme])
}
return (
<a
@ -38,7 +40,7 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
width={64}
height={64}
alt={skill}
src={image}
src={getImage()}
/>
<p className='mt-1'>{skill}</p>
</div>

View File

@ -1,11 +1,11 @@
import { ShadowContainer } from 'components/design/ShadowContainer'
import { ShadowContainer } from '@/components/design/ShadowContainer'
export interface SkillsSectionProps {
title: string
children: React.ReactNode
}
export const SkillsSection: React.FC<SkillsSectionProps> = (props) => {
export const SkillsSection = (props: SkillsSectionProps): JSX.Element => {
const { title, children } = props
return (

View File

@ -1,14 +1,14 @@
import useTranslation from 'next-translate/useTranslation'
import { getI18n } from '@/i18n/i18n.server'
import { SkillComponent } from './Skill'
import { SkillsSection } from './SkillsSection'
export const Skills: React.FC = () => {
const { t } = useTranslation()
export const Skills = (): JSX.Element => {
const i18n = getI18n()
return (
<>
<SkillsSection title={t('home:skills.languages')}>
<SkillsSection title={i18n.translate('home.skills.languages')}>
<SkillComponent skill='TypeScript' />
<SkillComponent skill='Python' />
<SkillComponent skill='C/C++' />
@ -29,7 +29,7 @@ export const Skills: React.FC = () => {
<SkillComponent skill='PostgreSQL' />
</SkillsSection>
<SkillsSection title={t('home:skills.software-tools')}>
<SkillsSection title={i18n.translate('home.skills.software-tools')}>
<SkillComponent skill='GNU/Linux' />
<SkillComponent skill='Arch Linux' />
<SkillComponent skill='Visual Studio Code' />

View File

@ -0,0 +1,28 @@
import classNames from 'clsx'
export interface LoaderProps {
width?: number
height?: number
className?: string
}
export const Loader = (props: LoaderProps): JSX.Element => {
const { width = 50, height = 50, className } = props
return (
<div
style={{
width,
height
}}
className={classNames(
'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'
>
<span className='sr-only'>Loading...</span>
</div>
)
}

View File

@ -1,6 +1,10 @@
'use client'
import { useEffect, useRef } from 'react'
export const RevealFade: React.FC<React.PropsWithChildren> = (props) => {
export type RevealFadeProps = React.PropsWithChildren
export const RevealFade = (props: RevealFadeProps): JSX.Element => {
const { children } = props
const htmlElement = useRef<HTMLDivElement>(null)

View File

@ -1,6 +1,6 @@
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'>
export const SectionHeading: React.FC<SectionHeadingProps> = (props) => {
export const SectionHeading = (props: SectionHeadingProps): JSX.Element => {
const { children, ...rest } = props
return (

View File

@ -1,5 +1,5 @@
import { ShadowContainer } from '../ShadowContainer'
import { SectionHeading } from './SectionHeading'
import { ShadowContainer } from '@/components/design/ShadowContainer'
import { SectionHeading } from '@/components/design/Section/SectionHeading'
type SectionProps = React.ComponentPropsWithRef<'section'> & {
heading?: string
@ -8,7 +8,7 @@ type SectionProps = React.ComponentPropsWithRef<'section'> & {
withoutShadowContainer?: boolean
}
export const Section: React.FC<SectionProps> = (props) => {
export const Section = (props: SectionProps): JSX.Element => {
const {
children,
heading,

View File

@ -2,7 +2,7 @@ import classNames from 'clsx'
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
export const ShadowContainer: React.FC<ShadowContainerProps> = (props) => {
export const ShadowContainer = (props: ShadowContainerProps): JSX.Element => {
const { children, className, ...rest } = props
return (

View File

@ -5,8 +5,7 @@ services:
restart: 'unless-stopped'
build:
context: './'
ports:
- '${PORT-3000}:${PORT-3000}'
network_mode: 'host'
environment:
PORT: ${PORT-3000}
env_file: '.env'

20
curriculum-vitae/build.js Normal file
View File

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

View File

@ -1,8 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json",
"meta": {
"theme": "custom"
},
"basics": {
"name": "Théo LUDWIG",
"label": "Développeur Full Stack • Étudiant",
@ -16,6 +13,19 @@
"summary": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne. <br/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets (disponible sur <a href=\"https://theoludwig.fr\">theoludwig.fr</a>)."
},
"education": [
{
"startDate": "2023",
"endDate": "2024",
"studyType": "Bachelor Universitaire de Technologie (BUT) Informatique",
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
"score": "3ème année",
"courses": [
"Développement Web en Node.js et React.js",
"Intégration/Déploiement Continue et Docker",
"Projet développement LLM (Large Language Model) et NLP (Natural Language Processing)",
"Base de données NoSQL (MongoDB)"
]
},
{
"startDate": "2022",
"endDate": "2023",
@ -24,10 +34,10 @@
"score": "2ème année",
"courses": [
"Développement Web avec le framework Laravel en PHP",
"Qualité de développement et Tests unitaires, d'intégration, fonctionnels/systèmes, d'acceptation etc.",
"Patrons et Principes de conceptions (Code maintenable et réutilisable) en UML",
"Programmation systèmes en C (Multi-Thread, Serveur/Client UDP/TCP)",
"Sécurisation des accès à la base de données et PL/SQL",
"Projet développement d'une application web en React.js en équipe de 3 personnes pendant 3 mois"
"Sécurisation des accès à la base de données et PL/SQL"
]
},
{
@ -59,29 +69,39 @@
// }
],
"work": [
{
"summary": "Développement de WebSurg, une université virtuelle consacrée à la formation médico-chirurgicale, en React.js/Next.js et API Platform avec Symfony.",
"website": "https://ircad.fr/",
"name": "IRCAD",
"location": "1 Place de l'Hôpital, 67000 Strasbourg",
"position": "Alternant Développeur Web Full Stack",
"startDate": "2023-08-28",
"endDate": "2024-09-02",
"duration": "1 an"
},
{
"summary": "Développement d'un outil GED (Gestion Électronique de Documents) en React.js, Laravel et GraphQL.",
"website": "https://numerize.com/",
"name": "Numerize",
"location": "4 Rue Sophie Germain, 67720 Hœrdt",
"position": "Stagiaire Développeur Web",
"position": "Stagiaire Développeur Web Full Stack",
"startDate": "2023-04-11",
"endDate": "2023-07-26",
"duration": "4 mois"
},
{
"summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.",
"website": "https://www.es.fr/",
"name": "ÉS (Électricité de Strasbourg)",
"location": "5 Rue André Marie Ampère, 67450 Mundolsheim",
"position": "Emploi d'été en qualité d'agent administratif",
"startDate": "2021-07-07",
"endDate": "2021-07-30",
"duration": "1 mois"
},
// {
// "summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.",
// "website": "https://www.es.fr/",
// "name": "ÉS (Électricité de Strasbourg)",
// "location": "5 Rue André Marie Ampère, 67450 Mundolsheim",
// "position": "Emploi d'été en qualité d'agent administratif",
// "startDate": "2021-07-07",
// "endDate": "2021-07-30",
// "duration": "1 mois"
// },
{
"summary": "Développement d'un site web pour trouver un restaurant à la pause repas.",
"website": "https://www.itpartners.fr/",
"website": "https://itpartners.fr/",
"name": "Tribe | IT Partners",
"location": "16 Rue du Parc, 67205 Oberhausbergen",
"position": "Stage initiation métier développeur web",
@ -91,8 +111,8 @@
},
{
"description": "interests",
"summary": "Développement site web en React.js et Strapi.<br /> Classé n°1 en France sur le Défi de l'entreprise <a href=\"https://www.toolpad.fr/\">ToolPad</a>.",
"website": "https://www.nuitdelinfo.com/",
"summary": "Développement site web en React.js et Strapi.<br /> Classé n°1 en France sur le Défi de l'entreprise <a href=\"https://toolpad.fr/\">ToolPad</a>.",
"website": "https://nuitdelinfo.com/",
"name": "La Nuit de l'info 2021",
"position": "Participation en équipe de 5 personnes",
"startDate": "2021-12-02",
@ -102,7 +122,7 @@
{
"description": "interests",
"summary": "Hackathon développement d'une landing page et web scraping.",
"website": "https://www.wildcodeschool.fr/",
"website": "https://wildcodeschool.fr/",
"name": "Wild Code School",
"location": "32 Rue du Bass. d'Austerlitz, 67100 Strasbourg",
"position": "Initiation métier Développeur web",
@ -112,7 +132,7 @@
}
// {
// "summary": "Apprentissage du métier \"Chargé de communication\" et des logiciels de graphisme tels que \"Adobe Photoshop\".",
// "website": "https://www.es.fr/",
// "website": "https://es.fr/",
// "name": "ÉS (Électricité de Strasbourg)",
// "location": "26 Bd du Président-Wilson, 67000 Strasbourg",
// "position": "Stage de découverte (3ème)",

View File

Before

Width:  |  Height:  |  Size: 1015 B

After

Width:  |  Height:  |  Size: 1015 B

View File

Before

Width:  |  Height:  |  Size: 986 B

After

Width:  |  Height:  |  Size: 986 B

View File

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 629 B

View File

Before

Width:  |  Height:  |  Size: 912 B

After

Width:  |  Height:  |  Size: 912 B

View File

Before

Width:  |  Height:  |  Size: 528 B

After

Width:  |  Height:  |  Size: 528 B

View File

@ -1,27 +1,27 @@
{
"name": "jsonresume-theme-custom",
"name": "curriculum-vitae",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jsonresume-theme-custom",
"name": "curriculum-vitae",
"version": "1.0.0",
"dependencies": {
"jsonc-parser": "3.2.0",
"modern-normalize": "2.0.0"
},
"devDependencies": {
"@types/node": "20.4.4",
"date-and-time": "3.0.2",
"vite": "4.4.6",
"@types/node": "20.6.2",
"date-and-time": "3.0.3",
"vite": "4.4.9",
"vite-plugin-html": "3.2.0"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.15.tgz",
"integrity": "sha512-wlkQBWb79/jeEEoRmrxt/yhn5T1lU236OCNpnfRzaCJHZ/5gf82uYx1qmADTBWE0AR/v7FiozE1auk2riyQd3w==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
@ -35,9 +35,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.15.tgz",
"integrity": "sha512-NI/gnWcMl2kXt1HJKOn2H69SYn4YNheKo6NZt1hyfKWdMbaGadxjZIkcj4Gjk/WPxnbFXs9/3HjGHaknCqjrww==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
@ -51,9 +51,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.15.tgz",
"integrity": "sha512-FM9NQamSaEm/IZIhegF76aiLnng1kEsZl2eve/emxDeReVfRuRNmvT28l6hoFD9TsCxpK+i4v8LPpEj74T7yjA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
@ -67,9 +67,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.15.tgz",
"integrity": "sha512-XmrFwEOYauKte9QjS6hz60FpOCnw4zaPAb7XV7O4lx1r39XjJhTN7ZpXqJh4sN6q60zbP6QwAVVA8N/wUyBH/w==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
@ -83,9 +83,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.15.tgz",
"integrity": "sha512-bMqBmpw1e//7Fh5GLetSZaeo9zSC4/CMtrVFdj+bqKPGJuKyfNJ5Nf2m3LknKZTS+Q4oyPiON+v3eaJ59sLB5A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
@ -99,9 +99,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.15.tgz",
"integrity": "sha512-LoTK5N3bOmNI9zVLCeTgnk5Rk0WdUTrr9dyDAQGVMrNTh9EAPuNwSTCgaKOKiDpverOa0htPcO9NwslSE5xuLA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
@ -115,9 +115,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.15.tgz",
"integrity": "sha512-62jX5n30VzgrjAjOk5orYeHFq6sqjvsIj1QesXvn5OZtdt5Gdj0vUNJy9NIpjfdNdqr76jjtzBJKf+h2uzYuTQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
@ -131,9 +131,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.15.tgz",
"integrity": "sha512-dT4URUv6ir45ZkBqhwZwyFV6cH61k8MttIwhThp2BGiVtagYvCToF+Bggyx2VI57RG4Fbt21f9TmXaYx0DeUJg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
@ -147,9 +147,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.15.tgz",
"integrity": "sha512-BWncQeuWDgYv0jTNzJjaNgleduV4tMbQjmk/zpPh/lUdMcNEAxy+jvneDJ6RJkrqloG7tB9S9rCrtfk/kuplsQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
@ -163,9 +163,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.15.tgz",
"integrity": "sha512-JPXORvgHRHITqfms1dWT/GbEY89u848dC08o0yK3fNskhp0t2TuNUnsrrSgOdH28ceb1hJuwyr8R/1RnyPwocw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
@ -179,9 +179,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.15.tgz",
"integrity": "sha512-kArPI0DopjJCEplsVj/H+2Qgzz7vdFSacHNsgoAKpPS6W/Ndh8Oe24HRDQ5QCu4jHgN6XOtfFfLpRx3TXv/mEg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
@ -195,9 +195,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.15.tgz",
"integrity": "sha512-b/tmngUfO02E00c1XnNTw/0DmloKjb6XQeqxaYuzGwHe0fHVgx5/D6CWi+XH1DvkszjBUkK9BX7n1ARTOst59w==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
@ -211,9 +211,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.15.tgz",
"integrity": "sha512-KXPY69MWw79QJkyvUYb2ex/OgnN/8N/Aw5UDPlgoRtoEfcBqfeLodPr42UojV3NdkoO4u10NXQdamWm1YEzSKw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
@ -227,9 +227,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.15.tgz",
"integrity": "sha512-komK3NEAeeGRnvFEjX1SfVg6EmkfIi5aKzevdvJqMydYr9N+pRQK0PGJXk+bhoPZwOUgLO4l99FZmLGk/L1jWg==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
@ -243,9 +243,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.15.tgz",
"integrity": "sha512-632T5Ts6gQ2WiMLWRRyeflPAm44u2E/s/TJvn+BP6M5mnHSk93cieaypj3VSMYO2ePTCRqAFXtuYi1yv8uZJNA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
@ -259,9 +259,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.15.tgz",
"integrity": "sha512-MsHtX0NgvRHsoOtYkuxyk4Vkmvk3PLRWfA4okK7c+6dT0Fu4SUqXAr9y4Q3d8vUf1VWWb6YutpL4XNe400iQ1g==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
@ -275,9 +275,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.15.tgz",
"integrity": "sha512-djST6s+jQiwxMIVQ5rlt24JFIAr4uwUnzceuFL7BQT4CbrRtqBPueS4GjXSiIpmwVri1Icj/9pFRJ7/aScvT+A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
@ -291,9 +291,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.15.tgz",
"integrity": "sha512-naeRhUIvhsgeounjkF5mvrNAVMGAm6EJWiabskeE5yOeBbLp7T89tAEw0j5Jm/CZAwyLe3c67zyCWH6fsBLCpw==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
@ -307,9 +307,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.15.tgz",
"integrity": "sha512-qkT2+WxyKbNIKV1AEhI8QiSIgTHMcRctzSaa/I3kVgMS5dl3fOeoqkb7pW76KwxHoriImhx7Mg3TwN/auMDsyQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
@ -323,9 +323,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.15.tgz",
"integrity": "sha512-HC4/feP+pB2Vb+cMPUjAnFyERs+HJN7E6KaeBlFdBv799MhD+aPJlfi/yk36SED58J9TPwI8MAcVpJgej4ud0A==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
@ -339,9 +339,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.15.tgz",
"integrity": "sha512-ovjwoRXI+gf52EVF60u9sSDj7myPixPxqzD5CmkEUmvs+W9Xd0iqISVBQn8xcx4ciIaIVlWCuTbYDOXOnOL44Q==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
@ -355,9 +355,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.15.tgz",
"integrity": "sha512-imUxH9a3WJARyAvrG7srLyiK73XdX83NXQkjKvQ+7vPh3ZxoLrzvPkQKKw2DwZ+RV2ZB6vBfNHP8XScAmQC3aA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
@ -385,9 +385,9 @@
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
"dev": true,
"engines": {
"node": ">=6.0.0"
@ -419,21 +419,15 @@
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.18",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
"integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
"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==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14"
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -483,9 +477,9 @@
}
},
"node_modules/@types/node": {
"version": "20.4.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.4.tgz",
"integrity": "sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==",
"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
},
"node_modules/acorn": {
@ -682,9 +676,9 @@
}
},
"node_modules/date-and-time": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.0.2.tgz",
"integrity": "sha512-MOqlRertOQmQI7ySbz6dKLM7Rxm9dgcPuBI9IL7NVe0UGqHPK+6hWSKVhLrVHxlSgQQtocE2R7+HFOf5aMz8vw==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.0.3.tgz",
"integrity": "sha512-CmHCeTixc3KA5pcLTVs9JCFhmJMFTBsmSsgHnNed4YDNw9yUOrjjRn3zALy8eMgqmTO+4U8k5jl1peC7IoezfA==",
"dev": true
},
"node_modules/dom-serializer": {
@ -798,9 +792,9 @@
}
},
"node_modules/esbuild": {
"version": "0.18.15",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.15.tgz",
"integrity": "sha512-3WOOLhrvuTGPRzQPU6waSDWrDTnQriia72McWcn6UCi43GhCHrXH4S59hKMeez+IITmdUuUyvbU9JIp+t3xlPQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"bin": {
@ -810,28 +804,28 @@
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.15",
"@esbuild/android-arm64": "0.18.15",
"@esbuild/android-x64": "0.18.15",
"@esbuild/darwin-arm64": "0.18.15",
"@esbuild/darwin-x64": "0.18.15",
"@esbuild/freebsd-arm64": "0.18.15",
"@esbuild/freebsd-x64": "0.18.15",
"@esbuild/linux-arm": "0.18.15",
"@esbuild/linux-arm64": "0.18.15",
"@esbuild/linux-ia32": "0.18.15",
"@esbuild/linux-loong64": "0.18.15",
"@esbuild/linux-mips64el": "0.18.15",
"@esbuild/linux-ppc64": "0.18.15",
"@esbuild/linux-riscv64": "0.18.15",
"@esbuild/linux-s390x": "0.18.15",
"@esbuild/linux-x64": "0.18.15",
"@esbuild/netbsd-x64": "0.18.15",
"@esbuild/openbsd-x64": "0.18.15",
"@esbuild/sunos-x64": "0.18.15",
"@esbuild/win32-arm64": "0.18.15",
"@esbuild/win32-ia32": "0.18.15",
"@esbuild/win32-x64": "0.18.15"
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/estree-walker": {
@ -922,9 +916,9 @@
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
@ -1206,9 +1200,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
"version": "8.4.29",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
"integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
"dev": true,
"funding": [
{
@ -1273,9 +1267,9 @@
}
},
"node_modules/rollup": {
"version": "3.26.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.3.tgz",
"integrity": "sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==",
"version": "3.29.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.2.tgz",
"integrity": "sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@ -1352,9 +1346,9 @@
}
},
"node_modules/terser": {
"version": "5.19.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz",
"integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==",
"version": "5.19.4",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz",
"integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@ -1388,9 +1382,9 @@
}
},
"node_modules/tslib": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
"integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==",
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
},
"node_modules/universalify": {
@ -1403,14 +1397,14 @@
}
},
"node_modules/vite": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.6.tgz",
"integrity": "sha512-EY6Mm8vJ++S3D4tNAckaZfw3JwG3wa794Vt70M6cNJ6NxT87yhq7EC8Rcap3ahyHdo8AhCmV9PTk+vG1HiYn1A==",
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.26",
"rollup": "^3.25.2"
"postcss": "^8.4.27",
"rollup": "^3.27.1"
},
"bin": {
"vite": "bin/vite.js"

View File

@ -1,5 +1,5 @@
{
"name": "jsonresume-theme-custom",
"name": "curriculum-vitae",
"private": true,
"version": "1.0.0",
"type": "module",
@ -13,9 +13,9 @@
"modern-normalize": "2.0.0"
},
"devDependencies": {
"@types/node": "20.4.4",
"date-and-time": "3.0.2",
"vite": "4.4.6",
"@types/node": "20.6.2",
"date-and-time": "3.0.3",
"vite": "4.4.9",
"vite-plugin-html": "3.2.0"
}
}

View File

@ -5,11 +5,17 @@ import { parse as JSONCParser } from 'jsonc-parser'
import { createHtmlPlugin } from 'vite-plugin-html'
import date from 'date-and-time'
const jsonResumeURL = new URL('../resume.jsonc', import.meta.url)
const dataResumeStringJSON = await fs.promises.readFile(jsonResumeURL, {
const jsonCurriculumVitaeURL = new URL(
'./curriculum-vitae.jsonc',
import.meta.url
)
const dataCurriculumVitaeStringJSON = await fs.promises.readFile(
jsonCurriculumVitaeURL,
{
encoding: 'utf-8'
})
const resume = JSONCParser(dataResumeStringJSON)
}
)
const curriculumVitae = JSONCParser(dataCurriculumVitaeStringJSON)
/**
* Documentation: <https://vitejs.dev/config/>
@ -24,7 +30,7 @@ export default defineConfig({
data: {
date,
locals: {
...resume
...curriculumVitae
}
}
}

View File

@ -1,16 +0,0 @@
import { Footer } from '@/components/Footer'
describe('<Footer />', () => {
it('should render with appropriate link tag version', () => {
const version = '1.0.0'
cy.mount(<Footer version={version} />)
cy.contains('Théo LUDWIG')
.get('[data-cy=version-link]')
.should('have.text', version)
.should(
'have.attr',
'href',
`https://github.com/theoludwig/theoludwig/releases/tag/v${version}`
)
})
})

View File

@ -1,4 +1,4 @@
import { getAge } from '../../../utils/getAge'
import { getAge } from '@/utils/getAge'
describe('utils/getAge', () => {
it('should calculate the right age of a person', () => {

View File

@ -37,24 +37,26 @@ describe('Common > Header', () => {
})
describe('Switch Language', () => {
it('should switch language from EN (default) to FR', () => {
it('should switch locale from English (default) to French', () => {
cy.get('h1').contains('Théo LUDWIG')
cy.get('[data-cy=language-flag-text]').contains('EN')
cy.get('[data-cy=languages-list]').should('not.be.visible')
cy.get('[data-cy=language-click]').click()
cy.get('[data-cy=languages-list]').should('be.visible')
cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click()
cy.get('[data-cy=languages-list]').should('not.be.visible')
cy.get('[data-cy=language-flag-text]').contains('FR')
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')
})
it('should close the language list menu when clicking outside', () => {
cy.get('[data-cy=languages-list]').should('not.be.visible')
cy.get('[data-cy=language-click]').click()
cy.get('[data-cy=languages-list]').should('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=languages-list]').should('not.be.visible')
cy.get('[data-cy=locales-list]').should('not.be.visible')
})
})
})

View File

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

View File

@ -1,7 +1,7 @@
describe('Page /blog/[slug]', () => {
it('should displays the first blog post (`hello-world`)', () => {
cy.visit('/blog/hello-world')
cy.get('[data-cy=language-flag-text]').should('not.exist')
cy.get('[data-cy=locale-flag-text]').should('not.exist')
cy.get('h1').should('have.text', '👋 Hello, world!')
cy.get('.prose a:visible').should('have.attr', 'target', '_blank')
})

View File

@ -1,7 +1,7 @@
import { mount } from 'cypress/react'
import './commands'
import '../../styles/global.css'
import '../../app/globals.css'
declare global {
namespace Cypress {

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