applied dark mode

This commit is contained in:
ATUL GUNJAL 2025-04-09 11:34:27 +05:30
parent abf12c8f68
commit a7b6b78890
9 changed files with 314 additions and 49 deletions

21
package-lock.json generated
View File

@ -12,9 +12,11 @@
"appwrite": "^17.0.1",
"lucide-react": "^0.487.0",
"next": "15.2.4",
"next-themes": "^0.4.6",
"postcss": "^8.5.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"tailwindcss": "^4.1.3"
},
"devDependencies": {
@ -4311,6 +4313,16 @@
}
}
},
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -4701,6 +4713,15 @@
"react": "^19.1.0"
}
},
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@ -13,9 +13,11 @@
"appwrite": "^17.0.1",
"lucide-react": "^0.487.0",
"next": "15.2.4",
"next-themes": "^0.4.6",
"postcss": "^8.5.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"tailwindcss": "^4.1.3"
},
"devDependencies": {

View File

@ -1,5 +1,6 @@
const config = {
plugins: {
darkMode: ['class', '[data-theme="dark"]'],
plugins: {
"@tailwindcss/postcss": {},
},
};

79
src/app/client-layout.js Normal file
View File

@ -0,0 +1,79 @@
'use client';
import { useState, useEffect } from "react";
import { account } from "./lib/appwrite";
import { useRouter } from "next/navigation";
import Sidebar from "./components/Sidebar";
import Navbar from "./components/Navbar";
import { Menu } from 'lucide-react';
export default function ClientLayout({ children }) {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isMobileOpen, setIsMobileOpen] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
const checkAuth = async () => {
try {
await account.get();
setIsLoggedIn(true);
} catch (error) {
setIsLoggedIn(false);
router.push('/login');
} finally {
setLoading(false);
}
};
checkAuth();
}, [router]);
const toggleMobileSidebar = () => {
setIsMobileOpen(!isMobileOpen);
};
if (loading) {
return <div className="flex items-center justify-center min-h-screen">Loading...</div>;
}
if (!isLoggedIn) {
return (
<div className="min-h-screen">
<main className="p-4 md:p-6">
{children}
</main>
</div>
);
}
return (
<div className="flex min-h-screen">
{/* Mobile Toggle Button (outside sidebar) */}
<button
onClick={toggleMobileSidebar}
className="md:hidden fixed top-4 left-4 z-50 bg-white dark:bg-gray-800 p-2 rounded-md shadow-md"
>
<Menu size={24} />
</button>
<Sidebar
isCollapsed={isSidebarCollapsed}
isMobileOpen={isMobileOpen}
toggleMobileSidebar={toggleMobileSidebar}
/>
<div className={`flex-1 flex flex-col transition-all duration-300 ${
isSidebarCollapsed ? 'md:pl-20' : 'md:pl-64'
}`}>
<Navbar
isSidebarCollapsed={isSidebarCollapsed}
toggleSidebar={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
/>
<main className="flex-1 p-4 md:p-6 overflow-auto">
{children}
</main>
</div>
</div>
);
}

View File

