From 336aa36d5a2882932d4a5ae60f26264c8a63e9b4 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Wed, 25 Feb 2026 20:01:52 +0800 Subject: [PATCH] feat(theme): add dark mode with next-themes and settings toggle --- app/(account)/settings/page.tsx | 38 +++++++++++++++++++++++++++++++++ app/layout.tsx | 2 +- app/providers.tsx | 15 ++++++++----- components/theme-sync.tsx | 17 +++++++++++++++ package.json | 1 + pnpm-lock.yaml | 14 ++++++++++++ store/auth.ts | 12 +++++++++-- 7 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 components/theme-sync.tsx diff --git a/app/(account)/settings/page.tsx b/app/(account)/settings/page.tsx index 3862ee0..0bfb74d 100644 --- a/app/(account)/settings/page.tsx +++ b/app/(account)/settings/page.tsx @@ -6,13 +6,22 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" import { Separator } from "@/components/ui/separator" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" import { notifySuccess } from "@/lib/toast" import type { UserRole } from "@/lib/types" +import type { ThemePreference } from "@/store/auth" import { useAuthStore } from "@/store/auth" import { Camera } from "lucide-react" +import { useTheme } from "next-themes" import Link from "next/link" import { useRef, useState } from "react" @@ -32,6 +41,14 @@ export default function SettingsPage() { const fileRef = useRef(null) const isRoleVerified = (role: UserRole) => verifiedRoles.includes(role) + const { theme, setTheme } = useTheme() + const setThemePreference = useAuthStore((s) => s.setThemePreference) + + function handleThemeChange(value: string) { + const pref = value as ThemePreference + setTheme(pref) + setThemePreference(pref) + } return (
@@ -174,6 +191,27 @@ export default function SettingsPage() { + + + 外观设置 + + +
+ + +
+
+
+ 通知偏好 diff --git a/app/layout.tsx b/app/layout.tsx index a28a453..800359f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -24,7 +24,7 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - + {children} diff --git a/app/providers.tsx b/app/providers.tsx index cf92b5f..effc46c 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -1,8 +1,10 @@ "use client" import { GlobalLoginDialog } from "@/components/global-login-dialog" +import { ThemeSyncEffect } from "@/components/theme-sync" import { TooltipProvider } from "@/components/ui/tooltip" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { ThemeProvider } from "next-themes" import { useState } from "react" import { Toaster } from "sonner" @@ -21,11 +23,14 @@ export function Providers({ children }: { children: React.ReactNode }) { return ( - - {children} - - - + + + + {children} + + + + ) } diff --git a/components/theme-sync.tsx b/components/theme-sync.tsx new file mode 100644 index 0000000..f697f18 --- /dev/null +++ b/components/theme-sync.tsx @@ -0,0 +1,17 @@ +"use client" + +import { useAuthStore } from "@/store/auth" +import { useTheme } from "next-themes" +import { useEffect } from "react" + +export function ThemeSyncEffect() { + const { setTheme } = useTheme() + const isAuthenticated = useAuthStore((s) => s.isAuthenticated) + const themePreference = useAuthStore((s) => s.themePreference) + + useEffect(() => { + setTheme(themePreference) + }, [isAuthenticated, themePreference, setTheme]) + + return null +} diff --git a/package.json b/package.json index 9c33c9e..d86db42 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "cmdk": "^1.1.1", "lucide-react": "^0.575.0", "next": "16.1.6", + "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "19.2.3", "react-dom": "19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b6e343..39e9c5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: next: specifier: 16.1.6 version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -3308,6 +3311,12 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + 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@16.1.6: resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} engines: {node: '>=20.9.0'} @@ -7525,6 +7534,11 @@ snapshots: negotiator@1.0.0: {} + next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 16.1.6 diff --git a/store/auth.ts b/store/auth.ts index c023553..0e6d6f5 100644 --- a/store/auth.ts +++ b/store/auth.ts @@ -13,6 +13,8 @@ const defaultNotificationPrefs: NotificationPrefs = { system: false, } +export type ThemePreference = "light" | "dark" | "system" + interface AuthState { isAuthenticated: boolean currentRole: UserRole @@ -20,14 +22,16 @@ interface AuthState { verificationStatus: Partial> verificationReasons: Partial> notificationPrefs: NotificationPrefs + themePreference: ThemePreference user: User | null switchRole: (role: UserRole) => void submitVerification: (role: UserRole) => void approveVerification: (role: UserRole) => void rejectVerification: (role: UserRole, reason: string) => void setNotificationPref: (type: keyof NotificationPrefs, enabled: boolean) => void + setThemePreference: (theme: ThemePreference) => void updateProfile: (patch: { nickname?: string; bio?: string; avatar?: string }) => void - login: (user: User, verifiedRoles?: UserRole[]) => void + login: (user: User, verifiedRoles?: UserRole[], themePreference?: ThemePreference) => void logout: () => void } @@ -38,6 +42,7 @@ export const useAuthStore = create((set, get) => ({ verificationStatus: { consumer: "approved" }, verificationReasons: {}, notificationPrefs: defaultNotificationPrefs, + themePreference: "system", user: null, switchRole: (role) => { const { verifiedRoles } = get() @@ -106,6 +111,7 @@ export const useAuthStore = create((set, get) => ({ [type]: enabled, }, })), + setThemePreference: (theme) => set({ themePreference: theme }), updateProfile: (patch) => set((state) => { if (!state.user) return state @@ -119,7 +125,7 @@ export const useAuthStore = create((set, get) => ({ }, } }), - login: (user, verifiedRoles = ["consumer"]) => + login: (user, verifiedRoles = ["consumer"], themePreference) => set((state) => ({ isAuthenticated: true, user, @@ -134,6 +140,7 @@ export const useAuthStore = create((set, get) => ({ ), verificationReasons: {}, notificationPrefs: state.notificationPrefs, + themePreference: themePreference ?? state.themePreference, })), logout: () => set({ @@ -143,6 +150,7 @@ export const useAuthStore = create((set, get) => ({ verificationStatus: { consumer: "approved" }, verificationReasons: {}, notificationPrefs: defaultNotificationPrefs, + themePreference: "system", user: null, }), }))