275 lines
12 KiB
TypeScript
275 lines
12 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Play, LogOut, Shield, User, GraduationCap, DollarSign, Bell, ChevronDown, Check, HelpCircle } from 'lucide-react';
|
|
import { SubscriptionStatus } from '../types';
|
|
|
|
interface NavbarProps {
|
|
user: {
|
|
name: string;
|
|
email: string;
|
|
role: 'admin' | 'student';
|
|
subscriptionStatus: SubscriptionStatus;
|
|
avatarUrl?: string;
|
|
} | null;
|
|
activeView: string;
|
|
setView: (view: string) => void;
|
|
onLogout: () => void;
|
|
settings?: { brandName?: string } | null;
|
|
}
|
|
|
|
export default function Navbar({ user, activeView, setView, onLogout, settings }: NavbarProps) {
|
|
const [showNotifications, setShowNotifications] = useState(false);
|
|
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
|
const [notifications, setNotifications] = useState<any[]>([]);
|
|
|
|
const notifRef = useRef<HTMLDivElement>(null);
|
|
const profileRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
// Close menus on outside click
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (notifRef.current && !notifRef.current.contains(e.target as Node)) {
|
|
setShowNotifications(false);
|
|
}
|
|
if (profileRef.current && !profileRef.current.contains(e.target as Node)) {
|
|
setShowProfileMenu(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
fetchNotifications();
|
|
}
|
|
}, [user]);
|
|
|
|
const fetchNotifications = async () => {
|
|
try {
|
|
// In a real app we would pass the token or it would be in cookies
|
|
// Assuming server uses some auth, for demo we might just fetch
|
|
const token = localStorage.getItem('app_token') || localStorage.getItem('admin_token');
|
|
if (!token) return;
|
|
const res = await fetch('/api/notifications', {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setNotifications(data);
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
const markAsRead = async (id: string) => {
|
|
try {
|
|
const token = localStorage.getItem('app_token') || localStorage.getItem('admin_token');
|
|
if (!token) return;
|
|
await fetch(`/api/notifications/${id}/read`, {
|
|
method: 'PUT',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
setNotifications(notifications.map(n => n.id === id ? { ...n, read: true } : n));
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
const unreadCount = notifications.filter(n => !n.read).length;
|
|
|
|
if (!user) return null;
|
|
|
|
const getStatusColor = (status: SubscriptionStatus) => {
|
|
switch (status) {
|
|
case 'ACTIVE': return 'bg-emerald-500 text-emerald-950';
|
|
case 'TRIAL': return 'bg-amber-500 text-amber-950';
|
|
case 'OVERDUE': return 'bg-red-500 text-white';
|
|
case 'CANCELED': return 'bg-zinc-600 text-zinc-200';
|
|
default: return 'bg-zinc-500 text-white';
|
|
}
|
|
};
|
|
|
|
const getStatusLabel = (status: SubscriptionStatus) => {
|
|
switch (status) {
|
|
case 'ACTIVE': return 'Assinatura Ativa';
|
|
case 'TRIAL': return 'Período de Teste';
|
|
case 'OVERDUE': return 'Pagamento Pendente';
|
|
case 'CANCELED': return 'Assinatura Cancelada';
|
|
default: return status;
|
|
}
|
|
};
|
|
|
|
const navLinks = (
|
|
<>
|
|
<button
|
|
onClick={() => setView('home')}
|
|
className={`font-semibold transition-colors hover:text-[#E50914] whitespace-nowrap flex-shrink-0 ${activeView === 'home' || activeView === 'course-view' ? 'text-white' : 'text-zinc-400'}`}
|
|
>
|
|
Cursos
|
|
</button>
|
|
<button
|
|
onClick={() => setView('subscription')}
|
|
className={`font-semibold transition-colors hover:text-[#E50914] flex items-center space-x-1.5 whitespace-nowrap flex-shrink-0 ${activeView === 'subscription' ? 'text-white' : 'text-zinc-400'}`}
|
|
>
|
|
<DollarSign className="w-4 h-4 text-[#E50914]" />
|
|
<span>Minha Assinatura</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setView('certificates')}
|
|
className={`font-semibold transition-colors hover:text-[#E50914] flex items-center space-x-1.5 whitespace-nowrap flex-shrink-0 ${activeView === 'certificates' ? 'text-white' : 'text-zinc-400'}`}
|
|
>
|
|
<GraduationCap className="w-4 h-4 text-[#E50914]" />
|
|
<span>Meus Certificados</span>
|
|
</button>
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<nav className="sticky top-0 z-50 glass-navbar px-4 py-3 md:px-8 flex flex-wrap items-center justify-between gap-y-3">
|
|
<div className="flex items-center space-x-8">
|
|
{/* LOGO */}
|
|
<div className="flex flex-col">
|
|
<div
|
|
onClick={() => setView('home')}
|
|
className="flex items-center space-x-2 cursor-pointer select-none group"
|
|
>
|
|
<div className="bg-[#E50914] text-white p-1.5 rounded flex items-center justify-center group-hover:scale-105 transition-transform">
|
|
<Play className="fill-white w-5 h-5" />
|
|
</div>
|
|
<span className="font-sans font-black text-xl md:text-2xl tracking-tighter text-[#E50914]">
|
|
{(settings?.brandName || 'Plataforma')}<span className="text-white">FLIX</span>
|
|
</span>
|
|
</div>
|
|
{settings?.brandSlogan && (
|
|
<span className="text-[10px] text-zinc-400 font-medium ml-10 mt-0.5 max-w-[350px] leading-tight hidden md:block">
|
|
{settings.brandSlogan}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* NAVIGATION LINKS */}
|
|
<div className="hidden md:flex items-center space-x-3 lg:space-x-6 text-sm flex-shrink-0">
|
|
{navLinks}
|
|
</div>
|
|
</div>
|
|
|
|
{/* USER ACTIONS & STATUS */}
|
|
<div className="flex items-center space-x-4">
|
|
{/* Subscription Status Tag */}
|
|
<span
|
|
onClick={() => setView('subscription')}
|
|
className={`hidden sm:inline-block px-2.5 py-0.5 rounded-full text-xs font-bold tracking-wide uppercase cursor-pointer hover:opacity-90 transition-opacity ${getStatusColor(user.subscriptionStatus)}`}
|
|
>
|
|
{getStatusLabel(user.subscriptionStatus)}
|
|
</span>
|
|
|
|
{/* User Info & Actions */}
|
|
<div className="flex items-center space-x-3 border-l border-zinc-800 pl-4">
|
|
|
|
{/* Notifications */}
|
|
<div className="relative" ref={notifRef}>
|
|
<button
|
|
onClick={() => setShowNotifications(!showNotifications)}
|
|
className="relative w-8 h-8 rounded-full bg-zinc-800 flex items-center justify-center text-zinc-300 hover:bg-zinc-700 transition-colors"
|
|
>
|
|
<Bell className="w-4 h-4" />
|
|
{unreadCount > 0 && (
|
|
<span className="absolute -top-1 -right-1 w-3.5 h-3.5 bg-[#E50914] text-white text-[8px] font-bold flex items-center justify-center rounded-full border-2 border-black">
|
|
{unreadCount}
|
|
</span>
|
|
)}
|
|
</button>
|
|
|
|
{showNotifications && (
|
|
<div className="absolute right-0 mt-2 w-72 bg-zinc-900 border border-white/10 rounded-xl shadow-2xl overflow-hidden flex flex-col z-50">
|
|
<div className="p-3 border-b border-white/10 bg-black/40">
|
|
<h4 className="text-xs font-bold text-white uppercase tracking-wider">Notificações</h4>
|
|
</div>
|
|
<div className="max-h-64 overflow-y-auto">
|
|
{notifications.filter(n => !n.read).length === 0 ? (
|
|
<div className="p-4 text-center text-zinc-500 text-xs">Nenhuma notificação no momento.</div>
|
|
) : (
|
|
notifications.filter(n => !n.read).map(n => (
|
|
<div
|
|
key={n.id}
|
|
className={`p-3 border-b border-white/5 text-xs transition-colors bg-white/5 text-white`}
|
|
>
|
|
<p>{n.message}</p>
|
|
<div className="flex items-center justify-between mt-2">
|
|
<span className="text-[10px] text-zinc-500">{new Date(n.createdAt).toLocaleDateString('pt-BR')}</span>
|
|
<button onClick={() => markAsRead(n.id)} className="text-[10px] text-[#E50914] hover:text-white flex items-center gap-1">
|
|
<Check className="w-3 h-3" /> Lida
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-right hidden lg:block">
|
|
<p className="text-xs font-medium text-white max-w-[120px] truncate">{user.name}</p>
|
|
<p className="text-[10px] text-zinc-500 uppercase font-semibold">{user.role === 'admin' ? 'Administrador' : 'Aluno'}</p>
|
|
</div>
|
|
|
|
{/* Profile Dropdown */}
|
|
<div className="relative" ref={profileRef}>
|
|
<button
|
|
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
|
className="w-10 h-10 rounded-full bg-zinc-800 flex items-center justify-center text-zinc-300 hover:bg-zinc-700 transition-colors border border-white/10 overflow-hidden"
|
|
>
|
|
{user.avatarUrl ? (
|
|
<img src={user.avatarUrl} alt="Avatar" className="w-full h-full object-cover" />
|
|
) : (
|
|
<User className="w-5 h-5" />
|
|
)}
|
|
</button>
|
|
|
|
{showProfileMenu && (
|
|
<div className="absolute right-0 mt-2 w-48 bg-zinc-900 border border-white/10 rounded-xl shadow-2xl overflow-hidden py-1 z-50">
|
|
<button
|
|
onClick={() => { setView('profile'); setShowProfileMenu(false); }}
|
|
className="w-full px-4 py-2 text-left flex items-center space-x-2 text-sm text-zinc-300 hover:bg-zinc-800 hover:text-white transition-colors"
|
|
>
|
|
<User className="w-4 h-4" />
|
|
<span>Meus dados</span>
|
|
</button>
|
|
<button
|
|
onClick={() => { setView('certificates'); setShowProfileMenu(false); }}
|
|
className="w-full px-4 py-2 text-left flex items-center space-x-2 text-sm text-zinc-300 hover:bg-zinc-800 hover:text-white transition-colors"
|
|
>
|
|
<GraduationCap className="w-4 h-4" />
|
|
<span>Meus Certificados</span>
|
|
</button>
|
|
<button
|
|
onClick={() => { setView('help'); setShowProfileMenu(false); }}
|
|
className="w-full px-4 py-2 text-left flex items-center space-x-2 text-sm text-zinc-300 hover:bg-zinc-800 hover:text-white transition-colors"
|
|
>
|
|
<HelpCircle className="w-4 h-4" />
|
|
<span>Ajuda</span>
|
|
</button>
|
|
<div className="border-t border-white/10 my-1"></div>
|
|
<button
|
|
onClick={() => { onLogout(); setShowProfileMenu(false); }}
|
|
className="w-full px-4 py-2 text-left flex items-center space-x-2 text-sm text-red-400 hover:bg-red-500/10 transition-colors"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
<span>Sair</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* MOBILE NAVIGATION LINKS */}
|
|
<div className="flex md:hidden items-center space-x-6 text-sm overflow-x-auto w-full pt-3 pb-1 border-t border-white/10 scrollbar-hide">
|
|
{navLinks}
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|