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

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:
root
2026-06-25 00:57:59 -04:00
parent 572d115003
commit 3b142ca4c7
36 changed files with 2987 additions and 1156 deletions
@@ -128,13 +128,23 @@ function hasMinRole(employeeRole: string, minRole: string): boolean {
return (ROLE_RANK[employeeRole] ?? 0) >= (ROLE_RANK[minRole] ?? 0)
}
export function hasRenderableMenuItems(items: GeneratedMenuItem[] | null): boolean {
if (!items?.length) return false
return items.some((item) => {
if (item.itemType === 'DIVIDER' || item.itemType === 'SECTION_LABEL') return false
if (item.itemType === 'PARENT_MENU') return hasRenderableMenuItems(item.children)
return Boolean(item.routeOrUrl)
})
}
function notifyParent(message: Record<string, unknown>) {
if (typeof window === 'undefined' || window.parent === window) return
window.parent.postMessage(message, '*')
}
export default function Sidebar() {
const { dict, language, setLanguage, theme } = useDashboardI18n()
const { dict, language, setLanguage } = useDashboardI18n()
const isRtl = language === 'ar'
const pathname = usePathname()
const appPath = toDashboardAppPath(pathname)
@@ -248,8 +258,9 @@ export default function Sidebar() {
const isGeneratedItemActive = (item: GeneratedMenuItem) => {
if (!item.routeOrUrl || item.itemType !== 'INTERNAL_PAGE') return false
if (item.routeOrUrl === '/') return appPath === '/'
return appPath.startsWith(item.routeOrUrl)
const route = toDashboardAppPath(item.routeOrUrl)
if (route === '/') return appPath === '/'
return appPath === route || appPath.startsWith(`${route}/`)
}
const fallbackMenuItems = NAV_ITEMS
@@ -267,7 +278,8 @@ export default function Sidebar() {
children: [],
}))
const resolvedMenuItems = menuItems ?? fallbackMenuItems
const useGeneratedMenu = hasRenderableMenuItems(menuItems)
const resolvedMenuItems = useGeneratedMenu && menuItems ? menuItems : fallbackMenuItems
function renderGeneratedMenu(items: GeneratedMenuItem[], depth = 0): ReactNode {
return items.map((item) => {
@@ -276,7 +288,7 @@ export default function Sidebar() {
const Icon = item.icon && item.icon in ICON_MAP ? ICON_MAP[item.icon as keyof typeof ICON_MAP] : null
if (item.itemType === 'DIVIDER') {
return <div key={item.id} className="my-3 border-t border-blue-900/50" />
return <div key={item.id} className="mx-3 my-3 h-px bg-gradient-to-r from-transparent via-blue-400/20 to-transparent" />
}
if (item.itemType === 'SECTION_LABEL') {
@@ -290,7 +302,7 @@ export default function Sidebar() {
if (item.itemType === 'PARENT_MENU') {
return (
<div key={item.id} className="space-y-1">
<div className={`flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium text-slate-300 ${paddingClass}`}>
<div className={`flex items-center gap-3 rounded-lg px-3.5 py-2.5 text-sm font-medium text-slate-600 dark:text-slate-300 ${paddingClass}`}>
{Icon ? <Icon className="h-4 w-4 flex-shrink-0" /> : null}
{label}
</div>
@@ -301,11 +313,11 @@ export default function Sidebar() {
const active = mounted && isGeneratedItemActive(item)
const className = [
'flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-medium transition-colors',
'relative flex items-center gap-3 rounded-lg px-3.5 py-3 text-sm font-medium transition-all duration-200',
paddingClass,
active
? 'bg-orange-500 text-white shadow-[0_0_12px_rgba(249,115,22,0.3)] dark:shadow-[0_0_16px_rgba(249,115,22,0.35)]'
: 'text-slate-400 hover:bg-blue-900/40 hover:text-white',
? 'bg-blue-500/10 text-blue-950 shadow-[inset_2px_0_0_#3b82f6,0_0_18px_rgba(59,130,246,0.10)] dark:text-slate-50'
: 'text-slate-500 hover:bg-blue-500/10 hover:text-blue-950 dark:text-slate-400 dark:hover:text-slate-50',
].join(' ')
if (item.itemType === 'EXTERNAL_LINK' && item.routeOrUrl) {
@@ -323,8 +335,10 @@ export default function Sidebar() {
)
}
const href = item.itemType === 'INTERNAL_PAGE' ? toDashboardAppPath(item.routeOrUrl) : (item.routeOrUrl || '/')
return (
<Link key={item.id} href={item.routeOrUrl || '/'} className={className}>
<Link key={item.id} href={href} className={className}>
{Icon ? <Icon className="h-4 w-4 flex-shrink-0" /> : null}
{label}
</Link>
@@ -353,6 +367,7 @@ export default function Sidebar() {
{/* Tab to open sidebar — only visible when sidebar is closed on mobile */}
{!open && (
<button
type="button"
onClick={() => setOpen(true)}
aria-label="Open sidebar"
className={[
@@ -366,10 +381,7 @@ export default function Sidebar() {
<aside
className={[
'fixed inset-y-0 z-40 flex w-64 flex-col border-r transition-colors duration-300',
theme === 'dark'
? 'border-blue-900/50 bg-blue-950/80 backdrop-blur-xl'
: 'border-stone-200/80 bg-white/92 backdrop-blur-xl shadow-[4px_0_20px_rgba(0,0,0,0.04)]',
'fixed inset-y-0 z-40 flex w-64 flex-col border-r border-blue-200/70 bg-white/90 text-blue-950 shadow-[8px_0_32px_rgba(15,23,42,0.08)] backdrop-blur-xl transition-[transform,background-color,border-color,color,box-shadow] duration-300 dark:border-blue-400/10 dark:bg-[#0a0f1a]/95 dark:text-slate-100 dark:shadow-[8px_0_32px_rgba(0,0,0,0.22)]',
isRtl ? 'right-0' : 'left-0',
'lg:translate-x-0',
open ? 'translate-x-0' : (isRtl ? 'translate-x-full' : '-translate-x-full'),
@@ -378,9 +390,9 @@ export default function Sidebar() {
<a
href={marketplaceUrl}
target="_top"
className="flex items-center gap-3 border-b px-6 py-5 transition-colors border-blue-900/50 dark:border-blue-900/50"
className="flex items-center gap-3 border-b border-blue-200/70 px-5 py-5 transition-colors dark:border-blue-400/10"
>
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-orange-400 to-orange-500 shadow-sm">
<div className="flex h-[38px] w-[38px] flex-shrink-0 items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 shadow-[0_4px_14px_rgba(59,130,246,0.34)]">
{brand?.logoUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={brand.logoUrl} alt={brand.displayName} className="h-8 w-8 object-cover" />
@@ -388,15 +400,13 @@ export default function Sidebar() {
<Car className="h-4 w-4 text-white" />
)}
</div>
<span className={`truncate text-sm font-bold tracking-wide ${
theme === 'dark' ? 'text-white' : 'text-blue-950'
}`}>
<span className="truncate bg-gradient-to-br from-blue-950 to-blue-600 bg-clip-text text-sm font-semibold tracking-[-0.01em] text-transparent dark:from-slate-100 dark:to-blue-300">
{brand?.displayName ?? 'RentalDriveGo'}
</span>
</a>
<nav className="flex-1 space-y-0.5 overflow-y-auto px-3 py-4">
{menuItems ? renderGeneratedMenu(resolvedMenuItems) : NAV_ITEMS.filter((item) => !mounted || hasMinRole(role, item.minRole)).map((item) => {
<nav className="flex-1 space-y-1.5 overflow-y-auto px-3 py-5">
{useGeneratedMenu ? renderGeneratedMenu(resolvedMenuItems) : NAV_ITEMS.filter((item) => !mounted || hasMinRole(role, item.minRole)).map((item) => {
const Icon = item.icon
const active = mounted && isActive(item)
return (
@@ -404,37 +414,48 @@ export default function Sidebar() {
key={item.href}
href={item.href}
className={[
'flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-medium transition-colors',
'relative flex items-center gap-3 rounded-lg px-3.5 py-3 text-sm font-medium transition-all duration-200',
active
? 'bg-orange-500 text-white shadow-[0_0_12px_rgba(249,115,22,0.3)] dark:shadow-[0_0_16px_rgba(249,115,22,0.35)]'
: 'text-slate-400 hover:bg-blue-900/40 hover:text-white',
? 'bg-blue-500/10 text-blue-950 shadow-[inset_2px_0_0_#3b82f6,0_0_18px_rgba(59,130,246,0.10)] dark:text-slate-50'
: 'text-slate-500 hover:bg-blue-500/10 hover:text-blue-950 dark:text-slate-400 dark:hover:text-slate-50',
].join(' ')}
>
<Icon className="h-4 w-4 flex-shrink-0" />
<Icon className={['h-[18px] w-[18px] flex-shrink-0', active ? 'text-blue-700 dark:text-blue-300' : ''].join(' ')} />
{dict.nav[item.key]}
{item.key === 'dashboard' ? (
<span className="ml-auto rounded-full border border-blue-400/30 bg-blue-500/15 px-2 py-0.5 text-[10px] font-medium text-blue-700 dark:text-blue-300">
Live
</span>
) : null}
{item.key === 'notifications' && active ? (
<span className="ml-auto h-1.5 w-1.5 rounded-full bg-orange-400 shadow-[0_0_10px_rgba(249,115,22,0.5)]" />
) : null}
</Link>
)
})}
</nav>
<div className="border-t border-blue-900/50 px-3 py-4">
<div className="flex items-center gap-3 rounded-2xl px-3 py-2">
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-orange-500 text-xs font-semibold text-white">
<div className="border-t border-blue-200/70 px-3 py-4 dark:border-blue-400/10">
<div className="rounded-xl border border-blue-200/70 bg-blue-50/75 p-3 backdrop-blur dark:border-blue-400/10 dark:bg-blue-500/[0.06]">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-blue-500 to-orange-500 text-xs font-semibold text-white">
{user.initials}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-white">{user.displayName}</p>
<p className="truncate text-xs text-slate-400">{user.subtitle}</p>
<p className="truncate text-sm font-medium text-blue-950 dark:text-white">{user.displayName}</p>
<p className="truncate text-xs text-slate-500 dark:text-slate-400">{user.subtitle}</p>
</div>
</div>
</div>
<button
type="button"
onClick={signOut}
className="mt-2 flex w-full items-center gap-3 rounded-2xl px-3 py-2 text-sm text-slate-400 transition-colors hover:bg-blue-900/40 hover:text-white"
className="mt-2 flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-slate-500 transition-colors hover:bg-blue-500/10 hover:text-blue-950 dark:text-slate-400 dark:hover:text-white"
>
<LogOut className="h-4 w-4" />
{dict.signOut}
</button>
<div className="mt-3 flex items-center gap-2 border-t border-blue-900/50 pt-3">
<div className="mt-3 flex items-center gap-2 border-t border-blue-200/70 pt-3 dark:border-blue-400/10">
<DashboardLanguageSwitcher />
<DashboardThemeSwitcher />
</div>
@@ -442,6 +463,7 @@ export default function Sidebar() {
{/* Arrow tab to collapse sidebar (mobile only, sticks to outer edge) */}
<button
type="button"
onClick={() => setOpen(false)}
aria-label="Close sidebar"
className={[