add first files
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Car,
|
||||
Calendar,
|
||||
Users,
|
||||
Tag,
|
||||
UserPlus,
|
||||
BarChart2,
|
||||
CreditCard,
|
||||
Settings,
|
||||
LogOut,
|
||||
} from 'lucide-react'
|
||||
import { useClerk, useUser } from '@clerk/nextjs'
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: '/dashboard', label: 'Dashboard', icon: LayoutDashboard, exact: true },
|
||||
{ href: '/dashboard/fleet', label: 'Fleet', icon: Car },
|
||||
{ href: '/dashboard/reservations', label: 'Reservations', icon: Calendar },
|
||||
{ href: '/dashboard/customers', label: 'Customers', icon: Users },
|
||||
{ href: '/dashboard/offers', label: 'Offers', icon: Tag },
|
||||
{ href: '/dashboard/team', label: 'Team', icon: UserPlus },
|
||||
{ href: '/dashboard/reports', label: 'Reports', icon: BarChart2 },
|
||||
{ href: '/dashboard/billing', label: 'Billing', icon: CreditCard },
|
||||
{ href: '/dashboard/settings', label: 'Settings', icon: Settings },
|
||||
]
|
||||
|
||||
export default function Sidebar() {
|
||||
const pathname = usePathname()
|
||||
const { signOut } = useClerk()
|
||||
const { user } = useUser()
|
||||
|
||||
const isActive = (item: typeof NAV_ITEMS[0]) => {
|
||||
if (item.exact) return pathname === item.href
|
||||
return pathname.startsWith(item.href)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="fixed inset-y-0 left-0 w-64 bg-slate-900 flex flex-col z-40">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 px-6 py-5 border-b border-slate-800">
|
||||
<div className="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center">
|
||||
<Car className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="font-bold text-white text-sm tracking-wide">RentalDriveGo</span>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-4 space-y-0.5 overflow-y-auto">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item)
|
||||
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',
|
||||
active
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-slate-400 hover:text-white hover:bg-slate-800',
|
||||
].join(' ')}
|
||||
>
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
{item.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* User menu */}
|
||||
<div className="px-3 py-4 border-t border-slate-800">
|
||||
<div className="flex items-center gap-3 px-3 py-2 rounded-lg">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-xs font-semibold flex-shrink-0">
|
||||
{user?.firstName?.[0]?.toUpperCase() ?? 'U'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
{user?.firstName} {user?.lastName}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 truncate">
|
||||
{user?.primaryEmailAddress?.emailAddress}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => signOut()}
|
||||
className="mt-2 w-full flex items-center gap-3 px-3 py-2 text-sm text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client'
|
||||
|
||||
import { Bell } from 'lucide-react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import { useUser } from '@clerk/nextjs'
|
||||
|
||||
const PAGE_TITLES: Record<string, string> = {
|
||||
'/dashboard': 'Dashboard',
|
||||
'/dashboard/fleet': 'Fleet Management',
|
||||
'/dashboard/reservations': 'Reservations',
|
||||
'/dashboard/customers': 'Customers',
|
||||
'/dashboard/offers': 'Offers',
|
||||
'/dashboard/team': 'Team',
|
||||
'/dashboard/reports': 'Reports',
|
||||
'/dashboard/billing': 'Billing',
|
||||
'/dashboard/settings': 'Settings',
|
||||
}
|
||||
|
||||
export default function TopBar() {
|
||||
const pathname = usePathname()
|
||||
const { user } = useUser()
|
||||
const [unreadCount, setUnreadCount] = useState(0)
|
||||
const [showNotifs, setShowNotifs] = useState(false)
|
||||
|
||||
const title = PAGE_TITLES[pathname] ?? 'Dashboard'
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<{ unread: number }>('/notifications/unread-count')
|
||||
.then((data) => setUnreadCount(data.unread))
|
||||
.catch(() => {})
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<header className="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-6">
|
||||
<h1 className="text-lg font-semibold text-slate-900">{title}</h1>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Notification bell */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowNotifs(!showNotifs)}
|
||||
className="relative p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
||||
>
|
||||
<Bell className="w-5 h-5" />
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute top-1 right-1 min-w-[16px] h-4 bg-red-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center px-1">
|
||||
{unreadCount > 99 ? '99+' : unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{showNotifs && (
|
||||
<div className="absolute right-0 top-full mt-2 w-80 bg-white border border-slate-200 rounded-xl shadow-lg z-50 p-4">
|
||||
<p className="text-sm font-medium text-slate-900 mb-2">Notifications</p>
|
||||
<p className="text-sm text-slate-500">
|
||||
{unreadCount === 0 ? 'No new notifications' : `${unreadCount} unread notification${unreadCount > 1 ? 's' : ''}`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* User avatar */}
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-xs font-semibold">
|
||||
{user?.firstName?.[0]?.toUpperCase() ?? 'U'}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { LucideIcon } from 'lucide-react'
|
||||
|
||||
interface StatCardProps {
|
||||
title: string
|
||||
value: string | number
|
||||
change?: number
|
||||
icon: LucideIcon
|
||||
iconColor?: string
|
||||
iconBg?: string
|
||||
}
|
||||
|
||||
export default function StatCard({
|
||||
title,
|
||||
value,
|
||||
change,
|
||||
icon: Icon,
|
||||
iconColor = 'text-blue-600',
|
||||
iconBg = 'bg-blue-50',
|
||||
}: StatCardProps) {
|
||||
const isPositive = change !== undefined && change >= 0
|
||||
const isNegative = change !== undefined && change < 0
|
||||
|
||||
return (
|
||||
<div className="card p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500 font-medium">{title}</p>
|
||||
<p className="text-2xl font-bold text-slate-900 mt-1">{value}</p>
|
||||
{change !== undefined && (
|
||||
<div className="flex items-center gap-1 mt-2">
|
||||
<span
|
||||
className={[
|
||||
'text-xs font-medium',
|
||||
isPositive ? 'text-green-600' : isNegative ? 'text-red-600' : 'text-slate-500',
|
||||
].join(' ')}
|
||||
>
|
||||
{isPositive ? '+' : ''}{change.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">vs last month</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`w-12 h-12 rounded-xl ${iconBg} flex items-center justify-center flex-shrink-0`}>
|
||||
<Icon className={`w-6 h-6 ${iconColor}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user