diff --git a/.eslintrc.json b/.eslintrc.json
index 7972bd1..0411c2c 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,6 +1,6 @@
{
"extends": ["conventions", "next/core-web-vitals", "prettier"],
- "plugins": ["prettier", "unicorn"],
+ "plugins": ["prettier"],
"parserOptions": {
"project": "./tsconfig.json"
},
diff --git a/.gitpod.yml b/.gitpod.yml
index 9229529..3bb54ce 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -2,7 +2,7 @@ image: 'gitpod/workspace-full'
tasks:
- before: 'cp .env.example .env'
- init: 'npm install'
+ init: 'npm clean-install'
command: 'npm run dev'
ports:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index de82f34..1655527 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -49,7 +49,7 @@ cd theoludwig
cp .env.example .env
# Install
-npm install
+npm clean-install
```
### Local Development environment
diff --git a/app/layout.tsx b/app/layout.tsx
index 1c3463d..0c048dd 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,13 +1,14 @@
import type { Metadata } from 'next'
+import classNames from 'clsx'
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'
+import { getTheme } from '@/theme/theme.server'
const title = 'Théo LUDWIG'
const description =
@@ -54,15 +55,23 @@ const RootLayout = (props: RootLayoutProps): JSX.Element => {
const { children } = props
const i18n = getI18n()
+ const theme = getTheme()
return (
-
+
-
-
- {children}
-
-
+
+ {children}
+
)
diff --git a/components/Header/Locales/LocaleFlag.tsx b/components/Header/Locales/LocaleFlag.tsx
index 0cba0a3..d434795 100644
--- a/components/Header/Locales/LocaleFlag.tsx
+++ b/components/Header/Locales/LocaleFlag.tsx
@@ -1,6 +1,6 @@
import Image from 'next/image'
-import type { CookiesStore } from '@/i18n/i18n.client'
+import type { CookiesStore } from '@/utils/constants'
import { useI18n } from '@/i18n/i18n.client'
export interface LocaleFlagProps {
diff --git a/components/Header/Locales/index.tsx b/components/Header/Locales/index.tsx
index e0da89c..689ecd2 100644
--- a/components/Header/Locales/index.tsx
+++ b/components/Header/Locales/index.tsx
@@ -3,8 +3,8 @@
import { useCallback, useEffect, useState, useRef } from 'react'
import classNames from 'clsx'
-import { AVAILABLE_LOCALES } from '@/utils/constants'
-import type { CookiesStore } from '@/i18n/i18n.client'
+import type { Locale as LocaleType, CookiesStore } from '@/utils/constants'
+import { LOCALES } from '@/utils/constants'
import { Arrow } from './Arrow'
import { LocaleFlag } from './LocaleFlag'
@@ -43,7 +43,7 @@ export const Locales = (props: LocalesProps): JSX.Element => {
}
}, [])
- const handleLocale = async (locale: string): Promise => {
+ const handleLocale = async (locale: LocaleType): Promise => {
const { setLocale } = await import('@/i18n/i18n.server')
setLocale(locale)
}
@@ -70,7 +70,7 @@ export const Locales = (props: LocalesProps): JSX.Element => {
{ hidden: hiddenMenu }
)}
>
- {AVAILABLE_LOCALES.filter((locale) => {
+ {LOCALES.filter((locale) => {
return locale !== currentLocale
}).map((locale) => {
return (
diff --git a/components/Header/SwitchTheme.tsx b/components/Header/SwitchTheme.tsx
index 8daa8f6..d9dbdeb 100644
--- a/components/Header/SwitchTheme.tsx
+++ b/components/Header/SwitchTheme.tsx
@@ -1,23 +1,22 @@
'use client'
-import { useEffect, useState } from 'react'
import classNames from 'clsx'
-import { useTheme } from 'next-themes'
-export const SwitchTheme: React.FC = () => {
- const [mounted, setMounted] = useState(false)
- const { theme, setTheme } = useTheme()
+import { useTheme } from '@/theme/theme.client'
+import type { CookiesStore } from '@/utils/constants'
- useEffect(() => {
- setMounted(true)
- }, [])
+export interface SwitchThemeProps {
+ cookiesStore: CookiesStore
+}
- if (!mounted) {
- return null
- }
+export const SwitchTheme = (props: SwitchThemeProps): JSX.Element => {
+ const { cookiesStore } = props
+ const theme = useTheme(cookiesStore)
- const handleClick = (): void => {
- setTheme(theme === 'dark' ? 'light' : 'dark')
+ const handleClick = async (): Promise => {
+ const { setTheme } = await import('@/theme/theme.server')
+ const newTheme = theme === 'dark' ? 'light' : 'dark'
+ setTheme(newTheme)
}
return (
diff --git a/components/Header/index.tsx b/components/Header/index.tsx
index dd3c1ee..08034b1 100644
--- a/components/Header/index.tsx
+++ b/components/Header/index.tsx
@@ -50,7 +50,7 @@ export const Header: React.FC = (props) => {
cookiesStore={cookiesStore.toString()}
/>
) : null}
-
+
)
diff --git a/components/Profile/ProfileList/index.tsx b/components/Profile/ProfileList/index.tsx
index 129225b..643af08 100644
--- a/components/Profile/ProfileList/index.tsx
+++ b/components/Profile/ProfileList/index.tsx
@@ -2,9 +2,9 @@
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 type { CookiesStore } from '@/utils/constants'
import { ProfileItem } from './ProfileItem'
diff --git a/components/Providers.tsx b/components/Providers.tsx
deleted file mode 100644
index ea00ebb..0000000
--- a/components/Providers.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-'use client'
-
-import { ThemeProvider } from 'next-themes'
-
-type ProvidersProps = React.PropsWithChildren
-
-export const Providers = (props: ProvidersProps): JSX.Element => {
- const { children } = props
-
- return (
-
- {children}
-
- )
-}
diff --git a/components/Skills/Skill.tsx b/components/Skills/Skill.tsx
index b76a5e5..409f2fa 100644
--- a/components/Skills/Skill.tsx
+++ b/components/Skills/Skill.tsx
@@ -1,9 +1,7 @@
-'use client'
-
-import { useState, useEffect, useMemo } from 'react'
-import { useTheme } from 'next-themes'
import Image from 'next/image'
+import { getTheme } from '@/theme/theme.server'
+
import type { SkillName } from './skills'
import { skills } from './skills'
@@ -15,28 +13,17 @@ export const SkillComponent: React.FC = (props) => {
const { skill } = props
const skillProperties = skills[skill]
- const [mounted, setMounted] = useState(false)
- const { theme } = useTheme()
- useEffect(() => {
- setMounted(true)
- }, [])
+ const theme = getTheme()
- const image = useMemo(() => {
+ const getImage = (): string => {
if (typeof skillProperties.image === 'string') {
return skillProperties.image
}
- if (!mounted) {
- return skillProperties.image.dark
- }
if (theme === 'light') {
return skillProperties.image.light
}
return skillProperties.image.dark
- }, [skillProperties, theme, mounted])
-
- if (!mounted) {
- return null
}
return (
@@ -53,7 +40,7 @@ export const SkillComponent: React.FC = (props) => {
width={64}
height={64}
alt={skill}
- src={image}
+ src={getImage()}
/>
{skill}
diff --git a/i18n/i18n.client.ts b/i18n/i18n.client.ts
index 3d2bd11..19f8647 100644
--- a/i18n/i18n.client.ts
+++ b/i18n/i18n.client.ts
@@ -1,9 +1,9 @@
import UniversalCookie from 'universal-cookie'
import type { I18n } from 'i18n-js'
-import { i18n } from './i18n'
+import type { CookiesStore } from '@/utils/constants'
-export type CookiesStore = string | object | null | undefined
+import { i18n } from './i18n'
export const useI18n = (cookiesStore: CookiesStore): I18n => {
const universalCookie = new UniversalCookie(cookiesStore)
diff --git a/i18n/i18n.server.ts b/i18n/i18n.server.ts
index 4f8e288..540b3d5 100644
--- a/i18n/i18n.server.ts
+++ b/i18n/i18n.server.ts
@@ -3,11 +3,12 @@
import { cookies } from 'next/headers'
import type { I18n } from 'i18n-js'
+import type { Locale } from '@/utils/constants'
import { COOKIE_MAX_AGE } from '@/utils/constants'
import { i18n } from './i18n'
-export const setLocale = (locale: string): void => {
+export const setLocale = (locale: Locale): void => {
cookies().set('locale', locale, {
path: '/',
maxAge: COOKIE_MAX_AGE
diff --git a/i18n/i18n.ts b/i18n/i18n.ts
index 7f9c92c..59cfdfd 100644
--- a/i18n/i18n.ts
+++ b/i18n/i18n.ts
@@ -1,6 +1,7 @@
import { I18n } from 'i18n-js'
-import { DEFAULT_LOCALE, AVAILABLE_LOCALES } from '@/utils/constants'
+import type { Locale } from '@/utils/constants'
+import { DEFAULT_LOCALE, LOCALES } from '@/utils/constants'
import commonEnglish from './translations/en-US/common.json'
import errorsEnglish from './translations/en-US/errors.json'
@@ -9,24 +10,21 @@ 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
- }
+const translations = {
+ 'en-US': {
+ common: commonEnglish,
+ errors: errorsEnglish,
+ home: homeEnglish
},
- i18nOptions
-)
+ 'fr-FR': {
+ common: commonFrench,
+ errors: errorsFrench,
+ home: homeFrench
+ }
+} satisfies Record>
+
+export const i18n = new I18n(translations, {
+ defaultLocale: DEFAULT_LOCALE,
+ availableLocales: LOCALES.slice(),
+ enableFallback: true
+})
diff --git a/middleware.ts b/middleware.ts
index 8854024..b2e8322 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -3,38 +3,39 @@ import type { NextRequest } from 'next/server'
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
-import type { AvailableLocale } from '@/utils/constants'
+import type { Locale, Theme } from '@/utils/constants'
import {
COOKIE_MAX_AGE,
DEFAULT_LOCALE,
- AVAILABLE_LOCALES
+ DEFAULT_THEME,
+ LOCALES,
+ THEMES
} 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)
- ) {
+ if (locale == null || !LOCALES.includes(locale as Locale)) {
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)
+ locale = match(languages, 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
- })
+ const theme = request.cookies.get('theme')?.value
+ if (theme == null || !THEMES.includes(theme as Theme)) {
+ response.cookies.set('theme', DEFAULT_THEME, {
+ path: '/',
+ maxAge: COOKIE_MAX_AGE
+ })
+ }
return response
}
diff --git a/package-lock.json b/package-lock.json
index 3dbc039..5cc0415 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,7 +25,6 @@
"negotiator": "0.6.3",
"next": "13.4.12",
"next-mdx-remote": "4.4.1",
- "next-themes": "0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"read-pkg": "8.0.0",
@@ -11102,16 +11101,6 @@
"react-dom": ">=16.x <=18.x"
}
},
- "node_modules/next-themes": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz",
- "integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==",
- "peerDependencies": {
- "next": "*",
- "react": "*",
- "react-dom": "*"
- }
- },
"node_modules/next/node_modules/postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
diff --git a/package.json b/package.json
index 81cfbdb..74101db 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,6 @@
"negotiator": "0.6.3",
"next": "13.4.12",
"next-mdx-remote": "4.4.1",
- "next-themes": "0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"read-pkg": "8.0.0",
diff --git a/theme/theme.client.ts b/theme/theme.client.ts
new file mode 100644
index 0000000..cdff004
--- /dev/null
+++ b/theme/theme.client.ts
@@ -0,0 +1,9 @@
+import UniversalCookie from 'universal-cookie'
+
+import { DEFAULT_THEME } from '@/utils/constants'
+import type { CookiesStore, Theme } from '@/utils/constants'
+
+export const useTheme = (cookiesStore: CookiesStore): Theme => {
+ const universalCookie = new UniversalCookie(cookiesStore)
+ return universalCookie.get('theme') ?? DEFAULT_THEME
+}
diff --git a/theme/theme.server.ts b/theme/theme.server.ts
new file mode 100644
index 0000000..9348b0d
--- /dev/null
+++ b/theme/theme.server.ts
@@ -0,0 +1,21 @@
+'use server'
+
+import { cookies } from 'next/headers'
+
+import type { Theme } from '@/utils/constants'
+import { COOKIE_MAX_AGE, DEFAULT_THEME, THEMES } from '@/utils/constants'
+
+export const setTheme = (theme: Theme): void => {
+ cookies().set('theme', theme, {
+ path: '/',
+ maxAge: COOKIE_MAX_AGE
+ })
+}
+
+export const getTheme = (): Theme => {
+ const theme = cookies().get('theme')?.value ?? DEFAULT_THEME
+ if (THEMES.includes(theme as Theme)) {
+ return theme as Theme
+ }
+ return DEFAULT_THEME
+}
diff --git a/utils/constants.ts b/utils/constants.ts
index 7c98c8e..9756a71 100644
--- a/utils/constants.ts
+++ b/utils/constants.ts
@@ -1,8 +1,11 @@
/** How long in milliseconds, until the cookie expires (10 years). */
export const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60 * 1000
+export type CookiesStore = string | object | null | undefined
-export const AVAILABLE_LOCALES = ['en-US', 'fr-FR'] as const
+export const LOCALES = ['en-US', 'fr-FR'] as const
+export type Locale = (typeof LOCALES)[number]
+export const DEFAULT_LOCALE = 'en-US' satisfies Locale
-export type AvailableLocale = (typeof AVAILABLE_LOCALES)[number]
-
-export const DEFAULT_LOCALE = 'en-US' satisfies AvailableLocale
+export const THEMES = ['light', 'dark'] as const
+export type Theme = (typeof THEMES)[number]
+export const DEFAULT_THEME = 'dark' satisfies Theme
diff --git a/utils/getAge.ts b/utils/getAge.ts
index 2c49784..8922a1b 100644
--- a/utils/getAge.ts
+++ b/utils/getAge.ts
@@ -8,9 +8,8 @@ export const BIRTH_DATE_ISO_8601 =
export const BIRTH_DATE = new Date(BIRTH_DATE_ISO_8601)
/**
- * Calculates the age of a person based on their birth date
+ * Calculates the age of a person based on their birth date.
* @param birthDate
- * @returns
*/
export const getAge = (birthDate: Date): number => {
const today = new Date()