fix first online resevation
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
export type DashboardLanguage = 'en' | 'fr' | 'ar'
|
||||
export type DashboardTheme = 'light' | 'dark'
|
||||
|
||||
type DashboardDictionary = {
|
||||
nav: Record<string, string>
|
||||
@@ -11,9 +12,10 @@ type DashboardDictionary = {
|
||||
noNewNotifications: string
|
||||
unreadNotifications: (count: number) => string
|
||||
signOut: string
|
||||
demoUser: string
|
||||
clerkDisabled: string
|
||||
workspaceUser: string
|
||||
localAuth: string
|
||||
language: string
|
||||
theme: string
|
||||
light: string
|
||||
dark: string
|
||||
}
|
||||
@@ -24,6 +26,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||||
dashboard: 'Dashboard',
|
||||
fleet: 'Fleet',
|
||||
reservations: 'Reservations',
|
||||
onlineReservations: 'Online Reservations',
|
||||
customers: 'Customers',
|
||||
offers: 'Offers',
|
||||
team: 'Team',
|
||||
@@ -36,6 +39,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||||
'/dashboard': 'Dashboard',
|
||||
'/dashboard/fleet': 'Fleet Management',
|
||||
'/dashboard/reservations': 'Reservations',
|
||||
'/dashboard/online-reservations': 'Online Reservations',
|
||||
'/dashboard/customers': 'Customers',
|
||||
'/dashboard/offers': 'Offers',
|
||||
'/dashboard/team': 'Team',
|
||||
@@ -47,9 +51,10 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||||
noNewNotifications: 'No new notifications',
|
||||
unreadNotifications: (count) => `${count} unread notification${count > 1 ? 's' : ''}`,
|
||||
signOut: 'Sign out',
|
||||
demoUser: 'Demo User',
|
||||
clerkDisabled: 'Clerk disabled in local dev',
|
||||
workspaceUser: 'Workspace user',
|
||||
localAuth: 'Local authentication',
|
||||
language: 'Language',
|
||||
theme: 'Theme',
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
},
|
||||
@@ -58,6 +63,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||||
dashboard: 'Tableau de bord',
|
||||
fleet: 'Flotte',
|
||||
reservations: 'Réservations',
|
||||
onlineReservations: 'Réservations en ligne',
|
||||
customers: 'Clients',
|
||||
offers: 'Offres',
|
||||
team: 'Équipe',
|
||||
@@ -70,6 +76,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||||
'/dashboard': 'Tableau de bord',
|
||||
'/dashboard/fleet': 'Gestion de flotte',
|
||||
'/dashboard/reservations': 'Réservations',
|
||||
'/dashboard/online-reservations': 'Réservations en ligne',
|
||||
'/dashboard/customers': 'Clients',
|
||||
'/dashboard/offers': 'Offres',
|
||||
'/dashboard/team': 'Équipe',
|
||||
@@ -81,9 +88,10 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||||
noNewNotifications: 'Aucune nouvelle notification',
|
||||
unreadNotifications: (count) => `${count} notification${count > 1 ? 's' : ''} non lue${count > 1 ? 's' : ''}`,
|
||||
signOut: 'Déconnexion',
|
||||
demoUser: 'Utilisateur démo',
|
||||
clerkDisabled: 'Clerk désactivé en local',
|
||||
workspaceUser: 'Utilisateur espace',
|
||||
localAuth: 'Authentification locale',
|
||||
language: 'Langue',
|
||||
theme: 'Mode',
|
||||
light: 'Clair',
|
||||
dark: 'Sombre',
|
||||
},
|
||||
@@ -92,6 +100,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||||
dashboard: 'لوحة التحكم',
|
||||
fleet: 'الأسطول',
|
||||
reservations: 'الحجوزات',
|
||||
onlineReservations: 'الحجوزات الإلكترونية',
|
||||
customers: 'العملاء',
|
||||
offers: 'العروض',
|
||||
team: 'الفريق',
|
||||
@@ -104,6 +113,7 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||||
'/dashboard': 'لوحة التحكم',
|
||||
'/dashboard/fleet': 'إدارة الأسطول',
|
||||
'/dashboard/reservations': 'الحجوزات',
|
||||
'/dashboard/online-reservations': 'الحجوزات الإلكترونية',
|
||||
'/dashboard/customers': 'العملاء',
|
||||
'/dashboard/offers': 'العروض',
|
||||
'/dashboard/team': 'الفريق',
|
||||
@@ -115,9 +125,10 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||||
noNewNotifications: 'لا توجد إشعارات جديدة',
|
||||
unreadNotifications: (count) => `${count} إشعار غير مقروء`,
|
||||
signOut: 'تسجيل الخروج',
|
||||
demoUser: 'مستخدم تجريبي',
|
||||
clerkDisabled: 'Clerk غير مفعّل محلياً',
|
||||
workspaceUser: 'مستخدم مساحة العمل',
|
||||
localAuth: 'مصادقة محلية',
|
||||
language: 'اللغة',
|
||||
theme: 'الوضع',
|
||||
light: 'فاتح',
|
||||
dark: 'داكن',
|
||||
},
|
||||
@@ -126,6 +137,8 @@ const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
|
||||
type I18nContextValue = {
|
||||
language: DashboardLanguage
|
||||
setLanguage: (value: DashboardLanguage) => void
|
||||
theme: DashboardTheme
|
||||
setTheme: (value: DashboardTheme) => void
|
||||
dict: DashboardDictionary
|
||||
}
|
||||
|
||||
@@ -133,12 +146,24 @@ const I18nContext = createContext<I18nContextValue | null>(null)
|
||||
|
||||
export function DashboardI18nProvider({ children }: { children: React.ReactNode }) {
|
||||
const [language, setLanguage] = useState<DashboardLanguage>('en')
|
||||
const [theme, setTheme] = useState<DashboardTheme>('light')
|
||||
|
||||
useEffect(() => {
|
||||
const stored = window.localStorage.getItem('dashboard-language')
|
||||
const storedTheme = window.localStorage.getItem('dashboard-theme')
|
||||
|
||||
if (stored === 'en' || stored === 'fr' || stored === 'ar') {
|
||||
setLanguage(stored)
|
||||
}
|
||||
|
||||
if (storedTheme === 'light' || storedTheme === 'dark') {
|
||||
setTheme(storedTheme)
|
||||
return
|
||||
}
|
||||
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
setTheme('dark')
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -147,7 +172,18 @@ export function DashboardI18nProvider({ children }: { children: React.ReactNode
|
||||
window.localStorage.setItem('dashboard-language', language)
|
||||
}, [language])
|
||||
|
||||
const value = useMemo(() => ({ language, setLanguage, dict: dictionaries[language] }), [language])
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.remove('light', 'dark')
|
||||
document.documentElement.classList.add(theme)
|
||||
document.documentElement.style.colorScheme = theme
|
||||
document.body.dataset.theme = theme
|
||||
window.localStorage.setItem('dashboard-theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ language, setLanguage, theme, setTheme, dict: dictionaries[language] }),
|
||||
[language, theme],
|
||||
)
|
||||
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>
|
||||
}
|
||||
|
||||
@@ -168,7 +204,7 @@ export function DashboardLanguageSwitcher() {
|
||||
if (embedded) return null
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm">
|
||||
<div className="flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm transition-colors dark:border-slate-700 dark:bg-slate-900">
|
||||
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
|
||||
{dict.language}
|
||||
</span>
|
||||
@@ -180,7 +216,9 @@ export function DashboardLanguageSwitcher() {
|
||||
type="button"
|
||||
onClick={() => setLanguage(value)}
|
||||
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
||||
active ? 'bg-slate-900 text-white' : 'text-slate-600 hover:bg-slate-100'
|
||||
active
|
||||
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-950'
|
||||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
|
||||
}`}
|
||||
>
|
||||
{value.toUpperCase()}
|
||||
@@ -190,3 +228,39 @@ export function DashboardLanguageSwitcher() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DashboardThemeSwitcher() {
|
||||
const { theme, setTheme, dict } = useDashboardI18n()
|
||||
const [embedded, setEmbedded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setEmbedded(window.self !== window.top)
|
||||
}, [])
|
||||
|
||||
if (embedded) return null
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm transition-colors dark:border-slate-700 dark:bg-slate-900">
|
||||
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
|
||||
{dict.theme}
|
||||
</span>
|
||||
{(['light', 'dark'] as DashboardTheme[]).map((value) => {
|
||||
const active = value === theme
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setTheme(value)}
|
||||
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
||||
active
|
||||
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-950'
|
||||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
|
||||
}`}
|
||||
>
|
||||
{value === 'light' ? dict.light : dict.dark}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { DashboardLanguageSwitcher, useDashboardI18n } from '@/components/I18nProvider'
|
||||
import {
|
||||
DashboardLanguageSwitcher,
|
||||
DashboardThemeSwitcher,
|
||||
useDashboardI18n,
|
||||
} from '@/components/I18nProvider'
|
||||
import { marketplaceUrl } from '@/lib/urls'
|
||||
|
||||
export default function PublicShell({ children }: { children: React.ReactNode }) {
|
||||
@@ -28,31 +32,32 @@ export default function PublicShell({ children }: { children: React.ReactNode })
|
||||
}[language]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top,#dbeafe,transparent_35%),linear-gradient(180deg,#f8fafc,white)]">
|
||||
<header className="sticky top-0 z-30 border-b border-slate-200 bg-white/85 backdrop-blur-md">
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top,#dbeafe,transparent_35%),linear-gradient(180deg,#f8fafc,white)] transition-colors dark:bg-[radial-gradient(circle_at_top,rgba(37,99,235,0.18),transparent_35%),linear-gradient(180deg,#020617,#0f172a)] dark:text-slate-100">
|
||||
<header className="sticky top-0 z-30 border-b border-slate-200 bg-white/85 backdrop-blur-md transition-colors dark:border-slate-800 dark:bg-slate-950/85">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-4">
|
||||
<Link href={marketplaceUrl} className="flex items-center gap-3 text-slate-900">
|
||||
<Link href={marketplaceUrl} className="flex items-center gap-3 text-slate-900 dark:text-slate-100">
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-600">RentalDriveGo</span>
|
||||
<span className="hidden text-sm font-semibold text-slate-500 sm:inline">{dict.workspace}</span>
|
||||
<span className="hidden text-sm font-semibold text-slate-500 dark:text-slate-400 sm:inline">{dict.workspace}</span>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-2">
|
||||
<Link href="/sign-in" className="rounded-full px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-100 hover:text-slate-900">
|
||||
<Link href="/sign-in" className="rounded-full px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-slate-100">
|
||||
{dict.signIn}
|
||||
</Link>
|
||||
<Link href="/sign-up" className="rounded-full bg-slate-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-slate-700">
|
||||
<Link href="/sign-up" className="rounded-full bg-slate-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-950 dark:hover:bg-slate-200">
|
||||
{dict.createWorkspace}
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div>{children}</div>
|
||||
<footer className="border-t border-stone-200 bg-white/90 px-4 py-4 backdrop-blur-md transition-colors">
|
||||
<footer className="border-t border-stone-200 bg-white/90 px-4 py-4 backdrop-blur-md transition-colors dark:border-slate-800 dark:bg-slate-950/90">
|
||||
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-3 lg:flex-row">
|
||||
<p className="text-xs font-medium uppercase tracking-[0.16em] text-stone-400">
|
||||
<p className="text-xs font-medium uppercase tracking-[0.16em] text-stone-400 dark:text-slate-500">
|
||||
{dict.preferences}
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-3 sm:flex-row">
|
||||
<DashboardLanguageSwitcher />
|
||||
<DashboardThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Car,
|
||||
Calendar,
|
||||
Globe,
|
||||
Users,
|
||||
Tag,
|
||||
UserPlus,
|
||||
@@ -15,15 +16,15 @@ import {
|
||||
Settings,
|
||||
LogOut,
|
||||
} from 'lucide-react'
|
||||
import { useClerk, useUser } from '@clerk/nextjs'
|
||||
import { clerkFrontendEnabled } from '@/lib/clerk'
|
||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||
import { marketplaceUrl } from '@/lib/urls'
|
||||
import { EMPLOYEE_TOKEN_KEY } from '@/lib/api'
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: '/dashboard', key: 'dashboard', icon: LayoutDashboard, exact: true },
|
||||
{ href: '/dashboard/fleet', key: 'fleet', icon: Car },
|
||||
{ href: '/dashboard/reservations', key: 'reservations', icon: Calendar },
|
||||
{ href: '/dashboard/online-reservations', key: 'onlineReservations', icon: Globe },
|
||||
{ href: '/dashboard/customers', key: 'customers', icon: Users },
|
||||
{ href: '/dashboard/offers', key: 'offers', icon: Tag },
|
||||
{ href: '/dashboard/team', key: 'team', icon: UserPlus },
|
||||
@@ -33,29 +34,32 @@ const NAV_ITEMS = [
|
||||
{ href: '/dashboard/settings', key: 'settings', icon: Settings },
|
||||
] as const
|
||||
|
||||
function SidebarWithClerk() {
|
||||
export default function Sidebar() {
|
||||
const { dict } = useDashboardI18n()
|
||||
const pathname = usePathname()
|
||||
const { signOut } = useClerk()
|
||||
const { user } = useUser()
|
||||
const router = useRouter()
|
||||
|
||||
const isActive = (item: typeof NAV_ITEMS[0]) => {
|
||||
if (item.exact) return pathname === item.href
|
||||
return pathname.startsWith(item.href)
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
localStorage.removeItem(EMPLOYEE_TOKEN_KEY)
|
||||
document.cookie = 'employee_token=; path=/; max-age=0; samesite=lax'
|
||||
router.push('/sign-in')
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="fixed inset-y-0 left-0 w-64 bg-slate-900 flex flex-col z-40">
|
||||
{/* Logo */}
|
||||
<Link href={marketplaceUrl} 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" />
|
||||
<aside className="fixed inset-y-0 left-0 z-40 flex w-64 flex-col bg-slate-900">
|
||||
<Link href={marketplaceUrl} className="flex items-center gap-3 border-b border-slate-800 px-6 py-5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-500">
|
||||
<Car className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span className="font-bold text-white text-sm tracking-wide">RentalDriveGo</span>
|
||||
<span className="text-sm font-bold tracking-wide text-white">RentalDriveGo</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-4 space-y-0.5 overflow-y-auto">
|
||||
<nav className="flex-1 space-y-0.5 overflow-y-auto px-3 py-4">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item)
|
||||
@@ -64,101 +68,35 @@ function SidebarWithClerk() {
|
||||
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',
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
|
||||
active ? 'bg-blue-600 text-white' : 'text-slate-400 hover:bg-slate-800 hover:text-white',
|
||||
].join(' ')}
|
||||
>
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||
{dict.nav[item.key]}
|
||||
</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 className="border-t border-slate-800 px-3 py-4">
|
||||
<div className="flex items-center gap-3 rounded-lg px-3 py-2">
|
||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-xs font-semibold text-white">
|
||||
W
|
||||
</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 className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-white">{dict.workspaceUser}</p>
|
||||
<p className="truncate text-xs text-slate-400">{dict.localAuth}</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"
|
||||
onClick={signOut}
|
||||
className="mt-2 flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-slate-400 transition-colors hover:bg-slate-800 hover:text-white"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<LogOut className="h-4 w-4" />
|
||||
{dict.signOut}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarWithoutClerk() {
|
||||
const { dict } = useDashboardI18n()
|
||||
const pathname = usePathname()
|
||||
|
||||
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">
|
||||
<Link href={marketplaceUrl} 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>
|
||||
</Link>
|
||||
|
||||
<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" />
|
||||
{dict.nav[item.key]}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<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">
|
||||
D
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{dict.demoUser}</p>
|
||||
<p className="text-xs text-slate-400 truncate">{dict.clerkDisabled}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Sidebar() {
|
||||
return clerkFrontendEnabled ? <SidebarWithClerk /> : <SidebarWithoutClerk />
|
||||
}
|
||||
|
||||
@@ -4,113 +4,54 @@ 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'
|
||||
import { clerkFrontendEnabled } from '@/lib/clerk'
|
||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||
|
||||
function TopBarWithClerk() {
|
||||
const { dict } = useDashboardI18n()
|
||||
const pathname = usePathname()
|
||||
const { user } = useUser()
|
||||
const [unreadCount, setUnreadCount] = useState(0)
|
||||
const [showNotifs, setShowNotifs] = useState(false)
|
||||
|
||||
const title = dict.titles[pathname] ?? dict.titles['/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">{dict.notifications}</p>
|
||||
<p className="text-sm text-slate-500">
|
||||
{unreadCount === 0 ? dict.noNewNotifications : dict.unreadNotifications(unreadCount)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
function TopBarWithoutClerk() {
|
||||
const { dict } = useDashboardI18n()
|
||||
const pathname = usePathname()
|
||||
const [unreadCount, setUnreadCount] = useState(0)
|
||||
const [showNotifs, setShowNotifs] = useState(false)
|
||||
|
||||
const title = dict.titles[pathname] ?? dict.titles['/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">
|
||||
<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">{dict.notifications}</p>
|
||||
<p className="text-sm text-slate-500">
|
||||
{unreadCount === 0 ? dict.noNewNotifications : dict.unreadNotifications(unreadCount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-xs font-semibold">
|
||||
D
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TopBar() {
|
||||
return clerkFrontendEnabled ? <TopBarWithClerk /> : <TopBarWithoutClerk />
|
||||
const { dict } = useDashboardI18n()
|
||||
const pathname = usePathname()
|
||||
const [unreadCount, setUnreadCount] = useState(0)
|
||||
const [showNotifs, setShowNotifs] = useState(false)
|
||||
|
||||
const title = dict.titles[pathname] ?? dict.titles['/dashboard']
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<{ unread: number }>('/notifications/unread-count')
|
||||
.then((data) => setUnreadCount(data.unread))
|
||||
.catch(() => {})
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<header className="flex h-16 items-center justify-between border-b border-slate-200 bg-white px-6 transition-colors dark:border-slate-800 dark:bg-slate-950">
|
||||
<h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">{title}</h1>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowNotifs(!showNotifs)}
|
||||
className="relative rounded-lg p-2 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 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">
|
||||
{unreadCount > 99 ? '99+' : unreadCount}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
{showNotifs ? (
|
||||
<div className="absolute right-0 top-full z-50 mt-2 w-80 rounded-xl border border-slate-200 bg-white p-4 shadow-lg dark:border-slate-700 dark:bg-slate-900">
|
||||
<p className="mb-2 text-sm font-medium text-slate-900 dark:text-slate-100">{dict.notifications}</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{unreadCount === 0 ? dict.noNewNotifications : dict.unreadNotifications(unreadCount)}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-xs font-semibold text-white">
|
||||
W
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function EditMemberModal({
|
||||
</p>
|
||||
<p className="text-xs text-red-600 dark:text-red-500 mb-3">
|
||||
{confirm === 'deactivate' && "They'll immediately lose dashboard access. You can reactivate anytime."}
|
||||
{confirm === 'remove' && "This will delete the employee record and revoke their Clerk access. This cannot be undone."}
|
||||
{confirm === 'remove' && "This will delete the employee record and revoke their dashboard access. This cannot be undone."}
|
||||
{confirm === 'reactivate' && "They'll regain dashboard access with their current role."}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
|
||||
Reference in New Issue
Block a user