unfied style
This commit is contained in:
+7
-5
@@ -5,10 +5,12 @@ API_URL=http://localhost:4000
|
|||||||
API_INTERNAL_URL=http://api:4000/api/v1
|
API_INTERNAL_URL=http://api:4000/api/v1
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:4000/api/v1
|
NEXT_PUBLIC_API_URL=http://localhost:4000/api/v1
|
||||||
NEXT_PUBLIC_MARKETPLACE_URL=http://localhost:3000
|
NEXT_PUBLIC_MARKETPLACE_URL=http://localhost:3000
|
||||||
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3001/dashboard
|
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3000/dashboard
|
||||||
NEXT_PUBLIC_ADMIN_URL=http://localhost:3002/admin
|
NEXT_PUBLIC_ADMIN_URL=http://localhost:3000/admin
|
||||||
DASHBOARD_URL=http://localhost:3001/dashboard
|
DASHBOARD_URL=http://localhost:3000/dashboard
|
||||||
ADMIN_URL=http://localhost:3002/admin
|
ADMIN_URL=http://localhost:3000/admin
|
||||||
|
DASHBOARD_INTERNAL_URL=http://host.docker.internal:3001
|
||||||
|
ADMIN_INTERNAL_URL=http://host.docker.internal:3002
|
||||||
JWT_SECRET=dev-secret
|
JWT_SECRET=dev-secret
|
||||||
JWT_EXPIRY=8h
|
JWT_EXPIRY=8h
|
||||||
RENTER_JWT_EXPIRY=7d
|
RENTER_JWT_EXPIRY=7d
|
||||||
@@ -19,7 +21,7 @@ ADMIN_SEED_LAST_NAME=Admin
|
|||||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
|
||||||
CLERK_SECRET_KEY=
|
CLERK_SECRET_KEY=
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:4000
|
CORS_ORIGINS=http://localhost:3000,http://localhost:4000
|
||||||
|
|
||||||
# Email - disable Resend (placeholder), use SMTP instead
|
# Email - disable Resend (placeholder), use SMTP instead
|
||||||
RESEND_API_KEY=re_...
|
RESEND_API_KEY=re_...
|
||||||
|
|||||||
+3
-3
@@ -75,12 +75,12 @@ REDIS_URL=redis://localhost:6379
|
|||||||
|
|
||||||
# ─── App URLs ──────────────────────────────────────────────────
|
# ─── App URLs ──────────────────────────────────────────────────
|
||||||
NEXT_PUBLIC_MARKETING_URL=http://localhost:3000
|
NEXT_PUBLIC_MARKETING_URL=http://localhost:3000
|
||||||
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3001/dashboard
|
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3000/dashboard
|
||||||
NEXT_PUBLIC_ADMIN_URL=http://localhost:3002/admin
|
NEXT_PUBLIC_ADMIN_URL=http://localhost:3000/admin
|
||||||
NEXT_PUBLIC_MARKETPLACE_URL=http://localhost:3000
|
NEXT_PUBLIC_MARKETPLACE_URL=http://localhost:3000
|
||||||
# Public site is subdomain-based; use this for local dev:
|
# Public site is subdomain-based; use this for local dev:
|
||||||
NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=localhost:3003
|
NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=localhost:3003
|
||||||
DASHBOARD_URL=http://localhost:3001
|
DASHBOARD_URL=http://localhost:3000/dashboard
|
||||||
|
|
||||||
# ─── Admin seed (first SUPER_ADMIN created on db:seed) ────────
|
# ─── Admin seed (first SUPER_ADMIN created on db:seed) ────────
|
||||||
ADMIN_SEED_EMAIL=admin@rentaldrivego.com
|
ADMIN_SEED_EMAIL=admin@rentaldrivego.com
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ docker compose -f docker-compose.dev.yml --profile full up --build
|
|||||||
Services:
|
Services:
|
||||||
|
|
||||||
- marketplace: `http://localhost:3000`
|
- marketplace: `http://localhost:3000`
|
||||||
- dashboard: `http://localhost:3001`
|
- dashboard: `http://localhost:3000/dashboard`
|
||||||
- admin: `http://localhost:3002`
|
- admin: `http://localhost:3000/admin`
|
||||||
- public-site: `http://localhost:3003`
|
- public-site: `http://localhost:3003`
|
||||||
- api: `http://localhost:4000`
|
- api: `http://localhost:4000`
|
||||||
- pgAdmin: `http://localhost:5050`
|
- pgAdmin: `http://localhost:5050`
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { resolveBrowserAppUrl } from '@/lib/appUrls'
|
import { resolveBrowserAppUrl } from '@/lib/appUrls'
|
||||||
|
|
||||||
function buildUnifiedLoginUrl(nextPath: string) {
|
function buildUnifiedLoginUrl(nextPath: string) {
|
||||||
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard')
|
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard')
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
portal: 'admin',
|
portal: 'admin',
|
||||||
next: nextPath || '/dashboard',
|
next: nextPath || '/dashboard',
|
||||||
@@ -50,18 +50,18 @@ export default function AdminDashboardLayout({ children }: { children: React.Rea
|
|||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen items-center justify-center bg-zinc-950">
|
<div className="flex h-screen items-center justify-center bg-[linear-gradient(180deg,#f8f5ef_0%,#f4efe6_28%,#fffdf8_58%,#ffffff_100%)] dark:bg-[linear-gradient(180deg,#14110f_0%,#17120d_35%,#0c0a09_100%)]">
|
||||||
<div className="h-6 w-6 rounded-full border-2 border-emerald-500 border-t-transparent animate-spin" aria-label={dict.loading} />
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-amber-500 border-t-transparent" aria-label={dict.loading} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-zinc-950 text-zinc-100 transition-colors">
|
<div className="flex h-screen bg-[linear-gradient(180deg,#f8f5ef_0%,#f4efe6_28%,#fffdf8_58%,#ffffff_100%)] text-stone-900 transition-colors dark:bg-[linear-gradient(180deg,#14110f_0%,#17120d_35%,#0c0a09_100%)] dark:text-stone-100">
|
||||||
<aside className="w-60 flex-shrink-0 flex flex-col border-r border-zinc-800 bg-zinc-900 transition-colors">
|
<aside className="flex w-60 flex-shrink-0 flex-col border-r border-stone-200/80 bg-white/78 backdrop-blur-xl transition-colors dark:border-stone-800 dark:bg-stone-950/78">
|
||||||
<Link href="/" className="block px-5 py-6">
|
<Link href="/" className="block px-5 py-6">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-emerald-400">{dict.admin}</p>
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700 dark:text-amber-300">{dict.admin}</p>
|
||||||
<p className="mt-0.5 text-sm font-semibold text-zinc-100">RentalDriveGo</p>
|
<p className="mt-0.5 text-sm font-semibold text-stone-950 dark:text-stone-100">RentalDriveGo</p>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex-1 px-3 space-y-0.5">
|
<nav className="flex-1 px-3 space-y-0.5">
|
||||||
{navLinks.map((link) => {
|
{navLinks.map((link) => {
|
||||||
@@ -71,7 +71,7 @@ export default function AdminDashboardLayout({ children }: { children: React.Rea
|
|||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-colors ${
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-colors ${
|
||||||
active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50'
|
active ? 'bg-stone-900 text-white dark:bg-amber-400 dark:text-stone-950' : 'text-stone-500 hover:text-stone-950 hover:bg-stone-100 dark:text-stone-400 dark:hover:text-white dark:hover:bg-stone-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<svg className="h-4 w-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="h-4 w-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
@@ -85,20 +85,20 @@ export default function AdminDashboardLayout({ children }: { children: React.Rea
|
|||||||
<div className="px-3 py-4">
|
<div className="px-3 py-4">
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="flex w-full items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium text-zinc-500 transition-colors hover:bg-zinc-800/50 hover:text-red-400"
|
className="flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-stone-500 transition-colors hover:bg-stone-100 hover:text-red-500 dark:text-stone-400 dark:hover:bg-stone-800 dark:hover:text-red-300"
|
||||||
>
|
>
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
</svg>
|
</svg>
|
||||||
{dict.logout}
|
{dict.logout}
|
||||||
</button>
|
</button>
|
||||||
<div className="mt-4 flex flex-col items-center gap-3 border-t border-zinc-800 pt-4">
|
<div className="mt-4 flex flex-col items-center gap-3 border-t border-stone-200/80 pt-4 dark:border-stone-800">
|
||||||
<AdminLanguageSwitcher />
|
<AdminLanguageSwitcher />
|
||||||
<AdminThemeSwitcher />
|
<AdminThemeSwitcher />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main className="flex-1 overflow-y-auto bg-zinc-950 transition-colors">{children}</main>
|
<main className="flex-1 overflow-y-auto transition-colors">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default function AdminForgotPasswordPage() {
|
|||||||
<main className="flex flex-1 items-center justify-center px-4">
|
<main className="flex flex-1 items-center justify-center px-4">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<Link href="/" className="text-xs font-semibold uppercase tracking-[0.2em] text-emerald-400">RentalDriveGo</Link>
|
<Link href="/" className="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700 dark:text-amber-300">RentalDriveGo</Link>
|
||||||
<h1 className="mt-3 text-3xl font-black tracking-tight">Forgot password</h1>
|
<h1 className="mt-3 text-3xl font-black tracking-tight">Forgot password</h1>
|
||||||
<p className="mt-1 text-sm text-zinc-400">Enter your admin email to receive a reset link.</p>
|
<p className="mt-1 text-sm text-zinc-400">Enter your admin email to receive a reset link.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,20 +57,20 @@ export default function AdminForgotPasswordPage() {
|
|||||||
<div className="rounded-xl border border-red-900/50 bg-red-950/50 px-4 py-3 text-sm text-red-400">{error}</div>
|
<div className="rounded-xl border border-red-900/50 bg-red-950/50 px-4 py-3 text-sm text-red-400">{error}</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-zinc-300 mb-1.5">Email</label>
|
<label className="mb-1.5 block text-sm font-medium text-zinc-300">Email</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="admin@rentaldrivego.com"
|
placeholder="admin@rentaldrivego.com"
|
||||||
className="w-full px-3 py-2.5 rounded-xl bg-zinc-800 border border-zinc-700 text-zinc-100 placeholder:text-zinc-500 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
|
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-amber-500 dark:border-stone-700 dark:bg-stone-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full py-2.5 px-4 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-semibold transition-colors disabled:opacity-50"
|
className="inline-flex w-full justify-center rounded-full bg-stone-900 px-6 py-3 text-sm font-semibold text-white transition hover:bg-stone-700 disabled:opacity-50 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300"
|
||||||
>
|
>
|
||||||
{loading ? 'Sending…' : 'Send reset link'}
|
{loading ? 'Sending…' : 'Send reset link'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+109
-13
@@ -11,7 +11,9 @@ html.dark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-zinc-950 text-zinc-50 antialiased transition-colors;
|
@apply text-stone-900 antialiased transition-colors dark:text-stone-100;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(180deg, #f8f5ef 0%, #f4efe6 28%, #fffdf8 58%, #ffffff 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
@@ -19,17 +21,111 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
@apply rounded-2xl border border-zinc-800 bg-zinc-900 shadow-sm;
|
@apply rounded-[1.75rem] border shadow-[0_30px_80px_rgba(28,25,23,0.08)] backdrop-blur transition-colors dark:shadow-[0_30px_80px_rgba(0,0,0,0.26)];
|
||||||
|
border-color: rgb(231 229 228 / 0.8);
|
||||||
|
background-color: rgb(255 255 255 / 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark body {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(180deg, #14110f 0%, #17120d 35%, #0c0a09 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .panel {
|
||||||
|
border-color: rgb(41 37 36 / 0.96);
|
||||||
|
background-color: rgb(28 25 23 / 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.light body {
|
.light body {
|
||||||
background-color: rgb(248 250 252);
|
color: rgb(28 25 23);
|
||||||
color: rgb(15 23 42);
|
}
|
||||||
|
|
||||||
|
.bg-zinc-950 {
|
||||||
|
background-color: rgb(12 10 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-zinc-950\/90 {
|
||||||
|
background-color: rgb(28 25 23 / 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-zinc-900 {
|
||||||
|
background-color: rgb(28 25 23);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-zinc-800,
|
||||||
|
.bg-zinc-800\/50 {
|
||||||
|
background-color: rgb(41 37 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-zinc-800,
|
||||||
|
.border-zinc-700 {
|
||||||
|
border-color: rgb(68 64 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-zinc-100,
|
||||||
|
.text-zinc-200 {
|
||||||
|
color: rgb(250 250 249);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-zinc-500 {
|
||||||
|
color: rgb(168 162 158);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-zinc-400,
|
||||||
|
.text-zinc-300 {
|
||||||
|
color: rgb(120 113 108);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-zinc-800:hover,
|
||||||
|
.hover\:bg-zinc-800\/50:hover {
|
||||||
|
background-color: rgb(68 64 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:text-zinc-200:hover {
|
||||||
|
color: rgb(250 250 249);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-emerald-400 {
|
||||||
|
color: rgb(252 211 77);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-emerald-600 {
|
||||||
|
background-color: rgb(28 25 23);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-emerald-500:hover {
|
||||||
|
background-color: rgb(68 64 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-emerald-600 {
|
||||||
|
background-color: rgb(251 191 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .hover\:bg-emerald-500:hover {
|
||||||
|
background-color: rgb(252 211 77);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-emerald-900\/30,
|
||||||
|
.bg-emerald-950\/40,
|
||||||
|
.bg-emerald-950\/50 {
|
||||||
|
background-color: rgb(120 53 15 / 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-emerald-500 {
|
||||||
|
border-color: rgb(245 158 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus\:ring-emerald-500:focus {
|
||||||
|
--tw-ring-color: rgb(217 119 6 / 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-emerald-900\/50:hover {
|
||||||
|
background-color: rgb(146 64 14 / 0.42);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .bg-zinc-950 {
|
.light .bg-zinc-950 {
|
||||||
background-color: rgb(248 250 252);
|
background-color: rgb(255 253 248);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .bg-zinc-950\/90 {
|
.light .bg-zinc-950\/90 {
|
||||||
@@ -37,39 +133,39 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.light .bg-zinc-900 {
|
.light .bg-zinc-900 {
|
||||||
background-color: rgb(255 255 255);
|
background-color: rgb(255 255 255 / 0.82);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .bg-zinc-800,
|
.light .bg-zinc-800,
|
||||||
.light .bg-zinc-800\/50 {
|
.light .bg-zinc-800\/50 {
|
||||||
background-color: rgb(241 245 249);
|
background-color: rgb(245 245 244);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .border-zinc-800,
|
.light .border-zinc-800,
|
||||||
.light .border-zinc-700 {
|
.light .border-zinc-700 {
|
||||||
border-color: rgb(226 232 240);
|
border-color: rgb(231 229 228);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .text-zinc-100,
|
.light .text-zinc-100,
|
||||||
.light .text-zinc-200 {
|
.light .text-zinc-200 {
|
||||||
color: rgb(15 23 42);
|
color: rgb(28 25 23);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .text-zinc-500 {
|
.light .text-zinc-500 {
|
||||||
color: rgb(100 116 139);
|
color: rgb(120 113 108);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .text-zinc-400,
|
.light .text-zinc-400,
|
||||||
.light .text-zinc-300 {
|
.light .text-zinc-300 {
|
||||||
color: rgb(71 85 105);
|
color: rgb(87 83 78);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .hover\:bg-zinc-800:hover,
|
.light .hover\:bg-zinc-800:hover,
|
||||||
.light .hover\:bg-zinc-800\/50:hover {
|
.light .hover\:bg-zinc-800\/50:hover {
|
||||||
background-color: rgb(241 245 249);
|
background-color: rgb(245 245 244);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .hover\:text-zinc-200:hover {
|
.light .hover\:text-zinc-200:hover {
|
||||||
color: rgb(15 23 42);
|
color: rgb(28 25 23);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="dark" suppressHydrationWarning>
|
<html lang="en" className="light" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html:
|
__html:
|
||||||
"(function(){try{var theme=localStorage.getItem('admin-theme');if(theme!=='light'&&theme!=='dark'){theme=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}document.documentElement.classList.remove('light','dark');document.documentElement.classList.add(theme);document.documentElement.style.colorScheme=theme}catch(e){}})();",
|
"(function(){try{var m=document.cookie.match(/(?:^|; )rentaldrivego-theme=([^;]+)/);var theme=m?decodeURIComponent(m[1]):(localStorage.getItem('rentaldrivego-theme')||localStorage.getItem('admin-theme'));if(theme!=='light'&&theme!=='dark'){theme=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}document.documentElement.classList.remove('light','dark');document.documentElement.classList.add(theme);document.documentElement.style.colorScheme=theme;document.body&&document.body.setAttribute('data-theme',theme)}catch(e){}})();",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { resolveServerAppUrl } from '@/lib/appUrls'
|
|||||||
export default function AdminLoginPage() {
|
export default function AdminLoginPage() {
|
||||||
const requestHeaders = headers()
|
const requestHeaders = headers()
|
||||||
const dashboardUrl = resolveServerAppUrl(
|
const dashboardUrl = resolveServerAppUrl(
|
||||||
process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard',
|
process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard',
|
||||||
requestHeaders.get('host'),
|
requestHeaders.get('host'),
|
||||||
requestHeaders.get('x-forwarded-proto'),
|
requestHeaders.get('x-forwarded-proto'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function AdminResetPasswordContent() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push('/login')}
|
onClick={() => router.push('/login')}
|
||||||
className="mt-2 w-full py-2.5 px-4 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-semibold transition-colors"
|
className="mt-2 inline-flex w-full justify-center rounded-full bg-stone-900 px-6 py-3 text-sm font-semibold text-white transition hover:bg-stone-700 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</button>
|
</button>
|
||||||
@@ -93,7 +93,7 @@ function AdminResetPasswordContent() {
|
|||||||
<div className="rounded-xl border border-red-900/50 bg-red-950/50 px-4 py-3 text-sm text-red-400">{error}</div>
|
<div className="rounded-xl border border-red-900/50 bg-red-950/50 px-4 py-3 text-sm text-red-400">{error}</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-zinc-300 mb-1.5">New password</label>
|
<label className="mb-1.5 block text-sm font-medium text-zinc-300">New password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
@@ -101,11 +101,11 @@ function AdminResetPasswordContent() {
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
className="w-full px-3 py-2.5 rounded-xl bg-zinc-800 border border-zinc-700 text-zinc-100 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
|
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-amber-500 dark:border-stone-700 dark:bg-stone-950/80 dark:text-stone-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-zinc-300 mb-1.5">Confirm password</label>
|
<label className="mb-1.5 block text-sm font-medium text-zinc-300">Confirm password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
@@ -113,13 +113,13 @@ function AdminResetPasswordContent() {
|
|||||||
value={confirm}
|
value={confirm}
|
||||||
onChange={(e) => setConfirm(e.target.value)}
|
onChange={(e) => setConfirm(e.target.value)}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
className="w-full px-3 py-2.5 rounded-xl bg-zinc-800 border border-zinc-700 text-zinc-100 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
|
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-amber-500 dark:border-stone-700 dark:bg-stone-950/80 dark:text-stone-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full py-2.5 px-4 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-semibold transition-colors disabled:opacity-50"
|
className="inline-flex w-full justify-center rounded-full bg-stone-900 px-6 py-3 text-sm font-semibold text-white transition hover:bg-stone-700 disabled:opacity-50 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300"
|
||||||
>
|
>
|
||||||
{loading ? 'Resetting…' : 'Reset password'}
|
{loading ? 'Resetting…' : 'Reset password'}
|
||||||
</button>
|
</button>
|
||||||
@@ -140,7 +140,7 @@ function AdminResetShell({ children }: { children: React.ReactNode }) {
|
|||||||
<main className="flex flex-1 items-center justify-center px-4">
|
<main className="flex flex-1 items-center justify-center px-4">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<Link href="/" className="text-xs font-semibold uppercase tracking-[0.2em] text-emerald-400">RentalDriveGo</Link>
|
<Link href="/" className="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700 dark:text-amber-300">RentalDriveGo</Link>
|
||||||
<h1 className="mt-3 text-3xl font-black tracking-tight">Reset password</h1>
|
<h1 className="mt-3 text-3xl font-black tracking-tight">Reset password</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel p-8">{children}</div>
|
<div className="panel p-8">{children}</div>
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ const languageMeta = {
|
|||||||
ar: { flag: '🇲🇦', shortLabel: 'AR' },
|
ar: { flag: '🇲🇦', shortLabel: 'AR' },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
const SHARED_THEME_KEY = 'rentaldrivego-theme'
|
||||||
|
|
||||||
type AdminI18nContext = {
|
type AdminI18nContext = {
|
||||||
language: AdminLanguage
|
language: AdminLanguage
|
||||||
setLanguage: (value: AdminLanguage) => void
|
setLanguage: (value: AdminLanguage) => void
|
||||||
@@ -99,14 +101,14 @@ const Context = createContext<AdminI18nContext | null>(null)
|
|||||||
|
|
||||||
export function AdminI18nProvider({ children }: { children: React.ReactNode }) {
|
export function AdminI18nProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [language, setLanguage] = useState<AdminLanguage>('en')
|
const [language, setLanguage] = useState<AdminLanguage>('en')
|
||||||
const [theme, setTheme] = useState<AdminTheme>('dark')
|
const [theme, setTheme] = useState<AdminTheme>('light')
|
||||||
// Skip the very first write so we don't overwrite a stored preference with
|
// Skip the very first write so we don't overwrite a stored preference with
|
||||||
// the default 'en' value before the hydration read-effect has applied it.
|
// the default 'en' value before the hydration read-effect has applied it.
|
||||||
const skipFirstLangWrite = useRef(true)
|
const skipFirstLangWrite = useRef(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stored = window.localStorage.getItem('admin-language')
|
const stored = window.localStorage.getItem('admin-language')
|
||||||
const storedTheme = window.localStorage.getItem('admin-theme')
|
const storedTheme = window.localStorage.getItem(SHARED_THEME_KEY) ?? window.localStorage.getItem('admin-theme')
|
||||||
if (stored === 'en' || stored === 'fr' || stored === 'ar') {
|
if (stored === 'en' || stored === 'fr' || stored === 'ar') {
|
||||||
setLanguage(stored)
|
setLanguage(stored)
|
||||||
}
|
}
|
||||||
@@ -136,6 +138,8 @@ export function AdminI18nProvider({ children }: { children: React.ReactNode }) {
|
|||||||
document.documentElement.classList.add(theme)
|
document.documentElement.classList.add(theme)
|
||||||
document.documentElement.style.colorScheme = theme
|
document.documentElement.style.colorScheme = theme
|
||||||
document.body.dataset.theme = theme
|
document.body.dataset.theme = theme
|
||||||
|
document.cookie = `${SHARED_THEME_KEY}=${encodeURIComponent(theme)}; path=/; max-age=31536000; SameSite=Lax`
|
||||||
|
window.localStorage.setItem(SHARED_THEME_KEY, theme)
|
||||||
window.localStorage.setItem('admin-theme', theme)
|
window.localStorage.setItem('admin-theme', theme)
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
@@ -163,8 +167,8 @@ export function AdminLanguageSwitcher() {
|
|||||||
if (embedded) return null
|
if (embedded) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 rounded-full border border-zinc-700 bg-zinc-900 px-2 py-1 shadow-sm transition-colors">
|
<div className="flex items-center gap-1 rounded-full border border-stone-200/80 bg-white/95 px-2 py-1 shadow-sm transition-colors dark:border-stone-700 dark:bg-stone-900/85">
|
||||||
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-zinc-500">{dict.language}</span>
|
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-stone-500 dark:text-stone-400">{dict.language}</span>
|
||||||
{(['en', 'fr', 'ar'] as AdminLanguage[]).map((value) => {
|
{(['en', 'fr', 'ar'] as AdminLanguage[]).map((value) => {
|
||||||
const active = value === language
|
const active = value === language
|
||||||
const meta = languageMeta[value]
|
const meta = languageMeta[value]
|
||||||
@@ -174,7 +178,7 @@ export function AdminLanguageSwitcher() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setLanguage(value)}
|
onClick={() => setLanguage(value)}
|
||||||
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
||||||
active ? 'bg-zinc-100 text-zinc-950' : 'text-zinc-300 hover:bg-zinc-800'
|
active ? 'bg-stone-900 text-white dark:bg-amber-400 dark:text-stone-950' : 'text-stone-600 hover:bg-stone-100 dark:text-stone-300 dark:hover:bg-stone-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
@@ -199,10 +203,7 @@ export function AdminThemeSwitcher() {
|
|||||||
if (embedded) return null
|
if (embedded) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 rounded-full border border-zinc-700 bg-zinc-900 px-2 py-1 shadow-sm transition-colors">
|
<div className="flex items-center gap-1 rounded-full border border-stone-200/80 bg-white/95 px-2 py-1 shadow-sm transition-colors dark:border-stone-700 dark:bg-stone-900/85">
|
||||||
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-zinc-500">
|
|
||||||
{dict.theme}
|
|
||||||
</span>
|
|
||||||
{(['light', 'dark'] as AdminTheme[]).map((value) => {
|
{(['light', 'dark'] as AdminTheme[]).map((value) => {
|
||||||
const active = value === theme
|
const active = value === theme
|
||||||
return (
|
return (
|
||||||
@@ -211,7 +212,7 @@ export function AdminThemeSwitcher() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTheme(value)}
|
onClick={() => setTheme(value)}
|
||||||
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
||||||
active ? 'bg-zinc-100 text-zinc-950' : 'text-zinc-300 hover:bg-zinc-800'
|
active ? 'bg-stone-900 text-white dark:bg-amber-400 dark:text-stone-950' : 'text-stone-600 hover:bg-stone-100 dark:text-stone-300 dark:hover:bg-stone-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{value === 'light' ? dict.light : dict.dark}
|
{value === 'light' ? dict.light : dict.dark}
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ export default function PublicFooter() {
|
|||||||
}[language]
|
}[language]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-stone-200 bg-white/90 px-4 py-4 backdrop-blur-md transition-colors dark:border-zinc-800 dark:bg-zinc-950/90">
|
<footer className="border-t border-stone-200/80 bg-white/72 px-4 py-4 backdrop-blur-xl transition-colors dark:border-stone-800 dark:bg-stone-950/72">
|
||||||
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-3 lg:flex-row">
|
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-3 lg:flex-row">
|
||||||
<div className="flex items-center gap-3 text-xs font-medium uppercase tracking-[0.16em] text-stone-400 dark:text-zinc-500">
|
<div className="flex items-center gap-3 text-xs font-medium uppercase tracking-[0.16em] text-stone-400 dark:text-stone-500">
|
||||||
<p>{dict.preferences}</p>
|
<p>{dict.preferences}</p>
|
||||||
<span className="inline-flex items-center gap-1 rounded-full border border-zinc-700 bg-zinc-900 px-2.5 py-1 text-zinc-200">
|
<span className="inline-flex items-center gap-1 rounded-full border border-stone-200/80 bg-white/95 px-2.5 py-1 text-stone-700 dark:border-stone-700 dark:bg-stone-900/85 dark:text-stone-200">
|
||||||
<span aria-hidden="true">{currentLanguage.flag}</span>
|
<span aria-hidden="true">{currentLanguage.flag}</span>
|
||||||
<span>{currentLanguage.shortLabel}</span>
|
<span>{currentLanguage.shortLabel}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -28,18 +28,18 @@ export default function PublicHeader() {
|
|||||||
}[language]
|
}[language]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-30 border-b border-zinc-800 bg-zinc-950/90 backdrop-blur-md transition-colors">
|
<header className="sticky top-0 z-30 border-b border-stone-200/80 bg-white/78 backdrop-blur-xl transition-colors dark:border-stone-800 dark:bg-stone-950/76">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-4">
|
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-4">
|
||||||
<Link href="/" className="flex items-center gap-3">
|
<Link href="/" className="flex items-center gap-3">
|
||||||
<span className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-400">RentalDriveGo</span>
|
<span className="text-xs font-semibold uppercase tracking-[0.24em] text-amber-700 dark:text-amber-300">RentalDriveGo</span>
|
||||||
<span className="hidden text-sm font-semibold text-zinc-400 sm:inline">{dict.admin}</span>
|
<span className="hidden text-sm font-semibold text-stone-500 dark:text-stone-400 sm:inline">{dict.admin}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex items-center gap-2">
|
<nav className="flex items-center gap-2">
|
||||||
<span className="inline-flex min-w-[3.5rem] items-center justify-center gap-1 rounded-full border border-zinc-700 bg-zinc-900 px-2.5 py-2 text-xs font-semibold text-zinc-200">
|
<span className="inline-flex min-w-[3.5rem] items-center justify-center gap-1 rounded-full border border-stone-200/80 bg-white/95 px-2.5 py-2 text-xs font-semibold text-stone-700 dark:border-stone-700 dark:bg-stone-900/85 dark:text-stone-200">
|
||||||
<span aria-hidden="true">{currentLanguage.flag}</span>
|
<span aria-hidden="true">{currentLanguage.flag}</span>
|
||||||
<span>{currentLanguage.shortLabel}</span>
|
<span>{currentLanguage.shortLabel}</span>
|
||||||
</span>
|
</span>
|
||||||
<Link href="/login" className="rounded-full bg-zinc-100 px-4 py-2 text-sm font-semibold text-zinc-950 transition hover:bg-zinc-300">
|
<Link href="/login" className="rounded-full bg-stone-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-stone-700 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300">
|
||||||
{dict.signIn}
|
{dict.signIn}
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import PublicHeader from '@/components/PublicHeader'
|
|||||||
|
|
||||||
export default function PublicShell({ children }: { children: React.ReactNode }) {
|
export default function PublicShell({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-zinc-950 text-zinc-100 transition-colors">
|
<div className="flex min-h-screen flex-col bg-[linear-gradient(180deg,#f8f5ef_0%,#f4efe6_28%,#fffdf8_58%,#ffffff_100%)] text-stone-900 transition-colors dark:bg-[linear-gradient(180deg,#14110f_0%,#17120d_35%,#0c0a09_100%)] dark:text-stone-100">
|
||||||
<PublicHeader />
|
<PublicHeader />
|
||||||
<div className="flex flex-1 flex-col">{children}</div>
|
<div className="flex flex-1 flex-col">{children}</div>
|
||||||
<PublicFooter />
|
<PublicFooter />
|
||||||
|
|||||||
+2
-6
@@ -31,13 +31,9 @@ const v1 = '/api/v1'
|
|||||||
|
|
||||||
const defaultCorsOrigins = [
|
const defaultCorsOrigins = [
|
||||||
'http://localhost:3000',
|
'http://localhost:3000',
|
||||||
'http://localhost:3001',
|
'http://localhost:4000',
|
||||||
'http://localhost:3002',
|
|
||||||
'http://localhost:3003',
|
|
||||||
'http://127.0.0.1:3000',
|
'http://127.0.0.1:3000',
|
||||||
'http://127.0.0.1:3001',
|
'http://127.0.0.1:4000',
|
||||||
'http://127.0.0.1:3002',
|
|
||||||
'http://127.0.0.1:3003',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const corsOrigins = process.env.CORS_ORIGINS
|
export const corsOrigins = process.env.CORS_ORIGINS
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function getDashboardBase(): string {
|
|||||||
return (
|
return (
|
||||||
process.env.DASHBOARD_URL ??
|
process.env.DASHBOARD_URL ??
|
||||||
process.env.NEXT_PUBLIC_DASHBOARD_URL ??
|
process.env.NEXT_PUBLIC_DASHBOARD_URL ??
|
||||||
'http://localhost:3001/dashboard'
|
'http://localhost:3000/dashboard'
|
||||||
).replace(/\/$/, '')
|
).replace(/\/$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe('admin.service forgotPassword', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
process.env.ADMIN_URL = 'http://localhost:3002'
|
process.env.ADMIN_URL = 'http://localhost:3000/admin'
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export async function forgotPassword(email: string) {
|
|||||||
await repo.setAdminPasswordReset(admin.id, rawToken, expiresAt)
|
await repo.setAdminPasswordReset(admin.id, rawToken, expiresAt)
|
||||||
|
|
||||||
const adminUrl = ensureAdminBasePath(
|
const adminUrl = ensureAdminBasePath(
|
||||||
process.env.ADMIN_URL ?? process.env.NEXT_PUBLIC_ADMIN_URL ?? 'http://localhost:3002/admin',
|
process.env.ADMIN_URL ?? process.env.NEXT_PUBLIC_ADMIN_URL ?? 'http://localhost:3000/admin',
|
||||||
)
|
)
|
||||||
const resetUrl = `${adminUrl}/reset-password?token=${rawToken}`
|
const resetUrl = `${adminUrl}/reset-password?token=${rawToken}`
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe('auth.employee.service forgotPassword', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
process.env.DASHBOARD_URL = 'http://localhost:3001'
|
process.env.DASHBOARD_URL = 'http://localhost:3000/dashboard'
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export async function forgotPassword(email: string) {
|
|||||||
await repo.setPasswordResetToken(employee.id, rawToken, expiresAt)
|
await repo.setPasswordResetToken(employee.id, rawToken, expiresAt)
|
||||||
|
|
||||||
const dashboardUrl = ensureDashboardBasePath(
|
const dashboardUrl = ensureDashboardBasePath(
|
||||||
process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard',
|
process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard',
|
||||||
)
|
)
|
||||||
const resetUrl = `${dashboardUrl}/reset-password?token=${rawToken}`
|
const resetUrl = `${dashboardUrl}/reset-password?token=${rawToken}`
|
||||||
const lang = (employee.preferredLanguage as Lang) ?? 'fr'
|
const lang = (employee.preferredLanguage as Lang) ?? 'fr'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ vi.mock('../../lib/storage', () => ({
|
|||||||
uploadImage: vi.fn().mockResolvedValue('http://localhost:4000/storage/test.jpg'),
|
uploadImage: vi.fn().mockResolvedValue('http://localhost:4000/storage/test.jpg'),
|
||||||
deleteImage: vi.fn().mockResolvedValue(undefined),
|
deleteImage: vi.fn().mockResolvedValue(undefined),
|
||||||
resolveStoredFilePath: vi.fn().mockReturnValue('/tmp/license.jpg'),
|
resolveStoredFilePath: vi.fn().mockReturnValue('/tmp/license.jpg'),
|
||||||
getProtectedCustomerLicenseImageUrl: vi.fn((customerId: string) => `http://localhost:3001/dashboard/api/v1/customers/${customerId}/license-image`),
|
getProtectedCustomerLicenseImageUrl: vi.fn((customerId: string) => `http://localhost:3000/dashboard/api/v1/customers/${customerId}/license-image`),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const mockCustomer = {
|
const mockCustomer = {
|
||||||
@@ -106,7 +106,7 @@ describe('customer.service', () => {
|
|||||||
vi.mocked(repo.findByIdSimple).mockResolvedValue(mockCustomer as any)
|
vi.mocked(repo.findByIdSimple).mockResolvedValue(mockCustomer as any)
|
||||||
vi.mocked(repo.updateById).mockResolvedValue({ ...mockCustomer, licenseImageUrl: 'http://localhost:4000/storage/test.jpg' } as any)
|
vi.mocked(repo.updateById).mockResolvedValue({ ...mockCustomer, licenseImageUrl: 'http://localhost:4000/storage/test.jpg' } as any)
|
||||||
const result = await service.uploadLicenseImage('cust_1', 'comp_1', { buffer: Buffer.from(''), mimetype: 'image/jpeg' } as any)
|
const result = await service.uploadLicenseImage('cust_1', 'comp_1', { buffer: Buffer.from(''), mimetype: 'image/jpeg' } as any)
|
||||||
expect(result.licenseImageUrl).toBe('http://localhost:3001/dashboard/api/v1/customers/cust_1/license-image')
|
expect(result.licenseImageUrl).toBe('http://localhost:3000/dashboard/api/v1/customers/cust_1/license-image')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('resolves a stored file path for protected downloads', async () => {
|
it('resolves a stored file path for protected downloads', async () => {
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ describe('serializeReservationForDashboard', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.customer?.licenseImageUrl).toBe('http://localhost:3001/dashboard/api/v1/customers/customer-1/license-image')
|
expect(result.customer?.licenseImageUrl).toBe('http://localhost:3000/dashboard/api/v1/customers/customer-1/license-image')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ describe('teamService inviteEmployee', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
process.env.DASHBOARD_URL = 'http://localhost:3001'
|
process.env.DASHBOARD_URL = 'http://localhost:3000/dashboard'
|
||||||
process.env.NEXT_PUBLIC_DASHBOARD_URL = ''
|
process.env.NEXT_PUBLIC_DASHBOARD_URL = ''
|
||||||
|
|
||||||
vi.spyOn(crypto, 'randomBytes').mockReturnValue(Buffer.from('token-123'))
|
vi.spyOn(crypto, 'randomBytes').mockReturnValue(Buffer.from('token-123'))
|
||||||
@@ -62,8 +62,8 @@ describe('teamService inviteEmployee', () => {
|
|||||||
expect(sendTransactionalEmail).toHaveBeenCalledWith(
|
expect(sendTransactionalEmail).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
to: 'aya@example.com',
|
to: 'aya@example.com',
|
||||||
html: expect.stringContaining('http://localhost:3001/dashboard/reset-password?token=746f6b656e2d313233'),
|
html: expect.stringContaining('http://localhost:3000/dashboard/reset-password?token=746f6b656e2d313233'),
|
||||||
text: expect.stringContaining('http://localhost:3001/dashboard/reset-password?token=746f6b656e2d313233'),
|
text: expect.stringContaining('http://localhost:3000/dashboard/reset-password?token=746f6b656e2d313233'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export async function inviteEmployee(companyId: string, inviterId: string, paylo
|
|||||||
})
|
})
|
||||||
|
|
||||||
const dashboardUrl = ensureDashboardBasePath(
|
const dashboardUrl = ensureDashboardBasePath(
|
||||||
process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard',
|
process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard',
|
||||||
)
|
)
|
||||||
const resetUrl = `${dashboardUrl}/reset-password?token=${rawToken}`
|
const resetUrl = `${dashboardUrl}/reset-password?token=${rawToken}`
|
||||||
const message = buildInviteEmail(resetUrl, company.name, payload.firstName, payload.role)
|
const message = buildInviteEmail(resetUrl, company.name, payload.firstName, payload.role)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DashboardLanguageSwitcher, DashboardThemeSwitcher } from '@/components/
|
|||||||
|
|
||||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-slate-50 transition-colors dark:bg-slate-950 print:block print:min-h-0 print:bg-white">
|
<div className="flex min-h-screen bg-[linear-gradient(180deg,#f8f5ef_0%,#f4efe6_28%,#fffdf8_58%,#ffffff_100%)] transition-colors dark:bg-[linear-gradient(180deg,#14110f_0%,#17120d_35%,#0c0a09_100%)] print:block print:min-h-0 print:bg-white">
|
||||||
<div className="print:hidden">
|
<div className="print:hidden">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
@@ -13,9 +13,9 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
<TopBar />
|
<TopBar />
|
||||||
</div>
|
</div>
|
||||||
<main className="flex-1 overflow-y-auto p-6 print:p-0 print:overflow-visible">{children}</main>
|
<main className="flex-1 overflow-y-auto p-6 print:p-0 print:overflow-visible">{children}</main>
|
||||||
<footer className="border-t border-stone-200 bg-white/90 px-6 py-4 backdrop-blur-md transition-colors dark:border-slate-800 dark:bg-slate-950/90 print:hidden">
|
<footer className="border-t border-stone-200/80 bg-white/72 px-6 py-4 backdrop-blur-xl transition-colors dark:border-stone-800 dark:bg-stone-950/72 print:hidden">
|
||||||
<div className="flex flex-col items-center justify-between gap-3 lg:flex-row">
|
<div className="flex flex-col items-center justify-between gap-3 lg:flex-row">
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.16em] text-stone-400 dark:text-slate-500">
|
<p className="text-xs font-medium uppercase tracking-[0.16em] text-stone-400 dark:text-stone-500">
|
||||||
Workspace preferences
|
Workspace preferences
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col items-center gap-3 sm:flex-row">
|
<div className="flex flex-col items-center gap-3 sm:flex-row">
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ html.dark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-slate-50 text-slate-900 antialiased transition-colors dark:bg-slate-950 dark:text-slate-100;
|
@apply text-stone-900 antialiased transition-colors dark:text-stone-100;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(180deg, #f8f5ef 0%, #f4efe6 28%, #fffdf8 58%, #ffffff 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
@@ -33,49 +35,61 @@ html.dark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.dark body {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(180deg, #14110f 0%, #17120d 35%, #0c0a09 100%);
|
||||||
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@apply inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
|
@apply inline-flex items-center gap-2 rounded-full bg-stone-950 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-stone-800 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
@apply inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800;
|
@apply inline-flex items-center gap-2 rounded-full border border-stone-300 bg-white/85 px-5 py-2.5 text-sm font-semibold text-stone-700 transition hover:border-stone-950 hover:text-stone-950 dark:border-stone-700 dark:bg-stone-950/30 dark:text-stone-200 dark:hover:border-stone-200 dark:hover:text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
@apply inline-flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50;
|
@apply inline-flex items-center gap-2 rounded-full bg-red-600 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-red-700 disabled:opacity-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
@apply w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 transition-colors placeholder:text-slate-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500;
|
@apply w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-amber-500 dark:border-stone-700 dark:bg-stone-950/80 dark:text-stone-100 dark:placeholder:text-stone-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@apply rounded-xl border border-slate-200 bg-white shadow-sm transition-colors dark:border-slate-800 dark:bg-slate-900;
|
@apply rounded-[1.75rem] border shadow-[0_30px_80px_rgba(28,25,23,0.08)] backdrop-blur transition-colors dark:shadow-[0_30px_80px_rgba(0,0,0,0.26)];
|
||||||
|
border-color: rgb(231 229 228 / 0.8);
|
||||||
|
background-color: rgb(255 255 255 / 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .card {
|
||||||
|
border-color: rgb(41 37 36 / 0.96);
|
||||||
|
background-color: rgb(28 25 23 / 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-green {
|
.badge-green {
|
||||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700;
|
@apply inline-flex items-center rounded-full bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-blue {
|
.badge-blue {
|
||||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700;
|
@apply inline-flex items-center rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-950/40 dark:text-amber-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-amber {
|
.badge-amber {
|
||||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-700;
|
@apply inline-flex items-center rounded-full bg-orange-100 px-2.5 py-0.5 text-xs font-medium text-orange-700 dark:bg-orange-950/40 dark:text-orange-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-red {
|
.badge-red {
|
||||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700;
|
@apply inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-700 dark:bg-red-950/40 dark:text-red-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-gray {
|
.badge-gray {
|
||||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-600;
|
@apply inline-flex items-center rounded-full bg-stone-100 px-2.5 py-0.5 text-xs font-medium text-stone-600 dark:bg-stone-800 dark:text-stone-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-purple {
|
.badge-purple {
|
||||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-700;
|
@apply inline-flex items-center rounded-full bg-teal-100 px-2.5 py-0.5 text-xs font-medium text-teal-700 dark:bg-teal-950/40 dark:text-teal-300;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,46 +106,82 @@ html.dark {
|
|||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.dark .bg-slate-50 {
|
.dark .bg-slate-50 {
|
||||||
background-color: rgb(2 6 23);
|
background-color: rgb(12 10 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .bg-white {
|
.dark .bg-white {
|
||||||
background-color: rgb(15 23 42);
|
background-color: rgb(28 25 23);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .bg-white\/90 {
|
.dark .bg-white\/90 {
|
||||||
background-color: rgb(15 23 42 / 0.9);
|
background-color: rgb(28 25 23 / 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .border-slate-200,
|
.dark .border-slate-200,
|
||||||
.dark .border-stone-200 {
|
.dark .border-stone-200 {
|
||||||
border-color: rgb(30 41 59);
|
border-color: rgb(41 37 36);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .text-slate-900 {
|
.dark .text-slate-900 {
|
||||||
color: rgb(241 245 249);
|
color: rgb(250 250 249);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .text-slate-700 {
|
.dark .text-slate-700 {
|
||||||
color: rgb(203 213 225);
|
color: rgb(231 229 228);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .text-slate-600,
|
.dark .text-slate-600,
|
||||||
.dark .text-stone-500 {
|
.dark .text-stone-500 {
|
||||||
color: rgb(148 163 184);
|
color: rgb(168 162 158);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .text-slate-500,
|
.dark .text-slate-500,
|
||||||
.dark .text-slate-400,
|
.dark .text-slate-400,
|
||||||
.dark .text-stone-400 {
|
.dark .text-stone-400 {
|
||||||
color: rgb(100 116 139);
|
color: rgb(120 113 108);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .hover\:bg-slate-100:hover {
|
.dark .hover\:bg-slate-100:hover {
|
||||||
background-color: rgb(30 41 59);
|
background-color: rgb(41 37 36);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .hover\:text-slate-900:hover {
|
.dark .hover\:text-slate-900:hover {
|
||||||
color: rgb(248 250 252);
|
color: rgb(250 250 249);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-blue-600 {
|
||||||
|
background-color: rgb(217 119 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-blue-700:hover {
|
||||||
|
background-color: rgb(180 83 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-blue-500 {
|
||||||
|
background-color: rgb(245 158 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-blue-50 {
|
||||||
|
background-color: rgb(255 251 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-blue-600 {
|
||||||
|
color: rgb(180 83 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-blue-800 {
|
||||||
|
color: rgb(154 52 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-blue-200 {
|
||||||
|
border-color: rgb(253 230 138);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring-blue-400 {
|
||||||
|
--tw-ring-color: rgb(251 191 36 / 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus\:ring-blue-500:focus {
|
||||||
|
--tw-ring-color: rgb(217 119 6 / 0.45);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,14 +141,17 @@ export default function SignInPageClient({ embedded = false }: { embedded?: bool
|
|||||||
width={104}
|
width={104}
|
||||||
height={104}
|
height={104}
|
||||||
priority
|
priority
|
||||||
className="h-24 w-24 rounded-[1.75rem] border border-blue-100 bg-white p-1.5 shadow-sm transition-colors dark:border-slate-700 dark:bg-slate-900"
|
className="h-24 w-24 rounded-[1.75rem] border border-stone-200/80 bg-white/85 p-1.5 shadow-sm transition-colors dark:border-stone-700 dark:bg-stone-900/80"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mt-4 text-3xl font-black tracking-tight text-slate-900 dark:text-slate-100">
|
<p className="mt-5 text-xs font-bold uppercase tracking-[0.28em] text-amber-700 dark:text-amber-300">
|
||||||
|
RentalDriveGo
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-3 text-3xl font-black tracking-[-0.03em] text-stone-950 dark:text-stone-50">
|
||||||
{dict.brandName}
|
{dict.brandName}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
<p className="mt-2 text-sm text-stone-600 dark:text-stone-300">
|
||||||
{dict.subtitle}
|
{dict.subtitle}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 flex items-center justify-center gap-2">
|
<div className="mt-4 flex items-center justify-center gap-2">
|
||||||
@@ -157,10 +160,10 @@ export default function SignInPageClient({ embedded = false }: { embedded?: bool
|
|||||||
key={lang}
|
key={lang}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setLanguage(lang)}
|
onClick={() => setLanguage(lang)}
|
||||||
className={`rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wider transition-colors text-blue-600 dark:text-amber-400 ${
|
className={`rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wider transition-colors text-amber-700 dark:text-amber-300 ${
|
||||||
language === lang
|
language === lang
|
||||||
? 'ring-1 ring-current opacity-100'
|
? 'ring-1 ring-current opacity-100'
|
||||||
: 'opacity-40 hover:opacity-80'
|
: 'opacity-45 hover:opacity-80'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{lang}
|
{lang}
|
||||||
@@ -169,7 +172,7 @@ export default function SignInPageClient({ embedded = false }: { embedded?: bool
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm transition-colors dark:border-slate-800 dark:bg-slate-900/90">
|
<section className="rounded-[2rem] border border-stone-200/80 bg-white/82 p-8 shadow-[0_30px_80px_rgba(28,25,23,0.08)] backdrop-blur transition-colors dark:border-stone-800 dark:bg-stone-900/78 dark:shadow-[0_30px_80px_rgba(0,0,0,0.26)]">
|
||||||
<LocalSignInForm dict={dict} />
|
<LocalSignInForm dict={dict} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,7 +308,7 @@ function LocalSignInForm({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="mb-5 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
<div className="mb-5 rounded-[1.5rem] border border-red-200/80 bg-red-50/90 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-300">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -313,19 +316,19 @@ function LocalSignInForm({
|
|||||||
{step === 'credentials' ? (
|
{step === 'credentials' ? (
|
||||||
<form onSubmit={handleCredentials} className="space-y-5">
|
<form onSubmit={handleCredentials} className="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.email}</label>
|
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">{dict.email}</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="owner@company.com"
|
placeholder="owner@company.com"
|
||||||
className="input-field"
|
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-amber-500 dark:border-stone-700 dark:bg-stone-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.password}</label>
|
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">{dict.password}</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
@@ -333,12 +336,12 @@ function LocalSignInForm({
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
className="input-field pr-10"
|
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 pr-10 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-amber-500 dark:border-stone-700 dark:bg-stone-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword((v) => !v)}
|
onClick={() => setShowPassword((v) => !v)}
|
||||||
className="absolute inset-y-0 right-3 flex items-center text-slate-400 hover:text-slate-600"
|
className="absolute inset-y-0 right-3 flex items-center text-stone-400 hover:text-stone-700 dark:text-stone-500 dark:hover:text-stone-200"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{showPassword ? (
|
{showPassword ? (
|
||||||
@@ -358,25 +361,25 @@ function LocalSignInForm({
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="btn-primary w-full justify-center disabled:cursor-not-allowed disabled:opacity-60"
|
className="inline-flex w-full justify-center rounded-full bg-stone-950 px-6 py-3 text-sm font-semibold text-white transition hover:bg-stone-800 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300"
|
||||||
>
|
>
|
||||||
{loading ? dict.signingIn : dict.signIn}
|
{loading ? dict.signingIn : dict.signIn}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Link href="/forgot-password" className="text-sm text-slate-500 underline decoration-slate-300 underline-offset-4 hover:text-slate-700">
|
<Link href="/forgot-password" className="text-sm text-stone-500 underline decoration-stone-300 underline-offset-4 hover:text-stone-900 dark:text-stone-400 dark:decoration-stone-600 dark:hover:text-stone-100">
|
||||||
{dict.forgotPassword}
|
{dict.forgotPassword}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleTotp} className="space-y-5">
|
<form onSubmit={handleTotp} className="space-y-5">
|
||||||
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-center text-sm text-slate-600">
|
<div className="rounded-[1.5rem] border border-stone-200/80 bg-stone-50/90 px-4 py-4 text-center text-sm text-stone-600 dark:border-stone-700 dark:bg-stone-950/50 dark:text-stone-300">
|
||||||
{dict.enterCode}
|
{dict.enterCode}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-slate-700">{dict.authCode}</label>
|
<label className="mb-1.5 block text-sm font-medium text-stone-700 dark:text-stone-200">{dict.authCode}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
@@ -386,14 +389,14 @@ function LocalSignInForm({
|
|||||||
value={totpCode}
|
value={totpCode}
|
||||||
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, ''))}
|
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, ''))}
|
||||||
placeholder="000000"
|
placeholder="000000"
|
||||||
className="input-field text-center text-xl tracking-[0.45em]"
|
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-center text-xl tracking-[0.45em] text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-amber-500 dark:border-stone-700 dark:bg-stone-950/80 dark:text-stone-100 dark:placeholder:text-stone-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="btn-primary w-full justify-center disabled:cursor-not-allowed disabled:opacity-60"
|
className="inline-flex w-full justify-center rounded-full bg-stone-950 px-6 py-3 text-sm font-semibold text-white transition hover:bg-stone-800 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300"
|
||||||
>
|
>
|
||||||
{loading ? dict.verifying : dict.verify}
|
{loading ? dict.verifying : dict.verify}
|
||||||
</button>
|
</button>
|
||||||
@@ -405,7 +408,7 @@ function LocalSignInForm({
|
|||||||
setTotpCode('')
|
setTotpCode('')
|
||||||
setError(null)
|
setError(null)
|
||||||
}}
|
}}
|
||||||
className="w-full text-sm text-slate-500 hover:text-slate-700"
|
className="w-full text-sm text-stone-500 hover:text-stone-900 dark:text-stone-400 dark:hover:text-stone-100"
|
||||||
>
|
>
|
||||||
{dict.back}
|
{dict.back}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { headers } from 'next/headers'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
import SignInPageClient from './SignInPageClient'
|
import SignInPageClient from './SignInPageClient'
|
||||||
|
|
||||||
export default async function SignInPage({
|
export default async function SignInPage({
|
||||||
@@ -8,5 +10,27 @@ export default async function SignInPage({
|
|||||||
const params = await searchParams
|
const params = await searchParams
|
||||||
const embedded = params.embedded === '1'
|
const embedded = params.embedded === '1'
|
||||||
|
|
||||||
|
if (!embedded) {
|
||||||
|
const requestHeaders = headers()
|
||||||
|
const host = requestHeaders.get('host')
|
||||||
|
const proto = requestHeaders.get('x-forwarded-proto') ?? 'http'
|
||||||
|
const query = new URLSearchParams()
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (value === undefined) continue
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) query.append(key, item)
|
||||||
|
} else {
|
||||||
|
query.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const destination = host
|
||||||
|
? `${proto}://${host}/sign-in${query.size ? `?${query.toString()}` : ''}`
|
||||||
|
: `${process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'}/sign-in${query.size ? `?${query.toString()}` : ''}`
|
||||||
|
|
||||||
|
redirect(destination)
|
||||||
|
}
|
||||||
|
|
||||||
return <SignInPageClient embedded={embedded} />
|
return <SignInPageClient embedded={embedded} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1513,8 +1513,8 @@ export function DashboardLanguageSwitcher() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm transition-colors dark:border-slate-700 dark:bg-slate-900">
|
<div className="flex items-center gap-1 rounded-full border border-stone-200/80 bg-white/95 px-2 py-1 shadow-sm transition-colors dark:border-stone-700 dark:bg-stone-900/85">
|
||||||
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
|
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-stone-500 dark:text-stone-400">
|
||||||
{dict.language}
|
{dict.language}
|
||||||
</span>
|
</span>
|
||||||
{(['en', 'fr', 'ar'] as DashboardLanguage[]).map((value) => {
|
{(['en', 'fr', 'ar'] as DashboardLanguage[]).map((value) => {
|
||||||
@@ -1526,8 +1526,8 @@ export function DashboardLanguageSwitcher() {
|
|||||||
onClick={() => handleLanguageChange(value)}
|
onClick={() => handleLanguageChange(value)}
|
||||||
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
||||||
active
|
active
|
||||||
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-950'
|
? 'bg-stone-900 text-white dark:bg-amber-400 dark:text-stone-950'
|
||||||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
|
: 'text-stone-600 hover:bg-stone-100 dark:text-stone-300 dark:hover:bg-stone-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{value.toUpperCase()}
|
{value.toUpperCase()}
|
||||||
@@ -1549,10 +1549,7 @@ export function DashboardThemeSwitcher() {
|
|||||||
if (embedded) return null
|
if (embedded) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm transition-colors dark:border-slate-700 dark:bg-slate-900">
|
<div className="flex items-center gap-1 rounded-full border border-stone-200/80 bg-white/95 px-2 py-1 shadow-sm transition-colors dark:border-stone-700 dark:bg-stone-900/85">
|
||||||
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
|
|
||||||
{dict.theme}
|
|
||||||
</span>
|
|
||||||
{(['light', 'dark'] as DashboardTheme[]).map((value) => {
|
{(['light', 'dark'] as DashboardTheme[]).map((value) => {
|
||||||
const active = value === theme
|
const active = value === theme
|
||||||
return (
|
return (
|
||||||
@@ -1562,8 +1559,8 @@ export function DashboardThemeSwitcher() {
|
|||||||
onClick={() => setTheme(value)}
|
onClick={() => setTheme(value)}
|
||||||
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
|
||||||
active
|
active
|
||||||
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-950'
|
? 'bg-stone-900 text-white dark:bg-amber-400 dark:text-stone-950'
|
||||||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
|
: 'text-stone-600 hover:bg-stone-100 dark:text-stone-300 dark:hover:bg-stone-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{value === 'light' ? dict.light : dict.dark}
|
{value === 'light' ? dict.light : dict.dark}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function PublicFooter() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-stone-200 bg-white/90 px-4 py-8 text-stone-600 transition-colors dark:border-white/5 dark:bg-[#111111] dark:text-stone-300">
|
<footer className="border-t border-stone-200/80 bg-white/72 px-4 py-8 text-stone-600 backdrop-blur-xl transition-colors dark:border-stone-800 dark:bg-stone-950/72 dark:text-stone-300">
|
||||||
<div className="mx-auto flex max-w-7xl flex-col items-center gap-5 px-0 text-center sm:px-2 lg:px-0">
|
<div className="mx-auto flex max-w-7xl flex-col items-center gap-5 px-0 text-center sm:px-2 lg:px-0">
|
||||||
<nav className="flex flex-wrap items-center justify-center gap-y-3 text-sm">
|
<nav className="flex flex-wrap items-center justify-center gap-y-3 text-sm">
|
||||||
{footerContent.primary.map((item, index) => (
|
{footerContent.primary.map((item, index) => (
|
||||||
@@ -129,7 +129,7 @@ export default function PublicFooter() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setLocaleMenuOpen((open) => !open)}
|
onClick={() => setLocaleMenuOpen((open) => !open)}
|
||||||
className="inline-flex items-center gap-2 text-sky-600 transition hover:text-sky-700 dark:text-sky-400 dark:hover:text-sky-300"
|
className="inline-flex items-center gap-2 text-amber-700 transition hover:text-stone-950 dark:text-amber-300 dark:hover:text-stone-100"
|
||||||
aria-expanded={localeMenuOpen}
|
aria-expanded={localeMenuOpen}
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export default function PublicHeader() {
|
|||||||
createAccount: 'Create Agency Space',
|
createAccount: 'Create Agency Space',
|
||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
light: 'Light',
|
light: 'Light',
|
||||||
medium: 'Medium',
|
|
||||||
dark: 'Dark',
|
dark: 'Dark',
|
||||||
},
|
},
|
||||||
fr: {
|
fr: {
|
||||||
@@ -45,7 +44,6 @@ export default function PublicHeader() {
|
|||||||
createAccount: 'Créer un espace d’agence',
|
createAccount: 'Créer un espace d’agence',
|
||||||
theme: 'Mode',
|
theme: 'Mode',
|
||||||
light: 'Clair',
|
light: 'Clair',
|
||||||
medium: 'Moyen',
|
|
||||||
dark: 'Sombre',
|
dark: 'Sombre',
|
||||||
},
|
},
|
||||||
ar: {
|
ar: {
|
||||||
@@ -55,13 +53,11 @@ export default function PublicHeader() {
|
|||||||
createAccount: 'إنشاء مساحة الوكالة',
|
createAccount: 'إنشاء مساحة الوكالة',
|
||||||
theme: 'الوضع',
|
theme: 'الوضع',
|
||||||
light: 'فاتح',
|
light: 'فاتح',
|
||||||
medium: 'متوسط',
|
|
||||||
dark: 'داكن',
|
dark: 'داكن',
|
||||||
},
|
},
|
||||||
}[language]
|
}[language]
|
||||||
const themeOptions = [
|
const themeOptions = [
|
||||||
{ value: 'light' as const, label: dict.light },
|
{ value: 'light' as const, label: dict.light },
|
||||||
{ value: 'medium' as const, label: dict.medium },
|
|
||||||
{ value: 'dark' as const, label: dict.dark },
|
{ value: 'dark' as const, label: dict.dark },
|
||||||
]
|
]
|
||||||
const currentTheme = themeOptions.find((option) => option.value === theme) ?? themeOptions[0]
|
const currentTheme = themeOptions.find((option) => option.value === theme) ?? themeOptions[0]
|
||||||
@@ -101,7 +97,7 @@ export default function PublicHeader() {
|
|||||||
const redirect = searchParams.get('redirect')
|
const redirect = searchParams.get('redirect')
|
||||||
if (next) signInParams.set('next', next)
|
if (next) signInParams.set('next', next)
|
||||||
if (redirect) signInParams.set('redirect', redirect)
|
if (redirect) signInParams.set('redirect', redirect)
|
||||||
const signInHref = `/sign-in?${signInParams.toString()}`
|
const signInHref = `${marketplaceUrl}/sign-in?${signInParams.toString()}`
|
||||||
|
|
||||||
const signUpParams = new URLSearchParams()
|
const signUpParams = new URLSearchParams()
|
||||||
signUpParams.set('lang', language)
|
signUpParams.set('lang', language)
|
||||||
@@ -109,7 +105,7 @@ export default function PublicHeader() {
|
|||||||
const signUpHref = `/sign-up?${signUpParams.toString()}`
|
const signUpHref = `/sign-up?${signUpParams.toString()}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-30 border-b border-stone-200 bg-white shadow-[0_10px_30px_rgba(15,23,42,0.06)] transition-colors dark:border-stone-800 dark:bg-stone-950 dark:shadow-[0_10px_30px_rgba(0,0,0,0.28)]">
|
<header className="sticky top-0 z-30 border-b border-stone-200/80 bg-white/78 backdrop-blur-xl shadow-[0_10px_30px_rgba(15,23,42,0.06)] transition-colors dark:border-stone-800 dark:bg-stone-950/76 dark:shadow-[0_10px_30px_rgba(0,0,0,0.28)]">
|
||||||
<div className="mx-auto flex max-w-7xl flex-col gap-1.5 px-4 py-1.5 sm:px-6 lg:flex-row lg:items-center lg:justify-between lg:gap-3 lg:px-8 lg:py-2">
|
<div className="mx-auto flex max-w-7xl flex-col gap-1.5 px-4 py-1.5 sm:px-6 lg:flex-row lg:items-center lg:justify-between lg:gap-3 lg:px-8 lg:py-2">
|
||||||
<a href={marketplaceUrl} className="flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-[0.14em] text-stone-900 dark:text-stone-100 sm:text-sm sm:tracking-[0.2em]">
|
<a href={marketplaceUrl} className="flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-[0.14em] text-stone-900 dark:text-stone-100 sm:text-sm sm:tracking-[0.2em]">
|
||||||
<Image
|
<Image
|
||||||
@@ -131,9 +127,9 @@ export default function PublicHeader() {
|
|||||||
<a href={`${marketplaceUrl}/explore`} className="rounded-full px-2.5 py-1 text-[12px] font-medium text-stone-600 transition hover:bg-stone-100 hover:text-stone-900 dark:text-stone-300 dark:hover:bg-stone-800 dark:hover:text-stone-100 sm:px-4 sm:py-2 sm:text-sm">
|
<a href={`${marketplaceUrl}/explore`} className="rounded-full px-2.5 py-1 text-[12px] font-medium text-stone-600 transition hover:bg-stone-100 hover:text-stone-900 dark:text-stone-300 dark:hover:bg-stone-800 dark:hover:text-stone-100 sm:px-4 sm:py-2 sm:text-sm">
|
||||||
{dict.marketplace}
|
{dict.marketplace}
|
||||||
</a>
|
</a>
|
||||||
<Link href={signInHref} className="rounded-full px-2.5 py-1 text-[12px] font-medium text-stone-600 transition hover:bg-stone-100 hover:text-stone-900 dark:text-stone-300 dark:hover:bg-stone-800 dark:hover:text-stone-100 sm:px-4 sm:py-2 sm:text-sm">
|
<a href={signInHref} className="rounded-full px-2.5 py-1 text-[12px] font-medium text-stone-600 transition hover:bg-stone-100 hover:text-stone-900 dark:text-stone-300 dark:hover:bg-stone-800 dark:hover:text-stone-100 sm:px-4 sm:py-2 sm:text-sm">
|
||||||
{dict.signIn}
|
{dict.signIn}
|
||||||
</Link>
|
</a>
|
||||||
<Link href={signUpHref} className="ml-1 rounded-full bg-stone-900 px-3 py-1 text-[12px] font-semibold text-white transition hover:bg-stone-700 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300 sm:ml-2 sm:px-5 sm:py-2 sm:text-sm">
|
<Link href={signUpHref} className="ml-1 rounded-full bg-stone-900 px-3 py-1 text-[12px] font-semibold text-white transition hover:bg-stone-700 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300 sm:ml-2 sm:px-5 sm:py-2 sm:text-sm">
|
||||||
{dict.createAccount}
|
{dict.createAccount}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -176,13 +172,10 @@ export default function PublicHeader() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleThemeMenu}
|
onClick={toggleThemeMenu}
|
||||||
className="inline-flex items-center gap-1.5 rounded-full px-1.5 py-1 transition hover:bg-stone-100 dark:hover:bg-stone-800 sm:gap-2 sm:px-2 sm:py-1.5"
|
className="inline-flex items-center gap-1.5 rounded-full px-2 py-1 transition hover:bg-stone-100 dark:hover:bg-stone-800 sm:px-2.5 sm:py-1.5"
|
||||||
aria-expanded={themeMenuOpen}
|
aria-expanded={themeMenuOpen}
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
>
|
>
|
||||||
<span className="px-1.5 text-[10px] font-semibold uppercase tracking-[0.16em] text-stone-500 dark:text-stone-400 sm:px-2 sm:text-[11px] sm:tracking-[0.2em]">
|
|
||||||
{dict.theme}
|
|
||||||
</span>
|
|
||||||
<span className="rounded-full bg-stone-900 px-3 py-1 text-[11px] font-semibold text-white dark:bg-amber-400 dark:text-stone-950 sm:px-3.5 sm:py-1.5 sm:text-xs">
|
<span className="rounded-full bg-stone-900 px-3 py-1 text-[11px] font-semibold text-white dark:bg-amber-400 dark:text-stone-950 sm:px-3.5 sm:py-1.5 sm:text-xs">
|
||||||
{currentTheme.label}
|
{currentTheme.label}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function PublicShell({
|
|||||||
embedded?: boolean
|
embedded?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-[radial-gradient(circle_at_top,#dbeafe,transparent_35%),linear-gradient(180deg,#f8fafc,white)] transition-colors dark:bg-[radial-gradient(circle_at_top,rgba(37,99,235,0.18),transparent_35%),linear-gradient(180deg,#020617,#0f172a)] dark:text-slate-100">
|
<div className="flex min-h-screen flex-col bg-[linear-gradient(180deg,#f8f5ef_0%,#f4efe6_28%,#fffdf8_58%,#ffffff_100%)] text-stone-900 transition-colors dark:bg-[linear-gradient(180deg,#14110f_0%,#17120d_35%,#0c0a09_100%)] dark:text-stone-100">
|
||||||
{embedded ? null : <PublicHeader />}
|
{embedded ? null : <PublicHeader />}
|
||||||
<div className="flex flex-1 flex-col">{children}</div>
|
<div className="flex flex-1 flex-col">{children}</div>
|
||||||
{embedded ? null : <PublicFooter />}
|
{embedded ? null : <PublicFooter />}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export default function Sidebar() {
|
|||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
aria-label="Open sidebar"
|
aria-label="Open sidebar"
|
||||||
className={[
|
className={[
|
||||||
'fixed top-1/2 z-50 -translate-y-1/2 flex items-center justify-center w-6 h-14 bg-slate-900 text-white shadow-lg lg:hidden',
|
'fixed top-1/2 z-50 flex h-14 w-6 -translate-y-1/2 items-center justify-center bg-stone-950 text-white shadow-lg lg:hidden',
|
||||||
isRtl ? 'right-0 rounded-l-lg' : 'left-0 rounded-r-lg',
|
isRtl ? 'right-0 rounded-l-lg' : 'left-0 rounded-r-lg',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
@@ -190,19 +190,19 @@ export default function Sidebar() {
|
|||||||
|
|
||||||
<aside
|
<aside
|
||||||
className={[
|
className={[
|
||||||
'fixed inset-y-0 z-40 flex w-64 flex-col bg-slate-900 transition-transform duration-300',
|
'fixed inset-y-0 z-40 flex w-64 flex-col border-r border-stone-800/80 bg-stone-950/94 backdrop-blur-xl transition-transform duration-300',
|
||||||
isRtl ? 'right-0' : 'left-0',
|
isRtl ? 'right-0' : 'left-0',
|
||||||
'lg:translate-x-0',
|
'lg:translate-x-0',
|
||||||
open ? 'translate-x-0' : (isRtl ? 'translate-x-full' : '-translate-x-full'),
|
open ? 'translate-x-0' : (isRtl ? 'translate-x-full' : '-translate-x-full'),
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<a href={marketplaceUrl} target="_top" className="flex items-center gap-3 border-b border-slate-800 px-6 py-5">
|
<a href={marketplaceUrl} target="_top" className="flex items-center gap-3 border-b border-stone-800/80 px-6 py-5">
|
||||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-500 overflow-hidden">
|
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg bg-amber-400">
|
||||||
{brand?.logoUrl ? (
|
{brand?.logoUrl ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img src={brand.logoUrl} alt={brand.displayName} className="h-8 w-8 object-cover" />
|
<img src={brand.logoUrl} alt={brand.displayName} className="h-8 w-8 object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<Car className="h-4 w-4 text-white" />
|
<Car className="h-4 w-4 text-stone-950" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="truncate text-sm font-bold tracking-wide text-white">
|
<span className="truncate text-sm font-bold tracking-wide text-white">
|
||||||
@@ -219,8 +219,8 @@ export default function Sidebar() {
|
|||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={[
|
className={[
|
||||||
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
|
'flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-medium transition-colors',
|
||||||
active ? 'bg-blue-600 text-white' : 'text-slate-400 hover:bg-slate-800 hover:text-white',
|
active ? 'bg-amber-400 text-stone-950' : 'text-stone-400 hover:bg-stone-800 hover:text-white',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<Icon className="h-4 w-4 flex-shrink-0" />
|
<Icon className="h-4 w-4 flex-shrink-0" />
|
||||||
@@ -230,19 +230,19 @@ export default function Sidebar() {
|
|||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="border-t border-slate-800 px-3 py-4">
|
<div className="border-t border-stone-800/80 px-3 py-4">
|
||||||
<div className="flex items-center gap-3 rounded-lg px-3 py-2">
|
<div className="flex items-center gap-3 rounded-2xl px-3 py-2">
|
||||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-xs font-semibold text-white">
|
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-amber-400 text-xs font-semibold text-stone-950">
|
||||||
{user.initials}
|
{user.initials}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-sm font-medium text-white">{user.displayName}</p>
|
<p className="truncate text-sm font-medium text-white">{user.displayName}</p>
|
||||||
<p className="truncate text-xs text-slate-400">{user.subtitle}</p>
|
<p className="truncate text-xs text-stone-400">{user.subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={signOut}
|
onClick={signOut}
|
||||||
className="mt-2 flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-slate-400 transition-colors hover:bg-slate-800 hover:text-white"
|
className="mt-2 flex w-full items-center gap-3 rounded-2xl px-3 py-2 text-sm text-stone-400 transition-colors hover:bg-stone-800 hover:text-white"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4" />
|
<LogOut className="h-4 w-4" />
|
||||||
{dict.signOut}
|
{dict.signOut}
|
||||||
@@ -254,7 +254,7 @@ export default function Sidebar() {
|
|||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
aria-label="Close sidebar"
|
aria-label="Close sidebar"
|
||||||
className={[
|
className={[
|
||||||
'absolute top-1/2 z-10 -translate-y-1/2 flex items-center justify-center w-5 h-14 bg-slate-900 text-white shadow-lg lg:hidden',
|
'absolute top-1/2 z-10 flex h-14 w-5 -translate-y-1/2 items-center justify-center bg-stone-950 text-white shadow-lg lg:hidden',
|
||||||
isRtl ? '-left-5 rounded-l-lg' : '-right-5 rounded-r-lg',
|
isRtl ? '-left-5 rounded-l-lg' : '-right-5 rounded-r-lg',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -143,14 +143,14 @@ export default function TopBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex h-16 items-center justify-between border-b border-slate-200 bg-white px-6 transition-colors dark:border-slate-800 dark:bg-slate-950">
|
<header className="flex h-16 items-center justify-between border-b border-stone-200/80 bg-white/78 px-6 backdrop-blur-xl transition-colors dark:border-stone-800 dark:bg-stone-950/76">
|
||||||
<h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">{title}</h1>
|
<h1 className="text-lg font-semibold text-stone-950 dark:text-stone-50">{title}</h1>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowNotifs(!showNotifs)}
|
onClick={() => setShowNotifs(!showNotifs)}
|
||||||
className="relative rounded-lg p-2 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
|
className="relative rounded-2xl p-2 text-stone-500 transition-colors hover:bg-stone-100 hover:text-stone-700 dark:text-stone-400 dark:hover:bg-stone-800 dark:hover:text-stone-100"
|
||||||
>
|
>
|
||||||
<Bell className="h-5 w-5" />
|
<Bell className="h-5 w-5" />
|
||||||
{unreadCount > 0 ? (
|
{unreadCount > 0 ? (
|
||||||
@@ -161,22 +161,22 @@ export default function TopBar() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showNotifs ? (
|
{showNotifs ? (
|
||||||
<div className="absolute right-0 top-full z-50 mt-2 w-80 rounded-xl border border-slate-200 bg-white p-4 shadow-lg dark:border-slate-700 dark:bg-slate-900">
|
<div className="absolute right-0 top-full z-50 mt-2 w-80 rounded-[1.75rem] border border-stone-200/80 bg-white/95 p-4 shadow-[0_20px_60px_rgba(0,0,0,0.12)] backdrop-blur dark:border-stone-800 dark:bg-stone-950/95 dark:shadow-[0_20px_60px_rgba(0,0,0,0.35)]">
|
||||||
<div className="mb-2 flex items-center justify-between gap-2">
|
<div className="mb-2 flex items-center justify-between gap-2">
|
||||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">{dict.notifications}</p>
|
<p className="text-sm font-medium text-stone-950 dark:text-stone-100">{dict.notifications}</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openNotificationsInbox()}
|
onClick={() => openNotificationsInbox()}
|
||||||
className="text-xs font-medium text-blue-600 hover:text-blue-700"
|
className="text-xs font-medium text-amber-700 hover:text-stone-950 dark:text-amber-300 dark:hover:text-white"
|
||||||
>
|
>
|
||||||
View all
|
View all
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loadingNotifs ? (
|
{loadingNotifs ? (
|
||||||
<p className="text-sm text-slate-500 dark:text-slate-400">Loading…</p>
|
<p className="text-sm text-stone-500 dark:text-stone-400">Loading…</p>
|
||||||
) : notifications.length === 0 ? (
|
) : notifications.length === 0 ? (
|
||||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
<p className="text-sm text-stone-500 dark:text-stone-400">
|
||||||
{unreadCount === 0 ? dict.noNewNotifications : dict.unreadNotifications(unreadCount)}
|
{unreadCount === 0 ? dict.noNewNotifications : dict.unreadNotifications(unreadCount)}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -187,14 +187,14 @@ export default function TopBar() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => openNotificationsInbox(notification.id)}
|
onClick={() => openNotificationsInbox(notification.id)}
|
||||||
className={[
|
className={[
|
||||||
'w-full rounded-lg px-3 py-2 text-left transition-colors hover:bg-slate-100 dark:hover:bg-slate-800',
|
'w-full rounded-2xl px-3 py-2 text-left transition-colors hover:bg-stone-100 dark:hover:bg-stone-800',
|
||||||
notification.readAt ? 'opacity-80' : '',
|
notification.readAt ? 'opacity-80' : '',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<p className="truncate text-sm font-medium text-slate-900 dark:text-slate-100">
|
<p className="truncate text-sm font-medium text-stone-950 dark:text-stone-100">
|
||||||
{notification.title}
|
{notification.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-xs text-slate-500 dark:text-slate-400">
|
<p className="truncate text-xs text-stone-500 dark:text-stone-400">
|
||||||
{notification.body}
|
{notification.body}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -205,7 +205,7 @@ export default function TopBar() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 text-xs font-semibold text-white">
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-amber-400 text-xs font-semibold text-stone-950">
|
||||||
{userInitials}
|
{userInitials}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export default function StatCard({
|
|||||||
change,
|
change,
|
||||||
vsLastMonthLabel = 'vs last month',
|
vsLastMonthLabel = 'vs last month',
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
iconColor = 'text-blue-600',
|
iconColor = 'text-amber-700 dark:text-amber-300',
|
||||||
iconBg = 'bg-blue-50',
|
iconBg = 'bg-amber-50 dark:bg-amber-950/30',
|
||||||
}: StatCardProps) {
|
}: StatCardProps) {
|
||||||
const isPositive = change !== undefined && change >= 0
|
const isPositive = change !== undefined && change >= 0
|
||||||
const isNegative = change !== undefined && change < 0
|
const isNegative = change !== undefined && change < 0
|
||||||
@@ -26,24 +26,24 @@ export default function StatCard({
|
|||||||
<div className="card p-6">
|
<div className="card p-6">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-slate-500 font-medium">{title}</p>
|
<p className="text-sm font-medium text-stone-500 dark:text-stone-400">{title}</p>
|
||||||
<p className="text-2xl font-bold text-slate-900 mt-1">{value}</p>
|
<p className="mt-1 text-2xl font-bold text-stone-950 dark:text-stone-50">{value}</p>
|
||||||
{change !== undefined && (
|
{change !== undefined && (
|
||||||
<div className="flex items-center gap-1 mt-2">
|
<div className="mt-2 flex items-center gap-1">
|
||||||
<span
|
<span
|
||||||
className={[
|
className={[
|
||||||
'text-xs font-medium',
|
'text-xs font-medium',
|
||||||
isPositive ? 'text-green-600' : isNegative ? 'text-red-600' : 'text-slate-500',
|
isPositive ? 'text-emerald-600 dark:text-emerald-300' : isNegative ? 'text-red-600 dark:text-red-300' : 'text-stone-500 dark:text-stone-400',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{isPositive ? '+' : ''}{change.toFixed(1)}%
|
{isPositive ? '+' : ''}{change.toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-slate-400">{vsLastMonthLabel}</span>
|
<span className="text-xs text-stone-400 dark:text-stone-500">{vsLastMonthLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={`w-12 h-12 rounded-xl ${iconBg} flex items-center justify-center flex-shrink-0`}>
|
<div className={`flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-[1.25rem] ${iconBg}`}>
|
||||||
<Icon className={`w-6 h-6 ${iconColor}`} />
|
<Icon className={`h-6 w-6 ${iconColor}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ export const marketplaceUrl = toAppBase(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const adminUrl = toAppBase(
|
export const adminUrl = toAppBase(
|
||||||
resolveBrowserAppUrl(process.env.NEXT_PUBLIC_ADMIN_URL ?? 'http://localhost:3002/admin'),
|
resolveBrowserAppUrl(process.env.NEXT_PUBLIC_ADMIN_URL ?? 'http://localhost:3000/admin'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,29 @@ const nextConfig = {
|
|||||||
domains: ['res.cloudinary.com'],
|
domains: ['res.cloudinary.com'],
|
||||||
},
|
},
|
||||||
transpilePackages: ['@rentaldrivego/types'],
|
transpilePackages: ['@rentaldrivego/types'],
|
||||||
|
async rewrites() {
|
||||||
|
const dashboardOrigin = process.env.DASHBOARD_INTERNAL_URL ?? 'http://dashboard:3001'
|
||||||
|
const adminOrigin = process.env.ADMIN_INTERNAL_URL ?? 'http://admin:3002'
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/dashboard',
|
||||||
|
destination: `${dashboardOrigin}/dashboard`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/dashboard/:path*',
|
||||||
|
destination: `${dashboardOrigin}/dashboard/:path*`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/admin',
|
||||||
|
destination: `${adminOrigin}/admin`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/admin/:path*',
|
||||||
|
destination: `${adminOrigin}/admin/:path*`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function HomeContent() {
|
|||||||
const [content, setContent] = useState<MarketplaceHomepageConfig>(cloneMarketplaceHomepageContent())
|
const [content, setContent] = useState<MarketplaceHomepageConfig>(cloneMarketplaceHomepageContent())
|
||||||
const dict = content[language]
|
const dict = content[language]
|
||||||
const sections = resolveMarketplaceHomepageSections(dict.sections)
|
const sections = resolveMarketplaceHomepageSections(dict.sections)
|
||||||
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard')
|
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard')
|
||||||
const starterSignupHref = `${dashboardUrl}/sign-up?plan=STARTER&billing=MONTHLY¤cy=MAD`
|
const starterSignupHref = `${dashboardUrl}/sign-up?plan=STARTER&billing=MONTHLY¤cy=MAD`
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -46,9 +46,9 @@ export default function HomeContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen overflow-hidden bg-[linear-gradient(180deg,#f8f5ef_0%,#f4efe6_28%,#fffdf8_58%,#ffffff_100%)] dark:bg-[linear-gradient(180deg,#14110f_0%,#17120d_35%,#0c0a09_100%)]">
|
<main className="min-h-screen overflow-hidden bg-transparent">
|
||||||
<section className="relative">
|
<section className="relative">
|
||||||
<div className="absolute inset-x-0 top-0 h-[38rem] bg-[radial-gradient(circle_at_top_left,rgba(217,119,6,0.24),transparent_34%),radial-gradient(circle_at_top_right,rgba(15,118,110,0.18),transparent_26%)] dark:bg-[radial-gradient(circle_at_top_left,rgba(251,191,36,0.16),transparent_34%),radial-gradient(circle_at_top_right,rgba(45,212,191,0.14),transparent_26%)]" />
|
<div className="site-glow absolute inset-x-0 top-0 h-[38rem]" />
|
||||||
<div className="shell relative py-10 sm:py-14 lg:py-20">
|
<div className="shell relative py-10 sm:py-14 lg:py-20">
|
||||||
<div className={`grid gap-8 ${hasSection('hero') && hasSection('surface') ? 'lg:grid-cols-[minmax(0,1.15fr)_24rem] lg:items-stretch xl:grid-cols-[minmax(0,1.2fr)_28rem]' : ''}`}>
|
<div className={`grid gap-8 ${hasSection('hero') && hasSection('surface') ? 'lg:grid-cols-[minmax(0,1.15fr)_24rem] lg:items-stretch xl:grid-cols-[minmax(0,1.2fr)_28rem]' : ''}`}>
|
||||||
{hasSection('hero') ? (
|
{hasSection('hero') ? (
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ export default async function CompanyProfilePage({ params }: { params: { slug: s
|
|||||||
|
|
||||||
if (!company) {
|
if (!company) {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-stone-50 dark:bg-stone-950 py-10">
|
<main className="site-page">
|
||||||
<div className="shell">
|
<div className="site-section">
|
||||||
<section className="card p-8">
|
<section className="card p-8">
|
||||||
<p className="text-sm uppercase tracking-[0.18em] text-amber-700 dark:text-amber-400">{dict.unavailable}</p>
|
<p className="text-sm uppercase tracking-[0.18em] text-amber-700 dark:text-amber-400">{dict.unavailable}</p>
|
||||||
<h1 className="mt-3 text-3xl font-black text-stone-900 dark:text-stone-100">{dict.unavailableTitle}</h1>
|
<h1 className="mt-3 text-3xl font-black text-stone-900 dark:text-stone-100">{dict.unavailableTitle}</h1>
|
||||||
@@ -98,8 +98,8 @@ export default async function CompanyProfilePage({ params }: { params: { slug: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-stone-50 dark:bg-stone-950 py-10">
|
<main className="site-page">
|
||||||
<div className="shell space-y-8">
|
<div className="site-section space-y-8">
|
||||||
<section className="card p-8">
|
<section className="card p-8">
|
||||||
<p className="text-sm uppercase tracking-[0.18em] text-amber-700 dark:text-amber-400">{company.brand?.publicCity ?? dict.partner}</p>
|
<p className="text-sm uppercase tracking-[0.18em] text-amber-700 dark:text-amber-400">{company.brand?.publicCity ?? dict.partner}</p>
|
||||||
<h1 className="mt-3 text-4xl font-black text-stone-900 dark:text-stone-100">{company.brand?.displayName ?? company.name}</h1>
|
<h1 className="mt-3 text-4xl font-black text-stone-900 dark:text-stone-100">{company.brand?.displayName ?? company.name}</h1>
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ export default async function VehicleDetailPage({ params }: { params: { slug: st
|
|||||||
|
|
||||||
if (!vehicle) {
|
if (!vehicle) {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-stone-50 dark:bg-stone-950 py-10">
|
<main className="site-page">
|
||||||
<div className="shell">
|
<div className="site-section">
|
||||||
<section className="card p-8">
|
<section className="card p-8">
|
||||||
<p className="text-sm uppercase tracking-[0.18em] text-amber-700 dark:text-amber-400">{dict.unavailable}</p>
|
<p className="text-sm uppercase tracking-[0.18em] text-amber-700 dark:text-amber-400">{dict.unavailable}</p>
|
||||||
<h1 className="mt-3 text-3xl font-black text-stone-900 dark:text-stone-100">{dict.unavailableTitle}</h1>
|
<h1 className="mt-3 text-3xl font-black text-stone-900 dark:text-stone-100">{dict.unavailableTitle}</h1>
|
||||||
@@ -130,8 +130,8 @@ export default async function VehicleDetailPage({ params }: { params: { slug: st
|
|||||||
const [heroPhoto, ...galleryPhotos] = vehicle.photos
|
const [heroPhoto, ...galleryPhotos] = vehicle.photos
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-stone-50 dark:bg-stone-950 py-10">
|
<main className="site-page">
|
||||||
<div className="shell space-y-8">
|
<div className="site-section space-y-8">
|
||||||
|
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<nav className="flex items-center gap-2 text-sm text-stone-500 dark:text-stone-400">
|
<nav className="flex items-center gap-2 text-sm text-stone-500 dark:text-stone-400">
|
||||||
|
|||||||
@@ -237,10 +237,10 @@ export default async function ExplorePage({ searchParams }: { searchParams?: Rec
|
|||||||
).sort((left, right) => left.localeCompare(right))
|
).sort((left, right) => left.localeCompare(right))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-stone-50 dark:bg-stone-950 py-10">
|
<main className="site-page">
|
||||||
<div className="shell space-y-10">
|
<div className="site-section space-y-10">
|
||||||
<section className="rounded-[2rem] bg-[linear-gradient(135deg,#1c1917,#57534e)] dark:bg-[linear-gradient(135deg,#0c0a09,#292524)] px-8 py-12 text-white">
|
<section className="site-panel-contrast bg-[linear-gradient(135deg,#1c1917,#57534e)] dark:bg-[linear-gradient(135deg,#0c0a09,#292524)] px-8 py-12">
|
||||||
<p className="text-sm uppercase tracking-[0.2em] text-amber-300">{dict.kicker}</p>
|
<p className="site-kicker text-amber-300 dark:text-amber-300">{dict.kicker}</p>
|
||||||
<h1 className="mt-4 text-4xl font-black tracking-tight">{dict.title}</h1>
|
<h1 className="mt-4 text-4xl font-black tracking-tight">{dict.title}</h1>
|
||||||
<p className="mt-4 max-w-2xl text-stone-300">{dict.body}</p>
|
<p className="mt-4 max-w-2xl text-stone-300">{dict.body}</p>
|
||||||
<ExploreSearchForm
|
<ExploreSearchForm
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const copy = {
|
|||||||
en: {
|
en: {
|
||||||
kicker: 'Features',
|
kicker: 'Features',
|
||||||
title: 'Operations, marketplace discovery, and branded booking.',
|
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.',
|
body: "RentalDriveGo combines company-only operations with a public marketplace that sends renters to each company's own payment flow.",
|
||||||
cards: [
|
cards: [
|
||||||
['Private dashboard', 'Fleet, reservations, CRM, team permissions, billing, and reports stay isolated per company.'],
|
['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.'],
|
['Marketplace discovery', 'Published vehicles and public offers appear on `/explore` for cross-company browsing.'],
|
||||||
@@ -49,12 +49,12 @@ export default function FeaturesPage() {
|
|||||||
const dict = copy[language]
|
const dict = copy[language]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-stone-50 dark:bg-stone-950">
|
<main className="site-page">
|
||||||
<div className="shell space-y-12 py-16">
|
<div className="site-section space-y-12">
|
||||||
<div className="max-w-3xl">
|
<div className="max-w-3xl">
|
||||||
<p className="text-sm font-semibold uppercase tracking-[0.24em] text-amber-700 dark:text-amber-400">{dict.kicker}</p>
|
<p className="site-kicker">{dict.kicker}</p>
|
||||||
<h1 className="mt-4 text-5xl font-black tracking-tight text-stone-900 dark:text-stone-100">{dict.title}</h1>
|
<h1 className="site-title">{dict.title}</h1>
|
||||||
<p className="mt-5 text-lg leading-8 text-stone-600 dark:text-stone-400">{dict.body}</p>
|
<p className="site-lead">{dict.body}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import { resolveBrowserAppUrl } from '@/lib/appUrls'
|
|||||||
|
|
||||||
export default function PublicLayout({ children }: { children: React.ReactNode }) {
|
export default function PublicLayout({ children }: { children: React.ReactNode }) {
|
||||||
const { language, theme, dict, companyName, setLanguage, setTheme } = useMarketplacePreferences()
|
const { language, theme, dict, companyName, setLanguage, setTheme } = useMarketplacePreferences()
|
||||||
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard')
|
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard')
|
||||||
const footerContent = getFooterContent(language)
|
const footerContent = getFooterContent(language)
|
||||||
const available = localeOptions.filter((o) => o.value !== language)
|
const available = localeOptions.filter((o) => o.value !== language)
|
||||||
const current = localeOptions.find((o) => o.value === language) ?? localeOptions[0]
|
const current = localeOptions.find((o) => o.value === language) ?? localeOptions[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-stone-50 text-stone-900 transition-colors dark:bg-stone-950 dark:text-stone-100">
|
<div className="site-page flex min-h-screen flex-col">
|
||||||
<MarketplaceHeader
|
<MarketplaceHeader
|
||||||
dict={dict}
|
dict={dict}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export default function PricingClient() {
|
|||||||
const dict = copy[language]
|
const dict = copy[language]
|
||||||
const [currency, setCurrency] = useState<Currency>('MAD')
|
const [currency, setCurrency] = useState<Currency>('MAD')
|
||||||
const [billing, setBilling] = useState<Billing>('monthly')
|
const [billing, setBilling] = useState<Billing>('monthly')
|
||||||
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard')
|
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard')
|
||||||
|
|
||||||
const savings = annualSavingsPct('STARTER', currency)
|
const savings = annualSavingsPct('STARTER', currency)
|
||||||
|
|
||||||
|
|||||||
@@ -47,19 +47,19 @@ export default function PricingPageContent() {
|
|||||||
const dict = copy[language]
|
const dict = copy[language]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-stone-50 dark:bg-stone-950">
|
<main className="site-page">
|
||||||
<div className="shell py-20">
|
<div className="site-section">
|
||||||
<div className="mx-auto max-w-2xl text-center">
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
<p className="text-sm font-semibold uppercase tracking-[0.24em] text-amber-700 dark:text-amber-400">{dict.kicker}</p>
|
<p className="site-kicker">{dict.kicker}</p>
|
||||||
<h1 className="mt-4 text-5xl font-black tracking-tight text-stone-900 dark:text-stone-100">{dict.title}</h1>
|
<h1 className="site-title">{dict.title}</h1>
|
||||||
<p className="mt-5 text-lg leading-8 text-stone-600 dark:text-stone-400">{dict.body}</p>
|
<p className="site-lead">{dict.body}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-16">
|
<div className="mt-16">
|
||||||
<PricingClient />
|
<PricingClient />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto mt-24 max-w-3xl divide-y divide-stone-200 dark:divide-stone-800">
|
<div className="site-panel mx-auto mt-24 max-w-3xl divide-y divide-stone-200/80 dark:divide-stone-800">
|
||||||
<h2 className="pb-8 text-2xl font-bold text-stone-900 dark:text-stone-100">{dict.faq}</h2>
|
<h2 className="pb-8 text-2xl font-bold text-stone-900 dark:text-stone-100">{dict.faq}</h2>
|
||||||
{dict.items.map(([q, a]) => (
|
{dict.items.map(([q, a]) => (
|
||||||
<div key={q} className="py-6">
|
<div key={q} className="py-6">
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ function ReviewPageContent() {
|
|||||||
|
|
||||||
if (loadState === 'loading') {
|
if (loadState === 'loading') {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen items-center justify-center bg-stone-50 dark:bg-stone-950 p-6">
|
<main className="site-page flex min-h-screen items-center justify-center p-6">
|
||||||
<p className="text-sm text-stone-500 dark:text-stone-400">Loading…</p>
|
<p className="text-sm text-stone-500 dark:text-stone-400">Loading…</p>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
@@ -109,7 +109,7 @@ function ReviewPageContent() {
|
|||||||
|
|
||||||
if (loadState === 'invalid') {
|
if (loadState === 'invalid') {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen items-center justify-center bg-stone-50 dark:bg-stone-950 p-6">
|
<main className="site-page flex min-h-screen items-center justify-center p-6">
|
||||||
<div className="card w-full max-w-md space-y-3 p-8 text-center">
|
<div className="card w-full max-w-md space-y-3 p-8 text-center">
|
||||||
<p className="text-2xl font-black text-stone-900 dark:text-stone-100">Link not found</p>
|
<p className="text-2xl font-black text-stone-900 dark:text-stone-100">Link not found</p>
|
||||||
<p className="text-sm text-stone-500 dark:text-stone-400">This review link is invalid or has already been used.</p>
|
<p className="text-sm text-stone-500 dark:text-stone-400">This review link is invalid or has already been used.</p>
|
||||||
@@ -120,7 +120,7 @@ function ReviewPageContent() {
|
|||||||
|
|
||||||
if (loadState === 'already_reviewed') {
|
if (loadState === 'already_reviewed') {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen items-center justify-center bg-stone-50 dark:bg-stone-950 p-6">
|
<main className="site-page flex min-h-screen items-center justify-center p-6">
|
||||||
<div className="card w-full max-w-md space-y-3 p-8 text-center">
|
<div className="card w-full max-w-md space-y-3 p-8 text-center">
|
||||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-950/50">
|
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-950/50">
|
||||||
<svg className="h-7 w-7 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
|
<svg className="h-7 w-7 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
|
||||||
@@ -134,7 +134,7 @@ function ReviewPageContent() {
|
|||||||
|
|
||||||
if (submitState === 'done') {
|
if (submitState === 'done') {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen items-center justify-center bg-stone-50 dark:bg-stone-950 p-6">
|
<main className="site-page flex min-h-screen items-center justify-center p-6">
|
||||||
<div className="card w-full max-w-md space-y-4 p-8 text-center">
|
<div className="card w-full max-w-md space-y-4 p-8 text-center">
|
||||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-950/50">
|
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-950/50">
|
||||||
<svg className="h-8 w-8 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
|
<svg className="h-8 w-8 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
|
||||||
@@ -147,7 +147,7 @@ function ReviewPageContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen items-center justify-center bg-stone-50 dark:bg-stone-950 p-6">
|
<main className="site-page flex min-h-screen items-center justify-center p-6">
|
||||||
<div className="card w-full max-w-lg overflow-hidden">
|
<div className="card w-full max-w-lg overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-4 border-b border-stone-200 dark:border-stone-800 p-6">
|
<div className="flex items-center gap-4 border-b border-stone-200 dark:border-stone-800 p-6">
|
||||||
@@ -201,7 +201,7 @@ export default function ReviewPage() {
|
|||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<main className="flex min-h-screen items-center justify-center bg-stone-50 dark:bg-stone-950 p-6">
|
<main className="site-page flex min-h-screen items-center justify-center p-6">
|
||||||
<p className="text-sm text-stone-500 dark:text-stone-400">Loading…</p>
|
<p className="text-sm text-stone-500 dark:text-stone-400">Loading…</p>
|
||||||
</main>
|
</main>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,93 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@apply rounded-2xl border border-stone-200 bg-white shadow-sm transition-colors dark:border-stone-800 dark:bg-stone-900;
|
@apply rounded-[2rem] border p-0 shadow-[0_30px_80px_rgba(28,25,23,0.08)] backdrop-blur transition-colors dark:shadow-[0_30px_80px_rgba(0,0,0,0.26)];
|
||||||
|
border-color: rgb(231 229 228 / 0.8);
|
||||||
|
background-color: rgb(255 255 255 / 0.82);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.dark .card {
|
||||||
|
border-color: rgb(41 37 36 / 0.96);
|
||||||
|
background-color: rgb(28 25 23 / 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-page {
|
||||||
|
@apply min-h-screen overflow-hidden text-stone-900;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(180deg, #f8f5ef 0%, #f4efe6 28%, #fffdf8 58%, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .site-page {
|
||||||
|
@apply text-stone-100;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(180deg, #14110f 0%, #17120d 35%, #0c0a09 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-glow {
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at top left, rgba(217, 119, 6, 0.24), transparent 34%),
|
||||||
|
radial-gradient(circle at top right, rgba(15, 118, 110, 0.18), transparent 26%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .site-glow {
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at top left, rgba(251, 191, 36, 0.16), transparent 34%),
|
||||||
|
radial-gradient(circle at top right, rgba(45, 212, 191, 0.14), transparent 26%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-section {
|
||||||
|
@apply shell py-10 sm:py-14 lg:py-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-panel {
|
||||||
|
@apply rounded-[2rem] border p-7 shadow-[0_30px_80px_rgba(28,25,23,0.08)] backdrop-blur transition-colors sm:p-8;
|
||||||
|
border-color: rgb(231 229 228 / 0.8);
|
||||||
|
background-color: rgb(255 255 255 / 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .site-panel {
|
||||||
|
border-color: rgb(41 37 36 / 0.96);
|
||||||
|
background-color: rgb(28 25 23 / 0.72);
|
||||||
|
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-panel-muted {
|
||||||
|
@apply rounded-[2rem] border p-7 transition-colors sm:p-8;
|
||||||
|
border-color: rgb(231 229 228 / 0.8);
|
||||||
|
background-image: linear-gradient(160deg, #fffdf8 0%, #f1ebe1 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .site-panel-muted {
|
||||||
|
border-color: rgb(41 37 36 / 0.96);
|
||||||
|
background-image: linear-gradient(160deg, #17120d 0%, #221a14 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-panel-contrast {
|
||||||
|
@apply rounded-[2rem] border p-7 text-white shadow-[0_30px_80px_rgba(28,25,23,0.18)] transition-colors sm:p-8;
|
||||||
|
border-color: rgb(231 229 228 / 0.8);
|
||||||
|
background-color: rgb(12 10 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .site-panel-contrast {
|
||||||
|
border-color: rgb(68 64 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-kicker {
|
||||||
|
@apply text-xs font-bold uppercase tracking-[0.28em] text-amber-700 dark:text-amber-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-title {
|
||||||
|
@apply mt-4 text-4xl font-black tracking-[-0.04em] text-stone-950 dark:text-stone-50 sm:text-5xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-lead {
|
||||||
|
@apply mt-5 text-base leading-8 text-stone-600 dark:text-stone-300 sm:text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-link-primary {
|
||||||
|
@apply rounded-full bg-stone-950 px-6 py-3 text-sm font-semibold text-white transition hover:bg-stone-800 dark:bg-amber-400 dark:text-stone-950 dark:hover:bg-amber-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-link-secondary {
|
||||||
|
@apply rounded-full border border-stone-300 bg-white/85 px-6 py-3 text-sm font-semibold text-stone-700 transition hover:border-stone-950 hover:text-stone-950 dark:border-stone-700 dark:bg-stone-950/30 dark:text-stone-200 dark:hover:border-stone-200 dark:hover:text-white;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
{/* Runs before hydration to prevent flash of wrong theme */}
|
{/* Runs before hydration to prevent flash of wrong theme */}
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `(function(){try{var t=document.cookie.split(';').map(function(c){return c.trim()}).find(function(c){return c.startsWith('rentaldrivego-theme=')});if(t)t=t.split('=')[1];if(!t)t=localStorage.getItem('rentaldrivego-theme');if(t==='dark'){document.documentElement.classList.add('dark');document.documentElement.style.colorScheme='dark';}}catch(e){}})()`,
|
__html:
|
||||||
|
"(function(){try{var m=document.cookie.match(/(?:^|; )rentaldrivego-theme=([^;]+)/);var theme=m?decodeURIComponent(m[1]):(localStorage.getItem('rentaldrivego-theme')||localStorage.getItem('marketplace-theme'));if(theme!=='light'&&theme!=='dark'){theme=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}document.documentElement.classList.toggle('dark',theme==='dark');document.documentElement.style.colorScheme=theme;document.body&&document.body.setAttribute('data-theme',theme)}catch(e){}})();",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { resolveServerAppUrl } from '@/lib/appUrls'
|
|||||||
export default function RenterSignInPage() {
|
export default function RenterSignInPage() {
|
||||||
const requestHeaders = headers()
|
const requestHeaders = headers()
|
||||||
const dashboardUrl = resolveServerAppUrl(
|
const dashboardUrl = resolveServerAppUrl(
|
||||||
process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard',
|
process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard',
|
||||||
requestHeaders.get('host'),
|
requestHeaders.get('host'),
|
||||||
requestHeaders.get('x-forwarded-proto'),
|
requestHeaders.get('x-forwarded-proto'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { resolveServerAppUrl } from '@/lib/appUrls'
|
|||||||
export default function RenterSignUpPage() {
|
export default function RenterSignUpPage() {
|
||||||
const requestHeaders = headers()
|
const requestHeaders = headers()
|
||||||
const dashboardUrl = resolveServerAppUrl(
|
const dashboardUrl = resolveServerAppUrl(
|
||||||
process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard',
|
process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard',
|
||||||
requestHeaders.get('host'),
|
requestHeaders.get('host'),
|
||||||
requestHeaders.get('x-forwarded-proto'),
|
requestHeaders.get('x-forwarded-proto'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ export default function FooterContentPage({ slug }: { slug: FooterPageSlug }) {
|
|||||||
const isArabic = language === 'ar'
|
const isArabic = language === 'ar'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-stone-50 dark:bg-stone-950">
|
<main className="site-page">
|
||||||
<div className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
|
<div className="site-section mx-auto max-w-4xl">
|
||||||
<section className="overflow-hidden rounded-[2rem] border border-stone-200 dark:border-stone-800 bg-white dark:bg-stone-900 shadow-[0_30px_80px_rgba(12,18,28,0.08)]">
|
<section className="site-panel overflow-hidden p-0">
|
||||||
<div className="bg-stone-50 dark:bg-stone-900 px-6 py-10 sm:px-10 sm:py-14 border-b border-stone-100 dark:border-stone-800">
|
<div className="site-panel-muted rounded-none border-0 border-b border-stone-100/80 px-6 py-10 sm:px-10 sm:py-14 dark:border-stone-800">
|
||||||
<p className="text-sm font-semibold uppercase tracking-[0.24em] text-amber-700 dark:text-amber-400">
|
<p className="site-kicker">
|
||||||
{meta.kicker}
|
{meta.kicker}
|
||||||
</p>
|
</p>
|
||||||
<h1 className="mt-4 text-4xl font-black tracking-tight text-stone-900 dark:text-stone-100 sm:text-5xl">
|
<h1 className="site-title">
|
||||||
{content.title}
|
{content.title}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +47,7 @@ export default function FooterContentPage({ slug }: { slug: FooterPageSlug }) {
|
|||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="rounded-3xl border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-950/30 px-6 py-5">
|
<div className="rounded-3xl border border-amber-200 bg-amber-50 px-6 py-5 dark:border-amber-800 dark:bg-amber-950/30">
|
||||||
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-amber-800 dark:text-amber-400">
|
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-amber-800 dark:text-amber-400">
|
||||||
{meta.cta}
|
{meta.cta}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function MarketplaceFooter({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-stone-200 bg-white/90 px-4 py-8 text-stone-600 transition-colors dark:border-white/5 dark:bg-[#111111] dark:text-stone-300">
|
<footer className="border-t border-stone-200/80 bg-white/72 px-4 py-8 text-stone-600 backdrop-blur-xl transition-colors dark:border-stone-800 dark:bg-stone-950/72 dark:text-stone-300">
|
||||||
<div className="shell flex flex-col items-center gap-5 text-center">
|
<div className="shell flex flex-col items-center gap-5 text-center">
|
||||||
<nav className="flex flex-wrap items-center justify-center gap-y-3 text-sm">
|
<nav className="flex flex-wrap items-center justify-center gap-y-3 text-sm">
|
||||||
{primaryItems.map((item, index) => (
|
{primaryItems.map((item, index) => (
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default function MarketplaceHeader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 border-b border-stone-200 bg-white shadow-[0_10px_30px_rgba(15,23,42,0.08)] transition-colors dark:border-stone-800 dark:bg-stone-950 dark:shadow-[0_10px_30px_rgba(0,0,0,0.32)]">
|
<header className="sticky top-0 z-50 border-b border-stone-200/80 bg-white/78 backdrop-blur-xl shadow-[0_10px_30px_rgba(15,23,42,0.08)] transition-colors dark:border-stone-800 dark:bg-stone-950/76 dark:shadow-[0_10px_30px_rgba(0,0,0,0.32)]">
|
||||||
<div className="shell flex min-h-14 flex-col gap-1.5 py-1.5 lg:flex-row lg:items-center lg:justify-between lg:gap-3 lg:py-2">
|
<div className="shell flex min-h-14 flex-col gap-1.5 py-1.5 lg:flex-row lg:items-center lg:justify-between lg:gap-3 lg:py-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<Link href="/" className="flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-[0.14em] text-stone-900 dark:text-stone-100 sm:text-sm sm:tracking-[0.2em]">
|
<Link href="/" className="flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-[0.14em] text-stone-900 dark:text-stone-100 sm:text-sm sm:tracking-[0.2em]">
|
||||||
@@ -174,13 +174,10 @@ export default function MarketplaceHeader({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleThemeMenu}
|
onClick={toggleThemeMenu}
|
||||||
className="inline-flex items-center gap-1.5 rounded-full px-1.5 py-1 transition hover:bg-stone-100 dark:hover:bg-stone-800 sm:gap-2 sm:px-2 sm:py-1.5"
|
className="inline-flex items-center gap-1.5 rounded-full px-2 py-1 transition hover:bg-stone-100 dark:hover:bg-stone-800 sm:px-2.5 sm:py-1.5"
|
||||||
aria-expanded={themeMenuOpen}
|
aria-expanded={themeMenuOpen}
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
>
|
>
|
||||||
<span className="px-1.5 text-[10px] font-semibold uppercase tracking-[0.16em] text-stone-500 dark:text-stone-400 sm:px-2 sm:text-[11px] sm:tracking-[0.2em]">
|
|
||||||
{dict.theme}
|
|
||||||
</span>
|
|
||||||
<span className="rounded-full bg-stone-900 px-3 py-1 text-[11px] font-semibold text-white dark:bg-amber-400 dark:text-stone-950 sm:px-3.5 sm:py-1.5 sm:text-xs">
|
<span className="rounded-full bg-stone-900 px-3 py-1 text-[11px] font-semibold text-white dark:bg-amber-400 dark:text-stone-950 sm:px-3.5 sm:py-1.5 sm:text-xs">
|
||||||
{currentTheme.label}
|
{currentTheme.label}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export default function RenterShell({ children }: { children: React.ReactNode })
|
|||||||
const pageTitle = activeNav ? (dict[activeNav.key as keyof typeof dict] as string) : dict.dashboard
|
const pageTitle = activeNav ? (dict[activeNav.key as keyof typeof dict] as string) : dict.dashboard
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-stone-50 dark:bg-stone-950">
|
<div className="site-page flex min-h-screen">
|
||||||
{open && (
|
{open && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-30 bg-black/50 lg:hidden"
|
className="fixed inset-0 z-30 bg-black/50 lg:hidden"
|
||||||
@@ -201,7 +201,7 @@ export default function RenterShell({ children }: { children: React.ReactNode })
|
|||||||
isRtl ? 'lg:mr-64' : 'lg:ml-64',
|
isRtl ? 'lg:mr-64' : 'lg:ml-64',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<header className="flex h-16 items-center justify-between border-b border-stone-200 bg-white px-6 transition-colors dark:border-stone-800 dark:bg-stone-950">
|
<header className="flex h-16 items-center justify-between border-b border-stone-200/80 bg-white/80 px-6 backdrop-blur transition-colors dark:border-stone-800 dark:bg-stone-900/70">
|
||||||
<h1 className="text-lg font-semibold text-stone-900 dark:text-stone-100">{pageTitle}</h1>
|
<h1 className="text-lg font-semibold text-stone-900 dark:text-stone-100">{pageTitle}</h1>
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-amber-500 text-xs font-semibold text-white">
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-amber-500 text-xs font-semibold text-white">
|
||||||
{userInitials}
|
{userInitials}
|
||||||
@@ -212,7 +212,7 @@ export default function RenterShell({ children }: { children: React.ReactNode })
|
|||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="border-t border-stone-200 bg-white/90 px-6 py-4 transition-colors dark:border-stone-800 dark:bg-stone-950/90">
|
<footer className="border-t border-stone-200/80 bg-white/80 px-6 py-4 backdrop-blur transition-colors dark:border-stone-800 dark:bg-stone-900/70">
|
||||||
<p className="text-center text-xs text-stone-400 dark:text-stone-500">
|
<p className="text-center text-xs text-stone-400 dark:text-stone-500">
|
||||||
© {new Date().getFullYear()} RentalDriveGo. {dict.rights}
|
© {new Date().getFullYear()} RentalDriveGo. {dict.rights}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type FrameId = 'sign-in'
|
|||||||
const frameConfig: Record<FrameId, { label: string; appUrl: string; path: string }> = {
|
const frameConfig: Record<FrameId, { label: string; appUrl: string; path: string }> = {
|
||||||
'sign-in': {
|
'sign-in': {
|
||||||
label: 'Sign in',
|
label: 'Sign in',
|
||||||
appUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard',
|
appUrl: process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard',
|
||||||
path: '/dashboard/sign-in',
|
path: '/dashboard/sign-in',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -84,19 +84,26 @@ export default function WorkspaceFrame({ target }: { target: FrameId }) {
|
|||||||
}, [config.appUrl, framePath, language, theme])
|
}, [config.appUrl, framePath, language, theme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="bg-stone-950" style={{ minHeight: FRAME_HEIGHT }}>
|
<main className="site-page">
|
||||||
{src ? (
|
<div className="site-section py-6 sm:py-8 lg:py-10">
|
||||||
<iframe
|
<section className="site-panel overflow-hidden p-0">
|
||||||
title={config.label}
|
{src ? (
|
||||||
src={src}
|
<iframe
|
||||||
className="w-full border-0 bg-white"
|
title={config.label}
|
||||||
style={{ height: FRAME_HEIGHT }}
|
src={src}
|
||||||
/>
|
className="w-full border-0 bg-white dark:bg-stone-950"
|
||||||
) : (
|
style={{ height: FRAME_HEIGHT }}
|
||||||
<div className="flex items-center justify-center text-sm text-stone-300" style={{ height: FRAME_HEIGHT }}>
|
/>
|
||||||
{loadingLabel} {config.label}…
|
) : (
|
||||||
</div>
|
<div
|
||||||
)}
|
className="flex items-center justify-center text-sm text-stone-500 dark:text-stone-300"
|
||||||
|
style={{ height: FRAME_HEIGHT }}
|
||||||
|
>
|
||||||
|
{loadingLabel} {config.label}…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { resolveBrowserAppUrl } from '@/lib/appUrls'
|
|||||||
|
|
||||||
export default function WorkspaceTabs() {
|
export default function WorkspaceTabs() {
|
||||||
const { language, theme } = useMarketplacePreferences()
|
const { language, theme } = useMarketplacePreferences()
|
||||||
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3001/dashboard')
|
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard')
|
||||||
|
|
||||||
const copy = {
|
const copy = {
|
||||||
en: {
|
en: {
|
||||||
|
|||||||
+18
-3
@@ -110,7 +110,12 @@ services:
|
|||||||
- api
|
- api
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker.dev
|
- .env.docker.dev
|
||||||
command: ["npm", "run", "dev", "--workspace", "@rentaldrivego/marketplace"]
|
command:
|
||||||
|
[
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
"npm run build --workspace @rentaldrivego/types && cd /app/apps/marketplace && exec env PATH=/app/docker/scripts:$PATH /app/node_modules/.bin/next dev -H 0.0.0.0 -p 3000",
|
||||||
|
]
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -127,7 +132,12 @@ services:
|
|||||||
- api
|
- api
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker.dev
|
- .env.docker.dev
|
||||||
command: ["npm", "run", "dev", "--workspace", "@rentaldrivego/dashboard"]
|
command:
|
||||||
|
[
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
"npm run build --workspace @rentaldrivego/types && cd /app/apps/dashboard && exec env PATH=/app/docker/scripts:$PATH /app/node_modules/.bin/next dev -H 0.0.0.0 -p 3001",
|
||||||
|
]
|
||||||
ports:
|
ports:
|
||||||
- "3001:3001"
|
- "3001:3001"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -144,7 +154,12 @@ services:
|
|||||||
- api
|
- api
|
||||||
env_file:
|
env_file:
|
||||||
- .env.docker.dev
|
- .env.docker.dev
|
||||||
command: ["npm", "run", "dev", "--workspace", "@rentaldrivego/admin"]
|
command:
|
||||||
|
[
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
"npm run build --workspace @rentaldrivego/types && cd /app/apps/admin && exec env PATH=/app/docker/scripts:$PATH /app/node_modules/.bin/next dev -H 0.0.0.0 -p 3002",
|
||||||
|
]
|
||||||
ports:
|
ports:
|
||||||
- "3002:3002"
|
- "3002:3002"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
REAL_NPM="/usr/local/bin/npm"
|
||||||
|
|
||||||
|
if [ "${1-}" = "config" ] && [ "${2-}" = "get" ] && [ "${3-}" = "registry" ]; then
|
||||||
|
exec "$REAL_NPM" --prefix "$PWD" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REAL_NPM" "$@"
|
||||||
Reference in New Issue
Block a user