Dark mode completed

This commit is contained in:
ATUL GUNJAL 2025-04-24 09:39:39 +05:30
parent afee8c8516
commit 950442ba3e
10 changed files with 570 additions and 315 deletions

View File

@ -6,8 +6,6 @@ import { useRouter } from "next/navigation";
import { useTheme } from "../context/ThemeContext";
import { account } from "../lib/appwrite";
import { useAuth } from "../context/AuthContext";
import { Bell } from "lucide-react";
import { motion } from "framer-motion";
import { MdOutlineSettings, MdSupport } from "react-icons/md";
import { AiOutlineUser } from "react-icons/ai";
import NotificationBell from './NotificationBell';
@ -68,18 +66,44 @@ const Navbar = ({ sidebarOpen, setSidebarOpen, isCollapsed, setIsCollapsed }) =>
console.error("Logout failed:", error);
}
};
// ----------------notificatio logic------------------
if (darkMode === undefined) {
return (
<nav className="bg-white dark:bg-gray-900 shadow-md sticky top-0 w-full z-30 p-2 sm:p-4 flex items-center justify-between">
<div className="animate-pulse flex space-x-4">
<div className="h-10 w-10 rounded-lg bg-gray-200 dark:bg-gray-700"></div>
<div className="h-10 w-[300px] rounded bg-gray-200 dark:bg-gray-700 hidden md:block"></div>
</div>
<div className="flex items-center gap-4">
<div className="h-8 w-8 rounded-full bg-gray-200 dark:bg-gray-700"></div>
</div>
</nav>
);
}
return (
<nav className={`
${darkMode ? 'bg-gray-900 border-gray-700 shadow-gray-800/30' : 'bg-white border-gray-200 shadow-md'}
sticky top-0 w-full z-30
p-2 sm:p-4
flex items-center justify-between
transition-colors duration-300
`}>
{/* Left Side */}
<div className="flex items-center">
{/* Hamburger Menu Button - Always visible */}
{/* Hamburger Menu Button */}
<button
onClick={handleSidebarToggle}
className={`flex h-10 w-10 items-center justify-center rounded-lg border border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-300 mr-2 ${
sidebarOpen || isCollapsed ? 'bg-gray-100 dark:bg-gray-800' : ''
}`}
className={`
flex h-10 w-10 items-center justify-center rounded-lg
border ${darkMode ? 'border-gray-600 bg-gray-800' : 'border-gray-300 bg-white'}
${sidebarOpen || isCollapsed ? (darkMode ? 'bg-gray-700' : 'bg-gray-100') : ''}
text-gray-500 dark:text-gray-300
mr-2
transition-all duration-200
hover:bg-gray-100 dark:hover:bg-gray-700
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50
`}
aria-label="Toggle sidebar"
>
{sidebarOpen || isCollapsed ? (
@ -115,17 +139,39 @@ const Navbar = ({ sidebarOpen, setSidebarOpen, isCollapsed, setIsCollapsed }) =>
)}
</button>
{/* Search Box - Hidden on mobile */}
<div className="hidden md:flex items-center gap-[2px] border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-2 rounded h-[45px] w-[300px] lg:w-[400px] ml-2">
{/* Search Box */}
<div className={`
hidden md:flex items-center gap-[2px]
border ${darkMode ? 'border-gray-600 bg-gray-800' : 'border-gray-300 bg-white'}
p-2 rounded-lg
h-[45px] w-[300px] lg:w-[400px] ml-2
transition-colors duration-300
focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-opacity-50
${darkMode ? 'focus-within:border-gray-500' : 'focus-within:border-blue-500'}
`}>
<div className="relative flex-1">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
<FaSearch className={`
absolute left-3 top-1/2 transform -translate-y-1/2
${darkMode ? 'text-gray-400' : 'text-gray-500'}
`} />
<input
type="text"
placeholder="Search or type command..."
className="w-full bg-transparent border-none pl-10 pr-10 py-2 focus:outline-none text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500"
className={`
w-full bg-transparent border-none
pl-10 pr-10 py-2
focus:outline-none
${darkMode ? 'text-gray-200 placeholder-gray-500' : 'text-gray-800 placeholder-gray-400'}
text-sm
`}
/>
</div>
<div className="px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-300 text-xs rounded-md">
<div className={`
px-2 py-1
${darkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-500'}
text-xs rounded-md
font-mono
`}>
K
</div>
</div>
@ -134,14 +180,26 @@ const Navbar = ({ sidebarOpen, setSidebarOpen, isCollapsed, setIsCollapsed }) =>
{/* Right Side */}
<div className="flex items-center gap-2 sm:gap-4">
{/* Mobile Search Button */}
<button className="md:hidden p-2 rounded-full text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-white">
<button className={`
md:hidden p-2 rounded-full
${darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-800'}
hover:bg-gray-200 dark:hover:bg-gray-700
transition-colors duration-200
`}>
<FaSearch className="text-lg" />
</button>
{/* Dark Mode Toggle */}
<button
onClick={toggleDarkMode}
className="p-2 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 transition duration-300 hover:bg-gray-100 dark:hover:bg-gray-800"
className={`
p-2 rounded-full
${darkMode ? 'bg-gray-700 hover:bg-gray-600' : 'bg-gray-100 hover:bg-gray-200'}
flex items-center justify-center
w-8 h-8 sm:w-10 sm:h-10
transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50
`}
aria-label={darkMode ? "Switch to light mode" : "Switch to dark mode"}
>
{darkMode ? (
@ -174,13 +232,20 @@ const Navbar = ({ sidebarOpen, setSidebarOpen, isCollapsed, setIsCollapsed }) =>
</button>
{/* Notifications */}
<NotificationBell />
<NotificationBell darkMode={darkMode} />
{/* User Profile */}
<div className="relative" ref={dropdownRef}>
<button
onClick={toggleDropdown}
className="flex items-center gap-1 sm:gap-2 cursor-pointer hover:text-blue-500 dark:hover:text-blue-400"
className={`
flex items-center gap-1 sm:gap-2
cursor-pointer
hover:text-blue-500 dark:hover:text-blue-400
transition-colors duration-200
p-1 rounded-full
${dropdownOpen ? (darkMode ? 'bg-gray-700' : 'bg-gray-100') : ''}
`}
aria-label="User menu"
>
<img
@ -188,49 +253,113 @@ const Navbar = ({ sidebarOpen, setSidebarOpen, isCollapsed, setIsCollapsed }) =>
alt="User profile"
width={32}
height={32}
className="rounded-full w-8 h-8 sm:w-10 sm:h-10"
className={`
rounded-full w-8 h-8 sm:w-10 sm:h-10
border-2 ${darkMode ? 'border-gray-600' : 'border-gray-200'}
transition-all duration-200
${dropdownOpen ? 'ring-2 ring-blue-500' : ''}
`}
/>
<span className="hidden sm:inline text-gray-700 dark:text-gray-200">
<span className={`
hidden sm:inline
${darkMode ? 'text-gray-500' : 'text-gray-700'}
text-sm font-medium
`}>
{currentUser?.name || "User"}
</span>
</button>
{dropdownOpen && currentUser && (
<div className="absolute right-0 mt-2 w-56 sm:w-64 bg-white dark:bg-gray-800 shadow-lg rounded-lg p-2 sm:p-4 z-50 border border-gray-200 dark:border-gray-700">
<div className={`
absolute right-0 mt-2 w-56 sm:w-64
${darkMode ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'}
shadow-lg rounded-lg
p-2 sm:p-4
z-50 border
transition-all duration-200
origin-top-right
animate-scaleIn
`}>
{/* User Info */}
<div className="mb-2 sm:mb-3">
<h3 className="font-semibold text-gray-800 dark:text-gray-200 text-sm sm:text-base">
<h3 className={`
font-semibold
${darkMode ? 'text-gray-800' : 'text-gray-500'}
text-sm sm:text-base
`}>
{currentUser.name || "User Name"}
</h3>
<p className="text-xs sm:text-sm text-gray-500 dark:text-gray-400">
<p className={`
text-xs sm:text-sm
${darkMode ? 'text-gray-400' : 'text-gray-500'}
truncate
`}>
{currentUser.email}
</p>
</div>
<hr className="border-gray-200 dark:border-gray-700 my-1 sm:my-2" />
<hr className={`
${darkMode ? 'border-gray-700' : 'border-gray-200'}
my-1 sm:my-2
`} />
{/* Menu Items */}
<div className="space-y-1 sm:space-y-2">
<button className="flex items-center gap-2 w-full text-left p-1 sm:p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md text-sm sm:text-base text-gray-700 dark:text-gray-200">
<AiOutlineUser className="text-gray-500 dark:text-gray-400" />
<button className={`
flex items-center gap-2 w-full text-left
p-1 sm:p-2
${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}
rounded-md
text-sm sm:text-base
${darkMode ? 'text-gray-200' : 'text-gray-700'}
transition-colors duration-200
`}>
<AiOutlineUser className={darkMode ? 'text-gray-400' : 'text-gray-500'} />
Edit profile
</button>
<button className="flex items-center gap-2 w-full text-left p-1 sm:p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md text-sm sm:text-base text-gray-700 dark:text-gray-200">
<MdOutlineSettings className="text-gray-500 dark:text-gray-400" />
<button className={`
flex items-center gap-2 w-full text-left
p-1 sm:p-2
${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}
rounded-md
text-sm sm:text-base
${darkMode ? 'text-gray-200' : 'text-gray-700'}
transition-colors duration-200
`}>
<MdOutlineSettings className={darkMode ? 'text-gray-400' : 'text-gray-500'} />
Account settings
</button>
<button className="flex items-center gap-2 w-full text-left p-1 sm:p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md text-sm sm:text-base text-gray-700 dark:text-gray-200">
<MdSupport className="text-gray-500 dark:text-gray-400" />
<button className={`
flex items-center gap-2 w-full text-left
p-1 sm:p-2
${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}
rounded-md
text-sm sm:text-base
${darkMode ? 'text-gray-200' : 'text-gray-700'}
transition-colors duration-200
`}>
<MdSupport className={darkMode ? 'text-gray-400' : 'text-gray-500'} />
Support
</button>
</div>
<hr className="border-gray-200 dark:border-gray-700 my-1 sm:my-2" />
<hr className={`
${darkMode ? 'border-gray-700' : 'border-gray-200'}
my-1 sm:my-2
`} />
{/* Sign Out */}
<button
onClick={handleSignOut}
className="flex items-center gap-2 w-full text-left p-1 sm:p-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md text-sm sm:text-base"
className={`
flex items-center gap-2 w-full text-left
p-1 sm:p-2
text-red-500 hover:text-red-600 dark:hover:text-red-400
${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}
rounded-md
text-sm sm:text-base
transition-colors duration-200
`}
>
<FiLogOut />
Sign out

View File

@ -1,5 +1,5 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { FileText, Moon, Sun } from "lucide-react";
@ -29,6 +29,40 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen, isCollapsed }) => {
localStorage.removeItem("token"); // or account.deleteSession("current") if using Appwrite
router.push("/signup");
};
// ---------------logo handling in dark mode-----------------------
// Create a separate LogoImage component for better error handling
function LogoImage({ darkSrc, lightSrc, darkMode, className, alt }) {
const [imgSrc, setImgSrc] = useState(darkMode ? darkSrc : lightSrc);
const [errored, setErrored] = useState(false);
useEffect(() => {
setImgSrc(darkMode ? darkSrc : lightSrc);
setErrored(false); // Reset error state when mode changes
}, [darkMode, darkSrc, lightSrc]);
const handleError = () => {
if (!errored) {
// Try falling back to opposite mode's image
setImgSrc(darkMode ? lightSrc : darkSrc);
setErrored(true);
} else {
// Ultimate fallback to placeholder
setImgSrc('/images/logo/placeholder.svg');
}
};
return (
<img
src={imgSrc}
alt={alt}
className={className}
onError={handleError}
loading="lazy"
/>
);
}
// ----------------------------------------
return (
<aside
className={`fixed lg:relative z-50 h-screen transition-all duration-300 ${darkMode ? 'bg-gray-800 text-gray-200' : 'bg-white text-gray-900'
@ -39,16 +73,20 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen, isCollapsed }) => {
{/* Logo Section */}
<div className={`flex items-center gap-2 mb-6 ${isCollapsed ? 'justify-center' : ''}`}>
{isCollapsed ? (
<img
src={darkMode ? "/images/logo/logo-icon-white.svg" : "/images/logo/logo-icon.svg"}
alt="Logo"
<LogoImage
darkSrc="/images/logo/logo-icon-white.svg"
lightSrc="/images/logo/logo-icon.svg"
darkMode={darkMode}
className="w-8"
alt="Collapsed Logo"
/>
) : (
<img
src={darkMode ? "/images/logo/logo-white.svg" : "/images/logo/logo.svg"}
alt="Logo"
<LogoImage
darkSrc="/images/logo/logo-white.svg"
lightSrc="/images/logo/logo.svg"
darkMode={darkMode}
className="h-8"
alt="Full Logo"
/>
)}
</div>
@ -71,7 +109,7 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen, isCollapsed }) => {
? "bg-blue-900 text-blue-200"
: "bg-blue-100 text-blue-800"
: darkMode
? "hover:bg-gray-700"
? "hover:bg-gray-700 text-gray-200"
: "hover:bg-gray-200"
} ${isCollapsed ? 'justify-center' : ''}`}
onClick={closeSidebarOnMobile}
@ -79,12 +117,15 @@ const Sidebar = ({ sidebarOpen, setSidebarOpen, isCollapsed }) => {
<img
src={darkMode ? "/images/icons/grid-white.svg" : "/images/icons/grid.svg"}
alt="Dashboard"
width={18}
className="w-5"
className={`w-5 ${darkMode ? 'filter brightness-0 invert' : ''}`}
onError={(e) => {
e.target.src = darkMode ? "/images/icons/grid.svg" : "/images/icons/grid-white.svg";
e.target.className = 'w-5'; // Remove filter if fallback
}}
/>
{!isCollapsed && "Dashboard"}
</Link>
{/* -----------singleBooking-------------- */}
{/* -----------singleBooking/Add token-------------- */}
<Link
href="/pages/SingleBooking"
className={`flex items-center gap-2 p-3 rounded-md text-sm font-semibold transition-colors ${isActive("/pages/UserProfile")

View File

@ -5,143 +5,123 @@ import { DATABASE_ID, COLLECTION_ID } from "../lib/api";
import { useTheme } from "../context/ThemeContext";
export default function TokenTable({ statusFilter }) {
const { darkMode } = useTheme();
const [entries, setEntries] = useState([]);
const [filteredEntries, setFilteredEntries] = useState([]);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const entriesPerPage = 20;
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const getEntries = async () => {
try {
const today = new Date().toISOString().split('T')[0];
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTION_ID,
[
Query.equal('date', today),
Query.orderAsc('tokenNumber')
],
100
);
return response.documents;
} catch (error) {
console.error("Fetch error:", error);
throw error;
}
};
const updateStatus = async (entryId, newStatus) => {
try {
setLoading(true);
await databases.updateDocument(
DATABASE_ID,
COLLECTION_ID,
entryId,
{ status: newStatus }
);
const data = await getEntries();
setEntries(data);
applyFilters(data, searchQuery);
} catch (error) {
console.error("Update error:", error);
setError(error.message);
} finally {
setLoading(false);
}
};
const applyFilters = (data, search) => {
let filtered = data.filter((entry) => {
const matchesSearch =
entry.patientName.toLowerCase().includes(search.toLowerCase()) ||
entry.tokenNumber.toString().includes(search);
const matchesFilter =
statusFilter === "all" ||
entry.status === statusFilter ||
(statusFilter === "booked" && entry.status === "booked");
return matchesSearch && matchesFilter;
});
setFilteredEntries(filtered);
};
useEffect(() => {
const loadData = async () => {
try {
const data = await getEntries();
setEntries(data);
applyFilters(data, searchQuery);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
loadData();
}, []);
useEffect(() => {
applyFilters(entries, searchQuery);
setCurrentPage(1);
}, [searchQuery, statusFilter, entries]);
const indexOfLastEntry = currentPage * entriesPerPage;
const indexOfFirstEntry = indexOfLastEntry - entriesPerPage;
const currentEntries = filteredEntries.slice(indexOfFirstEntry, indexOfLastEntry);
const totalPages = Math.ceil(filteredEntries.length / entriesPerPage);
if (loading) return (
<div className="text-center mt-50">
<div className={`animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mx-auto`}></div>
<p className={`mt-4 text-lg font-semibold`}>Loading...</p>
</div>
);
if (error) return <div className="p-4 text-red-500">Error: {error}</div>;
// ... (keep all your existing state and functions)
return (
<>
<div className={`transition-colors duration-300 ${darkMode ? 'text-gray-100' : 'text-gray-800'}`}>
<input
type="text"
placeholder="Search by token or name"
className="mb-4 p-2 border rounded bg-white w-full max-w-sm"
className={`mb-4 p-2 border rounded w-full transition-colors ${
darkMode
? 'bg-gray-800 border-gray-700 text-white placeholder-gray-400 focus:ring-blue-500 focus:border-blue-500'
: 'bg-white border-gray-300 placeholder-gray-500 focus:ring-blue-500 focus:border-blue-500'
}`}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{filteredEntries.length === 0 ? (
<div className="p-4">No entries found.</div>
<div className={`p-4 rounded ${darkMode ? 'bg-gray-800' : 'bg-gray-50'}`}>
No entries found.
</div>
) : (
<>
{/* Mobile View (Cards) */}
<div className="lg:hidden space-y-3">
{currentEntries.map((entry) => (
<div
key={entry.$id}
className={`p-4 rounded-lg border ${
darkMode ? 'bg-gray-900 border-gray-700' : 'bg-white border-gray-200'
}`}
>
<div className="flex justify-between items-start mb-2">
<div>
<p className="font-semibold">Token: {entry.tokenNumber}</p>
<p className="text-sm">{entry.patientName}</p>
</div>
<span
className={`px-2 py-1 rounded-full text-xs font-semibold ${
entry.status === "done"
? darkMode
? "bg-green-900 text-green-100"
: "bg-green-100 text-green-800"
: entry.status === "booked"
? darkMode
? "bg-yellow-900 text-yellow-100"
: "bg-yellow-100 text-yellow-800"
: darkMode
? "bg-red-900 text-red-100"
: "bg-red-100 text-red-800"
}`}
>
{entry.status === "booked" ? "In-Queue" : entry.status}
</span>
</div>
{entry.status === "booked" && (
<div className="flex gap-2 mt-3">
<button
onClick={() => updateStatus(entry.$id, "done")}
className={`border rounded-full px-3 py-1 text-xs transition-colors ${
darkMode
? 'border-green-400 text-green-400 hover:bg-green-900'
: 'border-green-600 text-green-600 hover:bg-green-50'
}`}
>
Done
</button>
<button
onClick={() => updateStatus(entry.$id, "missed")}
className={`border rounded-full px-3 py-1 text-xs transition-colors ${
darkMode
? 'border-red-400 text-red-400 hover:bg-red-900'
: 'border-red-600 text-red-600 hover:bg-red-50'
}`}
>
Missed
</button>
</div>
)}
</div>
))}
</div>
{/* Desktop View (Table) */}
<div className="hidden lg:block overflow-x-auto">
<table className="w-full border-collapse">
<thead className={darkMode ? "bg-gray-800" : "bg-gray-100"}>
<tr>
<th className="p-3 text-left">Token</th>
<th className="p-3 text-left">Name</th>
<th className="p-3 text-left">Status</th>
<th className="p-3 text-left">Actions</th>
<th className={`p-3 text-left ${darkMode ? 'text-gray-200' : 'text-gray-700'}`}>Token</th>
<th className={`p-3 text-left ${darkMode ? 'text-gray-200' : 'text-gray-700'}`}>Name</th>
<th className={`p-3 text-left ${darkMode ? 'text-gray-200' : 'text-gray-700'}`}>Status</th>
<th className={`p-3 text-left ${darkMode ? 'text-gray-200' : 'text-gray-700'}`}>Actions</th>
</tr>
</thead>
<tbody>
{currentEntries.map((entry) => (
<tr
key={entry.$id}
className={`border ${darkMode ? 'border-gray-700' : 'border-gray-200'}`}
className={`${darkMode ? 'bg-gray-900 border-gray-700 hover:bg-gray-800' : 'bg-white border-gray-200 hover:bg-gray-50'}`}
>
<td className="p-3 font-mono">{entry.tokenNumber}</td>
<td className="p-3">{entry.patientName}</td>
<td className="p-3">
<span
className={`px-2 py-1 rounded-full text-xs ${entry.status === "done"
? "bg-green-100 text-green-800"
className={`px-2 py-1 rounded-full text-xs font-semibold ${
entry.status === "done"
? darkMode
? "bg-green-900 text-green-100"
: "bg-green-100 text-green-800"
: entry.status === "booked"
? "bg-yellow-100 text-yellow-800"
? darkMode
? "bg-yellow-900 text-yellow-100"
: "bg-yellow-100 text-yellow-800"
: darkMode
? "bg-red-900 text-red-100"
: "bg-red-100 text-red-800"
} font-semibold`}
}`}
>
{entry.status === "booked" ? "In-Queue" : entry.status}
</span>
@ -151,13 +131,21 @@ export default function TokenTable({ statusFilter }) {
<>
<button
onClick={() => updateStatus(entry.$id, "done")}
className="border border-green-600 text-green-600 rounded-full px-2 py-1 text-xs hover:bg-green-50"
className={`border rounded-full px-2 py-1 text-xs transition-colors ${
darkMode
? 'border-green-400 text-green-400 hover:bg-green-900'
: 'border-green-600 text-green-600 hover:bg-green-50'
}`}
>
Done
</button>
<button
onClick={() => updateStatus(entry.$id, "missed")}
className="border border-red-600 text-red-600 rounded-full px-2 py-1 text-xs hover:bg-red-50"
className={`border rounded-full px-2 py-1 text-xs transition-colors ${
darkMode
? 'border-red-400 text-red-400 hover:bg-red-900'
: 'border-red-600 text-red-600 hover:bg-red-50'
}`}
>
Missed
</button>
@ -168,12 +156,20 @@ export default function TokenTable({ statusFilter }) {
))}
</tbody>
</table>
</div>
<div className={`flex justify-between items-center mt-4 ${darkMode ? 'text-white' : 'text-gray-700'}`}>
{/* Pagination (Works for both views) */}
<div className={`flex justify-between items-center mt-4 p-2 rounded ${
darkMode ? 'bg-gray-800' : 'bg-gray-100'
}`}>
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage((prev) => prev - 1)}
className="px-3 py-1 border rounded disabled:opacity-50"
className={`px-3 py-1 border rounded disabled:opacity-50 transition-colors ${
darkMode
? 'border-gray-600 hover:bg-gray-700 disabled:hover:bg-transparent'
: 'border-gray-300 hover:bg-gray-200 disabled:hover:bg-transparent'
}`}
>
Previous
</button>
@ -183,13 +179,17 @@ export default function TokenTable({ statusFilter }) {
<button
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((prev) => prev + 1)}
className="px-3 py-1 border rounded disabled:opacity-50"
className={`px-3 py-1 border rounded disabled:opacity-50 transition-colors ${
darkMode
? 'border-gray-600 hover:bg-gray-700 disabled:hover:bg-transparent'
: 'border-gray-300 hover:bg-gray-200 disabled:hover:bg-transparent'
}`}
>
Next
</button>
</div>
</>
)}
</>
</div>
);
}

View File

@ -1,84 +1,99 @@
"use client";
import { useTheme } from "../../context/ThemeContext";
export default function Header({
currentToken,
previousToken,
nextToken,
missedTokens,
entries = [] // Add entries prop to calculate counts
entries = []
}) {
// Calculate counts from entries
const totalTokens = entries.length;
const doneTokens = entries.filter(entry => entry.status === "done").length;
const missedTokensCount = entries.filter(entry => entry.status === "missed").length;
const inQueueTokens = entries.filter(entry => entry.status === "booked").length;
const { darkMode } = useTheme();
return (
<div className="flex grow flex-col items-center justify-between lg:flex-row lg:px-6 w-full">
<div className="hidden sm:hidden md:hidden lg:flex lg:justify-center lg:items-center lg:w-full lg:px-6 lg:py-3 xl:flex xl:justify-center xl:items-center xl:w-full xl:px-6 2xl:flex 2xl:justify-center 2xl:items-center 2xl:w-full 2xl:px-6">
<div className="flex flex-col gap-3">
<div className={`flex grow flex-col items-center ${darkMode ? 'bg-gray-500' : 'bg-white'} justify-between lg:flex-row lg:px-6 w-full`}> <div className="hidden lg:flex lg:justify-center lg:items-center lg:w-full lg:px-6 lg:py-3 xl:flex xl:justify-center xl:items-center xl:w-full xl:px-6 2xl:flex 2xl:justify-center 2xl:items-center 2xl:w-full 2xl:px-6">
<div className={`flex flex-col gap-3 p-4 rounded-lg ${darkMode ? 'bg-gray-800 text-gray-200' : 'bg-gray-50 text-gray-600'}`}>
<div className="grid grid-cols-4 gap-4">
{/* ----------Total Tokens Card---------------- */}
<div className="border-3 border-gray-300 rounded-[10px] px-2 py-2 flex flex-col gap-3 w-[10rem]">
<h2 className="font-semibold text-[14px] text-[#667085]">Total Tokens</h2>
<div className="flex gap-5 -mt-1">
<p className="text-[25px]">{totalTokens}</p>
<p className="text-[8px] text-[#667085] mt-[18px]">
<span className="text-green-400">+{inQueueTokens}</span> in queue
<div className={`border rounded-lg px-3 py-3 flex flex-col gap-3 w-[10rem] transition-colors ${
darkMode ? 'bg-gray-700 border-gray-600' : 'bg-white border-gray-200'
}`}>
<h2 className="font-semibold text-sm text-gray-500 dark:text-gray-300">Total Tokens</h2>
<div className="flex gap-5 items-baseline">
<p className="text-2xl font-medium dark:text-white">{totalTokens}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
<span className="text-green-500 dark:text-green-400">+{inQueueTokens}</span> in queue
</p>
</div>
<button className="bg-[#465FFF] text-white text-[8px] px-[24px] py-[4px] w-full rounded-[4px]">
<button className={`bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600
text-white text-xs py-1.5 w-full rounded transition-colors`}>
View All
</button>
</div>
{/* ----------Done Tokens Card---------------- */}
<div className="border-3 border-gray-300 rounded-[10px] px-2 py-2 flex flex-col gap-3 w-[10rem]">
<h2 className="font-semibold text-[14px] text-[#667085]">Done</h2>
<div className="flex gap-5 -mt-1">
<p className="text-[25px]">{doneTokens}</p>
<p className="text-[8px] text-[#667085] mt-[18px]">
<span className="text-green-400">
<div className={`border rounded-lg px-3 py-3 flex flex-col gap-3 w-[10rem] transition-colors ${
darkMode ? 'bg-gray-700 border-gray-600' : 'bg-white border-gray-200'
}`}>
<h2 className="font-semibold text-sm text-gray-500 dark:text-gray-300">Done</h2>
<div className="flex gap-5 items-baseline">
<p className="text-2xl font-medium dark:text-white">{doneTokens}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
<span className="text-green-500 dark:text-green-400">
{totalTokens > 0 ? Math.round((doneTokens/totalTokens)*100) : 0}%
</span> of total
</p>
</div>
<button className="bg-[#465FFF] text-white text-[8px] px-[24px] py-[4px] w-full rounded-[4px]">
<button className={`bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600
text-white text-xs py-1.5 w-full rounded transition-colors`}>
View List
</button>
</div>
{/* ----------Missed Tokens Card---------------- */}
<div className="border-3 border-gray-300 rounded-[10px] px-2 py-2 flex flex-col gap-3 w-[10rem]">
<h2 className="font-semibold text-[14px] text-[#667085]">Missed Tokens</h2>
<div className="flex gap-5 -mt-1">
<p className="text-[25px]">{missedTokensCount}</p>
<p className="text-[8px] text-[#667085] mt-[18px]">
<div className={`border rounded-lg px-3 py-3 flex flex-col gap-3 w-[10rem] transition-colors ${
darkMode ? 'bg-gray-700 border-gray-600' : 'bg-white border-gray-200'
}`}>
<h2 className="font-semibold text-sm text-gray-500 dark:text-gray-300">Missed Tokens</h2>
<div className="flex gap-5 items-baseline">
<p className="text-2xl font-medium dark:text-white">{missedTokensCount}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{missedTokensCount > 0 ? (
<span className="text-red-400">{missedTokens}</span>
<span className="text-red-500 dark:text-red-400">{missedTokens}</span>
) : (
"All attended"
)}
</p>
</div>
<button className="bg-[#465FFF] text-white text-[8px] px-[24px] py-[4px] w-full rounded-[4px]">
<button className={`bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600
text-white text-xs py-1.5 w-full rounded transition-colors`}>
View List
</button>
</div>
{/* ----------Current Token Card---------------- */}
<div className="border-3 border-gray-300 rounded-[10px] px-2 py-2 flex flex-col gap-3 w-[10rem]">
<h2 className="font-semibold text-[14px] text-[#667085]">Current Token</h2>
<div className="flex flex-col gap-1 -mt-1">
<p className="text-[25px] text-center">
<div className={`border rounded-lg px-3 py-3 flex flex-col gap-3 w-[10rem] transition-colors ${
darkMode ? 'bg-gray-700 border-gray-600' : 'bg-white border-gray-200'
}`}>
<h2 className="font-semibold text-sm text-gray-500 dark:text-gray-300">Current Token</h2>
<div className="flex flex-col gap-1">
<p className="text-2xl font-medium text-center dark:text-white">
{currentToken || "---"}
</p>
<div className="flex justify-between text-[8px] text-[#667085]">
<div className="flex justify-between text-xs text-gray-500 dark:text-gray-400">
<span>Prev: {previousToken || "---"}</span>
<span>Next: {nextToken || "---"}</span>
</div>
</div>
<button className="bg-[#465FFF] text-white text-[8px] px-[24px] py-[4px] w-full rounded-[4px]">
<button className={`bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600
text-white text-xs py-1.5 w-full rounded transition-colors`}>
Call Next
</button>
</div>

View File

@ -1,11 +1,11 @@
// src/context/ThemeContext.js
"use client";
import { createContext, useContext, useState, useEffect } from "react";
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [darkMode, setDarkMode] = useState(false);
// Initialize with undefined to detect client-side
const [darkMode, setDarkMode] = useState(undefined);
useEffect(() => {
// Check for saved preference or system preference
@ -14,6 +14,8 @@ export function ThemeProvider({ children }) {
setDarkMode(savedMode === 'true');
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
setDarkMode(true);
} else {
setDarkMode(false); // Explicitly set light mode if no preference
}
}, []);

View File

@ -6,9 +6,7 @@ import { useEffect } from "react";
import { useTheme } from "../context/ThemeContext";
import EntriesTable from "../pages/entries/EntriesTable";
export default function DashboardPage() {
const router = useRouter();
const { isAuthenticated, loading, userRole } = useAuth();
const { darkMode } = useTheme();
@ -19,12 +17,17 @@ export default function DashboardPage() {
}
}, [isAuthenticated, loading, router]);
if (loading) {
// Handle theme loading state
if (darkMode === undefined || loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-white dark:bg-gray-900">
<div className="text-center">
<div className={`animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mx-auto ${darkMode ? 'border-gray-100' : 'border-gray-900'} mx-auto`}></div>
<p className={`mt-4 text-lg font-semibold ${darkMode ? 'text-gray-100' : 'text-gray-900'}`}>
<div className={`animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mx-auto ${
darkMode === true ? 'border-gray-100' : 'border-gray-900'
}`}></div>
<p className={`mt-4 text-lg font-semibold ${
darkMode === true ? 'text-gray-100' : 'text-gray-900'
}`}>
Loading...
</p>
</div>
@ -37,8 +40,12 @@ export default function DashboardPage() {
}
return (
<div className="min-h-screen bg-white dark:bg-gray-900 p-6">
<main className={`min-h-screen transition-colors duration-300 ${
darkMode ? 'dark bg-gray-900' : 'bg-white'
}`}>
<div className="p-6">
<EntriesTable />
</div>
</main>
);
}

View File

@ -2,26 +2,12 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Head from 'next/head';
import Preloader from '../../components/partials/preloaders';
import { useTheme } from "../../context/ThemeContext";
export default function SingleBooking() {
const [darkMode, setDarkMode] = useState(false);
const [loaded, setLoaded] = useState(false);
const [name, setName] = useState('');
const router = useRouter();
useEffect(() => {
const savedDarkMode = JSON.parse(localStorage.getItem('darkMode'));
if (savedDarkMode !== null) {
setDarkMode(savedDarkMode);
}
setLoaded(true);
}, []);
useEffect(() => {
localStorage.setItem('darkMode', JSON.stringify(darkMode));
}, [darkMode]);
const { darkMode } = useTheme();
const handleSubmit = (e) => {
e.preventDefault();
@ -31,31 +17,47 @@ export default function SingleBooking() {
return;
}
// Get current date in YYYY-MM-DD format
const today = new Date();
const appointmentDate = today.toISOString().split('T')[0];
// Navigate to confirmation page with name and current date as query params
router.push(`/pages/SingleBooked?name=${encodeURIComponent(name)}&date=${encodeURIComponent(appointmentDate)}`);
};
if (!loaded) {
return <Preloader />;
// Handle theme loading state (same as dashboard)
if (darkMode === undefined) {
return (
<div className="flex items-center justify-center min-h-screen bg-white dark:bg-gray-900">
<div className="text-center">
<div className={`animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mx-auto ${
darkMode === true ? 'border-gray-100' : 'border-gray-900'
}`}></div>
<p className={`mt-4 text-lg font-semibold ${
darkMode === true ? 'text-gray-100' : 'text-gray-900'
}`}>
Loading...
</p>
</div>
</div>
);
}
return (
<div className={`flex flex-col min-h-screen ${darkMode ? 'dark bg-gray-900' : ''}`}>
<div className={`min-h-screen transition-colors duration-300 ${
darkMode ? 'dark bg-gray-900' : 'bg-white'
}`}>
<div className="flex h-screen overflow-hidden">
<div className="relative flex flex-col flex-1 overflow-x-hidden overflow-y-auto">
<main>
<div className="p-4 mx-auto max-w-4xl md:p-6">
<h2 className="text-2xl font-semibold text-gray-800 dark:text-white">Book Appointment</h2>
<p className='mb-6'>Date: {new Date().toLocaleDateString('en-GB')}</p>
<h2 className="text-2xl font-semibold text-gray-800 dark:text-gray-100">
Book Appointment
</h2>
<p className={`mb-6 dark:text-gray-300`}>
Date: {new Date().toLocaleDateString('en-GB')}
</p>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1.5">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
Full Name
</label>
<input
@ -63,20 +65,20 @@ export default function SingleBooking() {
placeholder="John Doe"
value={name}
onChange={(e) => setName(e.target.value)}
className="h-11 w-full rounded-lg border border-gray-300 bg-white dark:bg-gray-900 px-4 py-2.5 text-sm text-gray-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
className="h-11 w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2.5 text-sm text-gray-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<button
type="submit"
className="w-full rounded-lg bg-blue-700 px-5 py-3.5 text-sm font-medium text-white hover:bg-blue-800 transition duration-200"
className="w-full rounded-lg bg-blue-600 dark:bg-blue-700 px-5 py-3.5 text-sm font-medium text-white hover:bg-blue-700 dark:hover:bg-blue-800 transition duration-200"
>
Submit
</button>
</form>
<div className="mt-6 text-center text-sm text-gray-600 dark:text-gray-300">
<div className="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
Note: Token booking is available for today&apos;s date only.
</div>
</div>

View File

@ -1,11 +1,40 @@
"use client";
import TokenTable from "../../components/TokenTable";
import { useTheme } from "../../context/ThemeContext";
export default function DoneTokensPage() {
return (
<div>
<h1 className="text-2xl font-bold mb-4">Done Tokens</h1>
<TokenTable statusFilter="done" />
</div>
const { darkMode } = useTheme();
// Handle theme loading state (consistent with dashboard)
if (darkMode === undefined) {
return (
<div className="flex items-center justify-center min-h-screen bg-white dark:bg-gray-900">
<div className="text-center">
<div className={`animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mx-auto ${
darkMode === true ? 'border-gray-100' : 'border-gray-900'
}`}></div>
<p className={`mt-4 text-lg font-semibold ${
darkMode === true ? 'text-gray-800' : 'text-gray-900'
}`}>
Loading...
</p>
</div>
</div>
);
}
return (
<div className={`min-h-screen p-6 transition-colors duration-300 ${
darkMode ? 'dark bg-gray-900' : 'bg-white'
}`}>
<div className="max-w-7xl mx-auto">
<h1 className={`text-2xl font-bold mb-4 ${
darkMode ? 'text-gray-100' : 'text-gray-300'
}`}>
Done Tokens
</h1>
<TokenTable statusFilter="done" />
</div>
</div>
);
}

View File

@ -1,10 +1,40 @@
"use client";
import TokenTable from "../../components/TokenTable";
import { useTheme } from "../../context/ThemeContext";
export default function MissedTokensPage() {
const { darkMode } = useTheme();
// Handle theme loading state (consistent with dashboard)
if (darkMode === undefined) {
return (
<div>
<h1 className="text-2xl font-bold mb-4">Missed Tokens</h1>
<TokenTable statusFilter="missed" />
<div className="flex items-center justify-center min-h-screen bg-white dark:bg-gray-900">
<div className="text-center">
<div className={`animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mx-auto ${
darkMode === true ? 'border-gray-100' : 'border-gray-900'
}`}></div>
<p className={`mt-4 text-lg font-semibold ${
darkMode === true ? 'text-gray-100' : 'text-gray-900'
}`}>
Loading...
</p>
</div>
</div>
);
}
return (
<div className={`min-h-screen p-6 transition-colors duration-300 ${
darkMode ? 'dark bg-gray-900' : 'bg-white'
}`}>
<div className="max-w-7xl mx-auto">
<h1 className={`text-2xl font-bold mb-4 ${
darkMode ? 'text-gray-100' : 'text-gray-800'
}`}>
Missed Tokens
</h1>
<TokenTable statusFilter="missed" />
</div>
</div>
);
}