Initial commit
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { NotificationBell } from "@/components/notifications/NotificationBell";
|
||||
import { getInitials } from "@/lib/utils";
|
||||
import { LogOut, User, Settings } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface HeaderProps {
|
||||
title?: string;
|
||||
breadcrumbs?: { label: string; href?: string }[];
|
||||
}
|
||||
|
||||
export function Header({ title, breadcrumbs }: HeaderProps) {
|
||||
const { data: session } = useSession();
|
||||
const user = session?.user;
|
||||
|
||||
return (
|
||||
<header className="flex h-14 items-center justify-between border-b border-zinc-800 bg-zinc-950 px-6">
|
||||
{/* Left: Title / Breadcrumbs */}
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{breadcrumbs ? (
|
||||
<nav className="flex items-center gap-1.5 text-sm">
|
||||
{breadcrumbs.map((crumb, i) => (
|
||||
<span key={i} className="flex items-center gap-1.5">
|
||||
{i > 0 && <span className="text-muted-foreground">/</span>}
|
||||
{crumb.href ? (
|
||||
<Link
|
||||
href={crumb.href}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{crumb.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-foreground font-medium truncate">
|
||||
{crumb.label}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</nav>
|
||||
) : (
|
||||
title && (
|
||||
<h1 className="text-sm font-semibold truncate">{title}</h1>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: Notifications + User menu */}
|
||||
<div className="flex items-center gap-3">
|
||||
<NotificationBell />
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-secondary transition-colors">
|
||||
<Avatar className="h-7 w-7">
|
||||
{user?.image && <AvatarImage src={user.image} alt={user.name ?? ""} />}
|
||||
<AvatarFallback className="text-xs">
|
||||
{getInitials(user?.name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="hidden sm:block text-left">
|
||||
<p className="text-xs font-medium leading-none">{user?.name ?? user?.email}</p>
|
||||
<p className="text-xs text-muted-foreground capitalize">
|
||||
{user?.role?.toLowerCase()}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuLabel>
|
||||
<p className="font-medium truncate">{user?.name}</p>
|
||||
<p className="text-xs text-muted-foreground font-normal truncate">
|
||||
{user?.email}
|
||||
</p>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings" className="flex items-center gap-2 cursor-pointer">
|
||||
<Settings className="h-4 w-4" />
|
||||
Settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive cursor-pointer"
|
||||
onClick={() => signOut({ callbackUrl: "/login" })}
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30 * 1000, // 30s
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<SessionProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
{children}
|
||||
<Toaster />
|
||||
</TooltipProvider>
|
||||
{process.env.NODE_ENV === "development" && (
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Image from 'next/image';
|
||||
import { Josefin_Sans } from 'next/font/google';
|
||||
import { Montserrat } from 'next/font/google';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
FolderOpen,
|
||||
Users,
|
||||
UserCog,
|
||||
Settings,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ListTodo,
|
||||
CalendarRange,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const navItems = [
|
||||
{ href: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '/projects', label: 'Projects', icon: FolderOpen },
|
||||
{ href: '/tasks', label: 'My Tasks', icon: ListTodo, hideForClient: true },
|
||||
{ href: '/schedule', label: 'Schedule', icon: CalendarRange, adminOnly: true },
|
||||
{ href: '/clients', label: 'Clients', icon: Users, adminOnly: true },
|
||||
{ href: '/users', label: 'Users', icon: UserCog, adminOnly: true, adminStrictOnly: true },
|
||||
{ href: '/settings', label: 'Settings', icon: Settings },
|
||||
];
|
||||
|
||||
const josefin = Josefin_Sans({
|
||||
subsets: ['latin'],
|
||||
weight: ['300', '400'],
|
||||
});
|
||||
const montserrat = Montserrat({
|
||||
subsets: ['latin'],
|
||||
weight: ['200', '500', '600'],
|
||||
});
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const { data: session } = useSession();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const isAdmin = ['ADMIN', 'PRODUCER'].includes(session?.user?.role ?? '');
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'flex flex-col border-r border-zinc-800 bg-zinc-900 transition-all duration-200',
|
||||
collapsed ? 'w-16' : 'w-60',
|
||||
)}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-4 py-5 border-b border-zinc-800',
|
||||
collapsed && 'justify-center px-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-black">
|
||||
<Image src="/logo.svg" alt="Logo" width={32} height={32} />
|
||||
</div>
|
||||
|
||||
{!collapsed && (
|
||||
<div className={montserrat.className}>
|
||||
<span className="block text-2xl font-light text-white leading-none">
|
||||
TWO TALES
|
||||
</span>
|
||||
|
||||
<span className="block text-[11px] tracking-[0.18em] italic text-zinc-400 leading-none -mt-0.25">
|
||||
vfx review
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-2 py-4 space-y-1">
|
||||
{navItems.map((item) => {
|
||||
if (item.adminOnly && !isAdmin) return null;
|
||||
if ((item as any).adminStrictOnly && session?.user?.role !== 'ADMIN') return null;
|
||||
if ((item as any).hideForClient && session?.user?.role === 'CLIENT')
|
||||
return null;
|
||||
const Icon = item.icon;
|
||||
const isActive =
|
||||
pathname === item.href || pathname.startsWith(item.href + '/');
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
|
||||
collapsed && 'justify-center px-0 w-full',
|
||||
isActive
|
||||
? 'bg-amber-500/10 text-amber-400'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-zinc-800',
|
||||
)}
|
||||
title={collapsed ? item.label : undefined}
|
||||
>
|
||||
<Icon className="h-4 w-4 shrink-0" />
|
||||
{!collapsed && <span>{item.label}</span>}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Collapse toggle */}
|
||||
<div className="border-t border-zinc-800 p-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="w-full"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user