@ -1,34 +1,126 @@
'use client';
import { useState, useEffect } from "react";
import { account } from "../lib/appwrite";
import { useRouter } from 'next/navigation';
import { LogOut } from 'lucide-react';
import { useRouter } from "next/navigation";
import { FaUserCircle, FaMoon, FaSun } from "react-icons/fa";
import { LogOut, Menu } from "lucide-react";
export default function Navbar() {
export default function Navbar({ toggleSidebar }) {
const router = useRouter();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [user, setUser] = useState(null);
const [darkMode, setDarkMode] = useState(false);
// Initialize dark mode from localStorage or system preference
useEffect(() => {
const savedMode = localStorage.getItem('darkMode');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedMode !== null) {
setDarkMode(savedMode === 'true');
} else if (systemPrefersDark) {
setDarkMode(true);
}
}, []);
// Apply dark mode class to document element
useEffect(() => {
if (darkMode) {
document.documentElement.classList.add('dark');
localStorage.setItem('darkMode', 'true');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('darkMode', 'false');
}
}, [darkMode]);
// Toggle dark mode
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
// Fetch current user details
useEffect(() => {
const getUser = async () => {
try {
const userData = await account.get();
setUser(userData);
} catch (error) {
console.error("Error fetching user:", error);
}
};
getUser();
}, []);
const handleLogout = async () => {
try {
await account.deleteSession('current');
router.push('/login');
await account.deleteSession("current");
router.push("/login");
} catch (error) {
console.error('Logout failed:', error);
console.error("Logout failed:", error);
}
};
return (
<nav className="bg-white border-b border-gray-200 px-4 py-3 sm:px-6 lg:px-8">
<nav className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-3 sm:px-6 lg:px-8 relative">
<div className="flex justify-between items-center">
<div className="flex items-center">
<span className="font-semibold text-gray-800">Company Portal</span>
</div>
<div className="flex items-center space-x-4">
{/* Sidebar Toggle Button - Hidden on mobile */}
<button
onClick={handleLogout}
className="flex items-center space-x-1 text-red-600 hover:text-red-800 transition-colors"
onClick={toggleSidebar}
className="hidden md:block p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
>
<LogOut className="w-5 h-5" />
<span className="hidden sm:inline">Logout</span>
<Menu size={24} className="text-gray-800 dark:text-gray-200" />
</button>
<span className="font-semibold text-gray-800 dark:text-gray-200 text-lg">
FSPL HR Portal
</span>
</div>
<div className="flex items-center space-x-4">
{/* Dark Mode Toggle */}
<button
onClick={toggleDarkMode}
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
aria-label={darkMode ? "Switch to light mode" : "Switch to dark mode"}
>
{darkMode ? (
<FaSun className="text-yellow-400 text-xl" />
) : (
<FaMoon className="text-gray-700 dark:text-gray-200 text-xl" />
)}
</button>
{/* User Dropdown */}
<div className="relative">
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex items-center focus:outline-none"
>
<FaUserCircle className="text-3xl text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400" />
</button>
{isDropdownOpen && (
<div className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50">
<div className="p-4">
<p className="font-semibold text-gray-800 dark:text-gray-200">
{user?.name || "Guest"}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 truncate">
{user?.email || "No email"}
</p>
</div>
<hr className="border-gray-200 dark:border-gray-700" />
<button
onClick={handleLogout}
className="w-full text-left px-4 py-2 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-gray-700 flex items-center space-x-2"
>
<LogOut className="w-4 h-4" />
<span>Logout</span>
</button>
</div>
)}
</div>
</div>
</div>
</nav>

View File

@ -1,27 +1,62 @@
'use client';
import Link from 'next/link';
import { X, Home, Users, ClipboardList, User } from 'lucide-react';
export default function Sidebar({ isCollapsed, isMobileOpen, toggleMobileSidebar }) {
const navItems = [
{ href: "/pages/dashboard", icon: Home, label: "Home" },
{ href: "/pages/user", icon: Users, label: "User" },
{ href: "/pages/admin/members", icon: Users, label: "Members" },
{ href: "/pages/assignments", icon: ClipboardList, label: "Assignments" },
{ href: "/pages/user/profile", icon: User, label: "My Profile" },
];
export default function Sidebar() {
return (
<aside className="w-64 border-r border-gray-200 h-screen bg-white-800 text-gray-700 p-4 hidden md:block">
<h2 className="text-xl font-bold mb-6">Dashboard</h2>
<ul className="space-y-4">
<li>
<Link href="/pages/dashboard" className="hover:text-blue-500">Home</Link>
</li>
<li>
<Link href="/pages/user" className="hover:text-blue-500">user</Link>
</li>
<li>
<Link href="/pages/admin/members" className="hover:text-blue-500">Members</Link>
</li>
<li>
<Link href="/pages/assignments" className="hover:text-blue-500">Assignments</Link>
</li>
<li>
<Link href="/pages/user/profile" className="hover:text-blue-500">My Profile</Link>
</li>
</ul>
<aside className={`
fixed top-0 left-0 h-full bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 z-40
transition-all duration-300
${isCollapsed ? 'w-20' : 'w-64'}
${isMobileOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
`}>
<div className="p-4 h-full flex flex-col">
{/* Mobile Close Button (visible only on mobile) */}
<button
onClick={toggleMobileSidebar}
className="md:hidden absolute top-4 right-4 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700"
>
<X className="w-5 h-5" />
</button>
<div className={`flex items-center ${isCollapsed ? 'justify-center' : 'justify-between'} mb-6`}>
{!isCollapsed && (
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-purple-500">
HrMaTe
</h2>
)}
</div>
<nav className="flex-1">
<ul className="space-y-2">
{navItems.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className={`
flex items-center p-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700
${isCollapsed ? 'justify-center' : 'px-4'}
`}
onClick={toggleMobileSidebar} // Close sidebar when clicking a link on mobile
>
<item.icon className="w-5 h-5" />
{!isCollapsed && (
<span className="ml-3">{item.label}</span>
)}
</Link>
</li>
))}
</ul>
</nav>
</div>
</aside>
);
}
}

View File

@ -0,0 +1,10 @@
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}

View File

@ -1 +1,33 @@
@import "tailwindcss";
@import "tailwindcss";
@layer base {
body {
@apply bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100;
transition: background-color 0.2s ease;
}
}
/* globals.css */
html {
transition: color-scheme 0.3s ease;
}
body {
transition: background-color 0.3s ease, color 0.3s ease;
}
:root {
--bg-color: #ffffff;
--text-color: #111827;
/* other light mode variables */
}
.dark {
--bg-color: #111827;
--text-color: #f3f4f6;
/* other dark mode variables */
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}

View File

@ -1,7 +1,6 @@
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Sidebar from "./components/Sidebar"
import Navbar from "./components/Navbar";
import ClientLayout from "./client-layout";
const geistSans = Geist({
variable: "--font-geist-sans",
@ -20,16 +19,10 @@ export const metadata = {
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<div className="flex">
<Sidebar />
<div className="flex-1">
<Navbar />
<main className="p-4">{children}</main>
</div>
</div>
<html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}>
<body className="bg-gray-50">
<ClientLayout>{children}</ClientLayout>
</body>
</html>
);
}
}