This commit is contained in:
Jamie Brunton committed 2025-12-31 14:54:28 +00:00
1 parent e7379ccdb1
commit d18f9d32b0
49 files changed
+3739 -665

No files matched your search

+86 -31
View File
@@ -1,6 +1,6 @@
import { Link, useLocation } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext';
import { useAuth, PERMISSION_KEYS, PermissionKey } from '@/contexts/AuthContext';
import {
LayoutGrid,
Users,
@@ -8,20 +8,22 @@ import {
ChevronLeft,
ChevronRight,
LogOut,
Sparkles,
Shield,
Ticket,
MessageSquareText,
Target,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { useState } from 'react';
import logoImage from '@/assets/logo.png';
interface NavItem {
label: string;
href: string;
icon: React.ReactNode;
requireSuperAdmin?: boolean;
requiredPermission?: PermissionKey;
}
const contentNav: NavItem[] = [
@@ -29,6 +31,7 @@ const contentNav: NavItem[] = [
label: 'Categories',
href: '/categories',
icon: <LayoutGrid className="h-5 w-5" />,
requiredPermission: PERMISSION_KEYS.MANAGE_CATEGORIES,
},
];
@@ -37,7 +40,19 @@ const usersNav: NavItem[] = [
label: 'Users',
href: '/users',
icon: <Users className="h-5 w-5" />,
requireSuperAdmin: true,
requiredPermission: PERMISSION_KEYS.VIEW_USERS,
},
{
label: 'Usage Insights',
href: '/usage-insights',
icon: <Target className="h-5 w-5" />,
requiredPermission: PERMISSION_KEYS.VIEW_USERS,
},
{
label: 'Feedback',
href: '/feedback',
icon: <MessageSquareText className="h-5 w-5" />,
requiredPermission: PERMISSION_KEYS.VIEW_USERS,
},
];
@@ -46,29 +61,29 @@ const systemNav: NavItem[] = [
label: 'Admins',
href: '/admins',
icon: <Shield className="h-5 w-5" />,
requireSuperAdmin: true,
requiredPermission: PERMISSION_KEYS.MANAGE_ADMINS,
},
{
label: 'Redemption Codes',
href: '/redemption-codes',
icon: <Ticket className="h-5 w-5" />,
requireSuperAdmin: true,
requiredPermission: PERMISSION_KEYS.MANAGE_CODES,
},
{
label: 'Audit Logs',
href: '/audit-logs',
icon: <ClipboardList className="h-5 w-5" />,
requireSuperAdmin: true,
requiredPermission: PERMISSION_KEYS.VIEW_AUDIT_LOGS,
},
];
export function Sidebar() {
const location = useLocation();
const { user, permissions, logout, isSuperAdmin } = useAuth();
const { user, permissions, logout, hasPermission } = useAuth();
const [collapsed, setCollapsed] = useState(false);
const NavLink = ({ item }: { item: NavItem }) => {
if (item.requireSuperAdmin && !isSuperAdmin) return null;
if (item.requiredPermission && !hasPermission(item.requiredPermission)) return null;
const isActive = location.pathname.startsWith(item.href);
@@ -76,27 +91,32 @@ export function Sidebar() {
<Link
to={item.href}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors",
"group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
? "bg-gradient-rose text-white shadow-lg glow-rose-sm"
: "text-muted-foreground hover:bg-white/5 hover:text-foreground",
collapsed && "justify-center px-2"
)}
>
{item.icon}
<span className={cn(
"transition-transform duration-200",
!isActive && "group-hover:scale-110"
)}>
{item.icon}
</span>
{!collapsed && <span>{item.label}</span>}
</Link>
);
};
const NavSection = ({ title, items }: { title: string; items: NavItem[] }) => {
const visibleItems = items.filter(item => !item.requireSuperAdmin || isSuperAdmin);
const visibleItems = items.filter(item => !item.requiredPermission || hasPermission(item.requiredPermission));
if (visibleItems.length === 0) return null;
return (
<div className="space-y-1">
{!collapsed && (
<h4 className="px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
<h4 className="px-3 py-2 text-[11px] font-semibold uppercase tracking-widest text-muted-foreground">
{title}
</h4>
)}
@@ -110,23 +130,50 @@ export function Sidebar() {
return (
<div
className={cn(
"flex h-screen flex-col border-r bg-card transition-all duration-300",
collapsed ? "w-16" : "w-64"
"flex h-screen flex-col border-r border-white/5 bg-gradient-to-b from-card to-background transition-all duration-300",
collapsed ? "w-20" : "w-72"
)}
>
{/* Logo */}
<div className="flex h-16 items-center justify-between border-b px-4">
<div className="flex h-20 items-center justify-between px-4 border-b border-white/5">
{!collapsed && (
<div className="flex items-center gap-2">
<Sparkles className="h-6 w-6 text-primary" />
<span className="text-lg font-bold">Sauci Admin</span>
</div>
<Link to="/" className="flex items-center gap-3 group">
<div className="relative">
<img
src={logoImage}
alt="Sauci"
className="h-10 w-10 object-contain transition-transform duration-300 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-rose opacity-0 group-hover:opacity-20 rounded-full blur-xl transition-opacity duration-300" />
</div>
<div className="flex flex-col">
<span className="text-xl font-bold tracking-tight text-gradient-sunset">
Sauci
</span>
<span className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
Admin Portal
</span>
</div>
</Link>
)}
{collapsed && (
<Link to="/" className="mx-auto">
<img
src={logoImage}
alt="Sauci"
className="h-10 w-10 object-contain"
/>
</Link>
)}
</div>
{/* Collapse button */}
<div className="flex justify-end px-2 py-2">
<Button
variant="ghost"
size="icon"
onClick={() => setCollapsed(!collapsed)}
className={cn(collapsed && "mx-auto")}
className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-white/5"
>
{collapsed ? (
<ChevronRight className="h-4 w-4" />
@@ -137,25 +184,30 @@ export function Sidebar() {
</div>
{/* Navigation */}
<div className="flex-1 space-y-6 overflow-auto p-4">
<div className="flex-1 space-y-6 overflow-auto px-3 py-2">
<NavSection title="Content" items={contentNav} />
<NavSection title="Users" items={usersNav} />
<NavSection title="System" items={systemNav} />
</div>
<Separator />
<Separator className="bg-white/5" />
{/* User info */}
<div className={cn("p-4", collapsed && "flex flex-col items-center")}>
<div className={cn("flex items-center gap-3", collapsed && "flex-col")}>
<Avatar className="h-8 w-8">
<AvatarFallback>
<div className={cn(
"flex items-center gap-3 rounded-xl p-2 transition-colors hover:bg-white/5",
collapsed && "flex-col p-2"
)}>
<Avatar className="h-9 w-9 ring-2 ring-primary/20">
<AvatarFallback className="bg-gradient-rose text-white font-semibold">
{user?.email?.charAt(0).toUpperCase() || 'A'}
</AvatarFallback>
</Avatar>
{!collapsed && (
<div className="flex-1 overflow-hidden">
<p className="truncate text-sm font-medium">{user?.email}</p>
<p className="truncate text-sm font-medium text-foreground">
{user?.email}
</p>
<p className="text-xs text-muted-foreground capitalize">
{permissions?.role?.replace('_', ' ')}
</p>
@@ -165,11 +217,14 @@ export function Sidebar() {
<Button
variant="ghost"
size={collapsed ? "icon" : "sm"}
className={cn("mt-3", collapsed ? "w-8 h-8" : "w-full")}
className={cn(
"mt-2 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors",
collapsed ? "w-9 h-9" : "w-full justify-start"
)}
onClick={logout}
>
<LogOut className="h-4 w-4" />
{!collapsed && <span className="ml-2">Logout</span>}
{!collapsed && <span className="ml-2">Sign Out</span>}
</Button>
</div>
</div>