fix the homepage pages
Build & Deploy / Build & Push Docker Image (push) Failing after 49s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Has been cancelled
Test / Admin Unit Tests (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled

This commit is contained in:
root
2026-06-27 00:48:52 -04:00
parent e36a59b0bc
commit 0f083d8e5d
12 changed files with 437 additions and 143 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 KiB

+2 -1
View File
@@ -60,12 +60,13 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
dir={getDirection(locale)}
data-theme-preference={preference}
data-theme={resolvedTheme}
data-scroll-behavior="smooth"
suppressHydrationWarning
>
<head>
<script
id="theme-bootstrap"
nonce={nonce}
nonce={nonce || undefined}
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: themeBootstrapScript }}
/>
+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="8" fill="#0f172a" />
<text
x="16"
y="21"
text-anchor="middle"
font-family="Inter, Arial, sans-serif"
font-size="18"
font-weight="700"
fill="#ffffff"
>
R
</text>
</svg>

After

Width:  |  Height:  |  Size: 302 B

@@ -1,75 +1,18 @@
'use client';
import { localizedPath, locales, type Locale } from '@/lib/localization/config';
import { persistLocale } from '@/lib/localization/client';
import { persistTheme, themePreferenceEvent } from '@/lib/theme/client';
import { localizedPath, type Locale } from '@/lib/localization/config';
import { type ThemePreference } from '@/lib/theme/config';
import Image from 'next/image';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { LocalePill } from './LocalePill';
import { ThemePill } from './ThemePill';
import styles from './AuthHeader.module.css';
const langLabels: Record<Locale, string> = { en: 'EN', fr: 'FR', ar: 'AR' };
const langFlags: Record<Locale, string> = { en: '🇺🇸', fr: '🇫🇷', ar: '🇲🇦' };
const themeDict: Record<Locale, Record<string, string>> = {
en: { theme: 'Theme', light: 'Light', dark: 'Dark' },
fr: { theme: 'Mode', light: 'Clair', dark: 'Sombre' },
ar: { theme: 'الوضع', light: 'فاتح', dark: 'داكن' },
};
interface Props {
locale: Locale;
themePreference: ThemePreference;
}
export function AuthHeader({ locale, themePreference: initialPref }: Props) {
const pathname = usePathname();
const router = useRouter();
const [pref, setPref] = useState<ThemePreference>(initialPref);
const t = themeDict[locale] ?? themeDict.en;
const themeLabel = pref === 'dark' ? t.dark : t.light;
const localeMenuRef = useRef<HTMLDivElement | null>(null);
const [localeOpen, setLocaleOpen] = useState(false);
useEffect(() => {
setPref(initialPref);
}, [initialPref]);
useEffect(() => {
const handler = () => {
const stored = localStorage.getItem('hpc.theme.preference');
if (stored === 'light' || stored === 'dark' || stored === 'system') setPref(stored);
};
window.addEventListener(themePreferenceEvent, handler);
return () => window.removeEventListener(themePreferenceEvent, handler);
}, []);
useEffect(() => {
function handleClick(e: MouseEvent) {
if (!localeMenuRef.current?.contains(e.target as Node)) setLocaleOpen(false);
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, []);
const cycleTheme = () => {
const next = pref === 'dark' ? 'light' : 'dark';
persistTheme(next);
setPref(next);
};
const switchLocale = (next: Locale) => {
setLocaleOpen(false);
persistLocale(next);
const segments = pathname.split('/').filter(Boolean);
if (segments.length >= 2) segments[1] = next;
router.push('/' + segments.join('/'));
};
const otherLocales = locales.filter((l) => l !== locale);
export function AuthHeader({ locale, themePreference }: Props) {
const homePath = localizedPath('home', locale);
return (
@@ -92,54 +35,11 @@ export function AuthHeader({ locale, themePreference: initialPref }: Props) {
<div className={styles.controls}>
<div className={styles.pill}>
<div ref={localeMenuRef} className={styles.localeMenuWrap}>
<button
type="button"
onClick={() => setLocaleOpen((v) => !v)}
className={styles.localeBtn}
aria-expanded={localeOpen}
aria-haspopup="menu"
>
<span aria-hidden="true">{langFlags[locale]}</span>
<span>{langLabels[locale]}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`${styles.chevron} ${localeOpen ? styles.chevronOpen : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{localeOpen && (
<div className={styles.localeMenu}>
{otherLocales.map((l) => (
<button
key={l}
type="button"
onClick={() => switchLocale(l)}
className={styles.localeOption}
>
<span aria-hidden="true">{langFlags[l]}</span>
<span>{langLabels[l]}</span>
</button>
))}
</div>
)}
</div>
<LocalePill locale={locale} />
<span className={styles.divider} aria-hidden="true" />
<button
type="button"
onClick={cycleTheme}
className={styles.themeBtn}
aria-label={themeLabel}
>
<span className={styles.themeLabel}>{themeLabel}</span>
</button>
<ThemePill initialPreference={themePreference} />
</div>
</div>
</div>
@@ -0,0 +1,108 @@
.wrap {
position: relative;
}
.button {
display: inline-flex;
min-width: 4.75rem;
align-items: center;
justify-content: center;
gap: 0.375rem;
border-radius: 999px;
padding: 0.375rem 0.625rem;
font-size: 11px;
font-weight: 600;
color: #44403c;
background: transparent;
border: none;
cursor: pointer;
transition: background 0.15s;
}
.button:hover {
background: #f5f5f4;
}
:global(html[data-theme='dark']) .button {
color: #e7e5e4;
}
:global(html[data-theme='dark']) .button:hover {
background: rgb(30 58 138 / 0.4);
}
@media (min-width: 640px) {
.button {
min-width: 5.25rem;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
}
}
.chevron {
width: 0.875rem;
height: 0.875rem;
transition: transform 0.15s;
}
.chevronOpen {
transform: rotate(180deg);
}
.menu {
position: absolute;
top: 100%;
z-index: 30;
margin-top: 0.5rem;
width: 12rem;
max-width: calc(100vw - 2rem);
overflow: hidden;
border-radius: 1rem;
border: 1px solid #e7e5e4;
background: rgb(255 255 255 / 0.95);
text-align: left;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
backdrop-filter: blur(16px);
left: 0;
}
@media (min-width: 640px) {
.menu {
left: auto;
right: 0;
}
}
:global(html[data-theme='dark']) .menu {
border-color: #1e3a8a;
background: rgb(23 37 84 / 0.95);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
}
.option {
display: flex;
width: 100%;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
font-size: 0.875rem;
color: #44403c;
background: transparent;
border: none;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.option:hover {
background: #f5f5f4;
color: #1e3a8a;
}
:global(html[data-theme='dark']) .option {
color: #e7e5e4;
}
:global(html[data-theme='dark']) .option:hover {
background: rgb(30 58 138 / 0.4);
color: #fff;
}
@@ -0,0 +1,87 @@
'use client';
import { persistLocale } from '@/lib/localization/client';
import {
localeSwitchUrl,
locales,
routeIdFromPathname,
type Locale,
} from '@/lib/localization/config';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import styles from './LocalePill.module.css';
const langLabels: Record<Locale, string> = { en: 'EN', fr: 'FR', ar: 'AR' };
const langFlags: Record<Locale, string> = { en: '🇺🇸', fr: '🇫🇷', ar: '🇲🇦' };
interface Props {
locale: Locale;
}
export function LocalePill({ locale }: Props) {
const pathname = usePathname();
const router = useRouter();
const menuRef = useRef<HTMLDivElement | null>(null);
const [open, setOpen] = useState(false);
useEffect(() => {
function handleClick(e: MouseEvent) {
if (!menuRef.current?.contains(e.target as Node)) setOpen(false);
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, []);
const switchLocale = (next: Locale) => {
setOpen(false);
persistLocale(next);
const routeId = routeIdFromPathname(pathname) ?? 'home';
router.push(
localeSwitchUrl(new URL(window.location.href), next, routeId).pathname +
new URL(window.location.href).search +
new URL(window.location.href).hash,
);
};
const otherLocales = locales.filter((l) => l !== locale);
return (
<div ref={menuRef} className={styles.wrap}>
<button
type="button"
onClick={() => setOpen((v) => !v)}
className={styles.button}
aria-expanded={open}
aria-haspopup="menu"
>
<span aria-hidden="true">{langFlags[locale]}</span>
<span>{langLabels[locale]}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`${styles.chevron} ${open ? styles.chevronOpen : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{open && (
<div className={styles.menu}>
{otherLocales.map((l) => (
<button
key={l}
type="button"
onClick={() => switchLocale(l)}
className={styles.option}
>
<span aria-hidden="true">{langFlags[l]}</span>
<span>{langLabels[l]}</span>
</button>
))}
</div>
)}
</div>
);
}
@@ -5,10 +5,10 @@ import { ActionLink } from '@/components/actions/ActionLink';
import { accountCreateUrl } from '@/lib/account-urls';
import { BrandLink } from './BrandLink';
import { HeaderNavigation } from './HeaderNavigation';
import { LocaleSelector } from './LocaleSelector';
import { LocalePill } from './LocalePill';
import { MobileNavigation } from './MobileNavigation';
import styles from './SiteHeader.module.css';
import { ThemeSelector } from './ThemeSelector';
import { ThemePill } from './ThemePill';
function signInUrl(locale: Locale): string {
return localizedPath('sign-in', locale);
@@ -39,19 +39,8 @@ export function SiteHeader({ locale, messages, themePreference }: SiteHeaderProp
role="group"
aria-label={messages.header.controlsLabel}
>
<LocaleSelector
currentLocale={locale}
label={messages.controls.language}
localeNames={messages.controls.localeNames}
compact
/>
<ThemeSelector
initialPreference={themePreference}
label={messages.controls.theme}
statusTemplate={messages.controls.themeStatus}
optionLabels={messages.controls.themes}
compact
/>
<LocalePill locale={locale} />
<ThemePill initialPreference={themePreference} />
<ActionLink href={signInUrl(locale)} variant="button-primary">
{messages.header.login}
</ActionLink>
@@ -0,0 +1,115 @@
.wrap {
position: relative;
}
.button {
display: inline-flex;
align-items: center;
gap: 0.375rem;
border-radius: 999px;
padding: 0.25rem 0.5rem;
background: transparent;
border: none;
cursor: pointer;
transition: background 0.15s;
}
.button:hover {
background: #f5f5f4;
}
:global(html[data-theme='dark']) .button:hover {
background: rgb(30 58 138 / 0.4);
}
@media (min-width: 640px) {
.button {
padding: 0.375rem 0.625rem;
}
}
.label {
border-radius: 999px;
background: #1e3a8a;
padding: 0.25rem 0.75rem;
font-size: 11px;
font-weight: 600;
color: #fff;
}
:global(html[data-theme='dark']) .label {
background: #fb923c;
}
@media (min-width: 640px) {
.label {
padding: 0.375rem 0.875rem;
font-size: 0.75rem;
}
}
.chevron {
width: 0.875rem;
height: 0.875rem;
color: #78716c;
transition: transform 0.15s;
}
:global(html[data-theme='dark']) .chevron {
color: #a8a29e;
}
.chevronOpen {
transform: rotate(180deg);
}
.menu {
position: absolute;
top: 100%;
z-index: 30;
right: 0;
margin-top: 0.5rem;
width: 11rem;
max-width: calc(100vw - 2rem);
overflow: hidden;
border-radius: 1rem;
border: 1px solid #e7e5e4;
background: rgb(255 255 255 / 0.95);
text-align: left;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
backdrop-filter: blur(16px);
}
:global(html[data-theme='dark']) .menu {
border-color: #1e3a8a;
background: rgb(23 37 84 / 0.95);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
}
.option {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
font-size: 0.875rem;
color: #44403c;
background: transparent;
border: none;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.option:hover {
background: #f5f5f4;
color: #1e3a8a;
}
:global(html[data-theme='dark']) .option {
color: #e7e5e4;
}
:global(html[data-theme='dark']) .option:hover {
background: rgb(30 58 138 / 0.4);
color: #fff;
}
@@ -0,0 +1,93 @@
'use client';
import { persistTheme, themePreferenceEvent } from '@/lib/theme/client';
import { type ThemePreference } from '@/lib/theme/config';
import { useEffect, useRef, useState } from 'react';
import styles from './ThemePill.module.css';
interface Props {
initialPreference: ThemePreference;
}
const labels: Record<ThemePreference, string> = {
light: 'Light',
dark: 'Dark',
system: 'System',
};
export function ThemePill({ initialPreference }: Props) {
const [pref, setPref] = useState<ThemePreference>(initialPreference);
const menuRef = useRef<HTMLDivElement | null>(null);
const [open, setOpen] = useState(false);
useEffect(() => {
setPref(initialPreference);
}, [initialPreference]);
useEffect(() => {
const handler = () => {
const stored = localStorage.getItem('hpc.theme.preference');
if (stored === 'light' || stored === 'dark' || stored === 'system') setPref(stored);
};
window.addEventListener(themePreferenceEvent, handler);
return () => window.removeEventListener(themePreferenceEvent, handler);
}, []);
useEffect(() => {
function handleClick(e: MouseEvent) {
if (!menuRef.current?.contains(e.target as Node)) setOpen(false);
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, []);
const selectTheme = (next: ThemePreference) => {
setOpen(false);
persistTheme(next);
setPref(next);
};
const options: ThemePreference[] = ['light', 'dark', 'system'];
const otherOptions = options.filter((o) => o !== pref);
const displayLabel = pref === 'dark' ? 'Dark' : 'Light';
return (
<div ref={menuRef} className={styles.wrap}>
<button
type="button"
onClick={() => setOpen((v) => !v)}
className={styles.button}
aria-expanded={open}
aria-haspopup="menu"
aria-label={displayLabel}
>
<span className={styles.label}>{displayLabel}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`${styles.chevron} ${open ? styles.chevronOpen : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{open && (
<div className={styles.menu}>
{otherOptions.map((o) => (
<button
key={o}
type="button"
onClick={() => selectTheme(o)}
className={styles.option}
>
<span>{labels[o]}</span>
</button>
))}
</div>
)}
</div>
);
}
@@ -11,7 +11,6 @@
.brandImage {
object-fit: contain;
background: var(--surface-page);
}
@media (max-width: 767px) {
@@ -28,7 +28,7 @@
.heroGrid {
display: grid;
grid-template-columns: minmax(0, 1.02fr) minmax(22rem, 0.98fr);
grid-template-columns: minmax(0, 1fr) minmax(26rem, 1.15fr);
align-items: center;
gap: clamp(var(--space-10), 7vw, var(--space-20));
}
@@ -53,11 +53,10 @@
.previewStage {
position: relative;
min-inline-size: 0;
padding: clamp(var(--space-3), 2.5vw, var(--space-6));
padding: clamp(var(--space-4), 3vw, var(--space-8));
}
.previewStage::before,
.previewStage::after {
.previewStage::before {
position: absolute;
z-index: -1;
border-radius: var(--radius-md);
@@ -70,17 +69,9 @@
transform: rotate(-2deg);
}
.previewStage::after {
inset-block-start: 8%;
inset-inline: 8%;
block-size: 88%;
border: 1px solid var(--border-strong);
transform: rotate(2deg);
}
@media (max-width: 1023px) {
.heroGrid {
grid-template-columns: minmax(0, 1fr) minmax(18rem, 0.9fr);
grid-template-columns: minmax(0, 1fr) minmax(21rem, 1.05fr);
gap: var(--space-10);
}
}
@@ -103,8 +94,7 @@
padding-inline: 0;
}
.previewStage::before,
.previewStage::after {
.previewStage::before {
display: none;
}
}
@@ -116,15 +106,13 @@
}
@media (forced-colors: active) {
.previewStage::before,
.previewStage::after {
.previewStage::before {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.previewStage::before,
.previewStage::after {
.previewStage::before {
transform: none;
}
}
+1 -1
View File
@@ -441,7 +441,7 @@ export function buildHomepageContent(
image: localizedImage(
'rentaldrivego',
locale,
'jpeg',
'png',
{ width: 1220, height: 1167 },
'RentalDriveGo',
),