199 lines
7.3 KiB
TypeScript
199 lines
7.3 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { usePathname } from "next/navigation";
|
||
import { motion } from "framer-motion";
|
||
import { useAuth } from "@/lib/auth-context";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuSeparator,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import {
|
||
Trophy,
|
||
FileCode,
|
||
LogOut,
|
||
User,
|
||
ChevronDown,
|
||
LayoutDashboard,
|
||
} from "lucide-react";
|
||
import { cn } from "@/lib/utils";
|
||
import { VolguLogo } from "@/components/VolguLogo";
|
||
|
||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||
|
||
const navLinks = [
|
||
{ href: "/contests", label: "Контесты", icon: Trophy },
|
||
{ href: "/submissions", label: "Мои решения", icon: FileCode },
|
||
];
|
||
|
||
export function Navbar() {
|
||
const { user, logout, isLoading } = useAuth();
|
||
const pathname = usePathname();
|
||
|
||
const isActive = (href: string) => {
|
||
if (href === "/") return pathname === href;
|
||
return pathname.startsWith(href);
|
||
};
|
||
|
||
return (
|
||
<nav className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
|
||
{/* Logo and Nav Links */}
|
||
<div className="flex items-center gap-8">
|
||
<Link href="/" className="flex items-center gap-2 group">
|
||
<motion.div
|
||
whileHover={{ scale: 1.05 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<VolguLogo className="h-8 w-8" />
|
||
</motion.div>
|
||
<span className="text-xl font-bold">
|
||
<span className="text-[#2B4B7C]">ВолГУ</span>
|
||
<span className="text-muted-foreground">.</span>
|
||
<span className="text-primary">Контесты</span>
|
||
</span>
|
||
</Link>
|
||
|
||
{user && (
|
||
<div className="hidden md:flex items-center gap-1">
|
||
{navLinks.map((link) => {
|
||
const Icon = link.icon;
|
||
const active = isActive(link.href);
|
||
|
||
return (
|
||
<Link
|
||
key={link.href}
|
||
href={link.href}
|
||
className={cn(
|
||
"flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
||
active
|
||
? "bg-primary/10 text-primary"
|
||
: "text-muted-foreground hover:text-foreground hover:bg-muted"
|
||
)}
|
||
>
|
||
<Icon className="h-4 w-4" />
|
||
{link.label}
|
||
</Link>
|
||
);
|
||
})}
|
||
|
||
{user.role === "admin" && (
|
||
<Link
|
||
href="/admin"
|
||
className={cn(
|
||
"flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
||
isActive("/admin")
|
||
? "bg-primary/10 text-primary"
|
||
: "text-muted-foreground hover:text-foreground hover:bg-muted"
|
||
)}
|
||
>
|
||
<LayoutDashboard className="h-4 w-4" />
|
||
Админ
|
||
</Link>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Right side - User menu */}
|
||
<div className="flex items-center gap-4">
|
||
{isLoading ? (
|
||
<div className="flex items-center gap-3">
|
||
<Skeleton className="h-8 w-8 rounded-full" />
|
||
<Skeleton className="h-4 w-20" />
|
||
</div>
|
||
) : user ? (
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="flex items-center gap-2 px-2">
|
||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center text-primary-foreground font-semibold text-sm overflow-hidden">
|
||
{user.avatar_url ? (
|
||
<img
|
||
src={`${API_URL}${user.avatar_url}`}
|
||
alt={user.username}
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
) : (
|
||
user.username.charAt(0).toUpperCase()
|
||
)}
|
||
</div>
|
||
<span className="hidden sm:inline text-sm font-medium">
|
||
{user.username}
|
||
</span>
|
||
{user.role === "admin" && (
|
||
<Badge variant="secondary" className="hidden sm:inline-flex text-xs">
|
||
Admin
|
||
</Badge>
|
||
)}
|
||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end" className="w-48">
|
||
<div className="px-2 py-1.5">
|
||
<p className="text-sm font-medium">{user.username}</p>
|
||
<p className="text-xs text-muted-foreground">{user.email}</p>
|
||
</div>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuItem asChild>
|
||
<Link href="/profile" className="flex items-center gap-2">
|
||
<User className="h-4 w-4" />
|
||
Профиль
|
||
</Link>
|
||
</DropdownMenuItem>
|
||
<DropdownMenuSeparator />
|
||
|
||
{/* Mobile nav links */}
|
||
<div className="md:hidden">
|
||
{navLinks.map((link) => {
|
||
const Icon = link.icon;
|
||
return (
|
||
<DropdownMenuItem key={link.href} asChild>
|
||
<Link href={link.href} className="flex items-center gap-2">
|
||
<Icon className="h-4 w-4" />
|
||
{link.label}
|
||
</Link>
|
||
</DropdownMenuItem>
|
||
);
|
||
})}
|
||
{user.role === "admin" && (
|
||
<DropdownMenuItem asChild>
|
||
<Link href="/admin" className="flex items-center gap-2">
|
||
<LayoutDashboard className="h-4 w-4" />
|
||
Админ
|
||
</Link>
|
||
</DropdownMenuItem>
|
||
)}
|
||
<DropdownMenuSeparator />
|
||
</div>
|
||
|
||
<DropdownMenuItem
|
||
onClick={logout}
|
||
className="text-destructive focus:text-destructive"
|
||
>
|
||
<LogOut className="h-4 w-4 mr-2" />
|
||
Выйти
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
) : (
|
||
<div className="flex items-center gap-2">
|
||
<Button variant="ghost" asChild>
|
||
<Link href="/login">Войти</Link>
|
||
</Button>
|
||
<Button asChild>
|
||
<Link href="/register">Регистрация</Link>
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
);
|
||
}
|