chore: bulk commit of pre-existing changes
Build & Deploy / Build & Push Docker Image (push) Successful in 48s
Test / API Unit Tests (push) Successful in 9m48s
Test / Marketplace Unit Tests (push) Successful in 9m38s
Test / Admin Unit Tests (push) Successful in 9m33s
Build & Deploy / Deploy to VPS (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled
Build & Deploy / Build & Push Docker Image (push) Successful in 48s
Test / API Unit Tests (push) Successful in 9m48s
Test / Marketplace Unit Tests (push) Successful in 9m38s
Test / Admin Unit Tests (push) Successful in 9m33s
Build & Deploy / Deploy to VPS (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled
Includes: - Admin dashboard layout and page updates - API account auth module (routes, schemas, service) - Dashboard sign-up form extraction, middleware refactor - Marketplace proxy and middleware updates - CORS origins expansion for dev environment - Seed email domain correction (.com → .ma) - Docs: create-account guide, fleet page, signup plan - Various UI component and layout refinements
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Bell } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { Bell, Search, Settings } from 'lucide-react'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { io } from 'socket.io-client'
|
||||
@@ -48,6 +49,7 @@ export default function TopBar() {
|
||||
createdAt: string
|
||||
}>>([])
|
||||
const [loadingNotifs, setLoadingNotifs] = useState(false)
|
||||
const [socketEnabled, setSocketEnabled] = useState(false)
|
||||
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => { setMounted(true) }, [])
|
||||
@@ -63,8 +65,10 @@ export default function TopBar() {
|
||||
try {
|
||||
const data = await apiFetch<{ unread: number }>('/notifications/unread-count')
|
||||
setUnreadCount(data.unread)
|
||||
setSocketEnabled(true)
|
||||
} catch {
|
||||
setUnreadCount(0)
|
||||
setSocketEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +85,8 @@ export default function TopBar() {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!socketEnabled) return
|
||||
|
||||
const socketOrigin = resolveSocketOrigin()
|
||||
if (!socketOrigin) return
|
||||
|
||||
@@ -106,7 +112,7 @@ export default function TopBar() {
|
||||
socket.off('notification', handleNotification)
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [])
|
||||
}, [socketEnabled])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showNotifs) return
|
||||
@@ -188,52 +194,54 @@ export default function TopBar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="relative z-[70] flex h-16 items-center justify-between border-b border-stone-200/80 bg-white/78 px-6 backdrop-blur-xl transition-colors dark:border-blue-900 dark:bg-blue-950/76">
|
||||
<header className="relative z-[70] flex h-16 items-center justify-between border-b border-blue-200/70 bg-white/82 px-6 text-blue-950 backdrop-blur-xl transition-colors dark:border-blue-400/10 dark:bg-[#0a0f1a]/90 dark:text-slate-100">
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<h1 className="text-lg font-semibold text-blue-950 dark:text-stone-50 shrink-0">{title}</h1>
|
||||
<div className="hidden sm:block relative max-w-xs w-full">
|
||||
<svg className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-stone-400 dark:text-stone-500 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
||||
</svg>
|
||||
<h1 className="shrink-0 text-lg font-semibold text-blue-950 dark:text-slate-50">{title}</h1>
|
||||
<div className="relative hidden w-[min(380px,38vw)] sm:block">
|
||||
<Search className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search…"
|
||||
className="w-full rounded-2xl border border-stone-200 bg-stone-50/60 pl-9 pr-4 py-2 text-sm text-stone-900 placeholder:text-stone-400 transition-colors focus:border-orange-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-orange-500/20 dark:border-blue-800 dark:bg-blue-950/30 dark:text-slate-100 dark:placeholder:text-slate-500 dark:focus:border-orange-400 dark:focus:bg-blue-950/50"
|
||||
placeholder="Search vehicles, bookings, customers..."
|
||||
className="h-10 w-full rounded-lg border border-blue-200/80 bg-blue-50/70 pl-10 pr-4 text-sm text-blue-950 outline-none transition-all placeholder:text-slate-500 focus:border-blue-400/45 focus:bg-white focus:ring-4 focus:ring-blue-500/10 dark:border-blue-400/15 dark:bg-blue-500/[0.06] dark:text-slate-100 dark:focus:bg-blue-500/10"
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 text-sm text-slate-400 xl:flex">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400 shadow-[0_0_0_5px_rgba(16,185,129,0.10)]" />
|
||||
System operational
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowNotifs(!showNotifs)}
|
||||
className="relative rounded-2xl p-2 text-stone-500 transition-colors hover:bg-stone-100 hover:text-stone-700 dark:text-stone-400 dark:hover:bg-blue-900/40 dark:hover:text-stone-100"
|
||||
className="relative rounded-lg p-2 text-slate-500 transition-colors hover:bg-blue-500/10 hover:text-blue-950 dark:text-slate-400 dark:hover:text-slate-100"
|
||||
>
|
||||
<Bell className="h-5 w-5" />
|
||||
{unreadCount > 0 ? (
|
||||
<span className="absolute right-1 top-1 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-bold text-white">
|
||||
<span className="absolute right-1 top-1 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-orange-500 px-1 text-[10px] font-bold text-white ring-2 ring-white dark:ring-[#0a0f1a]">
|
||||
{unreadCount > 99 ? '99+' : unreadCount}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
{showNotifs ? (
|
||||
<div className="absolute right-0 top-full z-[80] mt-2 w-80 rounded-[1.75rem] border border-stone-200/80 bg-white/95 p-4 shadow-[0_20px_60px_rgba(0,0,0,0.12)] backdrop-blur dark:border-blue-900 dark:bg-blue-950/95 dark:shadow-[0_20px_60px_rgba(0,0,0,0.35)]">
|
||||
<div className="absolute right-0 top-full z-[80] mt-2 w-80 rounded-xl border border-blue-200/80 bg-white/95 p-4 shadow-[0_20px_60px_rgba(15,23,42,0.14)] backdrop-blur-xl dark:border-blue-400/15 dark:bg-[#0d1728]/95 dark:shadow-[0_20px_60px_rgba(0,0,0,0.35)]">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium text-blue-950 dark:text-stone-100">{dict.notifications}</p>
|
||||
<p className="text-sm font-medium text-blue-950 dark:text-slate-100">{dict.notifications}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openNotificationsInbox()}
|
||||
className="text-xs font-medium text-orange-700 hover:text-blue-900 dark:text-orange-300 dark:hover:text-white"
|
||||
className="text-xs font-medium text-orange-700 hover:text-blue-950 dark:text-orange-300 dark:hover:text-white"
|
||||
>
|
||||
View all
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loadingNotifs ? (
|
||||
<p className="text-sm text-stone-500 dark:text-stone-400">Loading…</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">Loading…</p>
|
||||
) : notifications.length === 0 ? (
|
||||
<p className="text-sm text-stone-500 dark:text-stone-400">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{unreadCount === 0 ? dict.noNewNotifications : dict.unreadNotifications(unreadCount)}
|
||||
</p>
|
||||
) : (
|
||||
@@ -244,14 +252,14 @@ export default function TopBar() {
|
||||
type="button"
|
||||
onClick={() => openNotificationsInbox(notification.id)}
|
||||
className={[
|
||||
'w-full rounded-2xl px-3 py-2 text-left transition-colors hover:bg-stone-100 dark:hover:bg-blue-900/40',
|
||||
'w-full rounded-lg px-3 py-2 text-left transition-colors hover:bg-blue-500/10',
|
||||
notification.readAt ? 'opacity-80' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="truncate text-sm font-medium text-blue-950 dark:text-stone-100">
|
||||
<p className="truncate text-sm font-medium text-blue-950 dark:text-slate-100">
|
||||
{notification.title}
|
||||
</p>
|
||||
<p className="truncate text-xs text-stone-500 dark:text-stone-400">
|
||||
<p className="truncate text-xs text-slate-500 dark:text-slate-400">
|
||||
{notification.body}
|
||||
</p>
|
||||
</button>
|
||||
@@ -262,7 +270,15 @@ export default function TopBar() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-orange-400 text-xs font-semibold text-blue-950">
|
||||
<Link
|
||||
href="/settings"
|
||||
className="hidden rounded-lg p-2 text-slate-500 transition-colors hover:bg-blue-500/10 hover:text-blue-950 dark:text-slate-400 dark:hover:text-slate-100 sm:block"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
</Link>
|
||||
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-blue-500 to-orange-500 text-xs font-semibold text-white">
|
||||
{userInitials}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user