mirror of
https://github.com/theoludwig/theoludwig.git
synced 2024-12-08 00:44:30 +01:00
refactor: implement light/dark themes using cookies
This commit is contained in:
parent
e82db952db
commit
caa6a90418
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": ["conventions", "next/core-web-vitals", "prettier"],
|
"extends": ["conventions", "next/core-web-vitals", "prettier"],
|
||||||
"plugins": ["prettier", "unicorn"],
|
"plugins": ["prettier"],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@ image: 'gitpod/workspace-full'
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- before: 'cp .env.example .env'
|
- before: 'cp .env.example .env'
|
||||||
init: 'npm install'
|
init: 'npm clean-install'
|
||||||
command: 'npm run dev'
|
command: 'npm run dev'
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
|
@ -49,7 +49,7 @@ cd theoludwig
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
npm install
|
npm clean-install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local Development environment
|
### Local Development environment
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import classNames from 'clsx'
|
||||||
|
|
||||||
import '@fontsource/montserrat/400.css'
|
import '@fontsource/montserrat/400.css'
|
||||||
import '@fontsource/montserrat/600.css'
|
import '@fontsource/montserrat/600.css'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
|
||||||
import { Providers } from '@/components/Providers'
|
|
||||||
import { Header } from '@/components/Header'
|
import { Header } from '@/components/Header'
|
||||||
import { Footer } from '@/components/Footer'
|
import { Footer } from '@/components/Footer'
|
||||||
import { getI18n } from '@/i18n/i18n.server'
|
import { getI18n } from '@/i18n/i18n.server'
|
||||||
|
import { getTheme } from '@/theme/theme.server'
|
||||||
|
|
||||||
const title = 'Théo LUDWIG'
|
const title = 'Théo LUDWIG'
|
||||||
const description =
|
const description =
|
||||||
@ -54,15 +55,23 @@ const RootLayout = (props: RootLayoutProps): JSX.Element => {
|
|||||||
const { children } = props
|
const { children } = props
|
||||||
|
|
||||||
const i18n = getI18n()
|
const i18n = getI18n()
|
||||||
|
const theme = getTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html suppressHydrationWarning lang={i18n.locale}>
|
<html
|
||||||
|
lang={i18n.locale}
|
||||||
|
className={classNames({
|
||||||
|
dark: theme === 'dark',
|
||||||
|
light: theme === 'light'
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
colorScheme: theme
|
||||||
|
}}
|
||||||
|
>
|
||||||
<body className='bg-white font-headline text-black dark:bg-black dark:text-white'>
|
<body className='bg-white font-headline text-black dark:bg-black dark:text-white'>
|
||||||
<Providers>
|
|
||||||
<Header showLocale />
|
<Header showLocale />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
</Providers>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
import type { CookiesStore } from '@/i18n/i18n.client'
|
import type { CookiesStore } from '@/utils/constants'
|
||||||
import { useI18n } from '@/i18n/i18n.client'
|
import { useI18n } from '@/i18n/i18n.client'
|
||||||
|
|
||||||
export interface LocaleFlagProps {
|
export interface LocaleFlagProps {
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||||
import classNames from 'clsx'
|
import classNames from 'clsx'
|
||||||
|
|
||||||
import { AVAILABLE_LOCALES } from '@/utils/constants'
|
import type { Locale as LocaleType, CookiesStore } from '@/utils/constants'
|
||||||
import type { CookiesStore } from '@/i18n/i18n.client'
|
import { LOCALES } from '@/utils/constants'
|
||||||
|
|
||||||
import { Arrow } from './Arrow'
|
import { Arrow } from './Arrow'
|
||||||
import { LocaleFlag } from './LocaleFlag'
|
import { LocaleFlag } from './LocaleFlag'
|
||||||
@ -43,7 +43,7 @@ export const Locales = (props: LocalesProps): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleLocale = async (locale: string): Promise<void> => {
|
const handleLocale = async (locale: LocaleType): Promise<void> => {
|
||||||
const { setLocale } = await import('@/i18n/i18n.server')
|
const { setLocale } = await import('@/i18n/i18n.server')
|
||||||
setLocale(locale)
|
setLocale(locale)
|
||||||
}
|
}
|
||||||
@ -70,7 +70,7 @@ export const Locales = (props: LocalesProps): JSX.Element => {
|
|||||||
{ hidden: hiddenMenu }
|
{ hidden: hiddenMenu }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{AVAILABLE_LOCALES.filter((locale) => {
|
{LOCALES.filter((locale) => {
|
||||||
return locale !== currentLocale
|
return locale !== currentLocale
|
||||||
}).map((locale) => {
|
}).map((locale) => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import classNames from 'clsx'
|
import classNames from 'clsx'
|
||||||
import { useTheme } from 'next-themes'
|
|
||||||
|
|
||||||
export const SwitchTheme: React.FC = () => {
|
import { useTheme } from '@/theme/theme.client'
|
||||||
const [mounted, setMounted] = useState(false)
|
import type { CookiesStore } from '@/utils/constants'
|
||||||
const { theme, setTheme } = useTheme()
|
|
||||||
|
|
||||||
useEffect(() => {
|
export interface SwitchThemeProps {
|
||||||
setMounted(true)
|
cookiesStore: CookiesStore
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
if (!mounted) {
|
export const SwitchTheme = (props: SwitchThemeProps): JSX.Element => {
|
||||||
return null
|
const { cookiesStore } = props
|
||||||
}
|
const theme = useTheme(cookiesStore)
|
||||||
|
|
||||||
const handleClick = (): void => {
|
const handleClick = async (): Promise<void> => {
|
||||||
setTheme(theme === 'dark' ? 'light' : 'dark')
|
const { setTheme } = await import('@/theme/theme.server')
|
||||||
|
const newTheme = theme === 'dark' ? 'light' : 'dark'
|
||||||
|
setTheme(newTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -50,7 +50,7 @@ export const Header: React.FC<HeaderProps> = (props) => {
|
|||||||
cookiesStore={cookiesStore.toString()}
|
cookiesStore={cookiesStore.toString()}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<SwitchTheme />
|
<SwitchTheme cookiesStore={cookiesStore.toString()} />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import type { CookiesStore } from '@/i18n/i18n.client'
|
|
||||||
import { useI18n } from '@/i18n/i18n.client'
|
import { useI18n } from '@/i18n/i18n.client'
|
||||||
import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from '@/utils/getAge'
|
import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from '@/utils/getAge'
|
||||||
|
import type { CookiesStore } from '@/utils/constants'
|
||||||
|
|
||||||
import { ProfileItem } from './ProfileItem'
|
import { ProfileItem } from './ProfileItem'
|
||||||
|
|
||||||
|
@ -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 (
|
|
||||||
<ThemeProvider attribute='class' defaultTheme='dark'>
|
|
||||||
{children}
|
|
||||||
</ThemeProvider>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,9 +1,7 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useState, useEffect, useMemo } from 'react'
|
|
||||||
import { useTheme } from 'next-themes'
|
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
import { getTheme } from '@/theme/theme.server'
|
||||||
|
|
||||||
import type { SkillName } from './skills'
|
import type { SkillName } from './skills'
|
||||||
import { skills } from './skills'
|
import { skills } from './skills'
|
||||||
|
|
||||||
@ -15,28 +13,17 @@ 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()
|
|
||||||
|
|
||||||
useEffect(() => {
|
const theme = getTheme()
|
||||||
setMounted(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const image = useMemo(() => {
|
const getImage = (): string => {
|
||||||
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, mounted])
|
|
||||||
|
|
||||||
if (!mounted) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -53,7 +40,7 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
|||||||
width={64}
|
width={64}
|
||||||
height={64}
|
height={64}
|
||||||
alt={skill}
|
alt={skill}
|
||||||
src={image}
|
src={getImage()}
|
||||||
/>
|
/>
|
||||||
<p className='mt-1'>{skill}</p>
|
<p className='mt-1'>{skill}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import UniversalCookie from 'universal-cookie'
|
import UniversalCookie from 'universal-cookie'
|
||||||
import type { I18n } from 'i18n-js'
|
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 => {
|
export const useI18n = (cookiesStore: CookiesStore): I18n => {
|
||||||
const universalCookie = new UniversalCookie(cookiesStore)
|
const universalCookie = new UniversalCookie(cookiesStore)
|
||||||
|
@ -3,11 +3,12 @@
|
|||||||
import { cookies } from 'next/headers'
|
import { cookies } from 'next/headers'
|
||||||
import type { I18n } from 'i18n-js'
|
import type { I18n } from 'i18n-js'
|
||||||
|
|
||||||
|
import type { Locale } from '@/utils/constants'
|
||||||
import { COOKIE_MAX_AGE } from '@/utils/constants'
|
import { COOKIE_MAX_AGE } from '@/utils/constants'
|
||||||
|
|
||||||
import { i18n } from './i18n'
|
import { i18n } from './i18n'
|
||||||
|
|
||||||
export const setLocale = (locale: string): void => {
|
export const setLocale = (locale: Locale): void => {
|
||||||
cookies().set('locale', locale, {
|
cookies().set('locale', locale, {
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge: COOKIE_MAX_AGE
|
maxAge: COOKIE_MAX_AGE
|
||||||
|
22
i18n/i18n.ts
22
i18n/i18n.ts
@ -1,6 +1,7 @@
|
|||||||
import { I18n } from 'i18n-js'
|
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 commonEnglish from './translations/en-US/common.json'
|
||||||
import errorsEnglish from './translations/en-US/errors.json'
|
import errorsEnglish from './translations/en-US/errors.json'
|
||||||
@ -9,14 +10,7 @@ import commonFrench from './translations/fr-FR/common.json'
|
|||||||
import errorsFrench from './translations/fr-FR/errors.json'
|
import errorsFrench from './translations/fr-FR/errors.json'
|
||||||
import homeFrench from './translations/fr-FR/home.json'
|
import homeFrench from './translations/fr-FR/home.json'
|
||||||
|
|
||||||
export const i18nOptions = {
|
const translations = {
|
||||||
defaultLocale: DEFAULT_LOCALE,
|
|
||||||
availableLocales: AVAILABLE_LOCALES.slice(),
|
|
||||||
enableFallback: true
|
|
||||||
}
|
|
||||||
|
|
||||||
export const i18n = new I18n(
|
|
||||||
{
|
|
||||||
'en-US': {
|
'en-US': {
|
||||||
common: commonEnglish,
|
common: commonEnglish,
|
||||||
errors: errorsEnglish,
|
errors: errorsEnglish,
|
||||||
@ -27,6 +21,10 @@ export const i18n = new I18n(
|
|||||||
errors: errorsFrench,
|
errors: errorsFrench,
|
||||||
home: homeFrench
|
home: homeFrench
|
||||||
}
|
}
|
||||||
},
|
} satisfies Record<Locale, Record<string, unknown>>
|
||||||
i18nOptions
|
|
||||||
)
|
export const i18n = new I18n(translations, {
|
||||||
|
defaultLocale: DEFAULT_LOCALE,
|
||||||
|
availableLocales: LOCALES.slice(),
|
||||||
|
enableFallback: true
|
||||||
|
})
|
||||||
|
@ -3,38 +3,39 @@ import type { NextRequest } from 'next/server'
|
|||||||
import { match } from '@formatjs/intl-localematcher'
|
import { match } from '@formatjs/intl-localematcher'
|
||||||
import Negotiator from 'negotiator'
|
import Negotiator from 'negotiator'
|
||||||
|
|
||||||
import type { AvailableLocale } from '@/utils/constants'
|
import type { Locale, Theme } from '@/utils/constants'
|
||||||
import {
|
import {
|
||||||
COOKIE_MAX_AGE,
|
COOKIE_MAX_AGE,
|
||||||
DEFAULT_LOCALE,
|
DEFAULT_LOCALE,
|
||||||
AVAILABLE_LOCALES
|
DEFAULT_THEME,
|
||||||
|
LOCALES,
|
||||||
|
THEMES
|
||||||
} from '@/utils/constants'
|
} from '@/utils/constants'
|
||||||
|
|
||||||
export const middleware = (request: NextRequest): NextResponse => {
|
export const middleware = (request: NextRequest): NextResponse => {
|
||||||
const response = NextResponse.next()
|
const response = NextResponse.next()
|
||||||
|
|
||||||
let locale = request.cookies.get('locale')?.value
|
let locale = request.cookies.get('locale')?.value
|
||||||
if (
|
if (locale == null || !LOCALES.includes(locale as Locale)) {
|
||||||
locale == null ||
|
|
||||||
!AVAILABLE_LOCALES.includes(locale as AvailableLocale)
|
|
||||||
) {
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'accept-language':
|
'accept-language':
|
||||||
request.headers.get('accept-language') ?? DEFAULT_LOCALE
|
request.headers.get('accept-language') ?? DEFAULT_LOCALE
|
||||||
}
|
}
|
||||||
const languages = new Negotiator({ headers }).languages()
|
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, {
|
response.cookies.set('locale', locale, {
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge: COOKIE_MAX_AGE
|
maxAge: COOKIE_MAX_AGE
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = request.cookies.get('theme')?.value ?? 'dark'
|
const theme = request.cookies.get('theme')?.value
|
||||||
response.cookies.set('theme', theme, {
|
if (theme == null || !THEMES.includes(theme as Theme)) {
|
||||||
|
response.cookies.set('theme', DEFAULT_THEME, {
|
||||||
path: '/',
|
path: '/',
|
||||||
expires: COOKIE_MAX_AGE
|
maxAge: COOKIE_MAX_AGE
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
11
package-lock.json
generated
11
package-lock.json
generated
@ -25,7 +25,6 @@
|
|||||||
"negotiator": "0.6.3",
|
"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",
|
|
||||||
"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",
|
||||||
@ -11102,16 +11101,6 @@
|
|||||||
"react-dom": ">=16.x <=18.x"
|
"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": {
|
"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",
|
||||||
|
@ -45,7 +45,6 @@
|
|||||||
"negotiator": "0.6.3",
|
"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",
|
|
||||||
"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",
|
||||||
|
9
theme/theme.client.ts
Normal file
9
theme/theme.client.ts
Normal file
@ -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
|
||||||
|
}
|
21
theme/theme.server.ts
Normal file
21
theme/theme.server.ts
Normal file
@ -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
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
/** How long in milliseconds, until the cookie expires (10 years). */
|
/** How long in milliseconds, until the cookie expires (10 years). */
|
||||||
export const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60 * 1000
|
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 THEMES = ['light', 'dark'] as const
|
||||||
|
export type Theme = (typeof THEMES)[number]
|
||||||
export const DEFAULT_LOCALE = 'en-US' satisfies AvailableLocale
|
export const DEFAULT_THEME = 'dark' satisfies Theme
|
||||||
|
@ -8,9 +8,8 @@ export const BIRTH_DATE_ISO_8601 =
|
|||||||
export const BIRTH_DATE = new Date(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
|
* @param birthDate
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export const getAge = (birthDate: Date): number => {
|
export const getAge = (birthDate: Date): number => {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
|
Loading…
Reference in New Issue
Block a user