fix notification and add billing page and contract

This commit is contained in:
root
2026-05-13 00:09:39 -04:00
committed by Administrator
parent 1a39aa8433
commit 89621a163b
52 changed files with 5631 additions and 1110 deletions
@@ -1,14 +1,5 @@
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { resolveServerAppUrl } from '@/lib/appUrls'
export default function CompanyWorkspacePage() {
const requestHeaders = headers()
const dashboardUrl = resolveServerAppUrl(
process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001',
requestHeaders.get('host'),
requestHeaders.get('x-forwarded-proto'),
)
redirect(`${dashboardUrl}/dashboard`)
redirect('/sign-in')
}
+120
View File
@@ -13,3 +13,123 @@ body {
.card {
@apply rounded-2xl border border-stone-200 bg-white shadow-sm transition-colors dark:border-stone-800 dark:bg-stone-900;
}
body[data-theme='medium'] {
background-color: rgb(41 37 36);
color: rgb(231 229 228);
}
body[data-theme='medium'] .dark\:bg-stone-950 {
background-color: rgb(41 37 36);
}
body[data-theme='medium'] .dark\:bg-stone-950\/80 {
background-color: rgb(41 37 36 / 0.8);
}
body[data-theme='medium'] .dark\:bg-stone-950\/95 {
background-color: rgb(41 37 36 / 0.95);
}
body[data-theme='medium'] .dark\:bg-stone-950\/50 {
background-color: rgb(68 64 60 / 0.5);
}
body[data-theme='medium'] .dark\:bg-stone-950\/40 {
background-color: rgb(68 64 60 / 0.4);
}
body[data-theme='medium'] .dark\:bg-stone-950\/30 {
background-color: rgb(68 64 60 / 0.3);
}
body[data-theme='medium'] .dark\:bg-stone-900,
body[data-theme='medium'] .dark\:bg-stone-900\/80,
body[data-theme='medium'] .dark\:bg-stone-900\/70,
body[data-theme='medium'] .dark\:bg-stone-900\/60 {
background-color: rgb(68 64 60 / 0.85);
}
body[data-theme='medium'] .dark\:bg-stone-800 {
background-color: rgb(87 83 78);
}
body[data-theme='medium'] .dark\:bg-amber-400 {
background-color: rgb(251 191 36);
}
body[data-theme='medium'] .dark\:border-stone-800 {
border-color: rgb(120 113 108);
}
body[data-theme='medium'] .dark\:border-stone-700,
body[data-theme='medium'] .dark\:border-white\/5 {
border-color: rgb(87 83 78);
}
body[data-theme='medium'] .dark\:border-amber-300 {
border-color: rgb(252 211 77);
}
body[data-theme='medium'] .dark\:text-stone-50,
body[data-theme='medium'] .dark\:text-stone-100,
body[data-theme='medium'] .dark\:text-white {
color: rgb(250 250 249);
}
body[data-theme='medium'] .dark\:text-stone-200 {
color: rgb(231 229 228);
}
body[data-theme='medium'] .dark\:text-stone-300,
body[data-theme='medium'] .dark\:text-stone-400 {
color: rgb(214 211 209);
}
body[data-theme='medium'] .dark\:text-stone-500,
body[data-theme='medium'] .dark\:text-stone-700,
body[data-theme='medium'] .dark\:text-stone-800 {
color: rgb(168 162 158);
}
body[data-theme='medium'] .dark\:text-stone-950 {
color: rgb(28 25 23);
}
body[data-theme='medium'] .dark\:text-slate-100 {
color: rgb(250 250 249);
}
body[data-theme='medium'] .dark\:text-stone-600 {
color: rgb(168 162 158);
}
body[data-theme='medium'] .dark\:text-amber-300 {
color: rgb(252 211 77);
}
body[data-theme='medium'] .dark\:text-sky-400 {
color: rgb(125 211 252);
}
body[data-theme='medium'] .dark\:hover\:bg-stone-800:hover,
body[data-theme='medium'] .dark\:hover\:bg-stone-900:hover {
background-color: rgb(28 25 23);
}
body[data-theme='medium'] .dark\:hover\:bg-amber-300:hover {
background-color: rgb(252 211 77);
}
body[data-theme='medium'] .dark\:hover\:text-stone-100:hover,
body[data-theme='medium'] .dark\:hover\:text-white:hover {
color: rgb(250 250 249);
}
body[data-theme='medium'] .dark\:hover\:border-stone-200:hover {
border-color: rgb(214 211 209);
}
body[data-theme='medium'] .dark\:bg-\[\#111111\] {
background-color: rgb(28 25 23);
}
@@ -1,14 +1,5 @@
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { resolveServerAppUrl } from '@/lib/appUrls'
export default function PlatformOperationsPage() {
const requestHeaders = headers()
const adminUrl = resolveServerAppUrl(
process.env.NEXT_PUBLIC_ADMIN_URL ?? 'http://localhost:3002',
requestHeaders.get('host'),
requestHeaders.get('x-forwarded-proto'),
)
redirect(`${adminUrl}/dashboard`)
redirect('/sign-in')
}
@@ -143,74 +143,54 @@ export default function RenterDashboardPage() {
})
}, [router])
function handleLogout() {
localStorage.removeItem('renter_token')
router.push('/explore')
}
// Loading skeleton
if (status === 'loading') {
return (
<main className="min-h-screen bg-stone-50 py-10">
<div className="shell">
<div className="animate-pulse space-y-6">
<div className="h-8 w-48 rounded-xl bg-stone-200" />
<div className="h-40 rounded-2xl bg-stone-200" />
<div className="h-40 rounded-2xl bg-stone-200" />
</div>
<div className="shell">
<div className="animate-pulse space-y-6">
<div className="h-8 w-48 rounded-xl bg-stone-200" />
<div className="h-40 rounded-2xl bg-stone-200" />
<div className="h-40 rounded-2xl bg-stone-200" />
</div>
</main>
</div>
)
}
if (status === 'error') {
return (
<main className="min-h-screen bg-stone-50 py-10">
<div className="shell">
<div className="card p-8 text-center">
<p className="text-sm text-rose-600">{errorMsg}</p>
<button
onClick={handleLogout}
className="mt-4 rounded-full bg-stone-900 px-6 py-2.5 text-sm font-semibold text-white"
>
{dict.backToExplore}
</button>
</div>
<div className="shell">
<div className="card p-8 text-center">
<p className="text-sm text-rose-600">{errorMsg}</p>
<Link
href="/explore"
className="mt-4 inline-flex rounded-full bg-stone-900 px-6 py-2.5 text-sm font-semibold text-white"
>
{dict.backToExplore}
</Link>
</div>
</main>
</div>
)
}
const savedCompanies = profile?.savedCompanies ?? []
return (
<main className="min-h-screen bg-stone-50 py-10">
<div className="shell space-y-8">
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">
{dict.renterDashboard}
</p>
<h1 className="mt-1 text-3xl font-black tracking-tight text-stone-900">
{dict.welcome} {profile?.firstName}
</h1>
</div>
<div className="flex items-center gap-3">
<Link
href="/explore"
className="rounded-full border border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-stone-700 shadow-sm hover:border-stone-300"
>
{dict.exploreVehicles}
</Link>
<button
onClick={handleLogout}
className="rounded-full bg-stone-900 px-5 py-2.5 text-sm font-semibold text-white hover:bg-stone-700"
>
{dict.logout}
</button>
</div>
<div className="shell space-y-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">
{dict.renterDashboard}
</p>
<h1 className="mt-1 text-3xl font-black tracking-tight text-stone-900">
{dict.welcome} {profile?.firstName}
</h1>
</div>
<Link
href="/explore"
className="self-start rounded-full border border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-stone-700 shadow-sm hover:border-stone-300"
>
{dict.exploreVehicles}
</Link>
</div>
<section className="grid gap-4 sm:grid-cols-3">
{[
@@ -325,7 +305,6 @@ export default function RenterDashboardPage() {
</div>
)}
</section>
</div>
</main>
</div>
)
}
@@ -0,0 +1,5 @@
import RenterShell from '@/components/RenterShell'
export default function RenterLayout({ children }: { children: React.ReactNode }) {
return <RenterShell>{children}</RenterShell>
}
@@ -1,7 +1,6 @@
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import {
RenterAuthError,
@@ -101,64 +100,54 @@ export default function RenterNotificationsPage() {
}
return (
<main className="min-h-screen bg-stone-50 py-10">
<div className="shell space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">{dict.renter}</p>
<h1 className="mt-1 text-3xl font-black tracking-tight text-stone-900">{dict.title}</h1>
</div>
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={markAllRead}
disabled={loading || updating}
className="rounded-full border border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-stone-700"
>
{dict.markAll}
</button>
<Link href="/renter/dashboard" className="rounded-full border border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-stone-700">
{dict.back}
</Link>
</div>
</div>
{loading ? <div className="card p-8 text-sm text-stone-500">{dict.loading}</div> : null}
{!loading && errorMsg ? <div className="card p-8 text-sm text-rose-600">{errorMsg}</div> : null}
{!loading && !errorMsg && notifications.length === 0 ? (
<div className="card p-10 text-center text-sm text-stone-500">{dict.empty}</div>
) : null}
{!loading && !errorMsg && notifications.length > 0 ? (
<div className="space-y-4">
{notifications.map((notification) => (
<article key={notification.id} className="card p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-amber-700">{notification.type.replaceAll('_', ' ')}</p>
<h2 className="mt-2 text-lg font-bold text-stone-900">{notification.title}</h2>
</div>
{notification.readAt ? (
<span className="rounded-full bg-stone-100 px-3 py-1 text-xs font-semibold text-stone-600">{dict.read}</span>
) : (
<button
type="button"
onClick={() => markRead(notification.id)}
disabled={updating}
className="rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-800"
>
{dict.markRead}
</button>
)}
</div>
<p className="mt-3 text-sm leading-7 text-stone-600">{notification.body}</p>
<p className="mt-4 text-xs text-stone-400">{new Date(notification.createdAt).toLocaleString()}</p>
</article>
))}
</div>
) : null}
<div className="shell space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<h1 className="text-3xl font-black tracking-tight text-stone-900">{dict.title}</h1>
<button
type="button"
onClick={markAllRead}
disabled={loading || updating}
className="rounded-full border border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-stone-700"
>
{dict.markAll}
</button>
</div>
</main>
{loading ? <div className="card p-8 text-sm text-stone-500">{dict.loading}</div> : null}
{!loading && errorMsg ? <div className="card p-8 text-sm text-rose-600">{errorMsg}</div> : null}
{!loading && !errorMsg && notifications.length === 0 ? (
<div className="card p-10 text-center text-sm text-stone-500">{dict.empty}</div>
) : null}
{!loading && !errorMsg && notifications.length > 0 ? (
<div className="space-y-4">
{notifications.map((notification) => (
<article key={notification.id} className="card p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-amber-700">{notification.type.replaceAll('_', ' ')}</p>
<h2 className="mt-2 text-lg font-bold text-stone-900">{notification.title}</h2>
</div>
{notification.readAt ? (
<span className="rounded-full bg-stone-100 px-3 py-1 text-xs font-semibold text-stone-600">{dict.read}</span>
) : (
<button
type="button"
onClick={() => markRead(notification.id)}
disabled={updating}
className="rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-800"
>
{dict.markRead}
</button>
)}
</div>
<p className="mt-3 text-sm leading-7 text-stone-600">{notification.body}</p>
<p className="mt-4 text-xs text-stone-400">{new Date(notification.createdAt).toLocaleString()}</p>
</article>
))}
</div>
) : null}
</div>
)
}
@@ -1,7 +1,6 @@
'use client'
import { useEffect, useState, type ReactNode } from 'react'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import {
RenterAuthError,
@@ -136,15 +135,15 @@ export default function RenterProfilePage() {
}, [router])
if (status === 'loading') {
return <RenterShell title={dict.profile} dict={dict}><div className="card p-8 text-sm text-stone-500">{dict.loading}</div></RenterShell>
return <div className="shell"><div className="card p-8 text-sm text-stone-500">{dict.loading}</div></div>
}
if (status === 'error') {
return <RenterShell title={dict.profile} dict={dict}><ErrorCard message={errorMsg} /></RenterShell>
return <div className="shell"><ErrorCard message={errorMsg} /></div>
}
return (
<RenterShell title={dict.profile} dict={dict}>
<div className="shell space-y-6">
<div className="card p-8">
<h1 className="text-3xl font-black tracking-tight text-stone-900">{dict.yourProfile}</h1>
<div className="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
@@ -265,39 +264,7 @@ export default function RenterProfilePage() {
</table>
</div>
</div>
</RenterShell>
)
}
function RenterShell({ title, children, dict }: { title: string; children: ReactNode; dict: Record<string, string> }) {
return (
<main className="min-h-screen bg-stone-50 py-10">
<div className="shell space-y-6">
<div className="flex flex-wrap items-center gap-3">
<Link href="/renter/dashboard" className="rounded-full border border-stone-200 bg-white px-4 py-2 text-sm font-semibold text-stone-700">
{dict.back}
</Link>
<Link href="/renter/saved-companies" className="rounded-full border border-stone-200 bg-white px-4 py-2 text-sm font-semibold text-stone-700">
{dict.savedCompanies}
</Link>
<Link href="/renter/notifications" className="rounded-full border border-stone-200 bg-white px-4 py-2 text-sm font-semibold text-stone-700">
{dict.notifications}
</Link>
<button
type="button"
onClick={() => {
clearRenterSession()
window.location.href = '/renter/sign-in'
}}
className="rounded-full bg-stone-900 px-5 py-2 text-sm font-semibold text-white"
>
{dict.logout}
</button>
</div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">{dict.renter} {title}</p>
{children}
</div>
</main>
</div>
)
}
@@ -59,55 +59,45 @@ export default function RenterSavedCompaniesPage() {
}, [router])
return (
<main className="min-h-screen bg-stone-50 py-10">
<div className="shell space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">{dict.renter}</p>
<h1 className="mt-1 text-3xl font-black tracking-tight text-stone-900">{dict.title}</h1>
</div>
<Link href="/renter/dashboard" className="rounded-full border border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-stone-700">
{dict.back}
<div className="shell space-y-6">
<h1 className="text-3xl font-black tracking-tight text-stone-900">{dict.title}</h1>
{loading ? <div className="card p-8 text-sm text-stone-500">{dict.loading}</div> : null}
{!loading && errorMsg ? <div className="card p-8 text-sm text-rose-600">{errorMsg}</div> : null}
{!loading && !errorMsg && companies.length === 0 ? (
<div className="card p-10 text-center">
<p className="text-sm text-stone-500">{dict.empty}</p>
<Link href="/explore" className="mt-5 inline-flex rounded-full bg-stone-900 px-6 py-2.5 text-sm font-semibold text-white">
{dict.browse}
</Link>
</div>
) : null}
{loading ? <div className="card p-8 text-sm text-stone-500">{dict.loading}</div> : null}
{!loading && errorMsg ? <div className="card p-8 text-sm text-rose-600">{errorMsg}</div> : null}
{!loading && !errorMsg && companies.length === 0 ? (
<div className="card p-10 text-center">
<p className="text-sm text-stone-500">{dict.empty}</p>
<Link href="/explore" className="mt-5 inline-flex rounded-full bg-stone-900 px-6 py-2.5 text-sm font-semibold text-white">
{dict.browse}
</Link>
</div>
) : null}
{!loading && !errorMsg && companies.length > 0 ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{companies.map((company) => (
<Link
key={company.id}
href={`/explore/${company.brand?.subdomain ?? company.id}`}
className="card flex items-center gap-4 p-5 hover:border-amber-300 transition-colors"
>
{company.brand?.logoUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={company.brand.logoUrl} alt={company.brand.displayName} className="h-11 w-11 rounded-xl object-contain" />
) : (
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100 text-sm font-bold text-amber-700">
{company.brand?.displayName?.charAt(0) ?? '?'}
</div>
)}
<div className="min-w-0">
<p className="truncate text-sm font-semibold text-stone-900">{company.brand?.displayName ?? dict.rentalCompany}</p>
<p className="mt-1 truncate text-xs text-stone-400">{company.brand?.subdomain ?? company.id}</p>
{!loading && !errorMsg && companies.length > 0 ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{companies.map((company) => (
<Link
key={company.id}
href={`/explore/${company.brand?.subdomain ?? company.id}`}
className="card flex items-center gap-4 p-5 hover:border-amber-300 transition-colors"
>
{company.brand?.logoUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={company.brand.logoUrl} alt={company.brand.displayName} className="h-11 w-11 rounded-xl object-contain" />
) : (
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-amber-100 text-sm font-bold text-amber-700">
{company.brand?.displayName?.charAt(0) ?? '?'}
</div>
</Link>
))}
</div>
) : null}
</div>
</main>
)}
<div className="min-w-0">
<p className="truncate text-sm font-semibold text-stone-900">{company.brand?.displayName ?? dict.rentalCompany}</p>
<p className="mt-1 truncate text-xs text-stone-400">{company.brand?.subdomain ?? company.id}</p>
</div>
</Link>
))}
</div>
) : null}
</div>
)
}
@@ -0,0 +1,5 @@
import WorkspaceFrame from '@/components/WorkspaceFrame'
export default function SignInPage() {
return <WorkspaceFrame target="sign-in" />
}