redesign the homepage
Build & Deploy / Build & Push Docker Image (push) Failing after 47s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Failing after 4m55s
Test / Admin Unit Tests (push) Successful in 9m37s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Successful in 9m54s

This commit is contained in:
root
2026-06-26 16:27:21 -04:00
parent 256ff0814e
commit d7fb7b7a7b
1030 changed files with 107374 additions and 2657 deletions
@@ -165,7 +165,7 @@ export default function PublicFooter() {
</div>
<p className="text-sm text-stone-500 dark:text-stone-400">
&copy; {new Date().getFullYear()} FleetOS. {footerContent.rightsLabel}
&copy; {new Date().getFullYear()} RentalDriveGo. {footerContent.rightsLabel}
</p>
</div>
</footer>
@@ -1,209 +1,212 @@
'use client'
"use client";
import { ChevronDown } from 'lucide-react'
import Image from 'next/image'
import { useSearchParams } from 'next/navigation'
import { useEffect, useRef, useState } from 'react'
import { useDashboardI18n } from '@/components/I18nProvider'
import { marketplaceUrl } from '@/lib/urls'
import { ChevronDown } from "lucide-react";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import { useDashboardI18n } from "@/components/I18nProvider";
import { marketplaceUrl } from "@/lib/urls";
const DASHBOARD_LOGO_SRC = '/dashboard/rentalcardrive.png'
const BRAND_LOGO_SRC = "/dashboard/rentaldrivego.jpeg";
const languageMeta = {
en: { flag: '🇺🇸', shortLabel: 'EN' },
fr: { flag: '🇫🇷', shortLabel: 'FR' },
ar: { flag: '🇲🇦', shortLabel: 'AR' },
} as const
en: { flag: "🇺🇸", shortLabel: "EN" },
fr: { flag: "🇫🇷", shortLabel: "FR" },
ar: { flag: "🇲🇦", shortLabel: "AR" },
} as const;
export default function PublicHeader() {
const { language, setLanguage, theme, setTheme } = useDashboardI18n()
const searchParams = useSearchParams()
const localeMenuRef = useRef<HTMLDivElement | null>(null)
const [localeMenuOpen, setLocaleMenuOpen] = useState(false)
const themeMenuRef = useRef<HTMLDivElement | null>(null)
const [themeMenuOpen, setThemeMenuOpen] = useState(false)
const currentLanguage = languageMeta[language]
const availableLanguages = (['en', 'fr', 'ar'] as const)
export default function PublicHeader({
hideActions = false,
}: {
hideActions?: boolean;
}) {
const { language, setLanguage, theme, setTheme } = useDashboardI18n();
const localeMenuRef = useRef<HTMLDivElement | null>(null);
const [localeMenuOpen, setLocaleMenuOpen] = useState(false);
const themeMenuRef = useRef<HTMLDivElement | null>(null);
const [themeMenuOpen, setThemeMenuOpen] = useState(false);
const currentLanguage = languageMeta[language];
const availableLanguages = (["en", "fr", "ar"] as const)
.filter((value) => value !== language)
.map((value) => ({ value, ...languageMeta[value] }))
.map((value) => ({ value, ...languageMeta[value] }));
const dict = {
en: {
home: 'Home',
marketplace: 'Marketplace',
signIn: 'Sign in',
createAccount: 'Create Agency Space',
theme: 'Theme',
light: 'Light',
dark: 'Dark',
signIn: "Sign in",
createAccount: "Get Started",
theme: "Theme",
light: "Light",
dark: "Dark",
},
fr: {
home: 'Accueil',
marketplace: 'Marketplace',
signIn: 'Connexion',
createAccount: 'Créer un espace dagence',
theme: 'Mode',
light: 'Clair',
dark: 'Sombre',
signIn: "Connexion",
createAccount: "Démarrer",
theme: "Mode",
light: "Clair",
dark: "Sombre",
},
ar: {
home: 'الرئيسية',
marketplace: 'السوق',
signIn: 'تسجيل الدخول',
createAccount: 'إنشاء مساحة الوكالة',
theme: 'الوضع',
light: 'فاتح',
dark: 'داكن',
signIn: "تسجيل الدخول",
createAccount: "ابدأ الآن",
theme: "الوضع",
light: "فاتح",
dark: "داكن",
},
}[language]
}[language];
const themeOptions = [
{ value: 'light' as const, label: dict.light },
{ value: 'dark' as const, label: dict.dark },
]
const currentTheme = themeOptions.find((option) => option.value === theme) ?? themeOptions[0]
{ value: "light" as const, label: dict.light },
{ value: "dark" as const, label: dict.dark },
];
const currentTheme =
themeOptions.find((option) => option.value === theme) ?? themeOptions[0];
const localeMenuPositionClass =
language === 'ar'
? 'right-0 sm:right-0'
: 'left-0 sm:left-auto sm:right-0'
language === "ar" ? "right-0 sm:right-0" : "left-0 sm:left-auto sm:right-0";
useEffect(() => {
function handlePointerDown(event: MouseEvent) {
if (!localeMenuRef.current?.contains(event.target as Node)) {
setLocaleMenuOpen(false)
setLocaleMenuOpen(false);
}
if (!themeMenuRef.current?.contains(event.target as Node)) {
setThemeMenuOpen(false)
setThemeMenuOpen(false);
}
}
document.addEventListener('mousedown', handlePointerDown)
return () => document.removeEventListener('mousedown', handlePointerDown)
}, [])
document.addEventListener("mousedown", handlePointerDown);
return () => document.removeEventListener("mousedown", handlePointerDown);
}, []);
function toggleLocaleMenu() {
setThemeMenuOpen(false)
setLocaleMenuOpen((open) => !open)
setThemeMenuOpen(false);
setLocaleMenuOpen((open) => !open);
}
function toggleThemeMenu() {
setLocaleMenuOpen(false)
setThemeMenuOpen((open) => !open)
setLocaleMenuOpen(false);
setThemeMenuOpen((open) => !open);
}
const signInParams = new URLSearchParams()
signInParams.set('lang', language)
signInParams.set('theme', theme)
const next = searchParams.get('next')
const redirect = searchParams.get('redirect')
if (next) signInParams.set('next', next)
if (redirect) signInParams.set('redirect', redirect)
const signInHref = `${marketplaceUrl}/sign-in?${signInParams.toString()}`
const signInHref = `${marketplaceUrl}/${language}#signin`;
const signUpParams = new URLSearchParams()
signUpParams.set('lang', language)
signUpParams.set('theme', theme)
const signUpHref = `/dashboard/sign-up?${signUpParams.toString()}`
const signUpHref = `${marketplaceUrl}/${language}#demo`;
return (
<header className="sticky top-0 z-30 border-b border-stone-200/80 bg-white/78 backdrop-blur-xl shadow-[0_10px_30px_rgba(15,23,42,0.06)] transition-colors dark:border-blue-900 dark:bg-blue-950/76 dark:shadow-[0_10px_30px_rgba(0,0,0,0.28)]">
<div className="mx-auto flex max-w-7xl flex-col gap-1.5 px-4 py-1.5 sm:px-6 lg:flex-row lg:items-center lg:justify-between lg:gap-3 lg:px-8 lg:py-2">
<a href={marketplaceUrl} className="flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-[0.14em] text-blue-950 dark:text-slate-100 sm:text-sm sm:tracking-[0.2em]">
<Image
src={DASHBOARD_LOGO_SRC}
alt="FleetOS"
width={36}
height={36}
priority
className="h-8 w-8 rounded-md object-contain sm:h-9 sm:w-9"
/>
<span>FleetOS</span>
</a>
<header className="sticky top-0 z-50 border-b border-stone-200/80 bg-white/78 backdrop-blur-xl shadow-[0_10px_30px_rgba(15,23,42,0.08)] transition-colors dark:border-blue-900 dark:bg-blue-950/76 dark:shadow-[0_10px_30px_rgba(0,0,0,0.32)]">
<div className="mx-auto flex min-h-14 max-w-7xl flex-col gap-1.5 px-4 py-1.5 sm:px-6 lg:flex-row lg:items-center lg:justify-between lg:gap-3 lg:px-8 lg:py-2">
<div className="flex items-center justify-between gap-3">
<a
href={`${marketplaceUrl}/${language}`}
className="flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-[0.14em] text-stone-900 no-underline sm:text-sm sm:tracking-[0.2em] dark:text-stone-100"
aria-label="RentalDriveGo home"
>
<Image
src={BRAND_LOGO_SRC}
alt="RentalDriveGo"
width={36}
height={36}
priority
className="h-8 w-8 rounded-md object-contain sm:h-9 sm:w-9"
/>
<span>RentalDriveGo</span>
</a>
</div>
<div className="flex flex-col gap-1.5 lg:flex-row lg:items-center lg:gap-3">
<nav className="flex items-center gap-0.5 overflow-x-auto">
<a href={marketplaceUrl} className="rounded-full px-2.5 py-1 text-[12px] font-medium text-stone-600 transition hover:bg-stone-100 hover:text-blue-900 dark:text-stone-300 dark:hover:bg-blue-900/40 dark:hover:text-stone-100 sm:px-4 sm:py-2 sm:text-sm">
{dict.home}
</a>
<a href={`${marketplaceUrl}/explore`} className="rounded-full px-2.5 py-1 text-[12px] font-medium text-stone-600 transition hover:bg-stone-100 hover:text-blue-900 dark:text-stone-300 dark:hover:bg-blue-900/40 dark:hover:text-stone-100 sm:px-4 sm:py-2 sm:text-sm">
{dict.marketplace}
</a>
<a href={signInHref} className="rounded-full px-2.5 py-1 text-[12px] font-medium text-stone-600 transition hover:bg-stone-100 hover:text-blue-900 dark:text-stone-300 dark:hover:bg-blue-900/40 dark:hover:text-stone-100 sm:px-4 sm:py-2 sm:text-sm">
{dict.signIn}
</a>
<a href={signUpHref} className="ml-1 rounded-full bg-blue-900 px-3 py-1 text-[12px] font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-400 dark:text-white dark:hover:bg-orange-300 sm:ml-2 sm:px-5 sm:py-2 sm:text-sm">
{dict.createAccount}
</a>
</nav>
<div className="shrink-0">
<div className="flex items-center self-start rounded-full border border-stone-200/80 bg-white/95 p-0.5 shadow-sm dark:border-blue-800 dark:bg-blue-950/85 sm:self-auto sm:p-1">
<div ref={localeMenuRef} className="relative">
<button
type="button"
onClick={toggleLocaleMenu}
className="inline-flex min-w-[4.75rem] items-center justify-center gap-1.5 rounded-full px-2.5 py-1.5 text-[11px] font-semibold text-stone-700 transition hover:bg-stone-100 dark:text-stone-200 dark:hover:bg-blue-900/40 sm:min-w-[5.25rem] sm:px-3 sm:py-2 sm:text-xs"
aria-expanded={localeMenuOpen}
aria-haspopup="menu"
<div className="flex items-center self-start rounded-full border border-stone-200/80 bg-white/95 p-0.5 shadow-sm sm:self-auto sm:p-1 dark:border-blue-800 dark:bg-blue-950/85">
<div ref={localeMenuRef} className="relative">
<button
type="button"
onClick={toggleLocaleMenu}
className="inline-flex min-w-[4.75rem] items-center justify-center gap-1.5 rounded-full px-2.5 py-1.5 text-[11px] font-semibold text-stone-700 transition hover:bg-stone-100 sm:min-w-[5.25rem] sm:px-3 sm:py-2 sm:text-xs dark:text-stone-200 dark:hover:bg-blue-900/40"
aria-expanded={localeMenuOpen}
aria-haspopup="menu"
>
<span aria-hidden="true">{currentLanguage.flag}</span>
<span>{currentLanguage.shortLabel}</span>
<ChevronDown
className={`h-3.5 w-3.5 transition-transform ${localeMenuOpen ? "rotate-180" : ""}`}
/>
</button>
{localeMenuOpen ? (
<div
className={`absolute top-full z-30 mt-2 w-48 max-w-[calc(100vw-2rem)] overflow-hidden rounded-2xl border border-stone-200 bg-white/95 text-left 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)] ${localeMenuPositionClass}`}
>
<span aria-hidden="true">{currentLanguage.flag}</span>
<span>{currentLanguage.shortLabel}</span>
<ChevronDown className={`h-3.5 w-3.5 transition-transform ${localeMenuOpen ? 'rotate-180' : ''}`} />
</button>
{localeMenuOpen ? (
<div className={`absolute top-full z-30 mt-2 w-48 max-w-[calc(100vw-2rem)] overflow-hidden rounded-2xl border border-stone-200 bg-white/95 text-left 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)] ${localeMenuPositionClass}`}>
{availableLanguages.map((option) => (
{availableLanguages.map((option) => (
<button
key={option.value}
type="button"
onClick={() => {
setLanguage(option.value);
setLocaleMenuOpen(false);
}}
className="flex w-full items-center gap-3 px-4 py-3 text-sm text-stone-700 transition hover:bg-stone-100 hover:text-blue-900 dark:text-stone-200 dark:hover:bg-blue-900/40 dark:hover:text-white"
>
<span aria-hidden="true">{option.flag}</span>
<span>{option.shortLabel}</span>
</button>
))}
</div>
) : null}
</div>
<span
className="mx-1 h-6 w-px bg-stone-200 dark:bg-stone-700"
aria-hidden="true"
/>
<div ref={themeMenuRef} className="relative">
<button
type="button"
onClick={toggleThemeMenu}
className="inline-flex items-center gap-1.5 rounded-full px-2 py-1 transition hover:bg-stone-100 sm:px-2.5 sm:py-1.5 dark:hover:bg-blue-900/40"
aria-expanded={themeMenuOpen}
aria-haspopup="menu"
>
<span className="rounded-full bg-blue-900 px-3 py-1 text-[11px] font-semibold text-white sm:px-3.5 sm:py-1.5 sm:text-xs dark:bg-orange-400 dark:text-white">
{currentTheme.label}
</span>
<ChevronDown
className={`h-3.5 w-3.5 text-stone-500 transition-transform dark:text-stone-400 ${themeMenuOpen ? "rotate-180" : ""}`}
/>
</button>
{themeMenuOpen ? (
<div
className={`absolute top-full z-30 mt-2 w-44 max-w-[calc(100vw-2rem)] overflow-hidden rounded-2xl border border-stone-200 bg-white/95 text-left 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)] ${localeMenuPositionClass}`}
>
{themeOptions
.filter((option) => option.value !== theme)
.map((option) => (
<button
key={option.value}
type="button"
onClick={() => {
setLanguage(option.value)
setLocaleMenuOpen(false)
setTheme(option.value);
setThemeMenuOpen(false);
}}
className="flex w-full items-center gap-3 px-4 py-3 text-sm text-stone-700 transition hover:bg-stone-100 hover:text-blue-900 dark:text-stone-200 dark:hover:bg-blue-900/40 dark:hover:text-white"
className="flex w-full items-center justify-between px-4 py-3 text-sm text-stone-700 transition hover:bg-stone-100 hover:text-blue-900 dark:text-stone-200 dark:hover:bg-blue-900/40 dark:hover:text-white"
>
<span aria-hidden="true">{option.flag}</span>
<span>{option.shortLabel}</span>
<span>{option.label}</span>
</button>
))}
</div>
) : null}
</div>
<span className="mx-1 h-6 w-px bg-stone-200 dark:bg-stone-700" aria-hidden="true" />
<div ref={themeMenuRef} className="relative">
<button
type="button"
onClick={toggleThemeMenu}
className="inline-flex items-center gap-1.5 rounded-full px-2 py-1 transition hover:bg-stone-100 dark:hover:bg-blue-900/40 sm:px-2.5 sm:py-1.5"
aria-expanded={themeMenuOpen}
aria-haspopup="menu"
>
<span className="rounded-full bg-blue-900 px-3 py-1 text-[11px] font-semibold text-white dark:bg-orange-400 dark:text-white sm:px-3.5 sm:py-1.5 sm:text-xs">
{currentTheme.label}
</span>
<ChevronDown className={`h-3.5 w-3.5 text-stone-500 transition-transform dark:text-stone-400 ${themeMenuOpen ? 'rotate-180' : ''}`} />
</button>
{themeMenuOpen ? (
<div className={`absolute top-full z-30 mt-2 w-44 max-w-[calc(100vw-2rem)] overflow-hidden rounded-2xl border border-stone-200 bg-white/95 text-left 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)] ${localeMenuPositionClass}`}>
{themeOptions
.filter((option) => option.value !== theme)
.map((option) => (
<button
key={option.value}
type="button"
onClick={() => {
setTheme(option.value)
setThemeMenuOpen(false)
}}
className="flex w-full items-center justify-between px-4 py-3 text-sm text-stone-700 transition hover:bg-stone-100 hover:text-blue-900 dark:text-stone-200 dark:hover:bg-blue-900/40 dark:hover:text-white"
>
<span>{option.label}</span>
</button>
))}
</div>
) : null}
</div>
</div>
) : null}
</div>
</div>
{hideActions ? null : (
<div className="flex items-center gap-2">
<a
href={signInHref}
className="rounded-full px-2.5 py-1 text-[12px] font-medium text-stone-600 no-underline transition hover:bg-stone-100 hover:text-stone-900 sm:px-4 sm:py-2 sm:text-sm dark:text-stone-300 dark:hover:bg-blue-900/40 dark:hover:text-stone-100"
>
{dict.signIn}
</a>
<a
href={signUpHref}
className="rounded-full bg-orange-600 px-3 py-1 text-[12px] font-semibold text-white no-underline transition hover:bg-orange-700 sm:px-5 sm:py-2 sm:text-sm dark:bg-orange-500 dark:hover:bg-orange-400"
>
{dict.createAccount}
</a>
</div>
)}
</div>
</div>
</header>
)
);
}
@@ -1,20 +1,24 @@
'use client'
"use client";
import * as React from "react";
import PublicFooter from '@/components/layout/PublicFooter'
import PublicHeader from '@/components/layout/PublicHeader'
import PublicFooter from "@/components/layout/PublicFooter";
import PublicHeader from "@/components/layout/PublicHeader";
export default function PublicShell({
children,
embedded = false,
hideFooter = false,
hideHeaderActions = false,
}: {
children: React.ReactNode
embedded?: boolean
children: React.ReactNode;
embedded?: boolean;
hideFooter?: boolean;
hideHeaderActions?: boolean;
}) {
return (
<div className="flex min-h-screen flex-col bg-[linear-gradient(180deg,#ffffff_0%,#f5f8ff_28%,#eef4ff_58%,#ffffff_100%)] text-blue-950 transition-colors dark:bg-[linear-gradient(180deg,#0f1f4a_0%,#112d6e_35%,#0d1f4f_100%)] dark:text-slate-100">
{embedded ? null : <PublicHeader />}
{embedded ? null : <PublicHeader hideActions={hideHeaderActions} />}
<div className="flex flex-1 flex-col">{children}</div>
{embedded ? null : <PublicFooter />}
{embedded || hideFooter ? null : <PublicFooter />}
</div>
)
);
}
@@ -401,7 +401,7 @@ export default function Sidebar() {
)}
</div>
<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 ?? 'FleetOS'}
{brand?.displayName ?? 'RentalDriveGo'}
</span>
</a>