From d4fda1c9d2cfc61a5aca0a4fbd2397f1b7d33b2d Mon Sep 17 00:00:00 2001 From: REHAAANNN <147967435+REHAAANNN@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:46:58 +0530 Subject: [PATCH] feat: implement dark/light mode toggle (issue #170) - Add ThemeContext with localStorage persistence and system preference detection - Create ThemeToggle component with sun/moon icons - Add ClientLayout wrapper for client-side components - Update Header with dark mode styles and toggle button placement - Configure Tailwind with class-based dark mode strategy - Add dark mode styles for body, scrollbar, and navigation - Ensure smooth transitions between themes --- client/app/components/ClientLayout.tsx | 18 ++ client/app/components/Header/Header.tsx | 25 ++- client/app/components/Pages/LoginPage.tsx | 2 +- client/app/components/ThemeToggle.tsx | 35 ++++ client/app/context/ThemeContext.tsx | 53 +++++ client/app/globals.css | 43 ++-- client/app/layout.tsx | 27 ++- client/package-lock.json | 240 +++++++++++----------- client/tailwind.config.ts | 1 + 9 files changed, 279 insertions(+), 165 deletions(-) create mode 100644 client/app/components/ClientLayout.tsx create mode 100644 client/app/components/ThemeToggle.tsx create mode 100644 client/app/context/ThemeContext.tsx diff --git a/client/app/components/ClientLayout.tsx b/client/app/components/ClientLayout.tsx new file mode 100644 index 00000000..59f06a4c --- /dev/null +++ b/client/app/components/ClientLayout.tsx @@ -0,0 +1,18 @@ +"use client"; +import React from "react"; +import Header from "./Header/Header"; +import ChatBot from "./ChatBot/ChatBot"; + +export default function ClientLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> +
+ + {children} + + ); +} diff --git a/client/app/components/Header/Header.tsx b/client/app/components/Header/Header.tsx index ef15d945..dfd64904 100644 --- a/client/app/components/Header/Header.tsx +++ b/client/app/components/Header/Header.tsx @@ -13,6 +13,7 @@ import { } from "@heroicons/react/24/outline"; import Web3Connect from "../Helper/Web3Connect"; import Image from "next/image"; +import ThemeToggle from "../ThemeToggle"; const menuItems = [ { name: "Home", href: "/", icon: HomeIcon }, @@ -29,7 +30,7 @@ const Header = () => { return ( <> { alt="Agora Blockchain" style={{ width: 'auto' }} /> -

+

Agora Blockchain

@@ -56,11 +57,11 @@ const Header = () => { {menuItems.map((item) => ( @@ -78,6 +79,7 @@ const Header = () => { )} ))} +
@@ -85,12 +87,13 @@ const Header = () => { {/* Mobile/Tablet Menu Button */}
+
@@ -111,7 +114,7 @@ const Header = () => { onClick={toggleSidebar} /> {
@@ -132,8 +135,8 @@ const Header = () => { onClick={toggleSidebar} className={`flex items-center p-4 rounded-lg transition-colors ${ pathname === item.href - ? "bg-indigo-50 text-indigo-600" - : "text-gray-700 hover:bg-gray-50" + ? "bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400" + : "text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800" }`} > diff --git a/client/app/components/Pages/LoginPage.tsx b/client/app/components/Pages/LoginPage.tsx index f3af82ce..f153cad9 100644 --- a/client/app/components/Pages/LoginPage.tsx +++ b/client/app/components/Pages/LoginPage.tsx @@ -3,7 +3,7 @@ import AossieImg from "../../../public/aossie.png"; const LoginPage = () => { return (
{ + setMounted(true); + }, []); + + // Prevent hydration mismatch by not rendering until mounted + if (!mounted) { + return ( +
// Placeholder to prevent layout shift + ); + } + + return ( + + ); +} diff --git a/client/app/context/ThemeContext.tsx b/client/app/context/ThemeContext.tsx new file mode 100644 index 00000000..0d7094e4 --- /dev/null +++ b/client/app/context/ThemeContext.tsx @@ -0,0 +1,53 @@ +"use client"; +import React, { createContext, useContext, useEffect, useState } from "react"; + +type Theme = "light" | "dark"; + +interface ThemeContextType { + theme: Theme; + toggleTheme: () => void; +} + +const ThemeContext = createContext(undefined); + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useState("light"); + const [mounted, setMounted] = useState(false); + + // Load theme from localStorage on mount + useEffect(() => { + setMounted(true); + const savedTheme = localStorage.getItem("theme") as Theme; + if (savedTheme) { + setTheme(savedTheme); + document.documentElement.classList.toggle("dark", savedTheme === "dark"); + } else { + // Check system preference + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + const initialTheme = prefersDark ? "dark" : "light"; + setTheme(initialTheme); + document.documentElement.classList.toggle("dark", prefersDark); + } + }, []); + + const toggleTheme = () => { + const newTheme = theme === "light" ? "dark" : "light"; + setTheme(newTheme); + localStorage.setItem("theme", newTheme); + document.documentElement.classList.toggle("dark", newTheme === "dark"); + }; + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +} diff --git a/client/app/globals.css b/client/app/globals.css index c898587a..ac2b4828 100644 --- a/client/app/globals.css +++ b/client/app/globals.css @@ -3,33 +3,38 @@ @tailwind utilities; body { - background-color: #ffffff; margin: 0; - overflow: hidden; + overflow-y: auto; +} + +@layer base { + body { + @apply bg-white dark:bg-gray-900 transition-colors duration-300; + } } html { scroll-behavior: smooth; } -::-webkit-scrollbar { - width: 1px; - height: 1px; -} +@layer utilities { + ::-webkit-scrollbar { + width: 1px; + height: 1px; + } -/* Scrollbar track */ -::-webkit-scrollbar-track { - background: #f0f0f0; - border-radius: 4px; -} + /* Scrollbar track */ + ::-webkit-scrollbar-track { + @apply bg-gray-200 dark:bg-gray-800 rounded; + } -/* Scrollbar thumb */ -::-webkit-scrollbar-thumb { - background-color: #cfcaca; - border-radius: 1px; - border: 2px solid #f0f0f0; -} + /* Scrollbar thumb */ + ::-webkit-scrollbar-thumb { + @apply bg-gray-400 dark:bg-gray-600 rounded; + border: 2px solid transparent; + } -::-webkit-scrollbar-thumb:hover { - background-color: #ebe4e4; + ::-webkit-scrollbar-thumb:hover { + @apply bg-gray-500 dark:bg-gray-500; + } } \ No newline at end of file diff --git a/client/app/layout.tsx b/client/app/layout.tsx index 5d344e2f..ab382187 100644 --- a/client/app/layout.tsx +++ b/client/app/layout.tsx @@ -7,11 +7,10 @@ import { WagmiProvider } from "wagmi"; import { sepolia } from "wagmi/chains"; import { QueryClientProvider } from "@tanstack/react-query"; import { config, queryClient } from "./helpers/client"; -import Header from "./components/Header/Header"; -import Web3Connect from "./components/Helper/Web3Connect"; import "rsuite/dist/rsuite-no-reset.min.css"; import { CustomProvider } from "rsuite"; -import ChatBot from "./components/ChatBot/ChatBot"; +import { ThemeProvider } from "./context/ThemeContext"; +import ClientLayout from "./components/ClientLayout"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { @@ -28,17 +27,17 @@ export default function RootLayout({ - - - - -
- - {children} - - - - + + + + + + {children} + + + + + ); diff --git a/client/package-lock.json b/client/package-lock.json index a3baa66c..10c3dab7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3955,6 +3955,126 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.4.tgz", + "integrity": "sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.4.tgz", + "integrity": "sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.4.tgz", + "integrity": "sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.4.tgz", + "integrity": "sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.4.tgz", + "integrity": "sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.4.tgz", + "integrity": "sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.4.tgz", + "integrity": "sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz", + "integrity": "sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nextui-org/theme": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/@nextui-org/theme/-/theme-2.2.6.tgz", @@ -16583,126 +16703,6 @@ "optional": true } } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.4.tgz", - "integrity": "sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.4.tgz", - "integrity": "sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.4.tgz", - "integrity": "sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.4.tgz", - "integrity": "sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.4.tgz", - "integrity": "sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.4.tgz", - "integrity": "sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.4.tgz", - "integrity": "sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz", - "integrity": "sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index f71309a3..11ee84d0 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -1,6 +1,7 @@ import type { Config } from "tailwindcss"; const config: Config = { + darkMode: 'class', // Enable class-based dark mode content: [ "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}",