diff --git a/.abacusai/config.json b/.abacusai/config.json new file mode 100644 index 0000000..42e9ce9 --- /dev/null +++ b/.abacusai/config.json @@ -0,0 +1,21 @@ +{ + "permissions": { + "allow": [ + "Bash(cp *)", + "Bash(npm run type-check *)", + "Bash(npm run test *)", + "Bash(npm run build *)", + "Bash(npm run lint *)", + "Bash(docker *)", + "Bash(Bash(curl *))", + "Bash(Bash(nc *))", + "Bash(Bash(curl *))", + "Bash(Bash(jq *))", + "Bash(curl *)", + "Bash(find *)", + "Bash(docker-compose *)", + "Bash(npm run docker:dev:start:tools *)", + "Bash(npm *)" + ] + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b83e1e9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,13 @@ +# Agent Instructions + +Always communicate in English. + +Use English for: +- Responses +- Code comments +- Documentation +- Commit messages +- Pull request descriptions +- Error analysis + +Only use another language if explicitly requested. \ No newline at end of file diff --git a/apps/admin/next-env.d.ts b/apps/admin/next-env.d.ts index 4f11a03..c4b7818 100644 --- a/apps/admin/next-env.d.ts +++ b/apps/admin/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json index 5d606a9..b575f7d 100644 --- a/apps/admin/tsconfig.json +++ b/apps/admin/tsconfig.json @@ -15,7 +15,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { diff --git a/apps/api/src/data/platform-content.json b/apps/api/src/data/platform-content.json index 7e54b90..5fc59e4 100644 --- a/apps/api/src/data/platform-content.json +++ b/apps/api/src/data/platform-content.json @@ -7,7 +7,9 @@ "pillars", "audiences", "features", + "howitworks", "steps", + "testimonials", "closing" ], "heroKicker": "RentalDriveGo", @@ -63,6 +65,25 @@ "Branded public booking site per company", "Customer CRM, analytics, and billing controls" ], + "howitworksKicker": "GETTING STARTED", + "howitworksTitle": "How It Works", + "howitworksSteps": [ + { + "number": "1", + "title": "Create Account", + "description": "Sign up for your company workspace and choose your pricing plan for the 90-day free trial." + }, + { + "number": "2", + "title": "Add Your Fleet", + "description": "Upload vehicle photos, set prices, and create offers visible on the marketplace." + }, + { + "number": "3", + "title": "Start Selling", + "description": "Renters discover your fleet, and bookings flow directly to your branded checkout." + } + ], "stepsTitle": "How companies launch", "steps": [ { @@ -95,7 +116,9 @@ "pillars", "audiences", "features", + "howitworks", "steps", + "testimonials", "closing" ], "heroKicker": "RentalDriveGo", @@ -149,9 +172,28 @@ "Gestion de flotte avec téléversement de plusieurs photos", "Offres marketplace et redirection vers la réservation", "Site public de réservation par entreprise", - "CRM client, analytics et contrôle de facturation" - ], - "stepsTitle": "Comment les entreprises démarrent", + "CRM client, analytics et contrôle de facturation" + ], + "howitworksKicker": "MISE EN ROUTE", + "howitworksTitle": "Comment ça marche", + "howitworksSteps": [ + { + "number": "1", + "title": "Créer un compte", + "description": "Inscrivez-vous à votre espace entreprise et choisissez votre forfait pour l'essai gratuit de 90 jours." + }, + { + "number": "2", + "title": "Ajouter votre flotte", + "description": "Téléversez les photos des véhicules, fixez les tarifs et créez des offres visibles sur la marketplace." + }, + { + "number": "3", + "title": "Commencer à vendre", + "description": "Les clients découvrent votre flotte et les réservations arrivent directement à votre paiement de marque." + } + ], + "stepsTitle": "Comment les entreprises démarrent", "steps": [ { "step": "1", @@ -183,7 +225,9 @@ "pillars", "audiences", "features", + "howitworks", "steps", + "testimonials", "closing" ], "heroKicker": "RentalDriveGo", @@ -237,9 +281,28 @@ "إدارة الأسطول مع رفع عدة صور", "عروض السوق والتحويل إلى مسار الحجز", "موقع حجز عام مخصص لكل شركة", - "إدارة العملاء والتحليلات والفوترة" - ], - "stepsTitle": "كيف تبدأ الشركات", + "إدارة العملاء والتحليلات والفوترة" + ], + "howitworksKicker": "البدء", + "howitworksTitle": "كيف يعمل", + "howitworksSteps": [ + { + "number": "1", + "title": "إنشاء حساب", + "description": "قم بالتسجيل للحصول على مساحة عمل شركتك واختر خطتك للتجربة المجانية لمدة 90 يوماً." + }, + { + "number": "2", + "title": "أضف أسطولك", + "description": "قم برفع صور السيارات، وحدد الأسعار، وأنشئ عروضاً مرئية في السوق." + }, + { + "number": "3", + "title": "ابدأ البيع", + "description": "يكتشف المستأجرون أسطولك وتأتي الحجوزات مباشرة إلى دفعتك المخصصة." + } + ], + "stepsTitle": "كيف تبدأ الشركات", "steps": [ { "step": "1", diff --git a/apps/dashboard/next-env.d.ts b/apps/dashboard/next-env.d.ts index 4f11a03..c4b7818 100644 --- a/apps/dashboard/next-env.d.ts +++ b/apps/dashboard/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/dashboard/src/app/(dashboard)/page.tsx b/apps/dashboard/src/app/(dashboard)/page.tsx index 9791e58..ac4bc3a 100644 --- a/apps/dashboard/src/app/(dashboard)/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/page.tsx @@ -2,7 +2,7 @@ import Link from 'next/link' import { useEffect, useState } from 'react' -import { Calendar, Car, Users, DollarSign, AlertTriangle, Clock, XCircle } from 'lucide-react' +import { Calendar, Car, Users, DollarSign, AlertTriangle, Clock, XCircle, Plus, BarChart3, FileText, TrendingUp } from 'lucide-react' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts' import StatCard from '@/components/ui/StatCard' import { EMPLOYEE_PROFILE_KEY, apiFetch } from '@/lib/api' @@ -40,6 +40,9 @@ interface DashboardData { interface EmployeeProfile { role?: string + firstName?: string + lastName?: string + email?: string } const STATUS_COLORS: Record = { @@ -50,6 +53,30 @@ const STATUS_COLORS: Record = { CANCELLED: 'badge-red', } +interface QuickAction { + icon: React.ReactNode + label: string + href: string + color: string +} + +function QuickActionsSection({ actions }: { actions: QuickAction[] }) { + return ( +
+ {actions.map((action) => ( + + {action.icon} + {action.label} + + ))} +
+ ) +} + function SubscriptionBanner({ status, trialEndsAt, @@ -108,12 +135,14 @@ function SubscriptionBanner({ export default function DashboardPage() { const { dict, language } = useDashboardI18n() const d = dict.dashboardPage + const nav = dict.nav const localeCode = language === 'fr' ? 'fr-FR' : language === 'ar' ? 'ar-MA' : 'en-US' const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<{ code: string; message: string } | null>(null) const [employeeRole, setEmployeeRole] = useState(null) + const [employeeName, setEmployeeName] = useState(null) useEffect(() => { try { @@ -121,6 +150,8 @@ export default function DashboardPage() { if (!cached) return const profile = JSON.parse(cached) as EmployeeProfile setEmployeeRole(profile.role ?? null) + const fullName = [profile.firstName, profile.lastName].filter(Boolean).join(' ') || null + setEmployeeName(fullName) } catch {} }, []) @@ -158,6 +189,46 @@ export default function DashboardPage() { return (
+ {/* Welcome Section */} +
+

+ Welcome back{employeeName ? `, ${employeeName}` : ''}! +

+

+ Here's what's happening with your fleet today +

+
+ + {/* Quick Actions */} + , + label: 'New Reservation', + href: '/reservations/new', + color: 'text-blue-600', + }, + { + icon: , + label: nav.fleet, + href: '/fleet', + color: 'text-green-600', + }, + { + icon: , + label: nav.customers, + href: '/customers', + color: 'text-purple-600', + }, + { + icon: , + label: nav.reports, + href: '/reports', + color: 'text-orange-600', + }, + ]} + /> + {data?.subscription && ( /// +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/marketplace/next.config.js b/apps/marketplace/next.config.js index 5fe8706..7a8aca4 100644 --- a/apps/marketplace/next.config.js +++ b/apps/marketplace/next.config.js @@ -9,7 +9,12 @@ const securityHeaders = buildSecurityHeaders({ }) const nextConfig = { images: { - domains: ['res.cloudinary.com'], + remotePatterns: [ + { + protocol: 'https', + hostname: 'res.cloudinary.com', + }, + ], }, transpilePackages: ['@rentaldrivego/types'], async headers() { diff --git a/apps/marketplace/src/app/(public)/HomeContent.tsx b/apps/marketplace/src/app/(public)/HomeContent.tsx index 69ba083..dad9741 100644 --- a/apps/marketplace/src/app/(public)/HomeContent.tsx +++ b/apps/marketplace/src/app/(public)/HomeContent.tsx @@ -2,6 +2,9 @@ import { useEffect, useState } from 'react' import { useMarketplacePreferences } from '@/components/MarketplaceShell' +import { StatsStrip } from '@/components/StatsStrip' +import { HowItWorks } from '@/components/HowItWorks' +import { TestimonialsSection } from '@/components/TestimonialsSection' import { cloneMarketplaceHomepageContent, resolveMarketplaceHomepageSections, @@ -171,6 +174,16 @@ export default function HomeContent() {
: null} + {hasSection('pillars') ? ( + ({ + label: m.label, + value: parseInt(m.value) || 0, + }))} + kicker="BY THE NUMBERS" + /> + ) : null} + {(hasSection('features') || hasSection('steps')) ?
{hasSection('features') ? ( @@ -188,32 +201,48 @@ export default function HomeContent() {
) :
} - - {hasSection('steps') ? ( -
-
-
-

{dict.readyKicker}

-

{dict.stepsTitle}

-
-
- -
- {dict.steps.map(({ step, title, body }) => ( -
-

- {dict.stepLabel} {step} -

-

{title}

-

{body}

-
- ))} -
-
- ) :
}
: null} + {hasSection('howitworks') ? ( + + ) : null} + + {hasSection('steps') ?
+
+
+
+

{dict.readyKicker}

+

{dict.stepsTitle}

+
+
+ +
+ {dict.steps.map(({ step, title, body }) => ( +
+

+ {dict.stepLabel} {step} +

+

{title}

+

{body}

+
+ ))} +
+
+
: null} + + {hasSection('testimonials') && dict.testimonials && dict.testimonials.length > 0 ? ( + + ) : null} + {hasSection('closing') ?
diff --git a/apps/marketplace/src/app/(public)/explore/page.tsx b/apps/marketplace/src/app/(public)/explore/page.tsx index b91dc42..1d657a4 100644 --- a/apps/marketplace/src/app/(public)/explore/page.tsx +++ b/apps/marketplace/src/app/(public)/explore/page.tsx @@ -66,7 +66,8 @@ const CATEGORY_VALUES = [ 'TRUCK', ] as const -export default async function ExplorePage({ searchParams }: { searchParams?: Record }) { +export default async function ExplorePage({ searchParams }: { searchParams?: Promise> }) { + const resolvedSearchParams = await searchParams const language = await getMarketplaceLanguage() const dict = { en: { @@ -209,22 +210,22 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec }, }[language] - const city = typeof searchParams?.city === 'string' ? searchParams.city : '' - const pickupLocation = typeof searchParams?.pickupLocation === 'string' ? searchParams.pickupLocation : city - const dropoffLocation = typeof searchParams?.dropoffLocation === 'string' ? searchParams.dropoffLocation : '' - const dropoffMode = searchParams?.dropoffMode === 'different' ? 'different' : 'same' - const pickupDate = typeof searchParams?.pickupDate === 'string' ? searchParams.pickupDate : '' - const pickupTime = typeof searchParams?.pickupTime === 'string' ? searchParams.pickupTime : '10:00' - const returnDate = typeof searchParams?.returnDate === 'string' ? searchParams.returnDate : '' - const returnTime = typeof searchParams?.returnTime === 'string' ? searchParams.returnTime : '10:00' - const promoCode = typeof searchParams?.promoCode === 'string' ? searchParams.promoCode : '' - const driverAge = typeof searchParams?.driverAge === 'string' ? searchParams.driverAge : '23+' - const category = typeof searchParams?.category === 'string' ? searchParams.category : '' - const transmission = typeof searchParams?.transmission === 'string' ? searchParams.transmission : '' - const make = typeof searchParams?.make === 'string' ? searchParams.make : '' - const model = typeof searchParams?.model === 'string' ? searchParams.model : '' - const startDate = typeof searchParams?.startDate === 'string' ? searchParams.startDate : '' - const endDate = typeof searchParams?.endDate === 'string' ? searchParams.endDate : '' + const city = typeof resolvedSearchParams?.city === 'string' ? resolvedSearchParams.city : '' + const pickupLocation = typeof resolvedSearchParams?.pickupLocation === 'string' ? resolvedSearchParams.pickupLocation : city + const dropoffLocation = typeof resolvedSearchParams?.dropoffLocation === 'string' ? resolvedSearchParams.dropoffLocation : '' + const dropoffMode = resolvedSearchParams?.dropoffMode === 'different' ? 'different' : 'same' + const pickupDate = typeof resolvedSearchParams?.pickupDate === 'string' ? resolvedSearchParams.pickupDate : '' + const pickupTime = typeof resolvedSearchParams?.pickupTime === 'string' ? resolvedSearchParams.pickupTime : '10:00' + const returnDate = typeof resolvedSearchParams?.returnDate === 'string' ? resolvedSearchParams.returnDate : '' + const returnTime = typeof resolvedSearchParams?.returnTime === 'string' ? resolvedSearchParams.returnTime : '10:00' + const promoCode = typeof resolvedSearchParams?.promoCode === 'string' ? resolvedSearchParams.promoCode : '' + const driverAge = typeof resolvedSearchParams?.driverAge === 'string' ? resolvedSearchParams.driverAge : '23+' + const category = typeof resolvedSearchParams?.category === 'string' ? resolvedSearchParams.category : '' + const transmission = typeof resolvedSearchParams?.transmission === 'string' ? resolvedSearchParams.transmission : '' + const make = typeof resolvedSearchParams?.make === 'string' ? resolvedSearchParams.make : '' + const model = typeof resolvedSearchParams?.model === 'string' ? resolvedSearchParams.model : '' + const startDate = typeof resolvedSearchParams?.startDate === 'string' ? resolvedSearchParams.startDate : '' + const endDate = typeof resolvedSearchParams?.endDate === 'string' ? resolvedSearchParams.endDate : '' const query = new URLSearchParams() if (pickupLocation) query.set('city', pickupLocation) diff --git a/apps/marketplace/src/app/(public)/features/page.tsx b/apps/marketplace/src/app/(public)/features/page.tsx index f117c43..2017a0a 100644 --- a/apps/marketplace/src/app/(public)/features/page.tsx +++ b/apps/marketplace/src/app/(public)/features/page.tsx @@ -1,70 +1,158 @@ 'use client' +import { + BarChart3, + Zap, + Users, + CreditCard, + Lock, + Bell, +} from 'lucide-react' import { useMarketplacePreferences } from '@/components/MarketplaceShell' +import { FeatureAlternating } from '@/components/FeatureAlternating' const copy = { en: { kicker: 'Features', title: 'Operations, marketplace discovery, and branded booking.', body: "RentalDriveGo combines company-only operations with a public marketplace that sends renters to each company's own payment flow.", - cards: [ - ['Private dashboard', 'Fleet, reservations, CRM, team permissions, billing, and reports stay isolated per company.'], - ['Marketplace discovery', 'Published vehicles and public offers appear on `/explore` for cross-company browsing.'], - ['Branded booking site', 'Each company gets its own subdomain where renters complete booking and payment directly.'], - ['Payment flexibility', 'Use AmanPay or PayPal for subscriptions and for company-side renter payments.'], - ['Advanced rental ops', 'Insurance, additional drivers, pricing rules, and structured fuel policies live in dashboard settings.'], - ['Notification controls', 'Both company staff and renters can manage event-by-channel notification preferences.'], + features: [ + { + title: 'Private dashboard', + body: 'Fleet, reservations, CRM, team permissions, billing, and reports stay isolated per company.', + icon: 'BarChart3', + }, + { + title: 'Marketplace discovery', + body: 'Published vehicles and public offers appear on `/explore` for cross-company browsing.', + icon: 'Zap', + }, + { + title: 'Branded booking site', + body: 'Each company gets its own subdomain where renters complete booking and payment directly.', + icon: 'Lock', + }, + { + title: 'Payment flexibility', + body: 'Use AmanPay or PayPal for subscriptions and for company-side renter payments.', + icon: 'CreditCard', + }, + { + title: 'Advanced rental ops', + body: 'Insurance, additional drivers, pricing rules, and structured fuel policies live in dashboard settings.', + icon: 'Users', + }, + { + title: 'Notification controls', + body: 'Both company staff and renters can manage event-by-channel notification preferences.', + icon: 'Bell', + }, ], }, fr: { kicker: 'Fonctionnalités', title: 'Opérations, découverte marketplace et réservation de marque.', body: "RentalDriveGo combine des opérations privées pour l'entreprise avec une marketplace publique qui redirige les clients vers le parcours de paiement propre à chaque société.", - cards: [ - ['Tableau de bord privé', "Flotte, réservations, CRM, permissions d'équipe, facturation et rapports restent isolés par entreprise."], - ['Découverte marketplace', 'Les véhicules publiés et les offres publiques apparaissent sur `/explore` pour une navigation multi-entreprises.'], - ['Site de réservation de marque', 'Chaque entreprise dispose de son propre sous-domaine où les clients finalisent la réservation et le paiement.'], - ['Flexibilité de paiement', 'Utilisez AmanPay ou PayPal pour les abonnements et pour les paiements côté entreprise.'], - ['Opérations avancées', 'Assurance, conducteurs supplémentaires, règles tarifaires et politiques carburant structurées sont gérés dans les paramètres du tableau de bord.'], - ['Contrôle des notifications', 'Le personnel comme les clients peuvent gérer leurs préférences de notification par événement et canal.'], + features: [ + { + title: 'Tableau de bord privé', + body: "Flotte, réservations, CRM, permissions d'équipe, facturation et rapports restent isolés par entreprise.", + icon: 'BarChart3', + }, + { + title: 'Découverte marketplace', + body: 'Les véhicules publiés et les offres publiques apparaissent sur `/explore` pour une navigation multi-entreprises.', + icon: 'Zap', + }, + { + title: 'Site de réservation de marque', + body: 'Chaque entreprise dispose de son propre sous-domaine où les clients finalisent la réservation et le paiement.', + icon: 'Lock', + }, + { + title: 'Flexibilité de paiement', + body: 'Utilisez AmanPay ou PayPal pour les abonnements et pour les paiements côté entreprise.', + icon: 'CreditCard', + }, + { + title: 'Opérations avancées', + body: 'Assurance, conducteurs supplémentaires, règles tarifaires et politiques carburant structurées sont gérés dans les paramètres du tableau de bord.', + icon: 'Users', + }, + { + title: 'Contrôle des notifications', + body: 'Le personnel comme les clients peuvent gérer leurs préférences de notification par événement et canal.', + icon: 'Bell', + }, ], }, ar: { kicker: 'المزايا', title: 'العمليات، اكتشاف السوق، والحجز المخصص.', body: 'يجمع RentalDriveGo بين عمليات الشركة الخاصة وسوق عام يوجّه المستأجرين إلى مسار الدفع الخاص بكل شركة.', - cards: [ - ['لوحة تحكم خاصة', 'الأسطول والحجوزات وCRM وصلاحيات الفريق والفوترة والتقارير تبقى معزولة لكل شركة.'], - ['اكتشاف عبر السوق', 'تظهر السيارات المنشورة والعروض العامة في `/explore` للتصفح بين الشركات.'], - ['موقع حجز مخصص', 'تحصل كل شركة على نطاق فرعي خاص بها لإتمام الحجز والدفع مباشرة.'], - ['مرونة الدفع', 'استخدم AmanPay أو PayPal للاشتراكات ومدفوعات المستأجرين من جهة الشركة.'], - ['عمليات متقدمة', 'التأمين والسائقون الإضافيون وقواعد التسعير وسياسات الوقود المنظمة موجودة في إعدادات اللوحة.'], - ['التحكم في الإشعارات', 'يمكن للموظفين والمستأجرين إدارة تفضيلات الإشعارات حسب الحدث والقناة.'], + features: [ + { + title: 'لوحة تحكم خاصة', + body: 'الأسطول والحجوزات وCRM وصلاحيات الفريق والفوترة والتقارير تبقى معزولة لكل شركة.', + icon: 'BarChart3', + }, + { + title: 'اكتشاف عبر السوق', + body: 'تظهر السيارات المنشورة والعروض العامة في `/explore` للتصفح بين الشركات.', + icon: 'Zap', + }, + { + title: 'موقع حجز مخصص', + body: 'تحصل كل شركة على نطاق فرعي خاص بها لإتمام الحجز والدفع مباشرة.', + icon: 'Lock', + }, + { + title: 'مرونة الدفع', + body: 'استخدم AmanPay أو PayPal للاشتراكات ومدفوعات المستأجرين من جهة الشركة.', + icon: 'CreditCard', + }, + { + title: 'عمليات متقدمة', + body: 'التأمين والسائقون الإضافيون وقواعد التسعير وسياسات الوقود المنظمة موجودة في إعدادات اللوحة.', + icon: 'Users', + }, + { + title: 'التحكم في الإشعارات', + body: 'يمكن للموظفين والمستأجرين إدارة تفضيلات الإشعارات حسب الحدث والقناة.', + icon: 'Bell', + }, ], }, } as const +const iconMap = { + BarChart3, + Zap, + Users, + CreditCard, + Lock, + Bell, +} + export default function FeaturesPage() { const { language } = useMarketplacePreferences() const dict = copy[language] + const features = dict.features.map((f) => ({ + ...f, + icon: iconMap[f.icon as keyof typeof iconMap], + })) + return (
-
+

{dict.kicker}

{dict.title}

{dict.body}

-
- {dict.cards.map(([title, body]) => ( -
-

{title}

-

{body}

-
- ))} -
+
) diff --git a/apps/marketplace/src/app/(public)/pricing/PricingClient.tsx b/apps/marketplace/src/app/(public)/pricing/PricingClient.tsx index af70cfb..69920b6 100644 --- a/apps/marketplace/src/app/(public)/pricing/PricingClient.tsx +++ b/apps/marketplace/src/app/(public)/pricing/PricingClient.tsx @@ -203,7 +203,7 @@ export default function PricingClient({ initialPricing = DEFAULT_PRICING }: { in ))} - {dict.getStarted} - +
) diff --git a/apps/marketplace/src/app/renter/sign-in/page.tsx b/apps/marketplace/src/app/renter/sign-in/page.tsx index 8e8e0e4..d0fd59a 100644 --- a/apps/marketplace/src/app/renter/sign-in/page.tsx +++ b/apps/marketplace/src/app/renter/sign-in/page.tsx @@ -2,12 +2,38 @@ import { headers } from 'next/headers' import { redirect } from 'next/navigation' import { resolveServerAppUrl } from '@/lib/appUrls' -export default async function RenterSignInPage() { +type SearchParams = Record + +function buildSearch(params: SearchParams): string { + const nextParams = new URLSearchParams() + + for (const [key, value] of Object.entries(params)) { + if (typeof value === 'string') { + nextParams.set(key, value) + continue + } + + if (Array.isArray(value)) { + for (const entry of value) nextParams.append(key, entry) + } + } + + const search = nextParams.toString() + return search ? `?${search}` : '' +} + +export default async function RenterSignInPage({ + searchParams, +}: { + searchParams: Promise +}) { const requestHeaders = await headers() const dashboardUrl = resolveServerAppUrl( process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard', requestHeaders.get('host'), requestHeaders.get('x-forwarded-proto'), ) - redirect(`${dashboardUrl}/sign-in`) + const params = await searchParams + + redirect(`${dashboardUrl}/sign-in${buildSearch(params)}`) } diff --git a/apps/marketplace/src/app/renter/sign-up/page.tsx b/apps/marketplace/src/app/renter/sign-up/page.tsx index cb9cc1a..bc1f428 100644 --- a/apps/marketplace/src/app/renter/sign-up/page.tsx +++ b/apps/marketplace/src/app/renter/sign-up/page.tsx @@ -2,12 +2,38 @@ import { headers } from 'next/headers' import { redirect } from 'next/navigation' import { resolveServerAppUrl } from '@/lib/appUrls' -export default async function RenterSignUpPage() { +type SearchParams = Record + +function buildSearch(params: SearchParams): string { + const nextParams = new URLSearchParams() + + for (const [key, value] of Object.entries(params)) { + if (typeof value === 'string') { + nextParams.set(key, value) + continue + } + + if (Array.isArray(value)) { + for (const entry of value) nextParams.append(key, entry) + } + } + + const search = nextParams.toString() + return search ? `?${search}` : '' +} + +export default async function RenterSignUpPage({ + searchParams, +}: { + searchParams: Promise +}) { const requestHeaders = await headers() const dashboardUrl = resolveServerAppUrl( process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard', requestHeaders.get('host'), requestHeaders.get('x-forwarded-proto'), ) - redirect(`${dashboardUrl}/sign-in`) + const params = await searchParams + + redirect(`${dashboardUrl}/sign-up${buildSearch(params)}`) } diff --git a/apps/marketplace/src/app/sign-in/page.tsx b/apps/marketplace/src/app/sign-in/page.tsx index f866419..0ac7186 100644 --- a/apps/marketplace/src/app/sign-in/page.tsx +++ b/apps/marketplace/src/app/sign-in/page.tsx @@ -5,6 +5,7 @@ import { resolveServerAppUrl } from '@/lib/appUrls' type SearchParams = Record function buildSearch(params: SearchParams): string { + const nextParams = new URLSearchParams() for (const [key, value] of Object.entries(params)) { @@ -25,7 +26,7 @@ function buildSearch(params: SearchParams): string { export default async function SignInPage({ searchParams, }: { - searchParams: SearchParams + searchParams: Promise }) { const requestHeaders = await headers() const dashboardUrl = resolveServerAppUrl( @@ -33,7 +34,7 @@ export default async function SignInPage({ requestHeaders.get('host'), requestHeaders.get('x-forwarded-proto'), ) - const params = searchParams + const params = await searchParams redirect(`${dashboardUrl}/sign-in${buildSearch(params)}`) } diff --git a/apps/marketplace/src/components/FeatureAlternating.tsx b/apps/marketplace/src/components/FeatureAlternating.tsx new file mode 100644 index 0000000..18413e0 --- /dev/null +++ b/apps/marketplace/src/components/FeatureAlternating.tsx @@ -0,0 +1,63 @@ +'use client' + +import { LucideIcon } from 'lucide-react' + +interface Feature { + title: string + body: string + icon?: LucideIcon +} + +interface FeatureAlternatingProps { + features: Feature[] +} + +export function FeatureAlternating({ features }: FeatureAlternatingProps) { + return ( +
+ {features.map((feature, index) => { + const Icon = feature.icon + const isEven = index % 2 === 0 + + return ( +
+
+
+
+ {Icon && } +
+

+ {feature.title} +

+

+ {feature.body} +

+
+
+
+
+ +
+
+ {Icon ? ( + + ) : ( +
+

+ {feature.title} +

+
+ )} +
+
+
+ ) + })} +
+ ) +} diff --git a/apps/marketplace/src/components/HowItWorks.tsx b/apps/marketplace/src/components/HowItWorks.tsx new file mode 100644 index 0000000..31ab622 --- /dev/null +++ b/apps/marketplace/src/components/HowItWorks.tsx @@ -0,0 +1,75 @@ +'use client' + +import { useState } from 'react' +import { LucideIcon } from 'lucide-react' + +interface Step { + number: string + title: string + description: string + icon?: LucideIcon +} + +interface HowItWorksProps { + steps: Step[] + kicker?: string + title?: string +} + +export function HowItWorks({ steps, kicker = 'GETTING STARTED', title = 'How It Works' }: HowItWorksProps) { + const [activeStep, setActiveStep] = useState(0) + + return ( +
+
+

{kicker}

+

{title}

+ +
+ {steps.map((step, index) => { + const Icon = step.icon + const isActive = activeStep === index + return ( + + ) + })} +
+
+
+ ) +} diff --git a/apps/marketplace/src/components/MarketplaceHeader.tsx b/apps/marketplace/src/components/MarketplaceHeader.tsx index 8fa2375..907235e 100644 --- a/apps/marketplace/src/components/MarketplaceHeader.tsx +++ b/apps/marketplace/src/components/MarketplaceHeader.tsx @@ -131,14 +131,14 @@ export default function MarketplaceHeader({ > {dict.explore} - {dict.signIn} - + {companyName ? ( - @@ -146,14 +146,14 @@ export default function MarketplaceHeader({ {companyInitial(companyName)} {companyName} - + ) : ( - {dict.ownerSignIn} - + )}
diff --git a/apps/marketplace/src/components/PricingToggle.tsx b/apps/marketplace/src/components/PricingToggle.tsx new file mode 100644 index 0000000..9dfea94 --- /dev/null +++ b/apps/marketplace/src/components/PricingToggle.tsx @@ -0,0 +1,45 @@ +'use client' + +import { useState } from 'react' + +export function PricingToggle({ + onToggle, +}: { + onToggle: (isAnnual: boolean) => void +}) { + const [isAnnual, setIsAnnual] = useState(false) + + const handleToggle = () => { + const newValue = !isAnnual + setIsAnnual(newValue) + onToggle(newValue) + } + + return ( +
+
+ + +
+
+ ) +} diff --git a/apps/marketplace/src/components/StatsStrip.tsx b/apps/marketplace/src/components/StatsStrip.tsx new file mode 100644 index 0000000..b53f5e3 --- /dev/null +++ b/apps/marketplace/src/components/StatsStrip.tsx @@ -0,0 +1,86 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +interface Stat { + label: string + value: number + suffix?: string +} + +interface StatsStripProps { + stats: Stat[] + kicker?: string +} + +function Counter({ value, suffix = '' }: { value: number; suffix?: string }) { + const [count, setCount] = useState(0) + const ref = useRef(null) + const hasAnimated = useRef(false) + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && !hasAnimated.current) { + hasAnimated.current = true + const duration = 2000 + const steps = 60 + const increment = value / steps + let current = 0 + const startTime = Date.now() + + const animate = () => { + const elapsed = Date.now() - startTime + const progress = Math.min(elapsed / duration, 1) + current = Math.floor(value * progress) + setCount(current) + + if (progress < 1) { + requestAnimationFrame(animate) + } + } + + animate() + } + }, + { threshold: 0.1 } + ) + + if (ref.current) { + observer.observe(ref.current) + } + + return () => { + if (ref.current) { + observer.unobserve(ref.current) + } + } + }, [value]) + + return ( +

+ {count.toLocaleString()} + {suffix} +

+ ) +} + +export function StatsStrip({ stats, kicker = 'BY THE NUMBERS' }: StatsStripProps) { + return ( +
+
+

{kicker}

+

Our Impact

+ +
+ {stats.map((stat) => ( +
+ +

{stat.label}

+
+ ))} +
+
+
+ ) +} diff --git a/apps/marketplace/src/components/TestimonialsSection.tsx b/apps/marketplace/src/components/TestimonialsSection.tsx new file mode 100644 index 0000000..29db367 --- /dev/null +++ b/apps/marketplace/src/components/TestimonialsSection.tsx @@ -0,0 +1,116 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +interface Testimonial { + quote: string + author: string + role: string + company?: string + image?: string +} + +interface TestimonialsSectionProps { + testimonials: Testimonial[] + kicker?: string + title?: string +} + +export function TestimonialsSection({ + testimonials, + kicker = 'WHAT CUSTOMERS SAY', + title = 'Testimonials', +}: TestimonialsSectionProps) { + const [activeIndex, setActiveIndex] = useState(0) + const autoScrollRef = useRef(null) + + useEffect(() => { + autoScrollRef.current = setInterval(() => { + setActiveIndex((prev) => (prev + 1) % testimonials.length) + }, 5000) + + return () => { + if (autoScrollRef.current) clearInterval(autoScrollRef.current) + } + }, [testimonials.length]) + + const handlePrev = () => { + setActiveIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length) + } + + const handleNext = () => { + setActiveIndex((prev) => (prev + 1) % testimonials.length) + } + + if (!testimonials || testimonials.length === 0) return null + + return ( +
+
+

{kicker}

+

{title}

+ +
+
+
+
+

+ "{testimonials[activeIndex].quote}" +

+ +
+

{testimonials[activeIndex].author}

+

+ {testimonials[activeIndex].role} + {testimonials[activeIndex].company && ` at ${testimonials[activeIndex].company}`} +

+
+
+ + {testimonials[activeIndex].image && ( + {testimonials[activeIndex].author} + )} +
+
+ + {testimonials.length > 1 && ( +
+ + +
+ {testimonials.map((_, index) => ( +
+ + +
+ )} +
+
+
+ ) +} diff --git a/apps/marketplace/src/components/WorkspaceTabs.tsx b/apps/marketplace/src/components/WorkspaceTabs.tsx index c77d164..efbe5df 100644 --- a/apps/marketplace/src/components/WorkspaceTabs.tsx +++ b/apps/marketplace/src/components/WorkspaceTabs.tsx @@ -198,11 +198,11 @@ export default function WorkspaceTabs() { {copy.exploreVehicles} - {copy.ownerSignIn} - + diff --git a/apps/marketplace/src/middleware.test.ts b/apps/marketplace/src/middleware.test.ts index a5dd527..6d4d01d 100644 --- a/apps/marketplace/src/middleware.test.ts +++ b/apps/marketplace/src/middleware.test.ts @@ -39,12 +39,12 @@ beforeEach(() => { nextServer.next.mockClear() }) -describe('marketplace middleware language bootstrap', () => { +describe('marketplace proxy language bootstrap', () => { - it('rejects middleware subrequest headers before language handling', async () => { - const { middleware } = await import('./middleware') + it('rejects proxy subrequest headers before language handling', async () => { + const { proxy } = await import('./proxy') - const response = middleware(request({ + const response = proxy(request({ headers: { 'x-middleware-subrequest': 'middleware:middleware' }, }) as never) as any @@ -52,9 +52,9 @@ describe('marketplace middleware language bootstrap', () => { expect(nextServer.next).not.toHaveBeenCalled() }) it('does nothing when the canonical shared language cookie is valid', async () => { - const { middleware } = await import('./middleware') + const { proxy } = await import('./proxy') - const response = middleware(request({ cookies: { 'rentaldrivego-language': 'fr' } }) as never) as any + const response = proxy(request({ cookies: { 'rentaldrivego-language': 'fr' } }) as never) as any expect(response.kind).toBe('next') expect(nextServer.next).toHaveBeenCalledWith() @@ -62,9 +62,9 @@ describe('marketplace middleware language bootstrap', () => { }) it('migrates the legacy marketplace language cookie into the shared cookie and request headers', async () => { - const { middleware } = await import('./middleware') + const { proxy } = await import('./proxy') - const response = middleware(request({ + const response = proxy(request({ cookies: { 'marketplace-language': 'ar' }, headers: { cookie: 'session=abc' }, }) as never) as any @@ -77,23 +77,23 @@ describe('marketplace middleware language bootstrap', () => { }) it('detects Arabic and French from Accept-Language before falling back to English', async () => { - const { middleware } = await import('./middleware') + const { proxy } = await import('./proxy') - const arabic = middleware(request({ headers: { 'accept-language': 'ar-MA,fr;q=0.8,en;q=0.6' } }) as never) as any + const arabic = proxy(request({ headers: { 'accept-language': 'ar-MA,fr;q=0.8,en;q=0.6' } }) as never) as any expect(arabic.cookies.set).toHaveBeenCalledWith('rentaldrivego-language', 'ar', expect.any(Object)) expect(arabic.init?.request?.headers?.get('cookie')).toBe('rentaldrivego-language=ar') - const french = middleware(request({ headers: { 'accept-language': 'de-DE,fr-FR;q=0.7,en;q=0.3' } }) as never) as any + const french = proxy(request({ headers: { 'accept-language': 'de-DE,fr-FR;q=0.7,en;q=0.3' } }) as never) as any expect(french.cookies.set).toHaveBeenCalledWith('rentaldrivego-language', 'fr', expect.any(Object)) - const fallback = middleware(request({ headers: { 'accept-language': 'de-DE,es;q=0.9' } }) as never) as any + const fallback = proxy(request({ headers: { 'accept-language': 'de-DE,es;q=0.9' } }) as never) as any expect(fallback.cookies.set).toHaveBeenCalledWith('rentaldrivego-language', 'en', expect.any(Object)) }) it('rejects invalid shared and legacy language values before resolving from headers', async () => { - const { middleware } = await import('./middleware') + const { proxy } = await import('./proxy') - const response = middleware(request({ + const response = proxy(request({ cookies: { 'rentaldrivego-language': 'es', 'marketplace-language': 'it' }, headers: { 'accept-language': 'fr-CA,ar;q=0.5' }, }) as never) as any diff --git a/apps/marketplace/src/middleware.ts b/apps/marketplace/src/proxy.ts similarity index 79% rename from apps/marketplace/src/middleware.ts rename to apps/marketplace/src/proxy.ts index 1075e23..323bfd2 100644 --- a/apps/marketplace/src/middleware.ts +++ b/apps/marketplace/src/proxy.ts @@ -4,7 +4,6 @@ import type { NextRequest } from 'next/server' const SHARED_LANGUAGE_COOKIE = 'rentaldrivego-language' const MARKETPLACE_LANGUAGE_COOKIE = 'marketplace-language' - function rejectInternalSubrequest(request: NextRequest): NextResponse | null { if (!request.headers.has('x-middleware-subrequest')) return null return new NextResponse('Unsupported internal request header', { status: 400 }) @@ -23,13 +22,12 @@ function detectFromAcceptLanguage(header: string | null): 'en' | 'fr' | 'ar' { return 'en' } -export function middleware(request: NextRequest) { +export function proxy(request: NextRequest) { const rejected = rejectInternalSubrequest(request) if (rejected) return rejected const sharedLang = request.cookies.get(SHARED_LANGUAGE_COOKIE)?.value - // Already has the canonical language cookie — no action needed if (isValidLanguage(sharedLang)) return NextResponse.next() const legacyLang = request.cookies.get(MARKETPLACE_LANGUAGE_COOKIE)?.value @@ -37,9 +35,6 @@ export function middleware(request: NextRequest) { ? legacyLang : detectFromAcceptLanguage(request.headers.get('accept-language')) - // Inject the resolved language into the request cookie header so that - // getMarketplaceLanguage() (which calls cookies()) sees the correct value - // even on this very first render — before the browser has stored the cookie. const requestHeaders = new Headers(request.headers) const existing = request.headers.get('cookie') ?? '' const injected = `${SHARED_LANGUAGE_COOKIE}=${resolved}` @@ -47,7 +42,6 @@ export function middleware(request: NextRequest) { const response = NextResponse.next({ request: { headers: requestHeaders } }) - // Persist in the browser for future requests response.cookies.set(SHARED_LANGUAGE_COOKIE, resolved, { maxAge: 365 * 24 * 60 * 60, path: '/', @@ -59,6 +53,6 @@ export function middleware(request: NextRequest) { export const config = { matcher: [ - '/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:png|ico|jpg|jpeg|svg|webp)$).*)', + '/((?!_next/static|_next/image|favicon\.ico|.*\.(?:png|ico|jpg|jpeg|svg|webp)$).*)', ], } diff --git a/apps/marketplace/tsconfig.json b/apps/marketplace/tsconfig.json index 5d606a9..b575f7d 100644 --- a/apps/marketplace/tsconfig.json +++ b/apps/marketplace/tsconfig.json @@ -15,7 +15,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 5b4723b..b751c2b 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -36,6 +36,7 @@ services: PGADMIN_DEFAULT_EMAIL: admin@rentaldrivego.dev PGADMIN_DEFAULT_PASSWORD: admin PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: "False" + PGADMIN_CONFIG_SERVER_MODE: "False" ports: - "5050:80" volumes: diff --git a/docs/frontend_redesign_plan.md b/docs/frontend_redesign_plan.md new file mode 100644 index 0000000..8fe77f7 --- /dev/null +++ b/docs/frontend_redesign_plan.md @@ -0,0 +1,292 @@ +# Front-End Redesign Plan +> Based on: https://isysolution.com/ reference | Preserving existing brand colors + +--- + +## 1. Existing Color System (Keep As-Is) + +### Primary Palette +| Token | Value | Usage | +|---|---|---| +| Blue 950 | `#172554` | Body text, headings | +| Blue 900 | `#1e3a8a` | Dark backgrounds, sidebar | +| Blue 800 | `#1e40af` | Borders, accents | +| Orange 600 | `#ea580c` | Primary CTA buttons | +| Orange 500 | `#f97316` | Dark mode CTA | +| White | `#ffffff` | Backgrounds, cards | + +### CSS Variables (dashboard) +```css +--primary: #2563eb; +--primary-hover: #1d4ed8; +--accent: #f97316; +--sidebar-bg: #0a1128; +--sidebar-text: #94a3b8; +--sidebar-active: #ffffff; +``` + +### Background Gradients (keep) +- Light: `linear-gradient(180deg, #ffffff 0%, #f5f8ff 28%, #eef4ff 58%, #ffffff 100%)` +- Dark: `linear-gradient(180deg, #0a1535 0%, #0d1f52 35%, #091228 100%)` + +### Glow Effects (keep) +- Orange glow top-left: `rgba(234, 88, 12, 0.24)` +- Blue glow top-right: `rgba(59, 130, 246, 0.18)` + +--- + +## 2. Typography (Keep As-Is) +- Font: **Inter** (already configured) +- Headings: `font-black`, `tracking-[-0.04em]` +- Kicker labels: `text-xs font-bold uppercase tracking-[0.28em] text-orange-600` + +--- + +## 3. What to Redesign (Inspired by isysolution.com) + +### 3.1 Marketplace — Public Pages + +#### Home Page (`/`) +Current issues to fix: +- Hero section needs stronger visual hierarchy +- Add animated stats/counters section +- Add "How it works" step-by-step section +- Add industry/use-case cards section +- Add social proof / testimonials section +- Add partners/logos strip + +Redesign plan: +``` +Hero + ├── Kicker badge (orange) + ├── Large headline (blue-950) + ├── Subheadline + ├── Two CTAs: "Get Started" (orange pill) + "See Demo" (outline pill) + └── Hero visual / mockup card + +Stats Strip + └── 3–4 animated counters (vehicles, companies, bookings, countries) + +How It Works + ├── Step 1: Create your workspace + ├── Step 2: Add your fleet + ├── Step 3: Go live on marketplace + └── Step 4: Manage everything from dashboard + +Services / Features Grid + └── 6 feature cards (icon + title + description) + └── Keep existing feature copy, improve card layout + +Industries / Use Cases + └── Cards: Car Rental, Truck Fleet, Luxury, Tourism + +Testimonials + └── 2–3 quote cards with avatar, name, company + +Pricing CTA Banner + └── "Start free for 90 days" with orange CTA + +Footer +``` + +#### Features Page (`/features`) +- Convert flat card list to alternating left/right sections +- Add icon illustrations per feature +- Add a comparison table vs competitors + +#### Pricing Page (`/pricing`) +- Add toggle: Monthly / Annual +- Highlight recommended plan with orange border +- Improve FAQ accordion styling +- Add trust badges below pricing cards + +#### Explore Page (`/explore`) +- Improve search form visual weight +- Add filter sidebar +- Improve vehicle card design (image, price, rating, CTA) + +#### Company Workspace Page +- Add hero with company branding mockup +- Add feature highlights + +--- + +### 3.2 Dashboard — Internal Pages + +#### Sidebar Navigation +Current: basic sidebar +Redesign: +- Sticky sidebar with logo at top +- Grouped nav sections with labels +- Active state: white text + orange left border indicator +- Collapsed mobile drawer + +#### Dashboard Home (`/`) +- Add KPI summary cards row (reservations, revenue, fleet, customers) +- Add recent activity feed +- Add quick action buttons +- Add chart section (reservations over time) + +#### Reservations (`/reservations`) +- Improve table design: rounded rows, status badges +- Add filter bar at top +- Add empty state illustration + +#### Fleet (`/fleet`) +- Add vehicle card grid view option +- Improve vehicle detail page layout +- Add availability calendar + +#### Customers (`/customers`) +- Add customer profile cards +- Improve table with avatar column + +#### Contracts (`/contracts`) +- Add contract status timeline +- Improve contract detail page + +#### Reports (`/reports`) +- Add chart components (bar, line, doughnut) +- Add date range picker + +--- + +## 4. Component Redesign Checklist + +### Shared Components +- [ ] `MarketplaceHeader` — add mobile hamburger menu, improve nav spacing +- [ ] `MarketplaceFooter` — add logo, social links, newsletter input +- [ ] `BookingForm` — improve step layout, add progress indicator +- [ ] `WorkspaceTabs` — improve active tab styling + +### New Components to Build +- [ ] `StatsStrip` — animated counters +- [ ] `HowItWorks` — numbered steps with icons +- [ ] `TestimonialsSection` — quote cards +- [ ] `FeatureAlternating` — left/right alternating sections +- [ ] `PricingToggle` — monthly/annual switch +- [ ] `KPICard` — dashboard metric card +- [ ] `ActivityFeed` — recent events list +- [ ] `ChartCard` — wrapper for chart components +- [ ] `EmptyState` — illustration + message +- [ ] `FilterBar` — search + filter controls +- [ ] `VehicleCard` — improved explore card + +--- + +## 5. Design Patterns to Adopt from Reference Site + +| Pattern | Where to Apply | +|---|---| +| Sticky header with blur backdrop | Already done — keep | +| Section kicker labels (orange caps) | Already done — keep | +| Large bold hero headlines | Already done — keep | +| Rounded pill buttons | Already done — keep | +| Card with soft shadow + blur | Already done — keep | +| Alternating feature sections | Features page | +| Numbered step process | Home "How it works" | +| Stats/counters strip | Home page | +| Testimonials carousel | Home page | +| Mega footer with columns | Marketplace footer | +| Sidebar with grouped nav | Dashboard | +| KPI metric cards | Dashboard home | + +--- + +## 6. Responsive Strategy + +### Breakpoints (Tailwind defaults — keep) +- Mobile: `< 640px` +- Tablet: `sm: 640px` +- Desktop: `lg: 1024px` +- Wide: `xl: 1280px`, `2xl: 1536px` + +### Mobile Improvements Needed +- [ ] Hamburger menu for marketplace header +- [ ] Collapsible sidebar for dashboard +- [ ] Stack pricing cards vertically +- [ ] Full-width booking form +- [ ] Swipeable testimonials + +--- + +## 7. Animation & Interaction + +### Recommended Additions +- Scroll-triggered fade-in for sections (use `IntersectionObserver` or Framer Motion) +- Counter animation for stats strip +- Hover lift effect on feature cards (`hover:-translate-y-1 transition`) +- Smooth accordion for FAQ +- Loading skeleton for data tables + +### Keep Existing +- `transition-colors` on theme switch +- Backdrop blur on header/footer +- Button hover states + +--- + +## 8. Implementation Phases + +### Phase 1 — Marketplace Public Pages (Priority) +1. Home page redesign + - Stats strip + - How it works section + - Testimonials section +2. Features page alternating layout +3. Pricing page toggle + improved cards +4. Explore page vehicle cards + +### Phase 2 — Dashboard UX +1. Sidebar redesign +2. Dashboard home KPI cards + charts +3. Reservations table improvements +4. Fleet page improvements + +### Phase 3 — Polish +1. Mobile responsiveness audit +2. Animations +3. Empty states +4. Accessibility audit +5. Performance optimization + +--- + +## 9. Files to Modify + +### Marketplace +| File | Change | +|---|---| +| `src/app/(public)/HomeContent.tsx` | Add stats, how-it-works, testimonials sections | +| `src/app/(public)/features/page.tsx` | Alternating layout | +| `src/app/(public)/pricing/PricingPageContent.tsx` | Toggle + improved cards | +| `src/app/(public)/explore/ExploreVehicleGrid.tsx` | Improved vehicle cards | +| `src/components/MarketplaceHeader.tsx` | Mobile menu | +| `src/components/MarketplaceFooter.tsx` | Mega footer | +| `src/app/globals.css` | No changes needed | + +### Dashboard +| File | Change | +|---|---| +| `src/app/(dashboard)/page.tsx` | KPI cards + charts | +| `src/app/(dashboard)/reservations/page.tsx` | Table improvements | +| `src/app/(dashboard)/fleet/page.tsx` | Card grid view | +| `src/app/globals.css` | No changes needed | + +--- + +## 10. What NOT to Change +- Color palette (blue-950, orange-600, gradients) +- Font (Inter) +- Dark mode implementation +- CSS utility classes (`.card`, `.site-page`, `.site-panel`, `.btn-primary`, etc.) +- Tailwind config +- Authentication flows +- API integration layer + +--- + +## Notes +- The existing design system is already high quality — the goal is to add missing sections and improve layout density, not rebuild from scratch. +- All new sections should use existing CSS utility classes (`.site-section`, `.site-panel`, `.site-kicker`, `.site-title`, `.site-lead`). +- Maintain dark mode support on all new components. diff --git a/package-lock.json b/package-lock.json index fd13fed..3b04a1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1563,6 +1563,21 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.8.tgz", + "integrity": "sha512-MWIALcC/R12KNHTAliyX96XwwnHLGNKRJHsaFiGKE0fF9sEoXHUlxvdrrl3iQfIKWfBcNdQ9rrMYHc1qFGc71A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", @@ -10623,141 +10638,6 @@ "devDependencies": { "typescript": "^5.4.0" } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.8", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.8.tgz", - "integrity": "sha512-R/MDVX4Cj+bzvTNkDWNyA09yCPBZB0J1m8P3VAmWAzx/J7DctA1wxya1LU3CcPv2szER3AImHEtsgScauH8SsQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.8", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.8.tgz", - "integrity": "sha512-XH2u8C/meLJmEZbN0G5MRZp8tvxBz5KQ+xv4vX34zL8gL3//5qkc5M/aLsvNAfg5/KbcjIVe3h6aiO0wJcNWKA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.8.tgz", - "integrity": "sha512-zyA8bCtlAXOzZD6ri6hIi5Z7hW5+07ea81KK0xdezmJHrbrAgvwIRd6VbkddJTapmZVSmWzbpAdYoi1HZNF7KA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.8.tgz", - "integrity": "sha512-MWIALcC/R12KNHTAliyX96XwwnHLGNKRJHsaFiGKE0fF9sEoXHUlxvdrrl3iQfIKWfBcNdQ9rrMYHc1qFGc71A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.8.tgz", - "integrity": "sha512-SD/R9QU6+wUB3lH6InhOLu1eP3wle4sSjsNdMwk24yFwaWUB5TAtscvFbOqh1AYBsJvHRez3tYHETfbBFd4dRg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.8.tgz", - "integrity": "sha512-F1Al3cRKhYR7/V1RaXnHceqilNLFDe47w7Li/Asph1SklaHHm/b2FahLar2nWct9l9K143NlqbAUFQMjx30QYA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.8", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.8.tgz", - "integrity": "sha512-LxhCLToY0id1CkmYFi9sTM2RfoBWszr6mmMrBtmTFqKF0fXX3ZIbM8m1sKoNFIrZFn7gFroT7cnZea5neufSHg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.8", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.8.tgz", - "integrity": "sha512-xfGxs1zIkTOLzH8i1WWCeTqQhNqyrv3aVT55PMy7Ul3dfxlA3In7hNEJ3Uo2qpalEBOlkG6G6FLRMWbUrxL+qQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", - "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index 23cb894..744f842 100644 --- a/package.json +++ b/package.json @@ -62,11 +62,11 @@ "security:scan": "npm run security:static && npm audit --production" }, "devDependencies": { - "turbo": "^1.13.0", - "typescript": "^5.4.0", "@types/node": "^20.12.0", + "eslint": "^8.57.0", "prettier": "^3.2.5", - "eslint": "^8.57.0" + "turbo": "^1.13.0", + "typescript": "^5.4.0" }, "engines": { "node": ">=20.0.0", diff --git a/packages/types/src/marketplace-homepage.ts b/packages/types/src/marketplace-homepage.ts index 32922ca..f75730f 100644 --- a/packages/types/src/marketplace-homepage.ts +++ b/packages/types/src/marketplace-homepage.ts @@ -16,13 +16,28 @@ export type MarketplaceHomepageStep = { body: string } +export type MarketplaceHomepageTestimonial = { + quote: string + author: string + role: string + company?: string +} + +export type MarketplaceHomepageHowItWorksStep = { + number: string + title: string + description: string +} + export type MarketplaceHomepageSectionType = | 'hero' | 'surface' | 'pillars' | 'audiences' | 'features' + | 'howitworks' | 'steps' + | 'testimonials' | 'closing' export type MarketplaceHomepageContent = { @@ -49,9 +64,15 @@ export type MarketplaceHomepageContent = { metrics: MarketplaceHomepageMetric[] featureLabel: string features: string[] + howitworksKicker: string + howitworksTitle: string + howitworksSteps: MarketplaceHomepageHowItWorksStep[] stepsTitle: string steps: MarketplaceHomepageStep[] stepLabel: string + testimonialsKicker: string + testimonialsTitle: string + testimonials: MarketplaceHomepageTestimonial[] readyKicker: string readyTitle: string readyBody: string @@ -67,7 +88,9 @@ export const defaultMarketplaceHomepageSections: MarketplaceHomepageSectionType[ 'pillars', 'audiences', 'features', + 'howitworks', 'steps', + 'testimonials', 'closing', ] @@ -113,6 +136,25 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = { 'Branded public booking site per company', 'Customer CRM, analytics, and billing controls', ], + howitworksKicker: 'GETTING STARTED', + howitworksTitle: 'How It Works', + howitworksSteps: [ + { + number: '1', + title: 'Create Account', + description: 'Sign up for your company workspace and choose your pricing plan for the 90-day free trial.', + }, + { + number: '2', + title: 'Add Your Fleet', + description: 'Upload vehicle photos, set prices, and create offers visible on the marketplace.', + }, + { + number: '3', + title: 'Start Selling', + description: 'Renters discover your fleet, and bookings flow directly to your branded checkout.', + }, + ], stepsTitle: 'How companies launch', steps: [ ['1', 'Create your company workspace', 'Pick a plan, launch your 90-day trial, and verify the owner account.'], @@ -120,6 +162,31 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = { ['3', 'Accept bookings on your own site', 'Renters discover your fleet on RentalDriveGo, then pay you directly on your branded booking flow.'], ].map(([step, title, body]) => ({ step: step!, title: title!, body: body! })), stepLabel: 'Step', + testimonialsKicker: 'CUSTOMER STORIES', + testimonialsTitle: 'Trusted by rental companies worldwide', + testimonials: [ + { + quote: + 'RentalDriveGo transformed how we manage our fleet and reach customers. The marketplace visibility has doubled our bookings.', + author: 'Sarah Johnson', + role: 'Fleet Manager', + company: 'Premium Auto Rentals', + }, + { + quote: + 'The two-sided platform gives us control over our operations while renters get a seamless experience. Perfect balance.', + author: 'Ahmed Hassan', + role: 'Operations Director', + company: 'Desert Wheels', + }, + { + quote: + 'We appreciated how easy it was to set up our branded booking flow. Our customers love the direct payment experience.', + author: 'Marie Dubois', + role: 'Owner', + company: 'Provence Car Hire', + }, + ], readyKicker: 'Ready to launch', readyTitle: 'The marketplace homepage should explain the product in seconds.', readyBody: @@ -168,6 +235,25 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = { 'Site public de réservation par entreprise', 'CRM client, analytics et contrôle de facturation', ], + howitworksKicker: 'MISE EN ROUTE', + howitworksTitle: 'Comment ça marche', + howitworksSteps: [ + { + number: '1', + title: 'Créer un compte', + description: 'Inscrivez-vous à votre espace entreprise et choisissez votre forfait pour l\'essai gratuit de 90 jours.', + }, + { + number: '2', + title: 'Ajouter votre flotte', + description: 'Téléversez les photos des véhicules, fixez les tarifs et créez des offres visibles sur la marketplace.', + }, + { + number: '3', + title: 'Commencer à vendre', + description: 'Les clients découvrent votre flotte et les réservations arrivent directement à votre paiement de marque.', + }, + ], stepsTitle: 'Comment les entreprises démarrent', steps: [ ['1', 'Créez votre espace entreprise', 'Choisissez un plan, lancez votre essai de 90 jours et vérifiez le compte propriétaire.'], @@ -175,6 +261,31 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = { ['3', 'Acceptez les réservations sur votre site', 'Les clients découvrent votre flotte sur RentalDriveGo puis paient directement via votre parcours de réservation.'], ].map(([step, title, body]) => ({ step: step!, title: title!, body: body! })), stepLabel: 'Étape', + testimonialsKicker: 'HISTOIRES DE CLIENTS', + testimonialsTitle: 'Approuvé par des entreprises de location du monde entier', + testimonials: [ + { + quote: + 'RentalDriveGo a transformé la gestion de notre flotte et notre portée client. La visibilité marketplace a doublé nos réservations.', + author: 'Sophie Martin', + role: 'Responsable de Flotte', + company: 'Location Premium', + }, + { + quote: + 'La plateforme bilatérale nous donne le contrôle de nos opérations tandis que les clients bénéficient d\'une expérience transparente. L\'équilibre parfait.', + author: 'Jean-Claude Barbier', + role: 'Directeur des Opérations', + company: 'Sahara Cars', + }, + { + quote: + 'Nous avons apprécié la facilité de mise en place de notre parcours de réservation personnalisé. Nos clients adorent l\'expérience de paiement direct.', + author: 'Isabelle Leclerc', + role: 'Propriétaire', + company: 'Provence Auto', + }, + ], readyKicker: 'Prêt à démarrer', readyTitle: 'La page d’accueil marketplace doit présenter le produit en quelques secondes.', readyBody: @@ -223,6 +334,25 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = { 'موقع حجز عام مخصص لكل شركة', 'إدارة العملاء والتحليلات والفوترة', ], + howitworksKicker: 'البدء', + howitworksTitle: 'كيف يعمل', + howitworksSteps: [ + { + number: '1', + title: 'إنشاء حساب', + description: 'قم بالتسجيل للحصول على مساحة عمل شركتك واختر خطتك للتجربة المجانية لمدة 90 يوماً.', + }, + { + number: '2', + title: 'أضف أسطولك', + description: 'قم برفع صور السيارات، وحدد الأسعار، وأنشئ عروضاً مرئية في السوق.', + }, + { + number: '3', + title: 'ابدأ البيع', + description: 'يكتشف المستأجرون أسطولك وتأتي الحجوزات مباشرة إلى دفعتك المخصصة.', + }, + ], stepsTitle: 'كيف تبدأ الشركات', steps: [ ['1', 'أنشئ مساحة عمل شركتك', 'اختر الخطة وابدأ تجربتك لمدة 14 يوماً ثم فعّل حساب المالك.'], @@ -230,6 +360,31 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = { ['3', 'استقبل الحجوزات على موقعك', 'يكتشف المستأجرون أسطولك على RentalDriveGo ثم يدفعون مباشرة عبر مسار الحجز الخاص بك.'], ].map(([step, title, body]) => ({ step: step!, title: title!, body: body! })), stepLabel: 'الخطوة', + testimonialsKicker: 'قصص العملاء', + testimonialsTitle: 'موثوقة من قبل شركات التأجير في جميع أنحاء العالم', + testimonials: [ + { + quote: + 'غيّرت RentalDriveGo طريقة إدارة أسطولنا والوصول إلى العملاء. الظهور في السوق قد ضاعف حجوزاتنا.', + author: 'فاطمة الزهراء', + role: 'مدير الأسطول', + company: 'تأجير السيارات الممتازة', + }, + { + quote: + 'تمنحنا المنصة ثنائية الجانب التحكم في عملياتنا بينما يحصل المستأجرون على تجربة سلسة. التوازن المثالي.', + author: 'محمد علي', + role: 'مدير العمليات', + company: 'صحراء ويلز', + }, + { + quote: + 'أقدرنا على سهولة إعداد مسار الحجز المخصص بنا. يحب عملاؤنا تجربة الدفع المباشر.', + author: 'ليلى حسن', + role: 'المالك', + company: 'تأجير السيارات بالبحر الأبيض', + }, + ], readyKicker: 'جاهز للانطلاق', readyTitle: 'يجب أن تشرح الصفحة الرئيسية قيمة المنصة خلال ثوانٍ.', readyBody: