mirror of
				https://github.com/theoludwig/theoludwig.git
				synced 2025-11-04 00:19:01 +01:00 
			
		
		
		
	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
This commit is contained in:
		
							
								
								
									
										21
									
								
								app/error.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/error.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					'use client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useEffect } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ErrorPage } from '@/components/ErrorPage'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ErrorHandlingProps {
 | 
				
			||||||
 | 
					  error: Error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ErrorHandling = (props: ErrorHandlingProps): JSX.Element => {
 | 
				
			||||||
 | 
					  const { error } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    console.error(error)
 | 
				
			||||||
 | 
					  }, [error])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <ErrorPage statusCode={500} message='Server error' />
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ErrorHandling
 | 
				
			||||||
							
								
								
									
										71
									
								
								app/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								app/layout.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					import type { Metadata } from 'next'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import '@fontsource/montserrat/400.css'
 | 
				
			||||||
 | 
					import '@fontsource/montserrat/600.css'
 | 
				
			||||||
 | 
					import './globals.css'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Providers } from '@/components/Providers'
 | 
				
			||||||
 | 
					import { Header } from '@/components/Header'
 | 
				
			||||||
 | 
					import { Footer } from '@/components/Footer'
 | 
				
			||||||
 | 
					import { getI18n } from '@/i18n/i18n.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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <html suppressHydrationWarning lang={i18n.locale}>
 | 
				
			||||||
 | 
					      <body className='bg-white font-headline text-black dark:bg-black dark:text-white'>
 | 
				
			||||||
 | 
					        <Providers>
 | 
				
			||||||
 | 
					          <Header showLocale />
 | 
				
			||||||
 | 
					          {children}
 | 
				
			||||||
 | 
					          <Footer />
 | 
				
			||||||
 | 
					        </Providers>
 | 
				
			||||||
 | 
					      </body>
 | 
				
			||||||
 | 
					    </html>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default RootLayout
 | 
				
			||||||
							
								
								
									
										11
									
								
								app/loading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/loading.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { Loader } from '@/components/Loader/Loader'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Loading = (): JSX.Element => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <main className='flex flex-col flex-1 items-center justify-center'>
 | 
				
			||||||
 | 
					      <Loader />
 | 
				
			||||||
 | 
					    </main>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Loading
 | 
				
			||||||
							
								
								
									
										12
									
								
								app/not-found.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/not-found.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { getI18n } from '@/i18n/i18n.server'
 | 
				
			||||||
 | 
					import { ErrorPage } from '@/components/ErrorPage'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NotFound = (): JSX.Element => {
 | 
				
			||||||
 | 
					  const i18n = getI18n()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ErrorPage statusCode={404} message={i18n.translate('errors.not-found')} />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default NotFound
 | 
				
			||||||
							
								
								
									
										59
									
								
								app/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								app/page.tsx
									
									
									
									
									
										Normal 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
 | 
				
			||||||
@@ -1,26 +1,20 @@
 | 
				
			|||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					 | 
				
			||||||
import Link from 'next/link'
 | 
					import Link from 'next/link'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { FooterProps } from './Footer'
 | 
					import { getI18n } from '@/i18n/i18n.server'
 | 
				
			||||||
import { Footer } from './Footer'
 | 
					 | 
				
			||||||
import { Header } from './Header'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ErrorPageProps extends FooterProps {
 | 
					export interface ErrorPageProps {
 | 
				
			||||||
  statusCode: number
 | 
					  statusCode: number
 | 
				
			||||||
  message: string
 | 
					  message: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
 | 
					export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
 | 
				
			||||||
  const { message, statusCode, version } = props
 | 
					  const { message, statusCode } = props
 | 
				
			||||||
  const { t } = useTranslation()
 | 
					  const i18n = getI18n()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <main className='flex flex-col flex-1 items-center justify-center'>
 | 
				
			||||||
      <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'>
 | 
					      <h1 className='my-6 text-4xl font-semibold'>
 | 
				
			||||||
            {t('errors:error')}{' '}
 | 
					        {i18n.translate('errors.error')}{' '}
 | 
				
			||||||
        <span
 | 
					        <span
 | 
				
			||||||
          className='text-yellow dark:text-yellow-dark'
 | 
					          className='text-yellow dark:text-yellow-dark'
 | 
				
			||||||
          data-cy='status-code'
 | 
					          data-cy='status-code'
 | 
				
			||||||
@@ -34,12 +28,9 @@ export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
 | 
				
			|||||||
          href='/'
 | 
					          href='/'
 | 
				
			||||||
          className='text-yellow hover:underline dark:text-yellow-dark'
 | 
					          className='text-yellow hover:underline dark:text-yellow-dark'
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
              {t('errors:return-to-home-page')}
 | 
					          {i18n.translate('errors.return-to-home-page')}
 | 
				
			||||||
        </Link>
 | 
					        </Link>
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
    </main>
 | 
					    </main>
 | 
				
			||||||
        <Footer version={version} />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										19
									
								
								components/Footer/FooterText.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								components/Footer/FooterText.tsx
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								components/Footer/FooterVersion.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								components/Footer/FooterVersion.tsx
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								components/Footer/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								components/Footer/index.tsx
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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>
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										30
									
								
								components/Header/Locales/LocaleFlag.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								components/Header/Locales/LocaleFlag.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import Image from 'next/image'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { CookiesStore } from '@/i18n/i18n.client'
 | 
				
			||||||
 | 
					import { useI18n } from '@/i18n/i18n.client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LocaleFlagProps {
 | 
				
			||||||
 | 
					  locale: string
 | 
				
			||||||
 | 
					  cookiesStore: CookiesStore
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const LocaleFlag: React.FC<LocaleFlagProps> = (props) => {
 | 
				
			||||||
 | 
					  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>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,17 +1,22 @@
 | 
				
			|||||||
'use client'
 | 
					'use client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useCallback, useEffect, useState, useRef } from 'react'
 | 
					import { useCallback, useEffect, useState, useRef } from 'react'
 | 
				
			||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					 | 
				
			||||||
import setLanguage from 'next-translate/setLanguage'
 | 
					 | 
				
			||||||
import classNames from 'clsx'
 | 
					import classNames from 'clsx'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import i18n from 'i18n.json'
 | 
					import { AVAILABLE_LOCALES } from '@/utils/constants'
 | 
				
			||||||
 | 
					import type { CookiesStore } from '@/i18n/i18n.client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Arrow } from './Arrow'
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Language: React.FC = () => {
 | 
					 | 
				
			||||||
  const { lang: currentLanguage } = useTranslation()
 | 
					 | 
				
			||||||
  const [hiddenMenu, setHiddenMenu] = useState(true)
 | 
					  const [hiddenMenu, setHiddenMenu] = useState(true)
 | 
				
			||||||
  const languageClickRef = useRef<HTMLDivElement | null>(null)
 | 
					  const languageClickRef = useRef<HTMLDivElement | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,42 +43,48 @@ export const Language: React.FC = () => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [])
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleLanguage = async (language: string): Promise<void> => {
 | 
					  const handleLocale = async (locale: string): Promise<void> => {
 | 
				
			||||||
    await setLanguage(language, false)
 | 
					    const { setLocale } = await import('@/i18n/i18n.server')
 | 
				
			||||||
 | 
					    setLocale(locale)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className='flex cursor-pointer flex-col items-center justify-center'>
 | 
					    <div className='flex cursor-pointer flex-col items-center justify-center'>
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        ref={languageClickRef}
 | 
					        ref={languageClickRef}
 | 
				
			||||||
        data-cy='language-click'
 | 
					        data-cy='locale-click'
 | 
				
			||||||
        className='mr-5 flex items-center'
 | 
					        className='mr-5 flex items-center'
 | 
				
			||||||
        onClick={handleHiddenMenu}
 | 
					        onClick={handleHiddenMenu}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <LanguageFlag language={currentLanguage} />
 | 
					        <LocaleFlag
 | 
				
			||||||
 | 
					          locale={currentLocale}
 | 
				
			||||||
 | 
					          cookiesStore={cookiesStore?.toString()}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <Arrow />
 | 
					        <Arrow />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <ul
 | 
					      <ul
 | 
				
			||||||
        data-cy='languages-list'
 | 
					        data-cy='locales-list'
 | 
				
			||||||
        className={classNames(
 | 
					        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 }
 | 
					          { hidden: hiddenMenu }
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {i18n.locales.map((language, index) => {
 | 
					        {AVAILABLE_LOCALES.filter((locale) => {
 | 
				
			||||||
          if (language === currentLanguage) {
 | 
					          return locale !== currentLocale
 | 
				
			||||||
            return <></>
 | 
					        }).map((locale) => {
 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <li
 | 
					            <li
 | 
				
			||||||
              key={index}
 | 
					              key={locale}
 | 
				
			||||||
              className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
 | 
					              className='flex h-12 w-full items-center justify-center hover:bg-[#4f545c] hover:bg-opacity-20'
 | 
				
			||||||
              onClick={async () => {
 | 
					              onClick={async () => {
 | 
				
			||||||
                return await handleLanguage(language)
 | 
					                return await handleLocale(locale)
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <LanguageFlag language={language} />
 | 
					              <LocaleFlag
 | 
				
			||||||
 | 
					                locale={locale}
 | 
				
			||||||
 | 
					                cookiesStore={cookiesStore?.toString()}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
            </li>
 | 
					            </li>
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        })}
 | 
					        })}
 | 
				
			||||||
@@ -71,7 +71,7 @@ export const SwitchTheme: React.FC = () => {
 | 
				
			|||||||
          data-cy='switch-theme-input'
 | 
					          data-cy='switch-theme-input'
 | 
				
			||||||
          type='checkbox'
 | 
					          type='checkbox'
 | 
				
			||||||
          aria-label='Dark mode toggle'
 | 
					          aria-label='Dark mode toggle'
 | 
				
			||||||
          className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0'
 | 
					          className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0 hidden'
 | 
				
			||||||
          defaultChecked
 | 
					          defaultChecked
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,21 @@
 | 
				
			|||||||
 | 
					import { cookies } from 'next/headers'
 | 
				
			||||||
import Link from 'next/link'
 | 
					import Link from 'next/link'
 | 
				
			||||||
import Image from 'next/image'
 | 
					import Image from 'next/image'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Language } from './Language'
 | 
					import { getI18n } from '@/i18n/i18n.server'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Locales } from './Locales'
 | 
				
			||||||
import { SwitchTheme } from './SwitchTheme'
 | 
					import { SwitchTheme } from './SwitchTheme'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface HeaderProps {
 | 
					export interface HeaderProps {
 | 
				
			||||||
  showLanguage?: boolean
 | 
					  showLocale?: boolean
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Header: React.FC<HeaderProps> = (props) => {
 | 
					export const Header: React.FC<HeaderProps> = (props) => {
 | 
				
			||||||
  const { showLanguage = false } = props
 | 
					  const { showLocale = false } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const cookiesStore = cookies()
 | 
				
			||||||
 | 
					  const i18n = getI18n()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
 | 
					    <header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
 | 
				
			||||||
@@ -38,7 +44,12 @@ export const Header: React.FC<HeaderProps> = (props) => {
 | 
				
			|||||||
            Blog
 | 
					            Blog
 | 
				
			||||||
          </Link>
 | 
					          </Link>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        {showLanguage ? <Language /> : null}
 | 
					        {showLocale ? (
 | 
				
			||||||
 | 
					          <Locales
 | 
				
			||||||
 | 
					            currentLocale={i18n.locale}
 | 
				
			||||||
 | 
					            cookiesStore={cookiesStore.toString()}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        ) : null}
 | 
				
			||||||
        <SwitchTheme />
 | 
					        <SwitchTheme />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </header>
 | 
					    </header>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,18 @@
 | 
				
			|||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					import { getI18n } from '@/i18n/i18n.server'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { InterestParagraphProps } from './InterestParagraph'
 | 
					import type { InterestParagraphProps } from './InterestParagraph'
 | 
				
			||||||
import { InterestParagraph } from './InterestParagraph'
 | 
					import { InterestParagraph } from './InterestParagraph'
 | 
				
			||||||
import { InterestsList } from './InterestsList'
 | 
					import { InterestsList } from './InterestsList'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Interests: React.FC = () => {
 | 
					export const Interests: React.FC = () => {
 | 
				
			||||||
  const { t } = useTranslation()
 | 
					  const i18n = getI18n()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const paragraphs: InterestParagraphProps[] = t(
 | 
					  let paragraphs = i18n.translate<InterestParagraphProps[]>(
 | 
				
			||||||
    'home:interests.paragraphs',
 | 
					    'home.interests.paragraphs'
 | 
				
			||||||
    {},
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      returnObjects: true
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					  if (!Array.isArray(paragraphs)) {
 | 
				
			||||||
 | 
					    paragraphs = []
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className='max-w-full'>
 | 
					    <div className='max-w-full'>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										39
									
								
								components/Loader/Loader.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								components/Loader/Loader.module.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					@keyframes progressSpinnerRotate {
 | 
				
			||||||
 | 
					  100% {
 | 
				
			||||||
 | 
					    transform: rotate(360deg);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@keyframes progressSpinnerDash {
 | 
				
			||||||
 | 
					  0% {
 | 
				
			||||||
 | 
					    stroke-dasharray: 1, 200;
 | 
				
			||||||
 | 
					    stroke-dashoffset: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  50% {
 | 
				
			||||||
 | 
					    stroke-dasharray: 89, 200;
 | 
				
			||||||
 | 
					    stroke-dashoffset: -35px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  100% {
 | 
				
			||||||
 | 
					    stroke-dasharray: 89, 200;
 | 
				
			||||||
 | 
					    stroke-dashoffset: -124px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.progressSpinnerSvg {
 | 
				
			||||||
 | 
					  animation: progressSpinnerRotate 2s linear infinite;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  transform-origin: center center;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  bottom: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  margin: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.progressSpinnerCircle {
 | 
				
			||||||
 | 
					  stroke-dasharray: 89, 200;
 | 
				
			||||||
 | 
					  stroke-dashoffset: 0;
 | 
				
			||||||
 | 
					  stroke: #ffd800;
 | 
				
			||||||
 | 
					  animation: progressSpinnerDash 1.5s ease-in-out infinite;
 | 
				
			||||||
 | 
					  stroke-linecap: round;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										33
									
								
								components/Loader/Loader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								components/Loader/Loader.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					import styles from './Loader.module.css'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface LoaderProps {
 | 
				
			||||||
 | 
					  width?: number
 | 
				
			||||||
 | 
					  height?: number
 | 
				
			||||||
 | 
					  className?: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Loader: React.FC<LoaderProps> = (props) => {
 | 
				
			||||||
 | 
					  const { width = 50, height = 50, className } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={className}>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        data-cy='progress-spinner'
 | 
				
			||||||
 | 
					        className='relative mx-auto my-0 before:block before:pt-[100%] before:content-none'
 | 
				
			||||||
 | 
					        style={{ width: `${width}px`, height: `${height}px` }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <svg className={styles['progressSpinnerSvg']} viewBox='25 25 50 50'>
 | 
				
			||||||
 | 
					          <circle
 | 
				
			||||||
 | 
					            className={styles['progressSpinnerCircle']}
 | 
				
			||||||
 | 
					            cx='50'
 | 
				
			||||||
 | 
					            cy='50'
 | 
				
			||||||
 | 
					            r='20'
 | 
				
			||||||
 | 
					            fill='none'
 | 
				
			||||||
 | 
					            strokeWidth='2'
 | 
				
			||||||
 | 
					            strokeMiterlimit='10'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </svg>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,13 +1,15 @@
 | 
				
			|||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					import { getI18n } from '@/i18n/i18n.server'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Repository } from './Repository'
 | 
					import { Repository } from './Repository'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const OpenSource: React.FC = () => {
 | 
					export const OpenSource: React.FC = () => {
 | 
				
			||||||
  const { t } = useTranslation()
 | 
					  const i18n = getI18n()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className='mt-0 flex max-w-full flex-col items-center'>
 | 
					    <div className='mt-0 flex max-w-full flex-col items-center'>
 | 
				
			||||||
      <p className='text-center'>{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'>
 | 
					      <div className='my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2'>
 | 
				
			||||||
        <Repository
 | 
					        <Repository
 | 
				
			||||||
          name='nodejs/node'
 | 
					          name='nodejs/node'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,15 @@
 | 
				
			|||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					import { getI18n } from '@/i18n/i18n.server'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { PortfolioItemProps } from './PortfolioItem'
 | 
					import type { PortfolioItemProps } from './PortfolioItem'
 | 
				
			||||||
import { PortfolioItem } from './PortfolioItem'
 | 
					import { PortfolioItem } from './PortfolioItem'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Portfolio: React.FC = () => {
 | 
					export const Portfolio: React.FC = () => {
 | 
				
			||||||
  const { t } = useTranslation('home')
 | 
					  const i18n = getI18n()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const items: PortfolioItemProps[] = t(
 | 
					  let items = i18n.translate<PortfolioItemProps[]>('home.portfolio.items')
 | 
				
			||||||
    'home:portfolio.items',
 | 
					  if (!Array.isArray(items)) {
 | 
				
			||||||
    {},
 | 
					    items = []
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      returnObjects: true
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className='flex w-full flex-wrap justify-center px-3'>
 | 
					    <div className='flex w-full flex-wrap justify-center px-3'>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					import { getI18n } from '@/i18n/i18n.server'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ProfileDescriptionBottom: React.FC = () => {
 | 
					export const ProfileDescriptionBottom: React.FC = () => {
 | 
				
			||||||
  const { t, lang } = useTranslation()
 | 
					  const i18n = getI18n()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'>
 | 
					    <p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'>
 | 
				
			||||||
      {t('home:about.description-bottom')}
 | 
					      {i18n.translate('home.about.description-bottom')}
 | 
				
			||||||
      {lang === 'fr' ? (
 | 
					      {i18n.locale === 'fr-FR' ? (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
          <br />
 | 
					          <br />
 | 
				
			||||||
          <br />
 | 
					          <br />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,16 @@
 | 
				
			|||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					import { getI18n } from '@/i18n/i18n.server'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ProfileInformation: React.FC = () => {
 | 
					export const ProfileInformation: React.FC = () => {
 | 
				
			||||||
  const { t } = useTranslation()
 | 
					  const i18n = getI18n()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'>
 | 
					    <div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'>
 | 
				
			||||||
      <h1 className='mb-2 text-4xl font-semibold text-yellow dark:text-yellow-dark'>
 | 
					      <h1 className='mb-2 text-4xl font-semibold text-yellow dark:text-yellow-dark'>
 | 
				
			||||||
        Théo LUDWIG
 | 
					        Théo LUDWIG
 | 
				
			||||||
      </h1>
 | 
					      </h1>
 | 
				
			||||||
      <h2 className='mb-3 text-base'>{t('home:about.description')}</h2>
 | 
					      <h2 className='mb-3 text-base'>
 | 
				
			||||||
 | 
					        {i18n.translate('home.about.description')}
 | 
				
			||||||
 | 
					      </h2>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,21 @@
 | 
				
			|||||||
'use client'
 | 
					'use client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					 | 
				
			||||||
import { useMemo } from 'react'
 | 
					import { useMemo } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { CookiesStore } from '@/i18n/i18n.client'
 | 
				
			||||||
 | 
					import { useI18n } from '@/i18n/i18n.client'
 | 
				
			||||||
import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from '@/utils/getAge'
 | 
					import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from '@/utils/getAge'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ProfileItem } from './ProfileItem'
 | 
					import { ProfileItem } from './ProfileItem'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ProfileList: React.FC = () => {
 | 
					export interface ProfileListProps {
 | 
				
			||||||
  const { t } = useTranslation('home')
 | 
					  cookiesStore: CookiesStore
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ProfileList = (props: ProfileListProps): JSX.Element => {
 | 
				
			||||||
 | 
					  const { cookiesStore } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const i18n = useI18n(cookiesStore)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const age = useMemo(() => {
 | 
					  const age = useMemo(() => {
 | 
				
			||||||
    return getAge(BIRTH_DATE)
 | 
					    return getAge(BIRTH_DATE)
 | 
				
			||||||
@@ -17,14 +24,19 @@ export const ProfileList: React.FC = () => {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ul className='m-0 list-none p-0'>
 | 
					    <ul className='m-0 list-none p-0'>
 | 
				
			||||||
      <ProfileItem
 | 
					      <ProfileItem
 | 
				
			||||||
        title={t('home:about.pronouns')}
 | 
					        title={i18n.translate('home.about.pronouns')}
 | 
				
			||||||
        value={t('home:about.pronouns-value')}
 | 
					        value={i18n.translate('home.about.pronouns-value')}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <ProfileItem
 | 
					      <ProfileItem
 | 
				
			||||||
        title={t('home:about.birth-date')}
 | 
					        title={i18n.translate('home.about.birth-date')}
 | 
				
			||||||
        value={`${BIRTH_DATE_STRING} (${age} ${t('home:about.years-old')})`}
 | 
					        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
 | 
					      <ProfileItem
 | 
				
			||||||
        title='Email'
 | 
					        title='Email'
 | 
				
			||||||
        value='contact@theoludwig.fr'
 | 
					        value='contact@theoludwig.fr'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,19 @@
 | 
				
			|||||||
 | 
					import { cookies } from 'next/headers'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
 | 
					import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
 | 
				
			||||||
import { ProfileInformation } from './ProfileInfo'
 | 
					import { ProfileInformation } from './ProfileInfo'
 | 
				
			||||||
import { ProfileList } from './ProfileList'
 | 
					import { ProfileList } from './ProfileList'
 | 
				
			||||||
import { ProfileLogo } from './ProfileLogo'
 | 
					import { ProfileLogo } from './ProfileLogo'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Profile: React.FC = () => {
 | 
					export const Profile: React.FC = () => {
 | 
				
			||||||
 | 
					  const cookiesStore = cookies()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className='flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10'>
 | 
					    <div className='flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10'>
 | 
				
			||||||
      <ProfileLogo />
 | 
					      <ProfileLogo />
 | 
				
			||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
        <ProfileInformation />
 | 
					        <ProfileInformation />
 | 
				
			||||||
        <ProfileList />
 | 
					        <ProfileList cookiesStore={cookiesStore.toString()} />
 | 
				
			||||||
        <ProfileDescriptionBottom />
 | 
					        <ProfileDescriptionBottom />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								components/Providers.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								components/Providers.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					'use client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ThemeProvider } from 'next-themes'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ProvidersProps = React.PropsWithChildren
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Providers = (props: ProvidersProps): JSX.Element => {
 | 
				
			||||||
 | 
					  const { children } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ThemeProvider attribute='class' defaultTheme='dark'>
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					    </ThemeProvider>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
 | 
					'use client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useState, useEffect, useMemo } from 'react'
 | 
				
			||||||
import { useTheme } from 'next-themes'
 | 
					import { useTheme } from 'next-themes'
 | 
				
			||||||
import Image from 'next/image'
 | 
					import Image from 'next/image'
 | 
				
			||||||
import { useMemo } from 'react'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { SkillName } from './skills'
 | 
					import type { SkillName } from './skills'
 | 
				
			||||||
import { skills } from './skills'
 | 
					import { skills } from './skills'
 | 
				
			||||||
@@ -11,18 +13,31 @@ export interface SkillComponentProps {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
 | 
					export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
 | 
				
			||||||
  const { skill } = props
 | 
					  const { skill } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const skillProperties = skills[skill]
 | 
					  const skillProperties = skills[skill]
 | 
				
			||||||
 | 
					  const [mounted, setMounted] = useState(false)
 | 
				
			||||||
  const { theme } = useTheme()
 | 
					  const { theme } = useTheme()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setMounted(true)
 | 
				
			||||||
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const image = useMemo(() => {
 | 
					  const image = useMemo(() => {
 | 
				
			||||||
    if (typeof skillProperties.image === 'string') {
 | 
					    if (typeof skillProperties.image === 'string') {
 | 
				
			||||||
      return skillProperties.image
 | 
					      return skillProperties.image
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (!mounted) {
 | 
				
			||||||
 | 
					      return skillProperties.image.dark
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (theme === 'light') {
 | 
					    if (theme === 'light') {
 | 
				
			||||||
      return skillProperties.image.light
 | 
					      return skillProperties.image.light
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return skillProperties.image.dark
 | 
					    return skillProperties.image.dark
 | 
				
			||||||
  }, [skillProperties, theme])
 | 
					  }, [skillProperties, theme, mounted])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!mounted) {
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <a
 | 
					    <a
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,14 @@
 | 
				
			|||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					import { getI18n } from '@/i18n/i18n.server'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { SkillComponent } from './Skill'
 | 
					import { SkillComponent } from './Skill'
 | 
				
			||||||
import { SkillsSection } from './SkillsSection'
 | 
					import { SkillsSection } from './SkillsSection'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Skills: React.FC = () => {
 | 
					export const Skills: React.FC = () => {
 | 
				
			||||||
  const { t } = useTranslation()
 | 
					  const i18n = getI18n()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <SkillsSection title={t('home:skills.languages')}>
 | 
					      <SkillsSection title={i18n.translate('home.skills.languages')}>
 | 
				
			||||||
        <SkillComponent skill='TypeScript' />
 | 
					        <SkillComponent skill='TypeScript' />
 | 
				
			||||||
        <SkillComponent skill='Python' />
 | 
					        <SkillComponent skill='Python' />
 | 
				
			||||||
        <SkillComponent skill='C/C++' />
 | 
					        <SkillComponent skill='C/C++' />
 | 
				
			||||||
@@ -29,7 +29,7 @@ export const Skills: React.FC = () => {
 | 
				
			|||||||
        <SkillComponent skill='PostgreSQL' />
 | 
					        <SkillComponent skill='PostgreSQL' />
 | 
				
			||||||
      </SkillsSection>
 | 
					      </SkillsSection>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <SkillsSection title={t('home:skills.software-tools')}>
 | 
					      <SkillsSection title={i18n.translate('home.skills.software-tools')}>
 | 
				
			||||||
        <SkillComponent skill='GNU/Linux' />
 | 
					        <SkillComponent skill='GNU/Linux' />
 | 
				
			||||||
        <SkillComponent skill='Arch Linux' />
 | 
					        <SkillComponent skill='Arch Linux' />
 | 
				
			||||||
        <SkillComponent skill='Visual Studio Code' />
 | 
					        <SkillComponent skill='Visual Studio Code' />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ import { createHtmlPlugin } from 'vite-plugin-html'
 | 
				
			|||||||
import date from 'date-and-time'
 | 
					import date from 'date-and-time'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jsonCurriculumVitaeURL = new URL(
 | 
					const jsonCurriculumVitaeURL = new URL(
 | 
				
			||||||
  '../curriculum-vitae.jsonc',
 | 
					  './curriculum-vitae.jsonc',
 | 
				
			||||||
  import.meta.url
 | 
					  import.meta.url
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
const dataCurriculumVitaeStringJSON = await fs.promises.readFile(
 | 
					const dataCurriculumVitaeStringJSON = await fs.promises.readFile(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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}`
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
@@ -37,24 +37,24 @@ describe('Common > Header', () => {
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('Switch Language', () => {
 | 
					  describe('Switch Language', () => {
 | 
				
			||||||
    it('should switch language from EN (default) to FR', () => {
 | 
					    it('should switch locale from EN (default) to FR', () => {
 | 
				
			||||||
      cy.get('h1').contains('Théo LUDWIG')
 | 
					      cy.get('h1').contains('Théo LUDWIG')
 | 
				
			||||||
      cy.get('[data-cy=language-flag-text]').contains('EN')
 | 
					      cy.get('[data-cy=locale-flag-text]').contains('EN')
 | 
				
			||||||
      cy.get('[data-cy=languages-list]').should('not.be.visible')
 | 
					      cy.get('[data-cy=locales-list]').should('not.be.visible')
 | 
				
			||||||
      cy.get('[data-cy=language-click]').click()
 | 
					      cy.get('[data-cy=locale-click]').click()
 | 
				
			||||||
      cy.get('[data-cy=languages-list]').should('be.visible')
 | 
					      cy.get('[data-cy=locales-list]').should('be.visible')
 | 
				
			||||||
      cy.get('[data-cy=languages-list] > li:first-child').contains('FR').click()
 | 
					      cy.get('[data-cy=locales-list] > li:first-child').contains('FR').click()
 | 
				
			||||||
      cy.get('[data-cy=languages-list]').should('not.be.visible')
 | 
					      cy.get('[data-cy=locales-list]').should('not.be.visible')
 | 
				
			||||||
      cy.get('[data-cy=language-flag-text]').contains('FR')
 | 
					      cy.get('[data-cy=locale-flag-text]').contains('FR')
 | 
				
			||||||
      cy.get('h1').contains('Théo LUDWIG')
 | 
					      cy.get('h1').contains('Théo LUDWIG')
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should close the language list menu when clicking outside', () => {
 | 
					    it('should close the locale list menu when clicking outside', () => {
 | 
				
			||||||
      cy.get('[data-cy=languages-list]').should('not.be.visible')
 | 
					      cy.get('[data-cy=locales-list]').should('not.be.visible')
 | 
				
			||||||
      cy.get('[data-cy=language-click]').click()
 | 
					      cy.get('[data-cy=locale-click]').click()
 | 
				
			||||||
      cy.get('[data-cy=languages-list]').should('be.visible')
 | 
					      cy.get('[data-cy=locales-list]').should('be.visible')
 | 
				
			||||||
      cy.get('h1').click()
 | 
					      cy.get('h1').click()
 | 
				
			||||||
      cy.get('[data-cy=languages-list]').should('not.be.visible')
 | 
					      cy.get('[data-cy=locales-list]').should('not.be.visible')
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
describe('Page /blog/[slug]', () => {
 | 
					describe('Page /blog/[slug]', () => {
 | 
				
			||||||
  it('should displays the first blog post (`hello-world`)', () => {
 | 
					  it('should displays the first blog post (`hello-world`)', () => {
 | 
				
			||||||
    cy.visit('/blog/hello-world')
 | 
					    cy.visit('/blog/hello-world')
 | 
				
			||||||
    cy.get('[data-cy=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('h1').should('have.text', '👋 Hello, world!')
 | 
				
			||||||
    cy.get('.prose a:visible').should('have.attr', 'target', '_blank')
 | 
					    cy.get('.prose a:visible').should('have.attr', 'target', '_blank')
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								i18n.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								i18n.json
									
									
									
									
									
								
							@@ -1,10 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "locales": ["en", "fr"],
 | 
					 | 
				
			||||||
  "defaultLocale": "en",
 | 
					 | 
				
			||||||
  "pages": {
 | 
					 | 
				
			||||||
    "*": ["common"],
 | 
					 | 
				
			||||||
    "/": ["home"],
 | 
					 | 
				
			||||||
    "/404": ["errors"],
 | 
					 | 
				
			||||||
    "/500": ["errors"]
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										12
									
								
								i18n/i18n.client.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								i18n/i18n.client.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import UniversalCookie from 'universal-cookie'
 | 
				
			||||||
 | 
					import type { I18n } from 'i18n-js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { i18n } from './i18n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type CookiesStore = string | object | null | undefined
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useI18n = (cookiesStore: CookiesStore): I18n => {
 | 
				
			||||||
 | 
					  const universalCookie = new UniversalCookie(cookiesStore)
 | 
				
			||||||
 | 
					  i18n.locale = universalCookie.get('locale') ?? i18n.defaultLocale
 | 
				
			||||||
 | 
					  return i18n
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								i18n/i18n.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								i18n/i18n.server.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					'use server'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cookies } from 'next/headers'
 | 
				
			||||||
 | 
					import type { I18n } from 'i18n-js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { COOKIE_MAX_AGE } from '@/utils/constants'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { i18n } from './i18n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const setLocale = (locale: string): void => {
 | 
				
			||||||
 | 
					  cookies().set('locale', locale, {
 | 
				
			||||||
 | 
					    path: '/',
 | 
				
			||||||
 | 
					    maxAge: COOKIE_MAX_AGE
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getI18n = (): I18n => {
 | 
				
			||||||
 | 
					  i18n.locale = cookies().get('locale')?.value ?? i18n.defaultLocale
 | 
				
			||||||
 | 
					  return i18n
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								i18n/i18n.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								i18n/i18n.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import { I18n } from 'i18n-js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DEFAULT_LOCALE, AVAILABLE_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'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const i18nOptions = {
 | 
				
			||||||
 | 
					  defaultLocale: DEFAULT_LOCALE,
 | 
				
			||||||
 | 
					  availableLocales: AVAILABLE_LOCALES.slice(),
 | 
				
			||||||
 | 
					  enableFallback: true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const i18n = new I18n(
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    'en-US': {
 | 
				
			||||||
 | 
					      common: commonEnglish,
 | 
				
			||||||
 | 
					      errors: errorsEnglish,
 | 
				
			||||||
 | 
					      home: homeEnglish
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    'fr-FR': {
 | 
				
			||||||
 | 
					      common: commonFrench,
 | 
				
			||||||
 | 
					      errors: errorsFrench,
 | 
				
			||||||
 | 
					      home: homeFrench
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  i18nOptions
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "english": "English",
 | 
					  "en-US": "English",
 | 
				
			||||||
  "french": "French",
 | 
					  "fr-FR": "French",
 | 
				
			||||||
  "all-rights-reserved": "All rights reserved",
 | 
					  "all-rights-reserved": "All rights reserved",
 | 
				
			||||||
  "home": "Home"
 | 
					  "home": "Home"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "english": "Anglais",
 | 
					  "en-US": "Anglais",
 | 
				
			||||||
  "french": "Français",
 | 
					  "fr-FR": "Français",
 | 
				
			||||||
  "all-rights-reserved": "Tous droits réservés",
 | 
					  "all-rights-reserved": "Tous droits réservés",
 | 
				
			||||||
  "home": "Accueil"
 | 
					  "home": "Accueil"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										53
									
								
								middleware.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								middleware.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					import { NextResponse } from 'next/server'
 | 
				
			||||||
 | 
					import type { NextRequest } from 'next/server'
 | 
				
			||||||
 | 
					import { match } from '@formatjs/intl-localematcher'
 | 
				
			||||||
 | 
					import Negotiator from 'negotiator'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { AvailableLocale } from '@/utils/constants'
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  COOKIE_MAX_AGE,
 | 
				
			||||||
 | 
					  DEFAULT_LOCALE,
 | 
				
			||||||
 | 
					  AVAILABLE_LOCALES
 | 
				
			||||||
 | 
					} from '@/utils/constants'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const middleware = (request: NextRequest): NextResponse => {
 | 
				
			||||||
 | 
					  const response = NextResponse.next()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let locale = request.cookies.get('locale')?.value
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    locale == null ||
 | 
				
			||||||
 | 
					    !AVAILABLE_LOCALES.includes(locale as AvailableLocale)
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const headers = {
 | 
				
			||||||
 | 
					      'accept-language':
 | 
				
			||||||
 | 
					        request.headers.get('accept-language') ?? DEFAULT_LOCALE
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const languages = new Negotiator({ headers }).languages()
 | 
				
			||||||
 | 
					    locale = match(languages, AVAILABLE_LOCALES.slice(), DEFAULT_LOCALE)
 | 
				
			||||||
 | 
					    response.cookies.set('locale', locale, {
 | 
				
			||||||
 | 
					      path: '/',
 | 
				
			||||||
 | 
					      maxAge: COOKIE_MAX_AGE
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const theme = request.cookies.get('theme')?.value ?? 'dark'
 | 
				
			||||||
 | 
					  response.cookies.set('theme', theme, {
 | 
				
			||||||
 | 
					    path: '/',
 | 
				
			||||||
 | 
					    expires: COOKIE_MAX_AGE
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return response
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const config = {
 | 
				
			||||||
 | 
					  matcher: [
 | 
				
			||||||
 | 
					    /*
 | 
				
			||||||
 | 
					     * Match all request paths except for the ones starting with:
 | 
				
			||||||
 | 
					     * - api (API routes)
 | 
				
			||||||
 | 
					     * - _next/static (static files)
 | 
				
			||||||
 | 
					     * - _next/image (image optimization files)
 | 
				
			||||||
 | 
					     * - favicon.ico (favicon file)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    '/((?!api|_next/static|_next/image|favicon.ico).*)'
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,9 +1,10 @@
 | 
				
			|||||||
const nextTranslate = require('next-translate-plugin')
 | 
					/** @type {import('next').NextConfig} */
 | 
				
			||||||
 | 
					 | 
				
			||||||
/** @type {import("next").NextConfig} */
 | 
					 | 
				
			||||||
const nextConfig = {
 | 
					const nextConfig = {
 | 
				
			||||||
  reactStrictMode: true,
 | 
					  reactStrictMode: true,
 | 
				
			||||||
  output: 'standalone'
 | 
					  output: 'standalone',
 | 
				
			||||||
 | 
					  experimental: {
 | 
				
			||||||
 | 
					    serverActions: true
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = nextTranslate(nextConfig)
 | 
					module.exports = nextConfig
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										185
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										185
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -10,6 +10,7 @@
 | 
				
			|||||||
      "hasInstallScript": true,
 | 
					      "hasInstallScript": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@fontsource/montserrat": "5.0.5",
 | 
					        "@fontsource/montserrat": "5.0.5",
 | 
				
			||||||
 | 
					        "@formatjs/intl-localematcher": "0.4.0",
 | 
				
			||||||
        "@fortawesome/fontawesome-svg-core": "6.4.0",
 | 
					        "@fortawesome/fontawesome-svg-core": "6.4.0",
 | 
				
			||||||
        "@fortawesome/free-brands-svg-icons": "6.4.0",
 | 
					        "@fortawesome/free-brands-svg-icons": "6.4.0",
 | 
				
			||||||
        "@fortawesome/free-solid-svg-icons": "6.4.0",
 | 
					        "@fortawesome/free-solid-svg-icons": "6.4.0",
 | 
				
			||||||
@@ -19,11 +20,12 @@
 | 
				
			|||||||
        "date-and-time": "3.0.2",
 | 
					        "date-and-time": "3.0.2",
 | 
				
			||||||
        "gray-matter": "4.0.3",
 | 
					        "gray-matter": "4.0.3",
 | 
				
			||||||
        "html-react-parser": "4.2.0",
 | 
					        "html-react-parser": "4.2.0",
 | 
				
			||||||
 | 
					        "i18n-js": "4.3.0",
 | 
				
			||||||
        "katex": "0.16.8",
 | 
					        "katex": "0.16.8",
 | 
				
			||||||
 | 
					        "negotiator": "0.6.3",
 | 
				
			||||||
        "next": "13.4.12",
 | 
					        "next": "13.4.12",
 | 
				
			||||||
        "next-mdx-remote": "4.4.1",
 | 
					        "next-mdx-remote": "4.4.1",
 | 
				
			||||||
        "next-themes": "0.2.1",
 | 
					        "next-themes": "0.2.1",
 | 
				
			||||||
        "next-translate": "2.5.2",
 | 
					 | 
				
			||||||
        "react": "18.2.0",
 | 
					        "react": "18.2.0",
 | 
				
			||||||
        "react-dom": "18.2.0",
 | 
					        "react-dom": "18.2.0",
 | 
				
			||||||
        "read-pkg": "8.0.0",
 | 
					        "read-pkg": "8.0.0",
 | 
				
			||||||
@@ -45,6 +47,7 @@
 | 
				
			|||||||
        "@semantic-release/git": "10.0.1",
 | 
					        "@semantic-release/git": "10.0.1",
 | 
				
			||||||
        "@tailwindcss/typography": "0.5.9",
 | 
					        "@tailwindcss/typography": "0.5.9",
 | 
				
			||||||
        "@tsconfig/strictest": "2.0.1",
 | 
					        "@tsconfig/strictest": "2.0.1",
 | 
				
			||||||
 | 
					        "@types/negotiator": "0.6.1",
 | 
				
			||||||
        "@types/node": "20.4.5",
 | 
					        "@types/node": "20.4.5",
 | 
				
			||||||
        "@types/react": "18.2.17",
 | 
					        "@types/react": "18.2.17",
 | 
				
			||||||
        "@types/unist": "3.0.0",
 | 
					        "@types/unist": "3.0.0",
 | 
				
			||||||
@@ -54,7 +57,7 @@
 | 
				
			|||||||
        "curriculum-vitae": "file:./curriculum-vitae",
 | 
					        "curriculum-vitae": "file:./curriculum-vitae",
 | 
				
			||||||
        "cypress": "12.17.2",
 | 
					        "cypress": "12.17.2",
 | 
				
			||||||
        "editorconfig-checker": "5.1.1",
 | 
					        "editorconfig-checker": "5.1.1",
 | 
				
			||||||
        "eslint": "8.45.0",
 | 
					        "eslint": "8.46.0",
 | 
				
			||||||
        "eslint-config-conventions": "11.0.1",
 | 
					        "eslint-config-conventions": "11.0.1",
 | 
				
			||||||
        "eslint-config-next": "13.4.12",
 | 
					        "eslint-config-next": "13.4.12",
 | 
				
			||||||
        "eslint-config-prettier": "8.9.0",
 | 
					        "eslint-config-prettier": "8.9.0",
 | 
				
			||||||
@@ -67,7 +70,6 @@
 | 
				
			|||||||
        "lint-staged": "13.2.3",
 | 
					        "lint-staged": "13.2.3",
 | 
				
			||||||
        "markdownlint-cli2": "0.8.1",
 | 
					        "markdownlint-cli2": "0.8.1",
 | 
				
			||||||
        "markdownlint-rule-relative-links": "2.1.0",
 | 
					        "markdownlint-rule-relative-links": "2.1.0",
 | 
				
			||||||
        "next-translate-plugin": "2.5.2",
 | 
					 | 
				
			||||||
        "postcss": "8.4.27",
 | 
					        "postcss": "8.4.27",
 | 
				
			||||||
        "prettier": "3.0.0",
 | 
					        "prettier": "3.0.0",
 | 
				
			||||||
        "prettier-plugin-tailwindcss": "0.4.1",
 | 
					        "prettier-plugin-tailwindcss": "0.4.1",
 | 
				
			||||||
@@ -927,9 +929,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@eslint/eslintrc": {
 | 
					    "node_modules/@eslint/eslintrc": {
 | 
				
			||||||
      "version": "2.1.0",
 | 
					      "version": "2.1.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==",
 | 
					      "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "ajv": "^6.12.4",
 | 
					        "ajv": "^6.12.4",
 | 
				
			||||||
@@ -972,9 +974,9 @@
 | 
				
			|||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@eslint/js": {
 | 
					    "node_modules/@eslint/js": {
 | 
				
			||||||
      "version": "8.44.0",
 | 
					      "version": "8.46.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==",
 | 
					      "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
 | 
					        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
 | 
				
			||||||
@@ -985,6 +987,14 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/@fontsource/montserrat/-/montserrat-5.0.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@fontsource/montserrat/-/montserrat-5.0.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-kR4hwC44p0GZQsy1oA2X8EdFMg/lC7qwVX9jNgNBQ+uSZXKoV4Vyc1VepcRAcGRKPIgcc/3OC1j7FNQDYpFLJA=="
 | 
					      "integrity": "sha512-kR4hwC44p0GZQsy1oA2X8EdFMg/lC7qwVX9jNgNBQ+uSZXKoV4Vyc1VepcRAcGRKPIgcc/3OC1j7FNQDYpFLJA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@formatjs/intl-localematcher": {
 | 
				
			||||||
 | 
					      "version": "0.4.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "tslib": "^2.4.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@fortawesome/fontawesome-common-types": {
 | 
					    "node_modules/@fortawesome/fontawesome-common-types": {
 | 
				
			||||||
      "version": "6.4.0",
 | 
					      "version": "6.4.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
 | 
				
			||||||
@@ -2460,6 +2470,12 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
 | 
				
			||||||
      "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
 | 
					      "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/negotiator": {
 | 
				
			||||||
 | 
					      "version": "0.6.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-c4mvXFByghezQ/eVGN5HvH/jI63vm3B7FiE81BUzDAWmuiohRecCO6ddU60dfq29oKUMiQujsoB2h0JQC7JHKA==",
 | 
				
			||||||
 | 
					      "dev": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/node": {
 | 
					    "node_modules/@types/node": {
 | 
				
			||||||
      "version": "20.4.5",
 | 
					      "version": "20.4.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz",
 | 
				
			||||||
@@ -3345,6 +3361,14 @@
 | 
				
			|||||||
        "node": ">=0.6"
 | 
					        "node": ">=0.6"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/bignumber.js": {
 | 
				
			||||||
 | 
					      "version": "9.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/binary-extensions": {
 | 
					    "node_modules/binary-extensions": {
 | 
				
			||||||
      "version": "2.2.0",
 | 
					      "version": "2.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
 | 
				
			||||||
@@ -3462,9 +3486,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/browserslist": {
 | 
					    "node_modules/browserslist": {
 | 
				
			||||||
      "version": "4.21.9",
 | 
					      "version": "4.21.10",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
 | 
				
			||||||
      "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
 | 
					      "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "funding": [
 | 
					      "funding": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -3481,9 +3505,9 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "caniuse-lite": "^1.0.30001503",
 | 
					        "caniuse-lite": "^1.0.30001517",
 | 
				
			||||||
        "electron-to-chromium": "^1.4.431",
 | 
					        "electron-to-chromium": "^1.4.477",
 | 
				
			||||||
        "node-releases": "^2.0.12",
 | 
					        "node-releases": "^2.0.13",
 | 
				
			||||||
        "update-browserslist-db": "^1.0.11"
 | 
					        "update-browserslist-db": "^1.0.11"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
@@ -3646,9 +3670,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/caniuse-lite": {
 | 
					    "node_modules/caniuse-lite": {
 | 
				
			||||||
      "version": "1.0.30001517",
 | 
					      "version": "1.0.30001518",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001518.tgz",
 | 
				
			||||||
      "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==",
 | 
					      "integrity": "sha512-rup09/e3I0BKjncL+FesTayKtPrdwKhUufQFd3riFw1hHg8JmIFoInYfB102cFcY/pPgGmdyl/iy+jgiDi2vdA==",
 | 
				
			||||||
      "funding": [
 | 
					      "funding": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          "type": "opencollective",
 | 
					          "type": "opencollective",
 | 
				
			||||||
@@ -5134,9 +5158,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/electron-to-chromium": {
 | 
					    "node_modules/electron-to-chromium": {
 | 
				
			||||||
      "version": "1.4.475",
 | 
					      "version": "1.4.477",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.475.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.477.tgz",
 | 
				
			||||||
      "integrity": "sha512-mTye5u5P98kSJO2n7zYALhpJDmoSQejIGya0iR01GpoRady8eK3bw7YHHnjA1Rfi4ZSLdpuzlAC7Zw+1Zu7Z6A==",
 | 
					      "integrity": "sha512-shUVy6Eawp33dFBFIoYbIwLHrX0IZ857AlH9ug2o4rvbWmpaCUdBpQ5Zw39HRrfzAFm4APJE9V+E2A/WB0YqJw==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/emoji-regex": {
 | 
					    "node_modules/emoji-regex": {
 | 
				
			||||||
@@ -5167,9 +5191,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/enquirer": {
 | 
					    "node_modules/enquirer": {
 | 
				
			||||||
      "version": "2.4.0",
 | 
					      "version": "2.4.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-ehu97t6FTYK2I3ZYtnp0BZ9vt0mvEL/cnHBds7Ct6jo9VX1VIkiFhOvVRWh6eblQqd7KOoICIQV+syZ3neXO/Q==",
 | 
					      "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "ansi-colors": "^4.1.1",
 | 
					        "ansi-colors": "^4.1.1",
 | 
				
			||||||
@@ -5473,27 +5497,27 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/eslint": {
 | 
					    "node_modules/eslint": {
 | 
				
			||||||
      "version": "8.45.0",
 | 
					      "version": "8.46.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==",
 | 
					      "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@eslint-community/eslint-utils": "^4.2.0",
 | 
					        "@eslint-community/eslint-utils": "^4.2.0",
 | 
				
			||||||
        "@eslint-community/regexpp": "^4.4.0",
 | 
					        "@eslint-community/regexpp": "^4.6.1",
 | 
				
			||||||
        "@eslint/eslintrc": "^2.1.0",
 | 
					        "@eslint/eslintrc": "^2.1.1",
 | 
				
			||||||
        "@eslint/js": "8.44.0",
 | 
					        "@eslint/js": "^8.46.0",
 | 
				
			||||||
        "@humanwhocodes/config-array": "^0.11.10",
 | 
					        "@humanwhocodes/config-array": "^0.11.10",
 | 
				
			||||||
        "@humanwhocodes/module-importer": "^1.0.1",
 | 
					        "@humanwhocodes/module-importer": "^1.0.1",
 | 
				
			||||||
        "@nodelib/fs.walk": "^1.2.8",
 | 
					        "@nodelib/fs.walk": "^1.2.8",
 | 
				
			||||||
        "ajv": "^6.10.0",
 | 
					        "ajv": "^6.12.4",
 | 
				
			||||||
        "chalk": "^4.0.0",
 | 
					        "chalk": "^4.0.0",
 | 
				
			||||||
        "cross-spawn": "^7.0.2",
 | 
					        "cross-spawn": "^7.0.2",
 | 
				
			||||||
        "debug": "^4.3.2",
 | 
					        "debug": "^4.3.2",
 | 
				
			||||||
        "doctrine": "^3.0.0",
 | 
					        "doctrine": "^3.0.0",
 | 
				
			||||||
        "escape-string-regexp": "^4.0.0",
 | 
					        "escape-string-regexp": "^4.0.0",
 | 
				
			||||||
        "eslint-scope": "^7.2.0",
 | 
					        "eslint-scope": "^7.2.2",
 | 
				
			||||||
        "eslint-visitor-keys": "^3.4.1",
 | 
					        "eslint-visitor-keys": "^3.4.2",
 | 
				
			||||||
        "espree": "^9.6.0",
 | 
					        "espree": "^9.6.1",
 | 
				
			||||||
        "esquery": "^1.4.2",
 | 
					        "esquery": "^1.4.2",
 | 
				
			||||||
        "esutils": "^2.0.2",
 | 
					        "esutils": "^2.0.2",
 | 
				
			||||||
        "fast-deep-equal": "^3.1.3",
 | 
					        "fast-deep-equal": "^3.1.3",
 | 
				
			||||||
@@ -5944,9 +5968,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/eslint-plugin-react": {
 | 
					    "node_modules/eslint-plugin-react": {
 | 
				
			||||||
      "version": "7.33.0",
 | 
					      "version": "7.33.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-qewL/8P34WkY8jAqdQxsiL82pDUeT7nhs8IsuXgfgnsEloKCT4miAV9N9kGtx7/KM9NH/NCGUE7Edt9iGxLXFw==",
 | 
					      "integrity": "sha512-L093k0WAMvr6VhNwReB8VgOq5s2LesZmrpPdKz/kZElQDzqS7G7+DnKoqT+w4JwuiGeAhAvHO0fvy0Eyk4ejDA==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "array-includes": "^3.1.6",
 | 
					        "array-includes": "^3.1.6",
 | 
				
			||||||
@@ -6070,9 +6094,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/eslint-scope": {
 | 
					    "node_modules/eslint-scope": {
 | 
				
			||||||
      "version": "7.2.1",
 | 
					      "version": "7.2.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==",
 | 
					      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "esrecurse": "^4.3.0",
 | 
					        "esrecurse": "^4.3.0",
 | 
				
			||||||
@@ -6086,9 +6110,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/eslint-visitor-keys": {
 | 
					    "node_modules/eslint-visitor-keys": {
 | 
				
			||||||
      "version": "3.4.1",
 | 
					      "version": "3.4.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
 | 
					      "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
 | 
					        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
 | 
				
			||||||
@@ -7791,6 +7815,15 @@
 | 
				
			|||||||
        "url": "https://github.com/sponsors/typicode"
 | 
					        "url": "https://github.com/sponsors/typicode"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/i18n-js": {
 | 
				
			||||||
 | 
					      "version": "4.3.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/i18n-js/-/i18n-js-4.3.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-PX93eT6WPV6Ym6mHtFKGDRZB0zwDX7HUPkgprjsZ28J6/Ohw1nvRYuM93or3pWv2VLxs6XfBf7X9Fc/YAZNEtQ==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "bignumber.js": "*",
 | 
				
			||||||
 | 
					        "make-plural": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/ieee754": {
 | 
					    "node_modules/ieee754": {
 | 
				
			||||||
      "version": "1.2.1",
 | 
					      "version": "1.2.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
 | 
				
			||||||
@@ -8678,9 +8711,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/jsx-ast-utils": {
 | 
					    "node_modules/jsx-ast-utils": {
 | 
				
			||||||
      "version": "3.3.4",
 | 
					      "version": "3.3.5",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==",
 | 
					      "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "array-includes": "^3.1.6",
 | 
					        "array-includes": "^3.1.6",
 | 
				
			||||||
@@ -9405,6 +9438,11 @@
 | 
				
			|||||||
      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
 | 
					      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/make-plural": {
 | 
				
			||||||
 | 
					      "version": "7.3.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.3.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-/K3BC0KIsO+WK2i94LkMPv3wslMrazrQhfi5We9fMbLlLjzoOSJWr7TAdupLlDWaJcWxwoNosBkhFDejiu5VDw=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/map-obj": {
 | 
					    "node_modules/map-obj": {
 | 
				
			||||||
      "version": "4.3.0",
 | 
					      "version": "4.3.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
 | 
				
			||||||
@@ -10975,6 +11013,14 @@
 | 
				
			|||||||
      "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
 | 
					      "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/negotiator": {
 | 
				
			||||||
 | 
					      "version": "0.6.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 0.6"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/neo-async": {
 | 
					    "node_modules/neo-async": {
 | 
				
			||||||
      "version": "2.6.2",
 | 
					      "version": "2.6.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
 | 
				
			||||||
@@ -11066,47 +11112,6 @@
 | 
				
			|||||||
        "react-dom": "*"
 | 
					        "react-dom": "*"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/next-translate": {
 | 
					 | 
				
			||||||
      "version": "2.5.2",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/next-translate/-/next-translate-2.5.2.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-5FusT+XlQGA1BAJaYu3jP8nPs3BDe/8f1u+MVM5CmIqmwOS62jUG5svq7HB4ORC6N6mvsPd7xGi3qxWyAYtwBQ==",
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=16.10.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "peerDependencies": {
 | 
					 | 
				
			||||||
        "next": ">= 13.2.5",
 | 
					 | 
				
			||||||
        "react": ">= 18.0.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/next-translate-plugin": {
 | 
					 | 
				
			||||||
      "version": "2.5.2",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/next-translate-plugin/-/next-translate-plugin-2.5.2.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-I2PDkbUM91CrOYNPVxhWxfQituSN62Y6f5EQfrZwtJToWc5HqnKs9dFjyQ+VGY3EXx6rPYgK03MVNtI+pc1HBA==",
 | 
					 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "hasInstallScript": true,
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "typescript": "4.5.2"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=14.15.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "peerDependencies": {
 | 
					 | 
				
			||||||
        "next-translate": ">= 2.4.1"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/next-translate-plugin/node_modules/typescript": {
 | 
					 | 
				
			||||||
      "version": "4.5.2",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
 | 
					 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "bin": {
 | 
					 | 
				
			||||||
        "tsc": "bin/tsc",
 | 
					 | 
				
			||||||
        "tsserver": "bin/tsserver"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=4.2.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/next/node_modules/postcss": {
 | 
					    "node_modules/next/node_modules/postcss": {
 | 
				
			||||||
      "version": "8.4.14",
 | 
					      "version": "8.4.14",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
 | 
				
			||||||
@@ -16367,9 +16372,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/rollup": {
 | 
					    "node_modules/rollup": {
 | 
				
			||||||
      "version": "3.26.3",
 | 
					      "version": "3.27.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.27.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==",
 | 
					      "integrity": "sha512-aOltLCrYZ0FhJDm7fCqwTjIUEVjWjcydKBV/Zeid6Mn8BWgDCUBBWT5beM5ieForYNo/1ZHuGJdka26kvQ3Gzg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "rollup": "dist/bin/rollup"
 | 
					        "rollup": "dist/bin/rollup"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							@@ -30,6 +30,7 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@fontsource/montserrat": "5.0.5",
 | 
					    "@fontsource/montserrat": "5.0.5",
 | 
				
			||||||
 | 
					    "@formatjs/intl-localematcher": "0.4.0",
 | 
				
			||||||
    "@fortawesome/fontawesome-svg-core": "6.4.0",
 | 
					    "@fortawesome/fontawesome-svg-core": "6.4.0",
 | 
				
			||||||
    "@fortawesome/free-brands-svg-icons": "6.4.0",
 | 
					    "@fortawesome/free-brands-svg-icons": "6.4.0",
 | 
				
			||||||
    "@fortawesome/free-solid-svg-icons": "6.4.0",
 | 
					    "@fortawesome/free-solid-svg-icons": "6.4.0",
 | 
				
			||||||
@@ -39,11 +40,12 @@
 | 
				
			|||||||
    "date-and-time": "3.0.2",
 | 
					    "date-and-time": "3.0.2",
 | 
				
			||||||
    "gray-matter": "4.0.3",
 | 
					    "gray-matter": "4.0.3",
 | 
				
			||||||
    "html-react-parser": "4.2.0",
 | 
					    "html-react-parser": "4.2.0",
 | 
				
			||||||
 | 
					    "i18n-js": "4.3.0",
 | 
				
			||||||
    "katex": "0.16.8",
 | 
					    "katex": "0.16.8",
 | 
				
			||||||
 | 
					    "negotiator": "0.6.3",
 | 
				
			||||||
    "next": "13.4.12",
 | 
					    "next": "13.4.12",
 | 
				
			||||||
    "next-mdx-remote": "4.4.1",
 | 
					    "next-mdx-remote": "4.4.1",
 | 
				
			||||||
    "next-themes": "0.2.1",
 | 
					    "next-themes": "0.2.1",
 | 
				
			||||||
    "next-translate": "2.5.2",
 | 
					 | 
				
			||||||
    "react": "18.2.0",
 | 
					    "react": "18.2.0",
 | 
				
			||||||
    "react-dom": "18.2.0",
 | 
					    "react-dom": "18.2.0",
 | 
				
			||||||
    "read-pkg": "8.0.0",
 | 
					    "read-pkg": "8.0.0",
 | 
				
			||||||
@@ -65,15 +67,17 @@
 | 
				
			|||||||
    "@semantic-release/git": "10.0.1",
 | 
					    "@semantic-release/git": "10.0.1",
 | 
				
			||||||
    "@tailwindcss/typography": "0.5.9",
 | 
					    "@tailwindcss/typography": "0.5.9",
 | 
				
			||||||
    "@tsconfig/strictest": "2.0.1",
 | 
					    "@tsconfig/strictest": "2.0.1",
 | 
				
			||||||
 | 
					    "@types/negotiator": "0.6.1",
 | 
				
			||||||
    "@types/node": "20.4.5",
 | 
					    "@types/node": "20.4.5",
 | 
				
			||||||
    "@types/react": "18.2.17",
 | 
					    "@types/react": "18.2.17",
 | 
				
			||||||
    "@types/unist": "3.0.0",
 | 
					    "@types/unist": "3.0.0",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "6.2.0",
 | 
					    "@typescript-eslint/eslint-plugin": "6.2.0",
 | 
				
			||||||
    "@typescript-eslint/parser": "6.2.0",
 | 
					    "@typescript-eslint/parser": "6.2.0",
 | 
				
			||||||
    "autoprefixer": "10.4.14",
 | 
					    "autoprefixer": "10.4.14",
 | 
				
			||||||
 | 
					    "curriculum-vitae": "file:./curriculum-vitae",
 | 
				
			||||||
    "cypress": "12.17.2",
 | 
					    "cypress": "12.17.2",
 | 
				
			||||||
    "editorconfig-checker": "5.1.1",
 | 
					    "editorconfig-checker": "5.1.1",
 | 
				
			||||||
    "eslint": "8.45.0",
 | 
					    "eslint": "8.46.0",
 | 
				
			||||||
    "eslint-config-conventions": "11.0.1",
 | 
					    "eslint-config-conventions": "11.0.1",
 | 
				
			||||||
    "eslint-config-next": "13.4.12",
 | 
					    "eslint-config-next": "13.4.12",
 | 
				
			||||||
    "eslint-config-prettier": "8.9.0",
 | 
					    "eslint-config-prettier": "8.9.0",
 | 
				
			||||||
@@ -83,11 +87,9 @@
 | 
				
			|||||||
    "eslint-plugin-unicorn": "48.0.1",
 | 
					    "eslint-plugin-unicorn": "48.0.1",
 | 
				
			||||||
    "html-w3c-validator": "1.4.0",
 | 
					    "html-w3c-validator": "1.4.0",
 | 
				
			||||||
    "husky": "8.0.3",
 | 
					    "husky": "8.0.3",
 | 
				
			||||||
    "curriculum-vitae": "file:./curriculum-vitae",
 | 
					 | 
				
			||||||
    "lint-staged": "13.2.3",
 | 
					    "lint-staged": "13.2.3",
 | 
				
			||||||
    "markdownlint-cli2": "0.8.1",
 | 
					    "markdownlint-cli2": "0.8.1",
 | 
				
			||||||
    "markdownlint-rule-relative-links": "2.1.0",
 | 
					    "markdownlint-rule-relative-links": "2.1.0",
 | 
				
			||||||
    "next-translate-plugin": "2.5.2",
 | 
					 | 
				
			||||||
    "postcss": "8.4.27",
 | 
					    "postcss": "8.4.27",
 | 
				
			||||||
    "prettier": "3.0.0",
 | 
					    "prettier": "3.0.0",
 | 
				
			||||||
    "prettier-plugin-tailwindcss": "0.4.1",
 | 
					    "prettier-plugin-tailwindcss": "0.4.1",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,32 +0,0 @@
 | 
				
			|||||||
import type { GetStaticProps, NextPage } from 'next'
 | 
					 | 
				
			||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { ErrorPage } from 'components/ErrorPage'
 | 
					 | 
				
			||||||
import { Head } from 'components/Head'
 | 
					 | 
				
			||||||
import type { FooterProps } from 'components/Footer'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface Error404Props extends FooterProps {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Error404: NextPage<Error404Props> = (props) => {
 | 
					 | 
				
			||||||
  const { t } = useTranslation()
 | 
					 | 
				
			||||||
  const { version } = props
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <Head title='404 | Théo LUDWIG' />
 | 
					 | 
				
			||||||
      <ErrorPage
 | 
					 | 
				
			||||||
        statusCode={404}
 | 
					 | 
				
			||||||
        message={t('errors:not-found')}
 | 
					 | 
				
			||||||
        version={version}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getStaticProps: GetStaticProps<Error404Props> = async () => {
 | 
					 | 
				
			||||||
  const { readPackage } = await import('read-pkg')
 | 
					 | 
				
			||||||
  const { version } = await readPackage()
 | 
					 | 
				
			||||||
  return { props: { version } }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Error404
 | 
					 | 
				
			||||||
@@ -1,32 +0,0 @@
 | 
				
			|||||||
import type { GetStaticProps, NextPage } from 'next'
 | 
					 | 
				
			||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { ErrorPage } from 'components/ErrorPage'
 | 
					 | 
				
			||||||
import { Head } from 'components/Head'
 | 
					 | 
				
			||||||
import type { FooterProps } from 'components/Footer'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface Error500Props extends FooterProps {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Error500: NextPage<Error500Props> = (props) => {
 | 
					 | 
				
			||||||
  const { t } = useTranslation()
 | 
					 | 
				
			||||||
  const { version } = props
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <Head title='500 | Théo LUDWIG' />
 | 
					 | 
				
			||||||
      <ErrorPage
 | 
					 | 
				
			||||||
        statusCode={500}
 | 
					 | 
				
			||||||
        message={t('errors:server-error')}
 | 
					 | 
				
			||||||
        version={version}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getStaticProps: GetStaticProps<Error500Props> = async () => {
 | 
					 | 
				
			||||||
  const { readPackage } = await import('read-pkg')
 | 
					 | 
				
			||||||
  const { version } = await readPackage()
 | 
					 | 
				
			||||||
  return { props: { version } }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Error500
 | 
					 | 
				
			||||||
@@ -1,33 +0,0 @@
 | 
				
			|||||||
import { useEffect } from 'react'
 | 
					 | 
				
			||||||
import type { AppType } from 'next/app'
 | 
					 | 
				
			||||||
import { ThemeProvider } from 'next-themes'
 | 
					 | 
				
			||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					 | 
				
			||||||
import UniversalCookie from 'universal-cookie'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'styles/global.css'
 | 
					 | 
				
			||||||
import '@fontsource/montserrat/400.css'
 | 
					 | 
				
			||||||
import '@fontsource/montserrat/600.css'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const universalCookie = new UniversalCookie()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/** how long in seconds, until the cookie expires (10 years) */
 | 
					 | 
				
			||||||
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Application: AppType = ({ Component, pageProps }) => {
 | 
					 | 
				
			||||||
  const { lang } = useTranslation()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    universalCookie.set('NEXT_LOCALE', lang, {
 | 
					 | 
				
			||||||
      path: '/',
 | 
					 | 
				
			||||||
      maxAge: COOKIE_MAX_AGE
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }, [lang])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <ThemeProvider attribute='class' defaultTheme='dark'>
 | 
					 | 
				
			||||||
      <Component {...pageProps} />
 | 
					 | 
				
			||||||
    </ThemeProvider>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Application
 | 
					 | 
				
			||||||
@@ -1,15 +0,0 @@
 | 
				
			|||||||
import { Html, Head, Main, NextScript } from 'next/document'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Document: React.FC = () => {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <Html>
 | 
					 | 
				
			||||||
      <Head />
 | 
					 | 
				
			||||||
      <body className='bg-white font-headline text-black dark:bg-black dark:text-white'>
 | 
					 | 
				
			||||||
        <Main />
 | 
					 | 
				
			||||||
        <NextScript />
 | 
					 | 
				
			||||||
      </body>
 | 
					 | 
				
			||||||
    </Html>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Document
 | 
					 | 
				
			||||||
@@ -1,152 +0,0 @@
 | 
				
			|||||||
import type { GetStaticProps, GetStaticPaths, NextPage } from 'next'
 | 
					 | 
				
			||||||
import Link from 'next/link'
 | 
					 | 
				
			||||||
import Image from 'next/image'
 | 
					 | 
				
			||||||
import { MDXRemote } from 'next-mdx-remote'
 | 
					 | 
				
			||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
 | 
					 | 
				
			||||||
import { faLink } from '@fortawesome/free-solid-svg-icons'
 | 
					 | 
				
			||||||
import date from 'date-and-time'
 | 
					 | 
				
			||||||
import Giscus from '@giscus/react'
 | 
					 | 
				
			||||||
import { useTheme } from 'next-themes'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'katex/dist/katex.min.css'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { Head } from 'components/Head'
 | 
					 | 
				
			||||||
import { Header } from 'components/Header'
 | 
					 | 
				
			||||||
import type { FooterProps } from 'components/Footer'
 | 
					 | 
				
			||||||
import { Footer } from 'components/Footer'
 | 
					 | 
				
			||||||
import type { Post } from 'utils/blog'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface BlogPostPageProps extends FooterProps {
 | 
					 | 
				
			||||||
  post: Post
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const BlogPostPage: NextPage<BlogPostPageProps> = (props) => {
 | 
					 | 
				
			||||||
  const { version, post } = props
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { theme = 'dark' } = useTheme()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <Head
 | 
					 | 
				
			||||||
        title={`${post.frontmatter.title} | Théo LUDWIG`}
 | 
					 | 
				
			||||||
        description={post.frontmatter.description}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <Header />
 | 
					 | 
				
			||||||
      <main className='break-wrap-words flex flex-1 flex-col flex-wrap items-center'>
 | 
					 | 
				
			||||||
        <div className='my-10 flex flex-col items-center text-center'>
 | 
					 | 
				
			||||||
          <h1 className='text-3xl font-semibold'>{post.frontmatter.title}</h1>
 | 
					 | 
				
			||||||
          <p className='mt-2' data-cy='blog-post-date'>
 | 
					 | 
				
			||||||
            {date.format(new Date(post.frontmatter.publishedOn), 'DD/MM/YYYY')}
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div className='prose mb-10'>
 | 
					 | 
				
			||||||
          <div className='px-8'>
 | 
					 | 
				
			||||||
            <MDXRemote
 | 
					 | 
				
			||||||
              {...post.source}
 | 
					 | 
				
			||||||
              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} />
 | 
					 | 
				
			||||||
                  )
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <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'
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </main>
 | 
					 | 
				
			||||||
      <Footer version={version} />
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getStaticProps: GetStaticProps<BlogPostPageProps> = async (
 | 
					 | 
				
			||||||
  context
 | 
					 | 
				
			||||||
) => {
 | 
					 | 
				
			||||||
  const slug = context?.params?.['slug']
 | 
					 | 
				
			||||||
  const { getPostBySlug } = await import('utils/blog')
 | 
					 | 
				
			||||||
  const post = await getPostBySlug(slug)
 | 
					 | 
				
			||||||
  if (post == null || (post != null && !post.frontmatter.isPublished)) {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      redirect: {
 | 
					 | 
				
			||||||
        destination: '/404',
 | 
					 | 
				
			||||||
        permanent: false
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const { readPackage } = await import('read-pkg')
 | 
					 | 
				
			||||||
  const { version } = await readPackage()
 | 
					 | 
				
			||||||
  return { props: { version, post } }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getStaticPaths: GetStaticPaths = async () => {
 | 
					 | 
				
			||||||
  const { getPosts } = await import('utils/blog')
 | 
					 | 
				
			||||||
  const posts = await getPosts()
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    paths: posts.map((post) => {
 | 
					 | 
				
			||||||
      return { params: { slug: post.slug } }
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    fallback: false
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default BlogPostPage
 | 
					 | 
				
			||||||
@@ -1,81 +0,0 @@
 | 
				
			|||||||
import type { GetStaticProps, NextPage } from 'next'
 | 
					 | 
				
			||||||
import Link from 'next/link'
 | 
					 | 
				
			||||||
import date from 'date-and-time'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { Head } from 'components/Head'
 | 
					 | 
				
			||||||
import { Header } from 'components/Header'
 | 
					 | 
				
			||||||
import type { FooterProps } from 'components/Footer'
 | 
					 | 
				
			||||||
import { Footer } from 'components/Footer'
 | 
					 | 
				
			||||||
import { ShadowContainer } from 'components/design/ShadowContainer'
 | 
					 | 
				
			||||||
import type { PostMetadata } from 'utils/blog'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const blogDescription =
 | 
					 | 
				
			||||||
  'The latest news about my journey of learning computer science.'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface BlogPageProps extends FooterProps {
 | 
					 | 
				
			||||||
  posts: PostMetadata[]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const BlogPage: NextPage<BlogPageProps> = (props) => {
 | 
					 | 
				
			||||||
  const { version, posts } = props
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <Head title='Blog | Théo LUDWIG' description={blogDescription} />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <Header />
 | 
					 | 
				
			||||||
      <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'>
 | 
					 | 
				
			||||||
            {blogDescription}
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <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>
 | 
					 | 
				
			||||||
      </main>
 | 
					 | 
				
			||||||
      <Footer version={version} />
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getStaticProps: GetStaticProps<BlogPageProps> = async () => {
 | 
					 | 
				
			||||||
  const { readPackage } = await import('read-pkg')
 | 
					 | 
				
			||||||
  const { getPosts } = await import('utils/blog')
 | 
					 | 
				
			||||||
  const posts = await getPosts()
 | 
					 | 
				
			||||||
  const { version } = await readPackage()
 | 
					 | 
				
			||||||
  return { props: { version, posts } }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default BlogPage
 | 
					 | 
				
			||||||
@@ -1,81 +0,0 @@
 | 
				
			|||||||
import type { GetStaticProps, NextPage } from 'next'
 | 
					 | 
				
			||||||
import useTranslation from 'next-translate/useTranslation'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { RevealFade } from 'components/design/RevealFade'
 | 
					 | 
				
			||||||
import { Section } from 'components/design/Section'
 | 
					 | 
				
			||||||
import { Head } from 'components/Head'
 | 
					 | 
				
			||||||
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 { Header } from 'components/Header'
 | 
					 | 
				
			||||||
import type { FooterProps } from 'components/Footer'
 | 
					 | 
				
			||||||
import { Footer } from 'components/Footer'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface HomeProps extends FooterProps {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Home: NextPage<HomeProps> = (props) => {
 | 
					 | 
				
			||||||
  const { t } = useTranslation()
 | 
					 | 
				
			||||||
  const { version } = props
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <Head />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <Header showLanguage />
 | 
					 | 
				
			||||||
      <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={t('home:interests.title')}>
 | 
					 | 
				
			||||||
            <Interests />
 | 
					 | 
				
			||||||
          </Section>
 | 
					 | 
				
			||||||
        </RevealFade>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <RevealFade>
 | 
					 | 
				
			||||||
          <Section
 | 
					 | 
				
			||||||
            id='skills'
 | 
					 | 
				
			||||||
            heading={t('home:skills.title')}
 | 
					 | 
				
			||||||
            withoutShadowContainer
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Skills />
 | 
					 | 
				
			||||||
          </Section>
 | 
					 | 
				
			||||||
        </RevealFade>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <RevealFade>
 | 
					 | 
				
			||||||
          <Section
 | 
					 | 
				
			||||||
            id='portfolio'
 | 
					 | 
				
			||||||
            heading={t('home:portfolio.title')}
 | 
					 | 
				
			||||||
            withoutShadowContainer
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Portfolio />
 | 
					 | 
				
			||||||
          </Section>
 | 
					 | 
				
			||||||
        </RevealFade>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <RevealFade>
 | 
					 | 
				
			||||||
          <Section
 | 
					 | 
				
			||||||
            id='open-source'
 | 
					 | 
				
			||||||
            heading='Open source'
 | 
					 | 
				
			||||||
            withoutShadowContainer
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <OpenSource />
 | 
					 | 
				
			||||||
          </Section>
 | 
					 | 
				
			||||||
        </RevealFade>
 | 
					 | 
				
			||||||
      </main>
 | 
					 | 
				
			||||||
      <Footer version={version} />
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getStaticProps: GetStaticProps<HomeProps> = async () => {
 | 
					 | 
				
			||||||
  const { readPackage } = await import('read-pkg')
 | 
					 | 
				
			||||||
  const { version } = await readPackage()
 | 
					 | 
				
			||||||
  return { props: { version } }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Home
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB  | 
| 
		 Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 659 B  | 
@@ -1,9 +1,6 @@
 | 
				
			|||||||
/** @type {import('tailwindcss').Config} */
 | 
					/** @type {import('tailwindcss').Config} */
 | 
				
			||||||
const tailwindConfig = {
 | 
					const tailwindConfig = {
 | 
				
			||||||
  content: [
 | 
					  content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
 | 
				
			||||||
    './pages/**/*.{js,ts,jsx,tsx}',
 | 
					 | 
				
			||||||
    './components/**/*.{js,ts,jsx,tsx}'
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
  darkMode: 'class',
 | 
					  darkMode: 'class',
 | 
				
			||||||
  theme: {
 | 
					  theme: {
 | 
				
			||||||
    extend: {
 | 
					    extend: {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,8 +17,13 @@
 | 
				
			|||||||
    "incremental": true,
 | 
					    "incremental": true,
 | 
				
			||||||
    "exactOptionalPropertyTypes": false,
 | 
					    "exactOptionalPropertyTypes": false,
 | 
				
			||||||
    "verbatimModuleSyntax": false,
 | 
					    "verbatimModuleSyntax": false,
 | 
				
			||||||
    "isolatedModules": true
 | 
					    "isolatedModules": true,
 | 
				
			||||||
 | 
					    "plugins": [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "name": "next"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
 | 
					  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
 | 
				
			||||||
  "exclude": ["node_modules"]
 | 
					  "exclude": ["node_modules"]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								utils/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								utils/constants.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					/** How long in milliseconds, until the cookie expires (10 years). */
 | 
				
			||||||
 | 
					export const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60 * 1000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AVAILABLE_LOCALES = ['en-US', 'fr-FR'] as const
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AvailableLocale = (typeof AVAILABLE_LOCALES)[number]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DEFAULT_LOCALE = 'en-US' satisfies AvailableLocale
 | 
				
			||||||
		Reference in New Issue
	
	Block a user