fix bug 16 and 17
Build & Push / Pipeline Tests (push) Successful in 1m53s
Test / Type Check (all packages) (push) Successful in 56s
Build & Push / Build & Push Docker Image (push) Failing after 4m53s
Test / API Unit Tests (push) Successful in 1m15s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Carplace Unit Tests (push) Successful in 41s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 42s
Test / API Integration Tests (push) Successful in 1m6s

This commit is contained in:
root
2026-08-04 00:52:58 -04:00
parent 626da23c2e
commit 149806a773
72 changed files with 834 additions and 174 deletions
@@ -1,3 +1,5 @@
'use client';
import { StatusBadge } from '@/components/foundation/StatusBadge';
import { accountCreateUrl } from '@/lib/account-urls';
import { localizedModePath, type Locale } from '@/lib/localization/config';
@@ -5,6 +7,7 @@ import type { ShellMessages } from '@/lib/localization/messages';
import type { ThemePreference } from '@/lib/theme/config';
import Image from 'next/image';
import styles from './SiteFooter.module.css';
import { useThemePreference } from './useThemePreference';
function signInUrl(locale: Locale, mode: ThemePreference): string {
return localizedModePath('sign-in', locale, mode);
@@ -61,6 +64,7 @@ function PendingItem({ label, pending }: { label: string; pending: string }) {
}
export function SiteFooter({ locale, mode, messages }: SiteFooterProps) {
const currentMode = useThemePreference(mode);
const footer = messages.footer;
const year = new Date().getUTCFullYear();
@@ -117,7 +121,7 @@ export function SiteFooter({ locale, mode, messages }: SiteFooterProps) {
<PendingItem label={footer.links.contact} pending={footer.pending} />
</li>
<li>
<a href={accountCreateUrl(locale, mode)}>{footer.links.demo}</a>
<a href={accountCreateUrl(locale, currentMode)}>{footer.links.demo}</a>
</li>
</ul>
</section>
@@ -126,7 +130,7 @@ export function SiteFooter({ locale, mode, messages }: SiteFooterProps) {
<h2 id="footer-account">{footer.groups.account}</h2>
<ul>
<li>
<a href={signInUrl(locale, mode)}>
<a href={signInUrl(locale, currentMode)}>
{footer.links.login}
</a>
</li>
@@ -1,11 +1,13 @@
'use client';
import { applyTheme, currentThemePreference } from '@/lib/theme/client';
import { applyTheme, currentThemePreference, persistTheme } from '@/lib/theme/client';
import { themeStorageKey } from '@/lib/theme/config';
import { useEffect } from 'react';
export function ThemeController() {
useEffect(() => {
persistTheme(currentThemePreference());
const media = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemChange = () => {
if (currentThemePreference() === 'system') applyTheme('system');
@@ -18,6 +18,10 @@
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(16rem, 0.5fr);
gap: var(--space-5);
position: relative;
left: 50%;
inline-size: min(100vw - (var(--layout-gutter) * 2), 104rem);
transform: translateX(-50%);
padding: var(--space-5);
border: 1px solid var(--border-standard);
border-radius: var(--radius-lg);
@@ -107,8 +111,12 @@
.planGrid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
align-items: stretch;
position: relative;
left: 50%;
inline-size: min(100vw - (var(--layout-gutter) * 2), 104rem);
transform: translateX(-50%);
gap: var(--space-5);
}
@@ -180,15 +188,15 @@
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: var(--space-2);
gap: var(--space-1);
color: var(--text-primary);
}
.price {
font-size: clamp(2rem, 4vw, 3rem);
font-size: clamp(1.65rem, 2.4vw, 2.35rem);
font-weight: 850;
line-height: 1;
letter-spacing: -0.035em;
letter-spacing: 0;
}
.pendingPrice,
@@ -330,10 +338,6 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.planCard:last-child {
grid-column: 1 / -1;
}
.compareBlock {
justify-items: start;
text-align: start;
@@ -356,10 +360,6 @@
transform: none;
}
.planCard:last-child {
grid-column: auto;
}
.audience,
.priceBlock {
min-block-size: auto;
@@ -1,6 +1,7 @@
'use client';
import { ActionLink } from '@/components/actions/ActionLink';
import { useThemePreference } from '@/components/app-shell/useThemePreference';
import { Icon } from '@/components/foundation/Icon';
import { DemoTrigger } from '@/components/integrations/DemoTrigger';
import { Container, Section, Stack } from '@/components/layout/LayoutPrimitives';
@@ -14,18 +15,27 @@ import {
} from '@/content/pricing-config';
import { trackAnalytics } from '@/lib/analytics/client';
import type { AnalyticsContext } from '@/lib/analytics/events';
import { accountCreateUrl, type AccountSubscriptionPlan } from '@/lib/account-urls';
import { classNames } from '@/lib/components/classNames';
import { getDirection, type Locale } from '@/lib/localization/config';
import type { ThemePreference } from '@/lib/theme/config';
import { useMemo, useState } from 'react';
import styles from './PricingSection.module.css';
interface PricingSectionProps {
content: HomepageContent['pricing'];
locale: Locale;
themePreference: ThemePreference;
homePath: string;
demoSubmissionEnabled: boolean;
}
const accountSubscriptionPlanByPricingPlan: Record<Exclude<PricingPlanId, 'enterprise'>, AccountSubscriptionPlan> = {
launch: 'STARTER',
growth: 'GROWTH',
pro: 'PRO',
};
function analyticsContext(
locale: Locale,
fleetBand: FleetBandId,
@@ -44,10 +54,10 @@ function analyticsContext(
};
}
function monthlyEquivalent(monthlyPrice: number, billingPeriod: BillingPeriod): number {
function calculateDisplayedPrice(monthlyPrice: number, billingPeriod: BillingPeriod): number {
if (billingPeriod === 'monthly') return monthlyPrice;
const discountPercent = pricingCommercialConfig.annualDiscountPercent ?? 0;
return monthlyPrice * (1 - discountPercent / 100);
return monthlyPrice * 12 * (1 - discountPercent / 100);
}
function formatPrice(value: number, locale: Locale): string {
@@ -69,9 +79,11 @@ function formatDiscount(locale: Locale): string {
export function PricingSection({
content,
locale,
themePreference,
homePath,
demoSubmissionEnabled,
}: PricingSectionProps) {
const currentThemePreference = useThemePreference(themePreference);
const [fleetBand, setFleetBand] = useState<FleetBandId>('small');
const [billingPeriod, setBillingPeriod] = useState<BillingPeriod>('monthly');
const recommendedPlan = pricingCommercialConfig.recommendedPlanByFleetBand[fleetBand];
@@ -84,7 +96,7 @@ export function PricingSection({
const publicPrice = pricingCommercialConfig.publicPricesApproved ? configuredPrice : null;
return [
plan.id,
publicPrice === null ? null : monthlyEquivalent(publicPrice, billingPeriod),
publicPrice === null ? null : calculateDisplayedPrice(publicPrice, billingPeriod),
];
}),
) as Record<PricingPlanId, number | null>,
@@ -198,7 +210,7 @@ export function PricingSection({
<bdi className={styles.price} dir="ltr">
{formatPrice(price, locale)}
</bdi>
<span>{content.perMonth}</span>
<span>{billingPeriod === 'monthly' ? content.perMonth : content.perYear}</span>
</div>
{billingPeriod === 'annual' ? (
<p className={styles.priceMeta}>{content.annualEquivalent}</p>
@@ -216,22 +228,42 @@ export function PricingSection({
))}
</ul>
<DemoTrigger
label={plan.cta}
source="pricing"
size="large"
fullWidth
className={styles.planAction}
disabledReason={
demoSubmissionEnabled ? undefined : content.actionDisabledReason
}
onActivate={() =>
trackAnalytics(
'pricing_plan_cta_clicked',
analyticsContext(locale, fleetBand, billingPeriod, plan.id),
)
}
/>
{plan.id === 'enterprise' ? (
<DemoTrigger
label={plan.cta}
source="pricing"
size="large"
fullWidth
className={styles.planAction}
disabledReason={
demoSubmissionEnabled ? undefined : content.actionDisabledReason
}
onActivate={() =>
trackAnalytics(
'pricing_plan_cta_clicked',
analyticsContext(locale, fleetBand, billingPeriod, plan.id),
)
}
/>
) : (
<ActionLink
href={accountCreateUrl(
locale,
currentThemePreference,
accountSubscriptionPlanByPricingPlan[plan.id],
)}
variant="button-conversion"
className={styles.planAction}
onClick={() =>
trackAnalytics(
'pricing_plan_cta_clicked',
analyticsContext(locale, fleetBand, billingPeriod, plan.id),
)
}
>
{plan.cta}
</ActionLink>
)}
</article>
);
})}
@@ -30,6 +30,7 @@ interface DemoDialogHostProps {
type FormStatus = 'idle' | 'submitting' | 'success' | 'error';
type FieldErrors = Record<string, string>;
type FleetOption = { value: (typeof fleetSizeValues)[number]; label: string };
function messageForCode(code: string, messages: DemoDialogHostProps['messages']): string {
switch (code) {
@@ -99,6 +100,14 @@ export function DemoDialogHost({ locale, messages, enabled }: DemoDialogHostProp
const idempotencyKeyRef = useRef('');
const startedAtRef = useRef(0);
const closeTrackedRef = useRef(false);
const pricingFleetOnly = source === 'pricing';
const dialogTitle = pricingFleetOnly ? messages.quoteTitle : messages.title;
const dialogDescription = pricingFleetOnly ? messages.quoteBody : messages.body;
const noticeLabel = pricingFleetOnly ? messages.quoteModeLabel : messages.localModeLabel;
const noticeBody = pricingFleetOnly ? messages.quotePrivacy : messages.privacy;
const submitLabel = pricingFleetOnly ? messages.quoteSubmit : messages.submit;
const successTitle = pricingFleetOnly ? messages.quoteSuccessTitle : messages.successTitle;
const successBody = pricingFleetOnly ? messages.quoteSuccessBody : messages.successBody;
const summaryErrors = useMemo<FormErrorItem[]>(
() =>
@@ -108,6 +117,18 @@ export function DemoDialogHost({ locale, messages, enabled }: DemoDialogHostProp
})),
[fieldErrors, messages],
);
const fleetOptions = useMemo<FleetOption[]>(() => {
if (pricingFleetOnly) {
return [{ value: '250+', label: messages.fleetOptions.at(-1) ?? '150 or more vehicles' }];
}
return fleetSizeValues
.map((value, index) => {
const label = messages.fleetOptions[index + 1];
return label ? { value, label } : null;
})
.filter((option): option is FleetOption => option !== null);
}, [messages.fleetOptions, pricingFleetOnly]);
useEffect(() => {
const handleOpen = (event: Event) => {
@@ -156,8 +177,8 @@ export function DemoDialogHost({ locale, messages, enabled }: DemoDialogHostProp
setOpen(true);
}
}}
title={messages.title}
description={messages.body}
title={dialogTitle}
description={dialogDescription}
closeLabel={messages.close}
initialFocusRef={firstFieldRef}
returnFocusRef={returnFocusRef}
@@ -166,8 +187,8 @@ export function DemoDialogHost({ locale, messages, enabled }: DemoDialogHostProp
{status === 'success' ? (
<section className={styles.success} aria-live="polite">
<StatusBadge tone="success">{messages.localModeLabel}</StatusBadge>
<h3>{messages.successTitle}</h3>
<p>{messages.successBody}</p>
<h3>{successTitle}</h3>
<p>{successBody}</p>
<div className={styles.actions}>
<Button intent="primary" onClick={reset}>
{messages.submitAnother}
@@ -270,8 +291,8 @@ export function DemoDialogHost({ locale, messages, enabled }: DemoDialogHostProp
}}
>
<div className={styles.notice}>
<StatusBadge tone="info">{messages.localModeLabel}</StatusBadge>
<p>{messages.privacy}</p>
<StatusBadge tone="info">{noticeLabel}</StatusBadge>
<p>{noticeBody}</p>
</div>
{formError ? (
@@ -355,17 +376,18 @@ export function DemoDialogHost({ locale, messages, enabled }: DemoDialogHostProp
}
>
<Select
key={pricingFleetOnly ? 'pricing-fleet-size' : 'default-fleet-size'}
id="fleetSize"
name="fleetSize"
required
defaultValue=""
defaultValue={pricingFleetOnly ? '250+' : ''}
invalid={Boolean(fieldErrors.fleetSize)}
describedBy={fieldErrors.fleetSize ? 'fleetSize-error' : undefined}
>
<option value="">{messages.fleetOptions[0]}</option>
{fleetSizeValues.map((value, index) => (
<option key={value} value={value}>
{messages.fleetOptions[index + 1]}
{pricingFleetOnly ? null : <option value="">{messages.fleetOptions[0]}</option>}
{fleetOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
@@ -428,7 +450,7 @@ export function DemoDialogHost({ locale, messages, enabled }: DemoDialogHostProp
? messages.submitting
: status === 'error'
? messages.retry
: messages.submit}
: submitLabel}
</Button>
<Button
type="button"