layout.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. 'use client';
  2. import { useEffect } from 'react';
  3. import { useRouter, usePathname } from 'next/navigation';
  4. import Link from 'next/link';
  5. import { useAuth } from '@/lib/auth-context';
  6. import { Avatar } from '@/components/ui/avatar';
  7. export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  8. const { user, loading, logout } = useAuth();
  9. const router = useRouter();
  10. const pathname = usePathname();
  11. useEffect(() => {
  12. if (!loading && !user) {
  13. router.push('/login');
  14. }
  15. }, [user, loading, router]);
  16. if (loading) {
  17. return (
  18. <div className="min-h-screen flex items-center justify-center"
  19. style={{ background: 'var(--bg)' }}>
  20. <div className="flex flex-col items-center gap-4 animate-fade-in">
  21. <div className="w-11 h-11 rounded-xl flex items-center justify-center"
  22. style={{ background: '#6366F1', boxShadow: '0 0 28px rgba(99,102,241,0.5)' }}>
  23. <svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
  24. <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" />
  25. <path strokeLinecap="round" strokeLinejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
  26. </svg>
  27. </div>
  28. <div className="flex items-center gap-2.5">
  29. <div className="w-4 h-4 rounded-full animate-spin"
  30. style={{ borderColor: '#6366F1', borderTopColor: 'transparent' }} />
  31. <span className="text-sm" style={{ color: 'var(--text-muted)' }}>Loading…</span>
  32. </div>
  33. </div>
  34. </div>
  35. );
  36. }
  37. if (!user) return null;
  38. const isActive = (href: string) =>
  39. href === '/' ? pathname === '/' : pathname.startsWith(href);
  40. return (
  41. <div className="min-h-screen flex" style={{ background: 'var(--bg)' }}>
  42. {/* ── Sidebar ───────────────────────────────────────────── */}
  43. <aside className="w-56 flex flex-col shrink-0"
  44. style={{
  45. background: 'rgba(10,11,20,0.95)',
  46. borderRight: '1px solid rgba(255,255,255,0.06)',
  47. }}>
  48. {/* Logo */}
  49. <div className="px-4 py-5 flex items-center gap-2.5"
  50. style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
  51. <Link href="/projects" className="flex items-center gap-2.5 group">
  52. <div className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0"
  53. style={{ background: '#6366F1', boxShadow: '0 0 16px rgba(99,102,241,0.4)' }}>
  54. <svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
  55. <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" />
  56. <path strokeLinecap="round" strokeLinejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
  57. </svg>
  58. </div>
  59. <span className="font-semibold tracking-tight text-sm" style={{ color: 'var(--text)' }}>
  60. VidReview
  61. </span>
  62. </Link>
  63. </div>
  64. {/* Nav */}
  65. <nav className="flex-1 px-3 py-4 overflow-y-auto">
  66. <NavSection label="Workspace">
  67. <NavLink
  68. href="/projects"
  69. active={isActive('/projects')}
  70. icon={
  71. <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
  72. <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" />
  73. </svg>
  74. }
  75. >
  76. Projects
  77. </NavLink>
  78. </NavSection>
  79. {/* Secondary links */}
  80. <div className="mt-2 pt-2" style={{ borderTop: '1px solid rgba(255,255,255,0.05)' }}>
  81. {user.globalRole === 'ADMIN' && (
  82. <NavLink
  83. href="/users"
  84. active={isActive('/users')}
  85. icon={
  86. <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
  87. <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" />
  88. </svg>
  89. }
  90. >
  91. Users
  92. </NavLink>
  93. )}
  94. <NavLink
  95. href="/settings"
  96. active={isActive('/settings')}
  97. icon={
  98. <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
  99. <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" />
  100. <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
  101. </svg>
  102. }
  103. >
  104. Settings
  105. </NavLink>
  106. </div>
  107. </nav>
  108. {/* User */}
  109. <div className="p-3" style={{ borderTop: '1px solid rgba(255,255,255,0.06)' }}>
  110. <div className="flex items-center gap-2.5 p-2 rounded-lg transition-colors cursor-default"
  111. style={{ border: '1px solid rgba(255,255,255,0.06)' }}>
  112. <Avatar name={user.name} size="md" />
  113. <div className="flex-1 min-w-0">
  114. <p className="text-sm font-medium truncate" style={{ color: 'var(--text)' }}>{user.name}</p>
  115. <p className="text-xs capitalize truncate" style={{ color: 'var(--text-muted)' }}>
  116. {user.globalRole.toLowerCase()}
  117. </p>
  118. </div>
  119. <button
  120. onClick={async () => {
  121. await logout();
  122. router.push('/login');
  123. }}
  124. className="p-1.5 rounded-md transition-colors hover:bg-white/5"
  125. title="Sign out"
  126. >
  127. <svg className="w-3.5 h-3.5" style={{ color: 'var(--text-subtle)' }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
  128. <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" />
  129. </svg>
  130. </button>
  131. </div>
  132. </div>
  133. </aside>
  134. {/* ── Main content ──────────────────────────────────────── */}
  135. <main className="flex-1 overflow-auto min-w-0" style={{ background: 'var(--bg)' }}>
  136. {children}
  137. </main>
  138. </div>
  139. );
  140. }
  141. function NavSection({ label, children }: { label?: string; children: React.ReactNode }) {
  142. return (
  143. <div>
  144. {label && (
  145. <p className="px-3 mb-1 text-[10px] font-semibold uppercase tracking-widest mb-1"
  146. style={{ color: 'var(--text-subtle)' }}>{label}</p>
  147. )}
  148. {children}
  149. </div>
  150. );
  151. }
  152. function NavLink({
  153. href,
  154. active,
  155. icon,
  156. children,
  157. }: {
  158. href: string;
  159. active: boolean;
  160. icon: React.ReactNode;
  161. children: React.ReactNode;
  162. }) {
  163. return (
  164. <Link
  165. href={href}
  166. className="nav-item mb-0.5"
  167. style={active ? {
  168. background: 'rgba(99,102,241,0.15)',
  169. color: '#A5B4FC',
  170. } : undefined}
  171. >
  172. <span className="shrink-0">{icon}</span>
  173. {children}
  174. </Link>
  175. );
  176. }