Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions client/app/components/ClientLayout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Header />
<ChatBot />
{children}
</>
);
}
25 changes: 14 additions & 11 deletions client/app/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -29,7 +30,7 @@ const Header = () => {
return (
<>
<motion.header
className="bg-white border-b border-gray-200 fixed w-full z-30 shadow-sm"
className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 fixed w-full z-30 shadow-sm transition-colors duration-200"
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
Expand All @@ -46,7 +47,7 @@ const Header = () => {
alt="Agora Blockchain"
style={{ width: 'auto' }}
/>
<h1 className="ml-3 text-xl font-bold text-gray-800 hidden sm:block">
<h1 className="ml-3 text-xl font-bold text-gray-800 dark:text-white hidden sm:block transition-colors duration-200">
Agora Blockchain
</h1>
</Link>
Expand All @@ -56,11 +57,11 @@ const Header = () => {
{menuItems.map((item) => (
<Link key={item.name} href={item.href} className="relative">
<motion.button
className={`inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md ${
className={`inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md transition-colors duration-200 ${
pathname === item.href
? "text-indigo-600"
: "text-gray-700 hover:text-indigo-600"
} bg-white hover:bg-gray-50`}
? "text-indigo-600 dark:text-indigo-400"
: "text-gray-700 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400"
} bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Expand All @@ -78,19 +79,21 @@ const Header = () => {
)}
</Link>
))}
<ThemeToggle />
<div className="hidden lg:block">
<Web3Connect />
</div>
</nav>

{/* Mobile/Tablet Menu Button */}
<div className="lg:hidden flex items-center space-x-2">
<ThemeToggle />
<div className="scale-90">
<Web3Connect />
</div>
<button
onClick={toggleSidebar}
className="p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100"
className="p-2 rounded-md text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
>
<Bars3Icon className="h-6 w-6" />
</button>
Expand All @@ -111,7 +114,7 @@ const Header = () => {
onClick={toggleSidebar}
/>
<motion.div
className="fixed right-0 top-0 h-full w-72 bg-white z-50 shadow-lg"
className="fixed right-0 top-0 h-full w-72 bg-white dark:bg-gray-900 z-50 shadow-lg transition-colors duration-200"
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
Expand All @@ -120,7 +123,7 @@ const Header = () => {
<div className="p-6">
<button
onClick={toggleSidebar}
className="absolute top-4 right-4 p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100"
className="absolute top-4 right-4 p-2 rounded-md text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
>
<XMarkIcon className="h-6 w-6" />
</button>
Expand All @@ -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"
}`}
>
<item.icon className="h-6 w-6 mr-4" />
Expand Down
2 changes: 1 addition & 1 deletion client/app/components/Pages/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import AossieImg from "../../../public/aossie.png";
const LoginPage = () => {
return (
<div
className="flex p-8 h-full w-full items-start justify-between"
className="flex p-8 h-full w-full items-start justify-between bg-white dark:bg-gray-900 transition-colors duration-300"
style={{
backgroundImage: `url(${AossieImg.src})`,
backgroundSize: "20%",
Expand Down
35 changes: 35 additions & 0 deletions client/app/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client";
import { SunIcon, MoonIcon } from "@heroicons/react/24/outline";
import { useTheme } from "../context/ThemeContext";
import { useEffect, useState } from "react";

export default function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

// Prevent hydration mismatch by not rendering until mounted
if (!mounted) {
return (
<div className="p-2 w-9 h-9" /> // Placeholder to prevent layout shift
);
}

return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors duration-200"
aria-label="Toggle theme"
title={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
>
{theme === "light" ? (
<MoonIcon className="h-5 w-5 text-gray-800 dark:text-gray-200" />
) : (
<SunIcon className="h-5 w-5 text-gray-800 dark:text-gray-200" />
)}
</button>
);
}
53 changes: 53 additions & 0 deletions client/app/context/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -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<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("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");
};
Comment on lines +13 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate localStorage theme + drop unused mounted.
Right now any truthy localStorage.getItem("theme") is trusted via a cast, and mounted is unused. Safer to validate and simplify.

 export function ThemeProvider({ children }: { children: React.ReactNode }) {
   const [theme, setTheme] = useState<Theme>("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");
+    const savedTheme = localStorage.getItem("theme");
+    if (savedTheme === "light" || savedTheme === "dark") {
+      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);
     }
   }, []);
🤖 Prompt for AI Agents
In client/app/context/ThemeContext.tsx around lines 13 to 38, the code trusts
any truthy localStorage value cast to Theme and keeps an unused mounted state;
validate the saved value against the allowed Theme union ("light" | "dark")
before applying it (fallback to system preference if invalid or missing), remove
the mounted state entirely, and simplify the effect to read localStorage, check
if value === "light" or "dark", apply document.documentElement.classList.toggle
appropriately, set the theme state, and keep the toggleTheme logic the same
(ensure it also writes only "light" or "dark" to localStorage).


return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}

export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
43 changes: 24 additions & 19 deletions client/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
27 changes: 13 additions & 14 deletions client/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -28,17 +27,17 @@ export default function RootLayout({
<html lang="en">
<link rel="icon" href="/aossie.png" sizes="any" />
<body className={inter.className}>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider initialChain={sepolia}>
<CustomProvider>
<Header />
<ChatBot />
{children}
</CustomProvider>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
<ThemeProvider>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider initialChain={sepolia}>
<CustomProvider>
<ClientLayout>{children}</ClientLayout>
</CustomProvider>
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
</ThemeProvider>
</body>
</html>
);
Expand Down
Loading