Kumari Vaishnavi commited on
Commit
38bb8dc
·
1 Parent(s): d85fcca

feat(ui): add dark/light theme toggle with next-themes

Browse files
frontend/package-lock.json CHANGED
@@ -13,6 +13,7 @@
13
  "clsx": "^2.1.1",
14
  "lucide-react": "^1.8.0",
15
  "next": "16.2.4",
 
16
  "pdfjs-dist": "^5.6.205",
17
  "react": "19.2.4",
18
  "react-dom": "19.2.4",
@@ -8606,6 +8607,16 @@
8606
  }
8607
  }
8608
  },
 
 
 
 
 
 
 
 
 
 
8609
  "node_modules/next/node_modules/postcss": {
8610
  "version": "8.4.31",
8611
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
 
13
  "clsx": "^2.1.1",
14
  "lucide-react": "^1.8.0",
15
  "next": "16.2.4",
16
+ "next-themes": "^0.4.6",
17
  "pdfjs-dist": "^5.6.205",
18
  "react": "19.2.4",
19
  "react-dom": "19.2.4",
 
8607
  }
8608
  }
8609
  },
8610
+ "node_modules/next-themes": {
8611
+ "version": "0.4.6",
8612
+ "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
8613
+ "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
8614
+ "license": "MIT",
8615
+ "peerDependencies": {
8616
+ "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
8617
+ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
8618
+ }
8619
+ },
8620
  "node_modules/next/node_modules/postcss": {
8621
  "version": "8.4.31",
8622
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
frontend/package.json CHANGED
@@ -14,6 +14,7 @@
14
  "clsx": "^2.1.1",
15
  "lucide-react": "^1.8.0",
16
  "next": "16.2.4",
 
17
  "pdfjs-dist": "^5.6.205",
18
  "react": "19.2.4",
19
  "react-dom": "19.2.4",
 
14
  "clsx": "^2.1.1",
15
  "lucide-react": "^1.8.0",
16
  "next": "16.2.4",
17
+ "next-themes": "^0.4.6",
18
  "pdfjs-dist": "^5.6.205",
19
  "react": "19.2.4",
20
  "react-dom": "19.2.4",
frontend/src/app/globals.css CHANGED
@@ -83,6 +83,35 @@
83
  --sidebar-ring: oklch(0.65 0.2 265);
84
  }
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  .light {
87
  --background: oklch(0.985 0 0);
88
  --foreground: oklch(0.145 0 0);
 
83
  --sidebar-ring: oklch(0.65 0.2 265);
84
  }
85
 
