| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- 'use client';
- import { useEffect } from 'react';
- import { useRouter, usePathname } from 'next/navigation';
- import Link from 'next/link';
- import { useAuth } from '@/lib/auth-context';
- import { Avatar } from '@/components/ui/avatar';
- export default function DashboardLayout({ children }: { children: React.ReactNode }) {
- const { user, loading, logout } = useAuth();
- const router = useRouter();
- const pathname = usePathname();
- useEffect(() => {
- if (!loading && !user) {
- router.push('/login');
- }
- }, [user, loading, router]);
- if (loading) {
- return (
- <div className="min-h-screen flex items-center justify-center"
- style={{ background: 'var(--bg)' }}>
- <div className="flex flex-col items-center gap-4 animate-fade-in">
- <div className="w-11 h-11 rounded-xl flex items-center justify-center"
- style={{ background: '#6366F1', boxShadow: '0 0 28px rgba(99,102,241,0.5)' }}>
- <svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
- <path strokeLinecap="round" strokeLinejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
- <path strokeLinecap="round" strokeLinejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
- </div>
- <div className="flex items-center gap-2.5">
- <div className="w-4 h-4 rounded-full animate-spin"
- style={{ borderColor: '#6366F1', borderTopColor: 'transparent' }} />
- <span className="text-sm" style={{ color: 'var(--text-muted)' }}>Loading…</span>
- </div>
- </div>
- </div>
- );
- }
- if (!user) return null;
- const isActive = (href: string) =>
- href === '/' ? pathname === '/' : pathname.startsWith(href);
- return (
- <div className="min-h-screen flex" style={{ background: 'var(--bg)' }}>
- {/* ── Sidebar ───────────────────────────────────────────── */}
- <aside className="w-56 flex flex-col shrink-0"
- style={{
- background: 'rgba(10,11,20,0.95)',
- borderRight: '1px solid rgba(255,255,255,0.06)',
- }}>
- {/* Logo */}
- <div className="px-4 py-5 flex items-center gap-2.5"
- style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
- <Link href="/projects" className="flex items-center gap-2.5 group">
- <div className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0"
- style={{ background: '#6366F1', boxShadow: '0 0 16px rgba(99,102,241,0.4)' }}>
- <svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
- <path strokeLinecap="round" strokeLinejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
- <path strokeLinecap="round" strokeLinejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
- </div>
- <span className="font-semibold tracking-tight text-sm" style={{ color: 'var(--text)' }}>
- VidReview
- </span>
- </Link>
- </div>
- {/* Nav */}
- <nav className="flex-1 px-3 py-4 overflow-y-auto">
- <NavSection label="Workspace">
- <NavLink
- href="/projects"
- active={isActive('/projects')}
- icon={
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
- <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
- </svg>
- }
- >
- Projects
- </NavLink>
- </NavSection>
- {/* Secondary links */}
- <div className="mt-2 pt-2" style={{ borderTop: '1px solid rgba(255,255,255,0.05)' }}>
- {user.globalRole === 'ADMIN' && (
- <NavLink
- href="/users"
- active={isActive('/users')}
- icon={
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
- <path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
- </svg>
- }
- >
- Users
- </NavLink>
- )}
- <NavLink
- href="/settings"
- active={isActive('/settings')}
- icon={
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
- <path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
- <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
- </svg>
- }
- >
- Settings
- </NavLink>
- </div>
- </nav>
- {/* User */}
- <div className="p-3" style={{ borderTop: '1px solid rgba(255,255,255,0.06)' }}>
- <div className="flex items-center gap-2.5 p-2 rounded-lg transition-colors cursor-default"
- style={{ border: '1px solid rgba(255,255,255,0.06)' }}>
- <Avatar name={user.name} size="md" />
- <div className="flex-1 min-w-0">
- <p className="text-sm font-medium truncate" style={{ color: 'var(--text)' }}>{user.name}</p>
- <p className="text-xs capitalize truncate" style={{ color: 'var(--text-muted)' }}>
- {user.globalRole.toLowerCase()}
- </p>
- </div>
- <button
- onClick={async () => {
- await logout();
- router.push('/login');
- }}
- className="p-1.5 rounded-md transition-colors hover:bg-white/5"
- title="Sign out"
- >
- <svg className="w-3.5 h-3.5" style={{ color: 'var(--text-subtle)' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
- <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
- </svg>
- </button>
- </div>
- </div>
- </aside>
- {/* ── Main content ──────────────────────────────────────── */}
- <main className="flex-1 overflow-auto min-w-0" style={{ background: 'var(--bg)' }}>
- {children}
- </main>
- </div>
- );
- }
- function NavSection({ label, children }: { label?: string; children: React.ReactNode }) {
- return (
- <div>
- {label && (
- <p className="px-3 mb-1 text-[10px] font-semibold uppercase tracking-widest mb-1"
- style={{ color: 'var(--text-subtle)' }}>{label}</p>
- )}
- {children}
- </div>
- );
- }
- function NavLink({
- href,
- active,
- icon,
- children,
- }: {
- href: string;
- active: boolean;
- icon: React.ReactNode;
- children: React.ReactNode;
- }) {
- return (
- <Link
- href={href}
- className="nav-item mb-0.5"
- style={active ? {
- background: 'rgba(99,102,241,0.15)',
- color: '#A5B4FC',
- } : undefined}
- >
- <span className="shrink-0">{icon}</span>
- {children}
- </Link>
- );
- }
|