fix bug#7
Build & Push / Build & Push Docker Image (push) Failing after 2m50s
Test / Type Check (all packages) (push) Failing after 55s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Carplace Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped

This commit is contained in:
root
2026-07-20 01:01:21 -04:00
parent 7ce0a5adf3
commit fb7b4eefc1
33 changed files with 342 additions and 87 deletions
+4 -4
View File
@@ -20,6 +20,7 @@ import {
} from '@/lib/theme/config';
import { cookies, headers } from 'next/headers';
import { notFound } from 'next/navigation';
import Script from 'next/script';
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
@@ -46,7 +47,6 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
const messages = getMessages(locale);
const integrationConfig = getPublicIntegrationConfig();
const requestHeaders = await headers();
const nonce = requestHeaders.get('x-nonce') ?? undefined;
const cookieStore = await cookies();
const headerPreference = requestHeaders.get('x-theme-preference');
const cookiePreference = cookieStore.get(themeCookie)?.value;
@@ -54,7 +54,7 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
? headerPreference
: isThemePreference(cookiePreference)
? cookiePreference
: 'system';
: 'dark';
const resolvedTheme = serverResolvedTheme(preference);
return (
@@ -67,9 +67,9 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
suppressHydrationWarning
>
<head>
<script
<Script
id="theme-bootstrap"
nonce={nonce || undefined}
strategy="beforeInteractive"
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: themeBootstrapScript }}
/>
@@ -6,6 +6,7 @@ import Image from 'next/image';
import { LocalePill } from './LocalePill';
import { ThemePill } from './ThemePill';
import styles from './AuthHeader.module.css';
import { useThemePreference } from './useThemePreference';
interface Props {
locale: Locale;
@@ -13,7 +14,8 @@ interface Props {
}
export function AuthHeader({ locale, themePreference }: Props) {
const homePath = localizedModePath('home', locale, themePreference);
const currentPreference = useThemePreference(themePreference);
const homePath = localizedModePath('home', locale, currentPreference);
return (
<header className={styles.header}>
@@ -35,7 +37,7 @@ export function AuthHeader({ locale, themePreference }: Props) {
<div className={styles.controls}>
<div className={styles.pill}>
<LocalePill locale={locale} />
<LocalePill locale={locale} initialPreference={themePreference} />
<span className={styles.divider} aria-hidden="true" />
@@ -5,6 +5,7 @@ import type { ThemePreference } from '@/lib/theme/config';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import styles from './SiteHeader.module.css';
import { useThemePreference } from './useThemePreference';
export function BrandLink({
locale,
@@ -16,10 +17,11 @@ export function BrandLink({
label: string;
}) {
const pathname = usePathname();
const currentMode = useThemePreference(mode);
return (
<a
className={styles.brand}
href={localizedModePath('home', locale, mode)}
href={localizedModePath('home', locale, currentMode)}
aria-label={label}
aria-current={routeIdFromPathname(pathname) === 'home' ? 'page' : undefined}
>
@@ -5,6 +5,7 @@ import type { ThemePreference } from '@/lib/theme/config';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import styles from './SiteHeader.module.css';
import { useThemePreference } from './useThemePreference';
export type HeaderNavId = 'product' | 'workflow' | 'modules' | 'pricing' | 'faq';
@@ -30,6 +31,7 @@ export function HeaderNavigation({
const pathname = usePathname();
const currentRoute = routeIdFromPathname(pathname);
const [currentHash, setCurrentHash] = useState('');
const currentMode = useThemePreference(mode);
useEffect(() => {
const updateHash = () => setCurrentHash(window.location.hash.replace(/^#/, ''));
@@ -38,7 +40,7 @@ export function HeaderNavigation({
return () => window.removeEventListener('hashchange', updateHash);
}, []);
const home = localizedModePath('home', locale, mode);
const home = localizedModePath('home', locale, currentMode);
return (
<nav aria-label={label} className={mobile ? styles.drawerNavigation : styles.navigation}>
@@ -2,27 +2,32 @@
import { persistLocale } from '@/lib/localization/client';
import {
localizedModePath,
localeSwitchUrl,
locales,
routeIdFromPathname,
type Locale,
} from '@/lib/localization/config';
import type { ThemePreference } from '@/lib/theme/config';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import styles from './LocalePill.module.css';
import { useThemePreference } from './useThemePreference';
const langLabels: Record<Locale, string> = { en: 'EN', fr: 'FR', ar: 'AR' };
const langFlags: Record<Locale, string> = { en: '🇺🇸', fr: '🇫🇷', ar: '🇲🇦' };
interface Props {
locale: Locale;
initialPreference: ThemePreference;
}
export function LocalePill({ locale }: Props) {
export function LocalePill({ locale, initialPreference }: Props) {
const pathname = usePathname();
const router = useRouter();
const menuRef = useRef<HTMLDivElement | null>(null);
const [open, setOpen] = useState(false);
const preference = useThemePreference(initialPreference);
useEffect(() => {
function handleClick(e: MouseEvent) {
@@ -37,6 +42,7 @@ export function LocalePill({ locale }: Props) {
persistLocale(next);
const routeId = routeIdFromPathname(pathname) ?? 'home';
const target = localeSwitchUrl(new URL(window.location.href), next, routeId);
target.pathname = localizedModePath(routeId, next, preference);
router.push(`${target.pathname}${target.search}${target.hash}`);
};
@@ -2,16 +2,20 @@
import { persistLocale } from '@/lib/localization/client';
import {
localizedModePath,
localeSwitchUrl,
locales,
routeIdFromPathname,
type Locale,
} from '@/lib/localization/config';
import type { ThemePreference } from '@/lib/theme/config';
import { usePathname } from 'next/navigation';
import styles from './Controls.module.css';
import { useThemePreference } from './useThemePreference';
interface LocaleSelectorProps {
currentLocale: Locale;
initialPreference: ThemePreference;
label: string;
localeNames: Record<Locale, string>;
compact?: boolean;
@@ -19,12 +23,14 @@ interface LocaleSelectorProps {
export function LocaleSelector({
currentLocale,
initialPreference,
label,
localeNames,
compact = false,
}: LocaleSelectorProps) {
const pathname = usePathname();
const routeId = routeIdFromPathname(pathname) ?? 'home';
const preference = useThemePreference(initialPreference);
return (
<label className={styles.field}>
@@ -36,9 +42,9 @@ export function LocaleSelector({
onChange={(event) => {
const targetLocale = event.target.value as Locale;
persistLocale(targetLocale);
window.location.assign(
localeSwitchUrl(new URL(window.location.href), targetLocale, routeId),
);
const target = localeSwitchUrl(new URL(window.location.href), targetLocale, routeId);
target.pathname = localizedModePath(routeId, targetLocale, preference);
window.location.assign(target);
}}
>
{locales.map((locale) => (
@@ -1,19 +1,15 @@
'use client';
import { ActionLink } from '@/components/actions/ActionLink';
import type { ShellMessages } from '@/lib/localization/messages';
import { localizedModePath, type Locale } from '@/lib/localization/config';
import { accountCreateUrl } from '@/lib/account-urls';
import { type Locale } from '@/lib/localization/config';
import type { ThemePreference } from '@/lib/theme/config';
import { useEffect, useRef, useState } from 'react';
import { HeaderNavigation } from './HeaderNavigation';
import { LocaleSelector } from './LocaleSelector';
import styles from './SiteHeader.module.css';
import { ThemedAccountCreateLink } from './ThemedAccountCreateLink';
import { ThemeSelector } from './ThemeSelector';
function signInUrl(locale: Locale, mode: ThemePreference): string {
return localizedModePath('sign-in', locale, mode);
}
import { ThemedRouteActionLink } from './ThemedRouteActionLink';
interface MobileNavigationProps {
locale: Locale;
@@ -116,6 +112,7 @@ export function MobileNavigation({
>
<LocaleSelector
currentLocale={locale}
initialPreference={themePreference}
label={messages.controls.language}
localeNames={messages.controls.localeNames}
/>
@@ -128,12 +125,21 @@ export function MobileNavigation({
</div>
<div className={styles.drawerActions}>
<ActionLink href={signInUrl(locale, themePreference)} variant="button-primary">
<ThemedRouteActionLink
locale={locale}
routeId="sign-in"
initialPreference={themePreference}
variant="button-primary"
>
{messages.header.login}
</ActionLink>
<ActionLink href={accountCreateUrl(locale)} variant="button-conversion">
</ThemedRouteActionLink>
<ThemedAccountCreateLink
locale={locale}
initialPreference={themePreference}
variant="button-conversion"
>
{messages.header.demo}
</ActionLink>
</ThemedAccountCreateLink>
</div>
</div>
</dialog>
@@ -117,7 +117,7 @@ export function SiteFooter({ locale, mode, messages }: SiteFooterProps) {
<PendingItem label={footer.links.contact} pending={footer.pending} />
</li>
<li>
<a href={accountCreateUrl(locale)}>{footer.links.demo}</a>
<a href={accountCreateUrl(locale, mode)}>{footer.links.demo}</a>
</li>
</ul>
</section>
@@ -1,18 +1,14 @@
import { localizedModePath, type Locale } from '@/lib/localization/config';
import { type Locale } from '@/lib/localization/config';
import type { ShellMessages } from '@/lib/localization/messages';
import type { ThemePreference } from '@/lib/theme/config';
import { ActionLink } from '@/components/actions/ActionLink';
import { accountCreateUrl } from '@/lib/account-urls';
import { BrandLink } from './BrandLink';
import { HeaderNavigation } from './HeaderNavigation';
import { LocalePill } from './LocalePill';
import { MobileNavigation } from './MobileNavigation';
import styles from './SiteHeader.module.css';
import { ThemedAccountCreateLink } from './ThemedAccountCreateLink';
import { ThemePill } from './ThemePill';
function signInUrl(locale: Locale, mode: ThemePreference): string {
return localizedModePath('sign-in', locale, mode);
}
import { ThemedRouteActionLink } from './ThemedRouteActionLink';
interface SiteHeaderProps {
locale: Locale;
@@ -40,24 +36,34 @@ export function SiteHeader({ locale, messages, themePreference }: SiteHeaderProp
role="group"
aria-label={messages.header.controlsLabel}
>
<LocalePill locale={locale} />
<LocalePill locale={locale} initialPreference={themePreference} />
<ThemePill initialPreference={themePreference} />
<ActionLink href={signInUrl(locale, themePreference)} variant="button-primary">
<ThemedRouteActionLink
locale={locale}
routeId="sign-in"
initialPreference={themePreference}
variant="button-primary"
>
{messages.header.login}
</ActionLink>
<ActionLink href={accountCreateUrl(locale)} variant="button-conversion">
</ThemedRouteActionLink>
<ThemedAccountCreateLink
locale={locale}
initialPreference={themePreference}
variant="button-conversion"
>
{messages.header.demo}
</ActionLink>
</ThemedAccountCreateLink>
</div>
<div className={styles.mobileActions}>
<ActionLink
href={accountCreateUrl(locale)}
<ThemedAccountCreateLink
locale={locale}
initialPreference={themePreference}
variant="button-conversion"
className={styles.mobileDemoTrigger}
>
{messages.header.demo}
</ActionLink>
</ThemedAccountCreateLink>
<MobileNavigation
locale={locale}
messages={messages}
@@ -0,0 +1,23 @@
'use client';
import { ActionLink } from '@/components/actions/ActionLink';
import { accountCreateUrl } from '@/lib/account-urls';
import type { Locale } from '@/lib/localization/config';
import type { ThemePreference } from '@/lib/theme/config';
import type { ComponentProps } from 'react';
import { useThemePreference } from './useThemePreference';
interface ThemedAccountCreateLinkProps
extends Omit<ComponentProps<typeof ActionLink>, 'href'> {
locale: Locale;
initialPreference: ThemePreference;
}
export function ThemedAccountCreateLink({
locale,
initialPreference,
...props
}: ThemedAccountCreateLinkProps) {
const preference = useThemePreference(initialPreference);
return <ActionLink href={accountCreateUrl(locale, preference)} {...props} />;
}
@@ -0,0 +1,24 @@
'use client';
import { ActionLink } from '@/components/actions/ActionLink';
import { localizedModePath, type Locale, type RouteId } from '@/lib/localization/config';
import type { ThemePreference } from '@/lib/theme/config';
import type { ComponentProps } from 'react';
import { useThemePreference } from './useThemePreference';
interface ThemedRouteActionLinkProps
extends Omit<ComponentProps<typeof ActionLink>, 'href'> {
locale: Locale;
routeId: RouteId;
initialPreference: ThemePreference;
}
export function ThemedRouteActionLink({
locale,
routeId,
initialPreference,
...props
}: ThemedRouteActionLinkProps) {
const preference = useThemePreference(initialPreference);
return <ActionLink href={localizedModePath(routeId, locale, preference)} {...props} />;
}
@@ -0,0 +1,25 @@
'use client';
import { currentThemePreference, themePreferenceEvent } from '@/lib/theme/client';
import type { ThemePreference } from '@/lib/theme/config';
import { useEffect, useState } from 'react';
export function useThemePreference(initialPreference: ThemePreference): ThemePreference {
const [preference, setPreference] = useState<ThemePreference>(initialPreference);
useEffect(() => {
setPreference(currentThemePreference());
const handlePreference = (event: Event) => {
const next = (event as CustomEvent<ThemePreference>).detail;
if (next === 'light' || next === 'dark' || next === 'system') {
setPreference(next);
}
};
window.addEventListener(themePreferenceEvent, handlePreference);
return () => window.removeEventListener(themePreferenceEvent, handlePreference);
}, []);
return preference;
}
@@ -140,3 +140,22 @@
margin-block-start: 1rem;
text-align: center;
}
.dashboardOrangeButton.dashboardOrangeButton {
border-color: transparent;
background: #ea580c;
color: #ffffff;
}
.dashboardOrangeButton.dashboardOrangeButton:hover:not(:disabled) {
background: #c2410c;
}
:global(html[data-theme='dark']) .dashboardOrangeButton.dashboardOrangeButton {
background: #f97316;
color: #ffffff;
}
:global(html[data-theme='dark']) .dashboardOrangeButton.dashboardOrangeButton:hover:not(:disabled) {
background: #fb923c;
}
@@ -140,7 +140,14 @@ export function ForgotPasswordForm({ locale }: { locale: Locale }) {
/>
</FormField>
<Button type="submit" intent="primary" fullWidth loading={loading} disabled={loading}>
<Button
type="submit"
intent="conversion"
fullWidth
loading={loading}
disabled={loading}
className={styles.dashboardOrangeButton}
>
{loading ? dict.submitting : dict.submit}
</Button>
</form>
@@ -298,7 +298,14 @@ export function SignInForm({ locale }: { locale: Locale }) {
</div>
</FormField>
<Button type="submit" intent="primary" fullWidth loading={loading} disabled={loading}>
<Button
type="submit"
intent="conversion"
fullWidth
loading={loading}
disabled={loading}
className={styles.dashboardOrangeButton}
>
{loading ? dict.signingIn : dict.signIn}
</Button>
@@ -336,7 +343,14 @@ export function SignInForm({ locale }: { locale: Locale }) {
/>
</FormField>
<Button type="submit" intent="primary" fullWidth loading={loading} disabled={loading}>
<Button
type="submit"
intent="conversion"
fullWidth
loading={loading}
disabled={loading}
className={styles.dashboardOrangeButton}
>
{loading ? dict.verifying : dict.verify}
</Button>
+2 -2
View File
@@ -274,7 +274,7 @@ export function buildHomepageContent(
eyebrow: homepage.hero.eyebrow,
title: homepage.hero.title,
body: homepage.hero.body,
primary: { label: homepage.hero.primary, href: accountCreateUrl(locale) },
primary: { label: homepage.hero.primary, href: accountCreateUrl(locale, 'dark') },
secondary: { label: homepage.hero.secondary, disabledReason: pendingReason },
preview: {
title: homepage.preview.title,
@@ -436,7 +436,7 @@ export function buildHomepageContent(
eyebrow: homepage.final.eyebrow,
title: homepage.final.title,
body: homepage.final.body,
primary: { label: homepage.final.primary, href: accountCreateUrl(locale) },
primary: { label: homepage.final.primary, href: accountCreateUrl(locale, 'dark') },
secondary: { label: homepage.final.secondary, disabledReason: pendingReason },
image: localizedImage(
'rentaldrivego',
+3 -1
View File
@@ -1,6 +1,8 @@
import type { Locale } from '@/lib/localization/config';
import type { ThemePreference } from '@/lib/theme/config';
export function accountCreateUrl(locale: Locale): string {
export function accountCreateUrl(locale: Locale, theme?: ThemePreference): string {
const params = new URLSearchParams({ lang: locale });
if (theme === 'light' || theme === 'dark') params.set('theme', theme);
return `/dashboard/sign-up?${params.toString()}`;
}
+1 -1
View File
@@ -12,7 +12,7 @@ export type Direction = 'ltr' | 'rtl';
export type RouteId = 'home' | 'sign-in' | 'forgot-password' | 'reset-password' | 'privacy' | 'terms' | 'accessibility';
export const defaultLocale: Locale = 'en';
export const defaultMode: ThemePreference = 'system';
export const defaultMode: ThemePreference = 'dark';
export const localeCookie = 'hpc-locale';
export const localeStorageKey = 'hpc.locale';
export const localeCookieMaxAgeSeconds = 31_536_000;
@@ -4,7 +4,7 @@ export const themeBootstrapScript = `(function () {
var cookieValue = cookieMatch ? decodeURIComponent(cookieMatch[1]) : null;
var stored = null;
try { stored = localStorage.getItem("hpc.theme.preference"); } catch (_) {}
var preference = allowed[cookieValue] ? cookieValue : (allowed[stored] ? stored : "system");
var preference = allowed[cookieValue] ? cookieValue : (allowed[stored] ? stored : "dark");
var dark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
var resolved = preference === "system" ? (dark ? "dark" : "light") : preference;
var root = document.documentElement;
+1 -1
View File
@@ -40,5 +40,5 @@ export function persistTheme(preference: ThemePreference): void {
export function currentThemePreference(): ThemePreference {
const value = document.documentElement.dataset.themePreference;
return isThemePreference(value) ? value : 'system';
return isThemePreference(value) ? value : 'dark';
}
+1 -1
View File
@@ -100,7 +100,7 @@ export function proxy(request: NextRequest): NextResponse {
const pathSegments = request.nextUrl.pathname.split('/').filter(Boolean);
// Redirect locale-less paths (e.g. /sign-in /en/system/sign-in), but not proxied app paths or files.
// Redirect locale-less paths (e.g. /sign-in -> /en/dark/sign-in), but not proxied app paths or files.
const firstSegment = request.nextUrl.pathname.split('/')[1];
const isFile = /\.\w+$/.test(request.nextUrl.pathname);
if (firstSegment && !isFile && !isLocale(firstSegment) && !reservedProxySegments.has(firstSegment)) {