feat: Next.js 14 web admin scaffold — auth, sidebar, dashboard, placeholders

This commit is contained in:
Forge
2026-03-25 08:23:25 +08:00
parent dc01eac449
commit 310a9453de
37 changed files with 2755 additions and 292 deletions

View File

@@ -0,0 +1,82 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store';
import {
LayoutDashboard,
Users,
FileText,
CreditCard,
ArrowLeftRight,
Ticket,
BarChart3,
Settings,
} from 'lucide-react';
const navItems = [
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard, roles: [] },
{ label: 'Clients', href: '/clients', icon: Users, roles: [] },
{ label: 'Invoices', href: '/invoices', icon: FileText, roles: [] },
{ label: 'Payments', href: '/payments', icon: CreditCard, roles: [] },
{ label: 'Remittances', href: '/remittances', icon: ArrowLeftRight, roles: [] },
{ label: 'Tickets', href: '/tickets', icon: Ticket, roles: [] },
{ label: 'Reports', href: '/reports', icon: BarChart3, roles: ['admin', 'staff'] },
{ label: 'Settings', href: '/settings/tenant', icon: Settings, roles: ['admin'] },
];
export default function Sidebar() {
const pathname = usePathname();
const user = useAuthStore((s) => s.user);
const userRoles = user?.roles ?? [];
const visibleItems = navItems.filter(
(item) => item.roles.length === 0 || item.roles.some((r) => userRoles.includes(r))
);
return (
<aside
className="flex flex-col w-64 min-h-screen"
style={{ backgroundColor: '#0F172A' }}
>
{/* Logo */}
<div className="flex items-center gap-2 px-6 py-5 border-b border-slate-700">
<div
className="w-8 h-8 rounded-lg flex items-center justify-center text-white font-bold text-sm"
style={{ backgroundColor: '#0891B2' }}
>
F
</div>
<span className="text-white font-semibold text-lg">FiberOps</span>
</div>
{/* Nav */}
<nav className="flex-1 px-3 py-4 space-y-1">
{visibleItems.map((item) => {
const Icon = item.icon;
const isActive =
pathname === item.href || pathname.startsWith(item.href + '/');
return (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors"
style={{
backgroundColor: isActive ? '#0891B2' : 'transparent',
color: isActive ? '#fff' : '#94A3B8',
}}
>
<Icon size={18} />
{item.label}
</Link>
);
})}
</nav>
{/* Footer */}
<div className="px-6 py-4 border-t border-slate-700">
<p className="text-slate-500 text-xs">FiberOps v1.0</p>
</div>
</aside>
);
}

View File

@@ -0,0 +1,47 @@
'use client';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store';
import { Button } from '@/components/ui/button';
import { LogOut, User } from 'lucide-react';
export default function Topbar() {
const router = useRouter();
const { user, logout } = useAuthStore();
const handleLogout = () => {
logout();
router.push('/login');
};
const fullName = user ? `${user.firstName} ${user.lastName}` : 'Admin';
return (
<header
className="h-14 border-b flex items-center justify-between px-6"
style={{ backgroundColor: '#fff', borderColor: '#E2E8F0' }}
>
<div />
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-sm text-slate-600">
<User size={16} />
<span>{fullName}</span>
{user?.roles?.[0] && (
<span className="text-xs bg-slate-100 text-slate-500 px-2 py-0.5 rounded-full capitalize">
{user.roles[0]}
</span>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={handleLogout}
className="text-slate-500 hover:text-red-500 gap-1.5"
>
<LogOut size={15} />
Logout
</Button>
</div>
</header>
);
}