fix signin signout and apply the new style

This commit is contained in:
root
2026-05-24 23:58:54 -04:00
parent bf97a072dd
commit 9bd0938951
68 changed files with 851 additions and 200 deletions
@@ -1,5 +1,5 @@
import { redirect } from 'next/navigation'
export default function CompanyWorkspacePage() {
redirect('/sign-in')
redirect('/')
}
@@ -8,6 +8,9 @@ type ExploreSearchFormDict = {
searchTitle: string
pickupLocation: string
dropoffLocation: string
sameDropoff: string
differentDropoff: string
sameAsPickup: string
pickupDate: string
returnDate: string
hour: string
@@ -29,6 +32,7 @@ const selectBase = 'w-full border-0 bg-white dark:bg-[#162038] py-4 text-sm text
export default function ExploreSearchForm({
pickupLocation,
dropoffLocation,
dropoffMode,
pickupDate,
pickupTime,
returnDate,
@@ -40,6 +44,7 @@ export default function ExploreSearchForm({
}: {
pickupLocation: string
dropoffLocation: string
dropoffMode: 'same' | 'different'
pickupDate: string
pickupTime: string
returnDate: string
@@ -55,6 +60,7 @@ export default function ExploreSearchForm({
const [filters, setFilters] = useState({
pickupLocation,
dropoffLocation,
dropoffMode,
pickupDate,
pickupTime: pickupTime || '10:00',
returnDate,
@@ -73,14 +79,19 @@ export default function ExploreSearchForm({
const query = new URLSearchParams()
const normalizedPickup = filters.pickupLocation.trim()
const normalizedDropoff = filters.dropoffLocation.trim()
const normalizedDropoff = filters.dropoffMode === 'different'
? filters.dropoffLocation.trim()
: normalizedPickup
const normalizedPromo = filters.promoCode.trim()
if (normalizedPickup) {
query.set('pickupLocation', normalizedPickup)
query.set('city', normalizedPickup)
}
if (normalizedDropoff) query.set('dropoffLocation', normalizedDropoff)
query.set('dropoffMode', filters.dropoffMode)
if (filters.dropoffMode === 'different' && normalizedDropoff) {
query.set('dropoffLocation', normalizedDropoff)
}
if (filters.pickupDate) query.set('pickupDate', filters.pickupDate)
if (filters.pickupTime) query.set('pickupTime', filters.pickupTime)
if (filters.returnDate) query.set('returnDate', filters.returnDate)
@@ -116,20 +127,56 @@ export default function ExploreSearchForm({
))}
</select>
</label>
<label className="flex items-center gap-3 bg-white dark:bg-[#162038] px-4">
<MapPin className="h-5 w-5 text-stone-300 dark:text-stone-500" />
<select
name="dropoffLocation"
value={filters.dropoffLocation}
onChange={(event) => updateFilter('dropoffLocation', event.target.value)}
className={selectBase}
>
<option value="">{dict.dropoffLocation}</option>
{cityOptions.map((city) => (
<option key={`dropoff-${city}`} value={city}>{city}</option>
))}
</select>
</label>
<div className="bg-white dark:bg-[#162038] px-4 py-3">
<div className="flex items-center justify-between gap-3">
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-stone-400 dark:text-stone-500">{dict.dropoffLocation}</span>
<div className="inline-flex rounded-full border border-stone-200 dark:border-blue-800 bg-stone-100 dark:bg-[#0d1b38] p-1">
<button
type="button"
onClick={() => setFilters((current) => ({ ...current, dropoffMode: 'same', dropoffLocation: '' }))}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
filters.dropoffMode === 'same'
? 'bg-stone-900 text-white dark:bg-orange-400 dark:text-stone-900'
: 'text-stone-500 dark:text-stone-400'
}`}
>
{dict.sameDropoff}
</button>
<button
type="button"
onClick={() => setFilters((current) => ({ ...current, dropoffMode: 'different' }))}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
filters.dropoffMode === 'different'
? 'bg-stone-900 text-white dark:bg-orange-400 dark:text-stone-900'
: 'text-stone-500 dark:text-stone-400'
}`}
>
{dict.differentDropoff}
</button>
</div>
</div>
{filters.dropoffMode === 'different' ? (
<label className="mt-3 flex items-center gap-3">
<MapPin className="h-5 w-5 text-stone-300 dark:text-stone-500" />
<select
name="dropoffLocation"
value={filters.dropoffLocation}
onChange={(event) => updateFilter('dropoffLocation', event.target.value)}
className={selectBase}
>
<option value="">{dict.dropoffLocation}</option>
{cityOptions.map((city) => (
<option key={`dropoff-${city}`} value={city}>{city}</option>
))}
</select>
</label>
) : (
<div className="mt-3 flex items-center gap-3 rounded-2xl bg-stone-50 px-4 py-4 text-sm font-medium text-stone-500 dark:bg-[#0d1b38] dark:text-stone-300">
<MapPin className="h-5 w-5 text-stone-300 dark:text-stone-500" />
<span>{filters.pickupLocation || dict.sameAsPickup}</span>
</div>
)}
</div>
</div>
<div className="grid gap-px overflow-hidden rounded-[1.35rem] border border-stone-200 dark:border-blue-800 bg-stone-200 dark:bg-stone-700 md:grid-cols-4">
@@ -16,6 +16,9 @@ interface VehicleDetail {
features: string[]
dailyRate: number
photos: string[]
pickupLocations: string[]
allowDifferentDropoff: boolean
dropoffLocations: string[]
availability: boolean
availabilityStatus: 'AVAILABLE' | 'RESERVED' | 'MAINTENANCE' | 'UNAVAILABLE'
nextAvailableAt: string | null
@@ -259,6 +262,9 @@ export default async function VehicleDetailPage({ params }: { params: { slug: st
vehicleId={vehicle.id}
companySlug={params.slug}
dailyRate={vehicle.dailyRate}
pickupLocations={vehicle.pickupLocations}
allowDifferentDropoff={vehicle.allowDifferentDropoff}
dropoffLocations={vehicle.dropoffLocations}
/>
)}
@@ -12,6 +12,9 @@ interface Vehicle {
category: string
dailyRate: number
photos: string[]
pickupLocations: string[]
allowDifferentDropoff: boolean
dropoffLocations: string[]
availability?: boolean | null
availabilityStatus?: 'AVAILABLE' | 'RESERVED' | 'MAINTENANCE' | 'UNAVAILABLE'
nextAvailableAt?: string | null
@@ -73,6 +76,9 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
searchTitle: 'Find a vehicle',
pickupLocation: 'Pick-up location',
dropoffLocation: 'Drop-off location',
sameDropoff: 'Same drop-off',
differentDropoff: 'Different drop-off',
sameAsPickup: 'Return to the same location',
pickupDate: 'Start date',
returnDate: 'Return date',
hour: 'Hour',
@@ -116,6 +122,9 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
searchTitle: 'Trouver un véhicule',
pickupLocation: 'Lieu de départ',
dropoffLocation: "Lieu de retour",
sameDropoff: 'Même retour',
differentDropoff: 'Retour différent',
sameAsPickup: 'Retour au même lieu',
pickupDate: 'Date de départ',
returnDate: 'Date de retour',
hour: 'Heure',
@@ -159,6 +168,9 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
searchTitle: 'احجز سيارة',
pickupLocation: 'موقع الاستلام',
dropoffLocation: 'موقع الإرجاع',
sameDropoff: 'نفس موقع الإرجاع',
differentDropoff: 'موقع إرجاع مختلف',
sameAsPickup: 'الإرجاع في نفس موقع الاستلام',
pickupDate: 'تاريخ البدء',
returnDate: 'تاريخ الإرجاع',
hour: 'الساعة',
@@ -200,6 +212,7 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
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 : ''
@@ -215,6 +228,9 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
const query = new URLSearchParams()
if (pickupLocation) query.set('city', pickupLocation)
if (pickupLocation) query.set('pickupLocation', pickupLocation)
query.set('dropoffMode', dropoffMode)
if (dropoffMode === 'different' && dropoffLocation) query.set('dropoffLocation', dropoffLocation)
if (startDate) query.set('startDate', startDate)
if (endDate) query.set('endDate', endDate)
if (category) query.set('category', category)
@@ -246,6 +262,7 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
<ExploreSearchForm
pickupLocation={pickupLocation}
dropoffLocation={dropoffLocation}
dropoffMode={dropoffMode}
pickupDate={pickupDate}
pickupTime={pickupTime}
returnDate={returnDate}
@@ -1,5 +1,5 @@
import { redirect } from 'next/navigation'
export default function PlatformOperationsPage() {
redirect('/sign-in')
redirect('/')
}
@@ -82,7 +82,7 @@ const copy = {
perMonth: '/mo',
billedAs: 'Billed as',
getStarted: 'Get started',
footer: 'All plans include a 14-day free trial. No credit card required.',
footer: 'All plans include a 90-day free trial. No credit card required.',
},
fr: {
monthly: 'Mensuel',
@@ -94,7 +94,7 @@ const copy = {
perMonth: '/mois',
billedAs: 'Facturé à',
getStarted: 'Commencer',
footer: "Toutes les formules incluent un essai gratuit de 14 jours. Aucune carte bancaire n'est requise.",
footer: "Toutes les formules incluent un essai gratuit de 90 jours. Aucune carte bancaire n'est requise.",
},
ar: {
monthly: 'شهري',
@@ -7,7 +7,7 @@ const copy = {
en: {
kicker: 'Pricing',
title: 'One platform, every fleet size.',
body: 'Start free for 14 days, then choose the plan that grows with your business. Switch plans or cancel at any time.',
body: 'Start free for 90 days, then choose the plan that grows with your business. Switch plans or cancel at any time.',
faq: 'Frequently asked',
items: [
['Can I change plans later?', 'Yes. Upgrade or downgrade at any time — changes take effect on your next billing cycle.'],
@@ -19,7 +19,7 @@ const copy = {
fr: {
kicker: 'Tarifs',
title: 'Une seule plateforme pour toutes les tailles de flotte.',
body: 'Commencez gratuitement pendant 14 jours, puis choisissez la formule qui accompagne votre activité. Vous pouvez changer de formule ou annuler à tout moment.',
body: 'Commencez gratuitement pendant 90 jours, puis choisissez la formule qui accompagne votre activité. Vous pouvez changer de formule ou annuler à tout moment.',
faq: 'Questions fréquentes',
items: [
['Puis-je changer de formule plus tard ?', 'Oui. Vous pouvez passer à une formule supérieure ou inférieure à tout moment. Les changements prennent effet au cycle de facturation suivant.'],
@@ -115,7 +115,7 @@ export default function RenterDashboardPage() {
useEffect(() => {
const token = localStorage.getItem('renter_token')
if (!token) {
router.replace('/renter/sign-in')
router.replace('/')
return
}
@@ -127,7 +127,7 @@ export default function RenterDashboardPage() {
if (!res.ok) {
if (res.status === 401) {
localStorage.removeItem('renter_token')
router.replace('/renter/sign-in')
router.replace('/')
} else {
setErrorMsg(json?.message ?? dict.loadProfile)
setStatus('error')
@@ -65,7 +65,7 @@ export default function RenterNotificationsPage() {
.then((items) => setNotifications(items))
.catch((err) => {
if (err instanceof RenterAuthError) {
router.replace('/renter/sign-in')
router.replace('/')
return
}
setErrorMsg(err instanceof Error ? err.message : dict.load)
@@ -126,7 +126,7 @@ export default function RenterProfilePage() {
})
.catch((err) => {
if (err instanceof RenterAuthError) {
router.replace('/renter/sign-in')
router.replace('/')
return
}
setErrorMsg(err instanceof Error ? err.message : dict.loadFailed)
@@ -50,7 +50,7 @@ export default function RenterSavedCompaniesPage() {
.then((profile) => setCompanies(profile.savedCompanies ?? []))
.catch((err) => {
if (err instanceof RenterAuthError) {
router.replace('/renter/sign-in')
router.replace('/')
return
}
setErrorMsg(err instanceof Error ? err.message : dict.load)
+101 -1
View File
@@ -8,6 +8,9 @@ type Props = {
vehicleId: string
companySlug: string
dailyRate: number
pickupLocations: string[]
allowDifferentDropoff: boolean
dropoffLocations: string[]
}
const copy = {
@@ -17,6 +20,11 @@ const copy = {
lastName: 'Last name',
email: 'Email',
phone: 'Phone',
pickupLocation: 'Pick-up location',
returnLocation: 'Drop-off location',
sameDropoff: 'Same drop-off',
differentDropoff: 'Different drop-off',
sameAsPickup: 'Return to the same location',
startDate: 'Pick-up date',
endDate: 'Return date',
notes: 'Notes (optional)',
@@ -30,6 +38,7 @@ const copy = {
estimatedTotal: 'Estimated total',
days: 'day(s)',
genericError: 'Something went wrong. Please try again.',
locationRequired: 'Please select the required pick-up and drop-off locations.',
},
fr: {
title: 'Réserver ce véhicule',
@@ -37,6 +46,11 @@ const copy = {
lastName: 'Nom',
email: 'E-mail',
phone: 'Téléphone',
pickupLocation: 'Lieu de départ',
returnLocation: 'Lieu de retour',
sameDropoff: 'Même retour',
differentDropoff: 'Retour différent',
sameAsPickup: 'Retour au même lieu',
startDate: 'Date de départ',
endDate: 'Date de retour',
notes: 'Notes (optionnelles)',
@@ -50,6 +64,7 @@ const copy = {
estimatedTotal: 'Total estimé',
days: 'jour(s)',
genericError: 'Une erreur est survenue. Veuillez réessayer.',
locationRequired: 'Veuillez sélectionner les lieux de départ et de retour requis.',
},
ar: {
title: 'احجز هذه السيارة',
@@ -57,6 +72,11 @@ const copy = {
lastName: 'اسم العائلة',
email: 'البريد الإلكتروني',
phone: 'الهاتف',
pickupLocation: 'موقع الاستلام',
returnLocation: 'موقع الإرجاع',
sameDropoff: 'نفس موقع الإرجاع',
differentDropoff: 'موقع إرجاع مختلف',
sameAsPickup: 'الإرجاع في نفس موقع الاستلام',
startDate: 'تاريخ الاستلام',
endDate: 'تاريخ الإرجاع',
notes: 'ملاحظات (اختيارية)',
@@ -70,18 +90,31 @@ const copy = {
estimatedTotal: 'الإجمالي التقديري',
days: 'يوم',
genericError: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
locationRequired: 'يرجى اختيار مواقع الاستلام والإرجاع المطلوبة.',
},
} as const
export default function BookingForm({ vehicleId, companySlug, dailyRate }: Props) {
export default function BookingForm({
vehicleId,
companySlug,
dailyRate,
pickupLocations,
allowDifferentDropoff,
dropoffLocations,
}: Props) {
const { language } = useMarketplacePreferences()
const t = copy[language]
const pickupOptions = Array.isArray(pickupLocations) ? pickupLocations : []
const dropoffOptions = Array.isArray(dropoffLocations) ? dropoffLocations : []
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
const [pickupLocation, setPickupLocation] = useState(pickupOptions[0] ?? '')
const [dropoffMode, setDropoffMode] = useState<'same' | 'different'>('same')
const [returnLocation, setReturnLocation] = useState(dropoffOptions[0] ?? '')
const [startDate, setStartDate] = useState('')
const [endDate, setEndDate] = useState('')
const [notes, setNotes] = useState('')
@@ -102,6 +135,10 @@ export default function BookingForm({ vehicleId, companySlug, dailyRate }: Props
setError(t.required)
return
}
if ((pickupOptions.length > 0 && !pickupLocation) || (dropoffMode === 'different' && dropoffOptions.length > 0 && !returnLocation)) {
setError(t.locationRequired)
return
}
const start = new Date(startDate)
const end = new Date(endDate)
@@ -121,6 +158,8 @@ export default function BookingForm({ vehicleId, companySlug, dailyRate }: Props
phone: phone.trim(),
startDate: start.toISOString(),
endDate: end.toISOString(),
pickupLocation: pickupLocation || undefined,
returnLocation: dropoffMode === 'different' ? (returnLocation || undefined) : (pickupLocation || undefined),
notes: notes.trim() || undefined,
language,
})
@@ -194,6 +233,67 @@ export default function BookingForm({ vehicleId, companySlug, dailyRate }: Props
/>
</div>
{pickupOptions.length > 0 ? (
<div className="space-y-1">
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.pickupLocation} <span className="text-red-500">*</span></label>
<select
value={pickupLocation}
onChange={(e) => setPickupLocation(e.target.value)}
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
>
{pickupOptions.map((location) => (
<option key={location} value={location}>{location}</option>
))}
</select>
</div>
) : null}
{allowDifferentDropoff ? (
<div className="space-y-3 rounded-2xl border border-stone-200 bg-stone-50 p-4 dark:border-blue-800 dark:bg-[#0d1b38]">
<div className="inline-flex rounded-full border border-stone-200 dark:border-blue-800 bg-white dark:bg-[#162038] p-1">
<button
type="button"
onClick={() => setDropoffMode('same')}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
dropoffMode === 'same'
? 'bg-stone-900 text-white dark:bg-orange-400 dark:text-stone-900'
: 'text-stone-500 dark:text-stone-400'
}`}
>
{t.sameDropoff}
</button>
<button
type="button"
onClick={() => setDropoffMode('different')}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
dropoffMode === 'different'
? 'bg-stone-900 text-white dark:bg-orange-400 dark:text-stone-900'
: 'text-stone-500 dark:text-stone-400'
}`}
>
{t.differentDropoff}
</button>
</div>
{dropoffMode === 'different' && dropoffOptions.length > 0 ? (
<div className="space-y-1">
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.returnLocation} <span className="text-red-500">*</span></label>
<select
value={returnLocation}
onChange={(e) => setReturnLocation(e.target.value)}
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
>
{dropoffOptions.map((location) => (
<option key={location} value={location}>{location}</option>
))}
</select>
</div>
) : (
<p className="text-sm text-stone-500 dark:text-stone-400">{pickupLocation || t.sameAsPickup}</p>
)}
</div>
) : null}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs font-medium text-stone-600 dark:text-stone-400">{t.startDate} <span className="text-red-500">*</span></label>
@@ -120,7 +120,7 @@ export default function MarketplaceHeader({
</Link>
{companyName ? (
<Link
href={`${dashboardUrl}/dashboard`}
href={dashboardUrl}
className="ml-1 flex items-center gap-1.5 rounded-full bg-orange-600 px-3 py-1 text-[12px] font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-500 dark:hover:bg-orange-400 sm:ml-2 sm:gap-2 sm:px-4 sm:py-2 sm:text-sm"
>
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-white/20 text-[10px] font-black dark:bg-[#07101e]/20">
@@ -88,7 +88,7 @@ export default function RenterShell({ children }: { children: React.ReactNode })
function signOut() {
clearRenterSession()
router.push('/renter/sign-in')
router.push('/')
}
const isActive = (item: (typeof NAV_ITEMS)[number]) => {
@@ -25,7 +25,7 @@ function getDefaultFramePath(target: FrameId) {
.find((c) => c.startsWith('employee_token='))
if (target === 'sign-in' && employeeToken) {
return '/dashboard/dashboard'
return '/dashboard'
}
return frameConfig[target].path
@@ -60,12 +60,30 @@ export default function WorkspaceFrame({ target }: { target: FrameId }) {
setFramePath(getDefaultFramePath(target))
}, [target])
useEffect(() => {
if (target !== 'sign-in') return
const employeeToken = document.cookie
.split(';')
.map((c) => c.trim())
.find((c) => c.startsWith('employee_token='))
if (employeeToken) {
window.location.replace('/dashboard')
}
}, [target])
useEffect(() => {
function handleFrameMessage(event: MessageEvent) {
if (!event.data || typeof event.data !== 'object') return
const message = event.data as { type?: string; path?: string }
if (target === 'sign-in' && message.type === 'rentaldrivego:employee-login' && typeof message.path === 'string') {
window.location.replace(message.path)
return
}
if ((message.type === 'rentaldrivego:embedded-path' || message.type === 'rentaldrivego:employee-login') && typeof message.path === 'string') {
setFramePath(message.path)
}
@@ -73,7 +91,7 @@ export default function WorkspaceFrame({ target }: { target: FrameId }) {
window.addEventListener('message', handleFrameMessage)
return () => window.removeEventListener('message', handleFrameMessage)
}, [])
}, [target])
useEffect(() => {
const nextUrl = new URL(buildFrameUrl(config.appUrl, framePath))