86
+ .dark {
87
+ --background: oklch(0.145 0 0);
88
+ --foreground: oklch(0.985 0 0);
89
+ --card: oklch(0.178 0 0);
90
+ --card-foreground: oklch(0.985 0 0);
91
+ --popover: oklch(0.178 0 0);
92
+ --popover-foreground: oklch(0.985 0 0);
93
+ --primary: oklch(0.65 0.2 265);
94
+ --primary-foreground: oklch(0.985 0 0);
95
+ --secondary: oklch(0.22 0 0);
96
+ --secondary-foreground: oklch(0.985 0 0);
97
+ --muted: oklch(0.22 0 0);
98
+ --muted-foreground: oklch(0.6 0 0);
99
+ --accent: oklch(0.55 0.18 265);
100
+ --accent-foreground: oklch(0.985 0 0);
101
+ --destructive: oklch(0.704 0.191 22.216);
102
+ --border: oklch(1 0 0 / 10%);
103
+ --input: oklch(1 0 0 / 12%);
104
+ --ring: oklch(0.65 0.2 265);
105
+ --sidebar: oklch(0.12 0 0);
106
+ --sidebar-foreground: oklch(0.985 0 0);
107
+ --sidebar-primary: oklch(0.65 0.2 265);
108
+ --sidebar-primary-foreground: oklch(0.985 0 0);
109
+ --sidebar-accent: oklch(0.22 0 0);
110
+ --sidebar-accent-foreground: oklch(0.985 0 0);
111
+ --sidebar-border: oklch(1 0 0 / 8%);
112
+ --sidebar-ring: oklch(0.65 0.2 265);
113
+ }
114
+
115
  .light {
116
  --background: oklch(0.985 0 0);
117
  --foreground: oklch(0.145 0 0);
frontend/src/app/layout.tsx CHANGED
@@ -3,6 +3,7 @@ import { Inter } from "next/font/google";
3
  import "./globals.css";
4
  import { AuthProvider } from "@/lib/auth";
5
  import { TooltipProvider } from "@/components/ui/tooltip";
 
6
 
7
  const inter = Inter({
8
  variable: "--font-sans",
@@ -23,14 +24,21 @@ export default function RootLayout({
23
  children: React.ReactNode;
24
  }>) {
25
  return (
26
- <html lang="en" className={`${inter.variable} dark h-full antialiased`}>
27
  <body className="min-h-full flex flex-col bg-background text-foreground">
28
- <AuthProvider>
29
- <TooltipProvider>
30
- {children}
31
- </TooltipProvider>
32
- </AuthProvider>
 
 
 
 
 
 
 
33
  </body>
34
  </html>
35
  );
36
- }
 
3
  import "./globals.css";
4
  import { AuthProvider } from "@/lib/auth";
5
  import { TooltipProvider } from "@/components/ui/tooltip";
6
+ import { ThemeProvider } from "@/components/layout/ThemeProvider";
7
 
8
  const inter = Inter({
9
  variable: "--font-sans",
 
24
  children: React.ReactNode;
25
  }>) {
26
  return (
27
+ <html lang="en" className={`${inter.variable} h-full antialiased`} suppressHydrationWarning>
28
  <body className="min-h-full flex flex-col bg-background text-foreground">
29
+ <ThemeProvider
30
+ attribute="class"
31
+ defaultTheme="dark"
32
+ enableSystem={false}
33
+ disableTransitionOnChange
34
+ >
35
+ <AuthProvider>
36
+ <TooltipProvider>
37
+ {children}
38
+ </TooltipProvider>
39
+ </AuthProvider>
40
+ </ThemeProvider>
41
  </body>
42
  </html>
43
  );
44
+ }
frontend/src/components/layout/Header.tsx CHANGED
@@ -21,7 +21,8 @@ import {
21
  Moon,
22
  Sun,
23
  } from "lucide-react";
24
- import { useState } from "react";
 
25
 
26
  interface HeaderProps {
27
  sidebarOpen: boolean;
@@ -33,19 +34,13 @@ interface HeaderProps {
33
  export default function Header({ sidebarOpen, onToggleSidebar, viewerOpen, onToggleViewer }: HeaderProps) {
34
  const { user, logout } = useAuth();
35
  const router = useRouter();
36
- const [isDark, setIsDark] = useState(true);
 
37
 
38
- const toggleTheme = () => {
39
- const html = document.documentElement;
40
- if (isDark) {
41
- html.classList.remove("dark");
42
- html.classList.add("light");
43
- } else {
44
- html.classList.remove("light");
45
- html.classList.add("dark");
46
- }
47
- setIsDark(!isDark);
48
- };
49
 
50
  const handleLogout = () => {
51
  logout();
@@ -74,9 +69,11 @@ export default function Header({ sidebarOpen, onToggleSidebar, viewerOpen, onTog
74
  {viewerOpen ? <PanelRightClose className="w-4 h-4" /> : <PanelRightOpen className="w-4 h-4" />}
75
  </Button>
76
 
77
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={toggleTheme} title={isDark ? "Light mode" : "Dark mode"}>
78
- {isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
79
- </Button>
 
 
80
 
81
  <DropdownMenu>
82
  <DropdownMenuTrigger className="flex items-center h-8 gap-2 px-2 rounded-md hover:bg-accent transition-colors cursor-pointer">
@@ -102,4 +99,4 @@ export default function Header({ sidebarOpen, onToggleSidebar, viewerOpen, onTog
102
  </div>
103
  </header>
104
  );
105
- }
 
21
  Moon,
22
  Sun,
23
  } from "lucide-react";
24
+ import { useTheme } from "next-themes";
25
+ import { useEffect, useState } from "react";
26
 
27
  interface HeaderProps {
28
  sidebarOpen: boolean;
 
34
  export default function Header({ sidebarOpen, onToggleSidebar, viewerOpen, onToggleViewer }: HeaderProps) {
35
  const { user, logout } = useAuth();
36
  const router = useRouter();
37
+ const { theme, setTheme } = useTheme();
38
+ const [mounted, setMounted] = useState(false);
39
 
40
+ useEffect(() => setMounted(true), []);
41
+
42
+ const isDark = theme === "dark";
43
+ const toggleTheme = () => setTheme(isDark ? "light" : "dark");
 
 
 
 
 
 
 
44
 
45
  const handleLogout = () => {
46
  logout();
 
69
  {viewerOpen ? <PanelRightClose className="w-4 h-4" /> : <PanelRightOpen className="w-4 h-4" />}
70
  </Button>
71
 
72
+ {mounted && (
73
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={toggleTheme} title={isDark ? "Light mode" : "Dark mode"}>
74
+ {isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
75
+ </Button>
76
+ )}
77
 
78
  <DropdownMenu>
79
  <DropdownMenuTrigger className="flex items-center h-8 gap-2 px-2 rounded-md hover:bg-accent transition-colors cursor-pointer">
 
99
  </div>
100
  </header>
101
  );
102
+ }
frontend/src/components/layout/ThemeProvider.tsx ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { ThemeProvider as NextThemesProvider } from "next-themes";
4
+ import { type ThemeProviderProps } from "next-themes";
5
+
6
+ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
7
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
8
+ }
frontend/src/components/layout/ThemeToggle.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useTheme } from "next-themes";
4
+ import { useEffect, useState } from "react";
5
+ import { Sun, Moon } from "lucide-react";
6
+
7
+ export function ThemeToggle() {
8
+ const { theme, setTheme } = useTheme();
9
+ const [mounted, setMounted] = useState(false);
10
+
11
+ // Avoid hydration mismatch
12
+ useEffect(() => setMounted(true), []);
13
+ if (!mounted) return null;
14
+
15
+ return (
16
+ <button
17
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
18
+ aria-label="Toggle theme"
19
+ className="rounded-md p-2 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
20
+ >
21
+ {theme === "dark" ? (
22
+ <Sun className="h-5 w-5 text-yellow-400" />
23
+ ) : (
24
+ <Moon className="h-5 w-5 text-gray-700" />
25
+ )}
26
+ </button>
27
+ );
28
+ }