327 lines
12 KiB
JavaScript
327 lines
12 KiB
JavaScript
import { useState } from "react";
|
|
import { Link, useLocation, Outlet } from "react-router-dom";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import {
|
|
Home,
|
|
Music,
|
|
ListMusic,
|
|
Users,
|
|
Settings,
|
|
Menu,
|
|
X,
|
|
ChevronRight,
|
|
Wifi,
|
|
WifiOff,
|
|
LogOut,
|
|
User,
|
|
Sun,
|
|
Moon,
|
|
Shield,
|
|
} from "lucide-react";
|
|
import { useAuth } from "@context/AuthContext";
|
|
import { useTheme } from "@context/ThemeContext";
|
|
|
|
const navLinks = [
|
|
{ path: "/", label: "Home", icon: Home },
|
|
{ path: "/database", label: "Songs", icon: Music },
|
|
{ path: "/worship-lists", label: "Lists", icon: ListMusic },
|
|
{ path: "/profiles", label: "Profiles", icon: Users },
|
|
{ path: "/admin", label: "Admin", icon: Shield },
|
|
{ path: "/settings", label: "Settings", icon: Settings },
|
|
];
|
|
|
|
export default function MainLayout() {
|
|
const location = useLocation();
|
|
const { user, logout } = useAuth();
|
|
const { theme, toggleTheme, isDark } = useTheme();
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
const [isOnline] = useState(navigator.onLine);
|
|
|
|
const isActive = (path) => {
|
|
if (path === "/") return location.pathname === "/";
|
|
return location.pathname.startsWith(path);
|
|
};
|
|
|
|
// Theme-aware classes
|
|
const textPrimary = isDark ? "text-white" : "text-gray-900";
|
|
const textSecondary = isDark ? "text-white/60" : "text-gray-600";
|
|
const textMuted = isDark ? "text-white/50" : "text-gray-500";
|
|
|
|
return (
|
|
<div
|
|
className={`min-h-screen transition-colors duration-300 ${
|
|
isDark
|
|
? "bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900"
|
|
: "bg-gradient-to-br from-gray-50 via-white to-gray-100"
|
|
}`}
|
|
>
|
|
{/* Navbar */}
|
|
<nav
|
|
className={`sticky top-0 z-40 backdrop-blur-xl border-b transition-colors duration-300 ${
|
|
isDark
|
|
? "bg-slate-900/80 border-white/10"
|
|
: "bg-white/80 border-gray-200"
|
|
}`}
|
|
role="navigation"
|
|
aria-label="Main navigation"
|
|
>
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex items-center justify-between h-16">
|
|
{/* Logo */}
|
|
<Link to="/" className="flex items-center gap-3 group">
|
|
<motion.div
|
|
className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500 to-purple-600
|
|
flex items-center justify-center shadow-lg shadow-violet-500/25"
|
|
whileHover={{ scale: 1.05, rotate: 5 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<Music className="text-white" size={22} />
|
|
</motion.div>
|
|
<div className="hidden sm:block">
|
|
<h1
|
|
className={`text-lg font-bold group-hover:text-violet-500 transition-colors ${textPrimary}`}
|
|
>
|
|
HOP Worship
|
|
</h1>
|
|
<p className={`text-xs -mt-0.5 ${textMuted}`}>Song Manager</p>
|
|
</div>
|
|
</Link>
|
|
|
|
{/* Desktop Navigation */}
|
|
<div className="hidden md:flex items-center gap-1">
|
|
{navLinks.map(({ path, label, icon: Icon }) => (
|
|
<Link
|
|
key={path}
|
|
to={path}
|
|
className={`relative px-4 py-2 rounded-lg flex items-center gap-2
|
|
transition-all duration-300 group
|
|
${
|
|
isActive(path)
|
|
? textPrimary
|
|
: `${textSecondary} ${isDark ? "hover:text-white hover:bg-white/5" : "hover:text-gray-900 hover:bg-gray-100"}`
|
|
}`}
|
|
>
|
|
<Icon
|
|
size={18}
|
|
className={
|
|
isActive(path)
|
|
? "text-violet-500"
|
|
: "group-hover:text-violet-500"
|
|
}
|
|
/>
|
|
<span className="text-sm font-medium">{label}</span>
|
|
{isActive(path) && (
|
|
<motion.div
|
|
layoutId="navbar-indicator"
|
|
className={`absolute inset-0 rounded-lg -z-10 ${isDark ? "bg-white/10" : "bg-violet-100"}`}
|
|
transition={{
|
|
type: "spring",
|
|
stiffness: 300,
|
|
damping: 30,
|
|
}}
|
|
/>
|
|
)}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
{/* Right Side */}
|
|
<div className="flex items-center gap-3">
|
|
{/* Theme Toggle */}
|
|
<button
|
|
onClick={toggleTheme}
|
|
className={`p-2 rounded-lg transition-colors ${
|
|
isDark
|
|
? "text-white/60 hover:text-white hover:bg-white/10"
|
|
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
|
}`}
|
|
aria-label={
|
|
isDark ? "Switch to Light Mode" : "Switch to Dark Mode"
|
|
}
|
|
title={isDark ? "Switch to Light Mode" : "Switch to Dark Mode"}
|
|
>
|
|
{isDark ? (
|
|
<Sun size={20} aria-hidden="true" />
|
|
) : (
|
|
<Moon size={20} aria-hidden="true" />
|
|
)}
|
|
</button>
|
|
|
|
{/* Online Status */}
|
|
<div
|
|
className={`hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full text-xs
|
|
${isOnline ? "bg-emerald-500/20 text-emerald-500" : "bg-red-500/20 text-red-500"}`}
|
|
role="status"
|
|
aria-label={isOnline ? "Online" : "Offline"}
|
|
>
|
|
{isOnline ? (
|
|
<Wifi size={14} aria-hidden="true" />
|
|
) : (
|
|
<WifiOff size={14} aria-hidden="true" />
|
|
)}
|
|
<span>{isOnline ? "Online" : "Offline"}</span>
|
|
</div>
|
|
|
|
{/* User Menu */}
|
|
{user && (
|
|
<div className="hidden sm:flex items-center gap-2">
|
|
<div
|
|
className="w-8 h-8 rounded-full bg-gradient-to-br from-violet-500 to-purple-600
|
|
flex items-center justify-center text-white text-sm font-medium"
|
|
aria-label={`Logged in as ${user.name || user.username}`}
|
|
>
|
|
{user.name?.[0] || user.username?.[0] || "U"}
|
|
</div>
|
|
<button
|
|
onClick={logout}
|
|
className={`p-2 rounded-lg transition-colors ${
|
|
isDark
|
|
? "text-white/50 hover:text-white hover:bg-white/10"
|
|
: "text-gray-400 hover:text-gray-700 hover:bg-gray-100"
|
|
}`}
|
|
aria-label="Logout"
|
|
title="Logout"
|
|
>
|
|
<LogOut size={18} aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Mobile Menu Button */}
|
|
<button
|
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
className={`md:hidden p-2 rounded-lg transition-colors ${
|
|
isDark
|
|
? "text-white/70 hover:text-white hover:bg-white/10"
|
|
: "text-gray-600 hover:text-gray-900 hover:bg-gray-100"
|
|
}`}
|
|
aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
|
|
aria-expanded={mobileMenuOpen}
|
|
aria-controls="mobile-menu"
|
|
>
|
|
{mobileMenuOpen ? (
|
|
<X size={24} aria-hidden="true" />
|
|
) : (
|
|
<Menu size={24} aria-hidden="true" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Menu */}
|
|
<AnimatePresence>
|
|
{mobileMenuOpen && (
|
|
<motion.div
|
|
id="mobile-menu"
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: "auto" }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
className={`md:hidden border-t overflow-hidden ${isDark ? "border-white/10" : "border-gray-200"}`}
|
|
role="menu"
|
|
aria-label="Mobile navigation menu"
|
|
>
|
|
<div className="p-4 space-y-1">
|
|
{navLinks.map(({ path, label, icon: Icon }, index) => (
|
|
<motion.div
|
|
key={path}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: index * 0.05 }}
|
|
>
|
|
<Link
|
|
to={path}
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
className={`flex items-center justify-between p-3 rounded-xl transition-all
|
|
${
|
|
isActive(path)
|
|
? isDark
|
|
? "bg-violet-500/20 text-white border border-violet-500/30"
|
|
: "bg-violet-100 text-violet-900 border border-violet-200"
|
|
: isDark
|
|
? "text-white/60 hover:text-white hover:bg-white/5"
|
|
: "text-gray-600 hover:text-gray-900 hover:bg-gray-100"
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Icon
|
|
size={20}
|
|
className={isActive(path) ? "text-violet-500" : ""}
|
|
/>
|
|
<span className="font-medium">{label}</span>
|
|
</div>
|
|
<ChevronRight
|
|
size={18}
|
|
className={isDark ? "text-white/30" : "text-gray-400"}
|
|
/>
|
|
</Link>
|
|
</motion.div>
|
|
))}
|
|
|
|
{/* Mobile User Section */}
|
|
{user && (
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: navLinks.length * 0.05 }}
|
|
className={`mt-4 pt-4 border-t ${isDark ? "border-white/10" : "border-gray-200"}`}
|
|
>
|
|
<div className="flex items-center justify-between p-3">
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className="w-10 h-10 rounded-full bg-gradient-to-br from-violet-500 to-purple-600
|
|
flex items-center justify-center text-white font-medium"
|
|
>
|
|
{user.name?.[0] || user.username?.[0] || "U"}
|
|
</div>
|
|
<div>
|
|
<p className={`font-medium ${textPrimary}`}>
|
|
{user.name || user.username}
|
|
</p>
|
|
<p className={`text-sm ${textMuted}`}>
|
|
{user.role || "User"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={logout}
|
|
className={`p-2 rounded-lg transition-colors ${
|
|
isDark
|
|
? "text-red-400 hover:bg-red-500/10"
|
|
: "text-red-500 hover:bg-red-50"
|
|
}`}
|
|
>
|
|
<LogOut size={20} />
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</nav>
|
|
|
|
{/* Main Content */}
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={location.pathname}
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -10 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<Outlet />
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer className={`mt-auto py-6 text-center text-sm ${textMuted}`}>
|
|
<p>© {new Date().getFullYear()} House of Prayer Worship Ministry</p>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|