Files
Church-Music/new-site/frontend/src/layouts/MainLayout.jsx

327 lines
12 KiB
React
Raw Normal View History

2026-01-27 18:04:50 -06:00
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>
);
}