Initial commit - Church Music Database
This commit is contained in:
326
new-site/frontend/src/layouts/MainLayout.jsx
Normal file
326
new-site/frontend/src/layouts/MainLayout.jsx
Normal file
@@ -0,0 +1,326 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user