Add themes

This commit is contained in:
cte 2025-05-11 23:12:16 -07:00
parent aee48a55a0
commit a988fd0542
11 changed files with 121 additions and 53 deletions

View file

@ -43,6 +43,7 @@
"lucide-react": "^0.509.0",
"next": "^15.3.2",
"next-intl": "^4.1.0",
"next-themes": "^0.4.6",
"pg": "^8.13.0",
"pino": "^9.5.0",
"pino-pretty": "^13.0.0",

14
pnpm-lock.yaml generated
View file

@ -65,6 +65,9 @@ importers:
next-intl:
specifier: ^4.1.0
version: 4.1.0(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
pg:
specifier: ^8.13.0
version: 8.15.6
@ -6449,6 +6452,12 @@ packages:
typescript:
optional: true
next-themes@0.4.6:
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
peerDependencies:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@15.3.2:
resolution: {integrity: sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
@ -15704,6 +15713,11 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@next/env': 15.3.2

View file

@ -4,12 +4,11 @@ import type { Metadata } from 'next';
import { NextIntlClientProvider } from 'next-intl';
import { getLocale } from 'next-intl/server';
import { ThemeProvider } from '@/components/ThemeProvider';
export const metadata: Metadata = {
icons: [
{
rel: 'apple-touch-icon',
url: '/apple-touch-icon.png',
},
{ rel: 'apple-touch-icon', url: '/apple-touch-icon.png' },
{
rel: 'icon',
type: 'image/png',
@ -22,14 +21,15 @@ export const metadata: Metadata = {
sizes: '16x16',
url: '/favicon-16x16.png',
},
{
rel: 'icon',
url: '/favicon.ico',
},
{ rel: 'icon', url: '/favicon.ico' },
],
};
export default async function RootLayout(props: { children: React.ReactNode }) {
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const locale = await getLocale();
return (
@ -38,7 +38,16 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
className="bg-background text-foreground antialiased"
suppressHydrationWarning
>
<NextIntlClientProvider>{props.children}</NextIntlClientProvider>
<NextIntlClientProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</NextIntlClientProvider>
</body>
</html>
);

View file

@ -6,14 +6,15 @@ import { useLocale } from 'next-intl';
import { Locales, isLocale } from '@/types/locale';
import { setLocale } from '@/actions/locale';
import { Button } from '@/components/ui/button';
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
} from '@/components/ui';
import { Globe } from 'lucide-react';
export const LocaleSwitcher = () => {
const router = useRouter();
@ -32,24 +33,8 @@ export const LocaleSwitcher = () => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="p-2 focus-visible:ring-offset-0"
variant="ghost"
size="icon"
aria-label="lang-switcher"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="size-6 stroke-current stroke-2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
viewBox="0 0 24 24"
>
<path stroke="none" d="M0 0h24v24H0z" />
<path d="M3 12a9 9 0 1 0 18 0 9 9 0 0 0-18 0M3.6 9h16.8M3.6 15h16.8" />
<path d="M11.5 3a17 17 0 0 0 0 18M12.5 3a17 17 0 0 1 0 18" />
</svg>
<Button variant="ghost" size="icon">
<Globe />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>

View file

@ -0,0 +1,11 @@
'use client';
import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View file

@ -0,0 +1,38 @@
'use client';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui';
export function ThemeSwitcher() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View file

@ -1,6 +1,6 @@
import { type ForwardedRef, forwardRef } from 'react';
import { Button } from '@/components/ui/button';
import { Button } from '@/components/ui';
type ToggleMenuButtonProps = {
onClick?: () => void;

View file

@ -11,8 +11,8 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Separator } from '@/components/ui/separator';
Separator,
} from '@/components/ui';
import { Logo } from '@/components/templates/Logo';
type DashboardHeaderProps = {

View file

@ -6,11 +6,17 @@ import { ToggleMenuButton } from '@/components/ToggleMenuButton';
import { useMenu } from '@/hooks/useMenu';
import { cn } from '@/lib/utils';
export const CenteredMenu = (props: {
type CenteredMenuProps = {
logo: React.ReactNode;
children: React.ReactNode;
rightMenu: React.ReactNode;
}) => {
children?: React.ReactNode;
};
export const CenteredMenu = ({
logo,
rightMenu,
children,
}: CenteredMenuProps) => {
const { showMenu, handleToggleMenu } = useMenu();
const navClass = cn('max-lg:w-full max-lg:bg-secondary max-lg:p-5', {
@ -19,18 +25,17 @@ export const CenteredMenu = (props: {
return (
<div className="flex flex-wrap items-center justify-between">
<Link href="/">{props.logo}</Link>
<Link href="/">{logo}</Link>
<div className="lg:hidden [&_button:hover]:opacity-100 [&_button]:opacity-60">
<ToggleMenuButton onClick={handleToggleMenu} />
</div>
<nav className={cn('rounded-t max-lg:mt-2', navClass)}>
<ul className="flex gap-x-6 gap-y-1 text-lg font-medium max-lg:flex-col [&_a:hover]:opacity-100 [&_a]:opacity-60 max-lg:[&_a]:inline-block max-lg:[&_a]:w-full">
{props.children}
</ul>
</nav>
{children && (
<nav className={cn('rounded-t max-lg:mt-2', navClass)}>
<ul className="flex gap-x-6 gap-y-1 text-lg font-medium max-lg:flex-col [&_a:hover]:opacity-100 [&_a]:opacity-60 max-lg:[&_a]:inline-block max-lg:[&_a]:w-full">
{children}
</ul>
</nav>
)}
<div
className={cn(
'rounded-b max-lg:border-t max-lg:border-border',
@ -38,7 +43,7 @@ export const CenteredMenu = (props: {
)}
>
<ul className="flex flex-row items-center gap-x-1.5 text-lg font-medium [&_li[data-fade]:hover]:opacity-100 [&_li[data-fade]]:opacity-60">
{props.rightMenu}
{rightMenu}
</ul>
</div>
</div>

View file

@ -1,8 +1,9 @@
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui';
import { LocaleSwitcher } from '@/components/LocaleSwitcher';
import { buttonVariants } from '@/components/ui/button';
import { ThemeSwitcher } from '@/components/ThemeSwitcher';
import { CenteredMenu } from '@/components/landing/CenteredMenu';
import { Section } from '@/components/landing/Section';
@ -20,19 +21,20 @@ export const Navbar = () => {
<li data-fade>
<LocaleSwitcher />
</li>
<li data-fade>
<ThemeSwitcher />
</li>
<li className="ml-1 mr-2.5" data-fade>
<Link href="/sign-in">{t('sign_in')}</Link>
</li>
<li>
<Link className={buttonVariants()} href="/sign-up">
{t('sign_up')}
</Link>
<Button asChild>
<Link href="/sign-up">{t('sign_up')}</Link>
</Button>
</li>
</>
}
>
<div />
</CenteredMenu>
/>
</Section>
);
};

View file

@ -0,0 +1,3 @@
export * from './button';
export * from './dropdown-menu';
export * from './separator';