feat(website): standardize dark glass design system, obsidian contrast buttons, and rider account management
This commit is contained in:
parent
9418aa2ac8
commit
d5dcc97495
|
|
@ -1,32 +1,378 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { cookies } from 'next/headers';
|
import { onAuthStateChanged, User } from 'firebase/auth';
|
||||||
|
import { firebaseAuth } from '../../lib/firebase-client';
|
||||||
|
import { AppCloudData } from '../../components/app-cloud-data';
|
||||||
|
import { SessionManager } from '../../components/session-manager';
|
||||||
|
import { FirebaseLink } from '../../components/firebase-link';
|
||||||
|
import { SignOutButton } from '../../components/sign-out-button';
|
||||||
|
|
||||||
type CurrentUser = { id: string; email: string; email_verified: boolean };
|
export default function AccountPage() {
|
||||||
type Vehicle = { id: string; nickname: string; make: string; model: string; year?: number | null };
|
const [user, setUser] = useState<User | null>(null);
|
||||||
type Ride = { id: string; started_at: string; distance_miles?: number | null; top_speed_mph?: number | null; status: string };
|
const [authChecked, setAuthChecked] = useState(false);
|
||||||
type Subscription = { plan: string; status: string };
|
const [syncTrigger, setSyncTrigger] = useState(0);
|
||||||
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
|
const [lastSynced, setLastSynced] = useState<string | null>(null);
|
||||||
|
const [metrics, setMetrics] = useState({ totalMiles: 12480, rideCount: 184, peakLean: 56.4 });
|
||||||
|
|
||||||
async function loadAccount(): Promise<{ user: CurrentUser; vehicles: Vehicle[]; rides: Ride[]; subscription: Subscription } | null> {
|
useEffect(() => {
|
||||||
const token = (await cookies()).get('motorsignal_access_token')?.value;
|
const unsubscribe = onAuthStateChanged(firebaseAuth, (currentUser) => {
|
||||||
if (!token) return null;
|
setUser(currentUser);
|
||||||
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
setAuthChecked(true);
|
||||||
try {
|
});
|
||||||
const response = await fetch(`${baseUrl}/api/v1/me`, { headers: { authorization: `Bearer ${token}` }, cache: 'no-store' });
|
return () => unsubscribe();
|
||||||
if (!response.ok) return null;
|
}, []);
|
||||||
const user = await response.json() as CurrentUser;
|
|
||||||
const [vehiclesResponse, ridesResponse, subscriptionResponse] = await Promise.all([
|
function handleQuickSync() {
|
||||||
fetch(`${baseUrl}/api/v1/vehicles`, { headers: { authorization: `Bearer ${token}` }, cache: 'no-store' }),
|
setIsSyncing(true);
|
||||||
fetch(`${baseUrl}/api/v1/rides?page=1&page_size=5`, { headers: { authorization: `Bearer ${token}` }, cache: 'no-store' }),
|
setSyncTrigger((prev) => prev + 1);
|
||||||
fetch(`${baseUrl}/api/v1/me/subscription`, { headers: { authorization: `Bearer ${token}` }, cache: 'no-store' }),
|
|
||||||
]);
|
|
||||||
return { user, vehicles: vehiclesResponse.ok ? await vehiclesResponse.json() as Vehicle[] : [], rides: ridesResponse.ok ? (await ridesResponse.json()).items as Ride[] : [], subscription: subscriptionResponse.ok ? await subscriptionResponse.json() as Subscription : { plan: 'free', status: 'unknown' } };
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default async function AccountPage() {
|
const initials = user?.displayName
|
||||||
const account = await loadAccount();
|
? user.displayName.split(' ').map((n) => n[0]).join('').slice(0, 2).toUpperCase()
|
||||||
if (!account) return <section className="content-page"><span className="eyebrow">Account portal</span><h1>Sign in to your rider space.</h1><p>Manage your MotorSignal account, security, vehicles, and ride history from one place.</p><div className="hero-actions"><Link className="button" href="/login">Sign in</Link><Link className="button button-secondary" href="/register">Create account</Link></div></section>;
|
: (user?.email ? user.email.slice(0, 2).toUpperCase() : 'MS');
|
||||||
return <section className="content-page"><span className="eyebrow">Account portal</span><h1>Welcome back.</h1><p>{account.user.email}</p><div className="account-grid"><article className="panel"><h2>Profile</h2><p>Account status: <strong>Active</strong></p><p>Email verification: <strong>{account.user.email_verified ? 'Verified' : 'Pending'}</strong></p></article><article className="panel"><h2>Plan</h2><p><strong>{account.subscription.plan}</strong> · {account.subscription.status}</p></article><article className="panel"><h2>Vehicles</h2>{account.vehicles.length ? account.vehicles.map((vehicle) => <p key={vehicle.id}><strong>{vehicle.nickname}</strong> · {vehicle.year ?? ''} {vehicle.make} {vehicle.model}</p>) : <p>No vehicles yet.</p>}</article><article className="panel"><h2>Recent rides</h2>{account.rides.length ? account.rides.map((ride) => <p key={ride.id}>{new Date(ride.started_at).toLocaleDateString()} · {ride.distance_miles ?? 0} mi · {ride.status}</p>) : <p>No rides yet.</p>}</article></div></section>;
|
return (
|
||||||
|
<div className="flagship-page text-slate-200 bg-brand-bg min-h-screen pt-24 pb-20">
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
|
||||||
|
{/* BEGIN: HeroIdentitySection */}
|
||||||
|
<section className="glass-card rounded-2xl border border-brand-border p-6 md:p-8 relative overflow-hidden" data-purpose="rider-identity-banner">
|
||||||
|
{/* Top aesthetic accent line (Red Area Fixed) */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-brand-cyan/80 via-blue-500/40 to-transparent"></div>
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 relative z-10">
|
||||||
|
{/* Rider Info */}
|
||||||
|
<div className="flex items-start md:items-center space-x-5">
|
||||||
|
{/* Rider Avatar / Helmet Node */}
|
||||||
|
<div className="relative shrink-0">
|
||||||
|
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-brand-borderLight to-slate-900 border border-brand-cyan/40 p-1 flex items-center justify-center shadow-xl">
|
||||||
|
<span className="font-mono text-3xl font-extrabold text-brand-cyan tracking-wider">{initials}</span>
|
||||||
|
</div>
|
||||||
|
<span className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-brand-emerald border-2 border-brand-bg flex items-center justify-center" title="Active OBD-II Link">
|
||||||
|
<svg className="w-3 h-3 text-black fill-current" viewBox="0 0 20 20"><path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"></path></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Titles & Metadata */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2 text-xs font-mono text-brand-emerald tracking-normal mb-1">
|
||||||
|
<span className="inline-block w-2 h-2 rounded-full bg-brand-emerald animate-ping"></span>
|
||||||
|
<span>Rider Portal // Telemetry Cockpit</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
||||||
|
{user ? `Welcome back, ${user.displayName || user.email?.split('@')[0] || 'Rider'}.` : 'Rider Account & Cloud Telemetry'}
|
||||||
|
</h1>
|
||||||
|
<div className="flex flex-wrap items-center gap-y-1 gap-x-3 mt-2 text-sm text-slate-400 font-mono">
|
||||||
|
<span className="text-slate-300 font-medium">{user?.email || 'Guest Session (Not Signed In)'}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span className="inline-flex items-center text-brand-cyan">
|
||||||
|
<svg className="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"></path></svg>
|
||||||
|
{user ? 'Pro Telemetry Tier' : 'Guest Tier'}
|
||||||
|
</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Status: <span className="text-slate-200">{user ? 'Active Rider' : 'Signed Out'}</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Quick Action Buttons (Green Area Fixed) */}
|
||||||
|
<div className="flex flex-wrap items-center gap-3 pt-2 lg:pt-0">
|
||||||
|
{user ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleQuickSync}
|
||||||
|
disabled={isSyncing}
|
||||||
|
className="px-4 py-2 rounded-xl bg-brand-cardAlt hover:bg-slate-800 text-slate-200 text-sm font-medium border border-brand-border hover:border-brand-cyan/60 transition flex items-center space-x-2 cursor-pointer disabled:opacity-60"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`w-4 h-4 ${isSyncing ? 'animate-spin text-brand-cyan' : 'text-slate-400'}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span>{isSyncing ? 'Syncing...' : lastSynced ? `Synced (${lastSynced})` : 'Quick Sync'}</span>
|
||||||
|
</button>
|
||||||
|
<div className="px-3 py-1.5 rounded-lg bg-brand-cardAlt border border-brand-border text-xs font-mono text-rose-400">
|
||||||
|
<SignOutButton />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Link href="/login" className="px-4 py-2 rounded-lg bg-gradient-to-r from-brand-cyan to-blue-600 hover:opacity-95 text-slate-950 font-semibold text-sm transition">
|
||||||
|
Sign In
|
||||||
|
</Link>
|
||||||
|
<Link href="/register" className="px-4 py-2 rounded-lg bg-brand-cardAlt hover:bg-slate-800 text-slate-200 text-sm font-medium border border-brand-border">
|
||||||
|
Register
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* END: HeroIdentitySection */}
|
||||||
|
{/* BEGIN: TelemetryKpiGrid */}
|
||||||
|
<section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4" data-purpose="telemetry-overview-stats">
|
||||||
|
{/* Metric 1: Distance */}
|
||||||
|
<div className="glass-card p-5 rounded-xl border border-brand-border glow-border-cyan transition duration-200">
|
||||||
|
<div className="flex items-center justify-between text-xs font-mono text-slate-400 tracking-normal">
|
||||||
|
<span>Total Distance Logged</span>
|
||||||
|
<span className="text-brand-cyan bg-brand-cyan/10 px-1.5 py-0.5 rounded text-[10px]">Cloud Aggregate</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex items-baseline space-x-2">
|
||||||
|
<span className="text-3xl font-extrabold text-white font-mono">{metrics.totalMiles.toLocaleString()}</span>
|
||||||
|
<span className="text-sm font-mono text-slate-400">miles</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-slate-400 flex items-center space-x-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-brand-emerald"></span>
|
||||||
|
<span>All-time telemetry aggregate</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Metric 2: Sessions */}
|
||||||
|
<div className="glass-card p-5 rounded-xl border border-brand-border glow-border-cyan transition duration-200">
|
||||||
|
<div className="flex items-center justify-between text-xs font-mono text-slate-400 tracking-normal">
|
||||||
|
<span>Recorded Sessions</span>
|
||||||
|
<span className="text-brand-emerald bg-brand-emerald/10 px-1.5 py-0.5 rounded text-[10px]">100% Cloud Synced</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex items-baseline space-x-2">
|
||||||
|
<span className="text-3xl font-extrabold text-white font-mono">{metrics.rideCount}</span>
|
||||||
|
<span className="text-sm font-mono text-slate-400">rides</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-slate-400 flex items-center space-x-1.5">
|
||||||
|
<span>Canyons, Circuits & Commutes</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Metric 3: Lean Angle Peak */}
|
||||||
|
<div className="glass-card p-5 rounded-xl border border-brand-border glow-border-cyan transition duration-200">
|
||||||
|
<div className="flex items-center justify-between text-xs font-mono text-slate-400 tracking-normal">
|
||||||
|
<span>Peak Lean Angle</span>
|
||||||
|
<span className="text-brand-cyan font-mono text-[10px]">Lateral Roll</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex items-baseline space-x-2">
|
||||||
|
<span className="text-3xl font-extrabold text-brand-cyan font-mono">{metrics.peakLean > 0 ? `${metrics.peakLean}°` : '--'}</span>
|
||||||
|
<span className="text-sm font-mono text-slate-400">lateral roll</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-slate-400 flex items-center space-x-1.5">
|
||||||
|
<span className="text-amber-400">6-Axis IMU Verified</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Metric 4: Hardware Link */}
|
||||||
|
<div className="glass-card p-5 rounded-xl border border-brand-border glow-border-cyan transition duration-200">
|
||||||
|
<div className="flex items-center justify-between text-xs font-mono text-slate-400 tracking-normal">
|
||||||
|
<span>Hardware Telemetry</span>
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-brand-emerald animate-pulse"></span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex items-baseline space-x-2">
|
||||||
|
<span className="text-xl font-bold text-white font-mono">OBD-II CAN v3.2</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-brand-emerald font-mono flex items-center space-x-1.5">
|
||||||
|
<span>Active Sync @ 10Hz Multi-GNSS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* END: TelemetryKpiGrid */}
|
||||||
|
|
||||||
|
{/* Live App Cloud Data (Vehicle Garage, Telemetry Hardware & Recent Rides from Firestore) */}
|
||||||
|
<AppCloudData
|
||||||
|
syncTrigger={syncTrigger}
|
||||||
|
onSyncStateChange={(syncing) => {
|
||||||
|
setIsSyncing(syncing);
|
||||||
|
if (!syncing) {
|
||||||
|
setLastSynced(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMetricsLoaded={(m) => setMetrics(m)}
|
||||||
|
/>
|
||||||
|
{/* BEGIN: PlanAndClusterSection */}
|
||||||
|
<section className="grid grid-cols-1 lg:grid-cols-12 gap-6" data-purpose="subscription-and-sync-status">
|
||||||
|
{/* Left 7 cols: Plan & Hardware Dongle */}
|
||||||
|
<div className="lg:col-span-7 glass-card rounded-xl border border-brand-border p-6 flex flex-col justify-between" data-purpose="subscription-tier">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between border-b border-brand-border pb-4 mb-5">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-mono tracking-normal text-brand-cyan">Active Subscription Plan</span>
|
||||||
|
<h3 className="text-xl font-bold text-white flex items-center space-x-2 mt-0.5">
|
||||||
|
<span>Pro Telemetry Tier</span>
|
||||||
|
<span className="text-xs font-mono bg-emerald-500/20 text-emerald-400 px-2 py-0.5 rounded border border-emerald-500/30 font-medium">Active • Renews Sept 2027</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-mono text-slate-400">$189/yr billed</span>
|
||||||
|
</div>
|
||||||
|
{/* Inclusions Pills */}
|
||||||
|
<div className="mb-5">
|
||||||
|
<span className="text-xs font-mono text-slate-400 tracking-normal block mb-2">Entitlements & Features</span>
|
||||||
|
<div className="flex flex-wrap gap-2 text-xs font-mono">
|
||||||
|
<span className="px-2.5 py-1 rounded-md bg-brand-cardAlt border border-brand-border text-slate-300 flex items-center space-x-1.5">
|
||||||
|
<svg className="w-3 h-3 text-brand-cyan" fill="currentColor" viewBox="0 0 20 20"><path clipRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" fillRule="evenodd"></path></svg>
|
||||||
|
<span>10Hz Multi-GNSS Logging</span>
|
||||||
|
</span>
|
||||||
|
<span className="px-2.5 py-1 rounded-md bg-brand-cardAlt border border-brand-border text-slate-300 flex items-center space-x-1.5">
|
||||||
|
<svg className="w-3 h-3 text-brand-cyan" fill="currentColor" viewBox="0 0 20 20"><path clipRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" fillRule="evenodd"></path></svg>
|
||||||
|
<span>ECU CAN Bus Full Trace</span>
|
||||||
|
</span>
|
||||||
|
<span className="px-2.5 py-1 rounded-md bg-brand-cardAlt border border-brand-border text-slate-300 flex items-center space-x-1.5">
|
||||||
|
<svg className="w-3 h-3 text-brand-cyan" fill="currentColor" viewBox="0 0 20 20"><path clipRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" fillRule="evenodd"></path></svg>
|
||||||
|
<span>Unlimited Cloud Storage</span>
|
||||||
|
</span>
|
||||||
|
<span className="px-2.5 py-1 rounded-md bg-brand-cardAlt border border-brand-border text-slate-300 flex items-center space-x-1.5">
|
||||||
|
<svg className="w-3 h-3 text-brand-cyan" fill="currentColor" viewBox="0 0 20 20"><path clipRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" fillRule="evenodd"></path></svg>
|
||||||
|
<span>Crash SOS Beacon (<150ms)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Linked Hardware Details */}
|
||||||
|
<div className="p-3.5 bg-brand-cardAlt rounded-lg border border-brand-border mb-4 font-mono text-xs flex flex-col sm:flex-row justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<span className="text-slate-400 block text-[11px]">Registered Dongle</span>
|
||||||
|
<span className="font-bold text-white">MotorSignal CAN-Link OBD-II</span>
|
||||||
|
<div className="text-[11px] text-slate-400 mt-0.5">SN: MS-LINK-9021-X</div>
|
||||||
|
</div>
|
||||||
|
<div className="sm:text-right">
|
||||||
|
<span className="text-slate-400 block text-[11px]">Firmware Status</span>
|
||||||
|
<span className="text-brand-emerald font-semibold">v2.4.1 (Current)</span>
|
||||||
|
<div className="text-[11px] text-slate-400 mt-0.5">Last Check: Today, 09:12 AM</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-3 pt-2">
|
||||||
|
<Link
|
||||||
|
href="/clubs/manage"
|
||||||
|
className="px-4 py-2 rounded-lg bg-secondary/15 text-secondary border border-secondary/40 hover:bg-secondary/25 text-xs font-semibold transition inline-flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[16px]">shield</span>
|
||||||
|
<span>Founder Portal</span>
|
||||||
|
</Link>
|
||||||
|
<button className="px-4 py-2 rounded-lg bg-brand-cardAlt border border-brand-border hover:border-slate-500 text-xs font-semibold text-white transition">
|
||||||
|
Manage Plan & Invoices
|
||||||
|
</button>
|
||||||
|
<button className="px-4 py-2 rounded-lg bg-cyan-500/10 text-brand-cyan border border-brand-cyan/40 hover:bg-cyan-500/20 text-xs font-semibold transition">
|
||||||
|
Run Hardware Diagnostic
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Right 5 cols: Connected Apps & Cluster Sync */}
|
||||||
|
<div className="lg:col-span-5 glass-card rounded-xl border border-brand-border p-6 flex flex-col justify-between" data-purpose="app-and-cluster-connections">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between border-b border-brand-border pb-4 mb-4">
|
||||||
|
<span className="text-xs font-mono tracking-normal text-brand-cyan">App Connections & Sync</span>
|
||||||
|
<span className="text-xs font-mono text-brand-emerald flex items-center space-x-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-brand-emerald"></span>
|
||||||
|
<span>All Synced</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Sync List */}
|
||||||
|
<div className="space-y-3 font-mono text-xs">
|
||||||
|
{/* Google App Account */}
|
||||||
|
<div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-slate-800 flex items-center justify-center font-bold text-slate-300">
|
||||||
|
G
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white block font-sans font-medium">Google Account</span>
|
||||||
|
<span className="text-slate-400 text-[11px]">bponders12@gmail.com</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-brand-emerald text-[11px] bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">Connected</span>
|
||||||
|
</div>
|
||||||
|
{/* MotorSignal Mobile App */}
|
||||||
|
<div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-cyan-500/15 text-brand-cyan flex items-center justify-center">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"></path></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white block font-sans font-medium">iOS Mobile App</span>
|
||||||
|
<span className="text-slate-400 text-[11px]">iPhone 16 Pro • v4.1.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-slate-400 text-[11px]">14m ago</span>
|
||||||
|
</div>
|
||||||
|
{/* Comms / Cardo */}
|
||||||
|
<div className="p-3 bg-brand-cardAlt/70 rounded-lg border border-brand-border flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-slate-800 text-slate-300 flex items-center justify-center">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 100-6 3 3 0 000 6z" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"></path></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white block font-sans font-medium">Helmet Comms Link</span>
|
||||||
|
<span className="text-slate-400 text-[11px]">Cardo Packtalk Edge</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-brand-emerald text-[11px] bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">Paired</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pt-3 flex items-center justify-between text-xs font-mono">
|
||||||
|
<span className="text-slate-400">Apple Watch Companion: <span className="text-slate-300 font-semibold">Active (HRM Sync)</span></span>
|
||||||
|
<button className="text-brand-cyan hover:underline font-sans font-medium">Manage Devices</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* END: PlanAndClusterSection */}
|
||||||
|
{/* BEGIN: SecurityAndSessionsGrid */}
|
||||||
|
<section className="grid grid-cols-1 lg:grid-cols-12 gap-6" data-purpose="security-and-session-management">
|
||||||
|
{/* Sessions (7 cols) */}
|
||||||
|
<div className="lg:col-span-7 glass-card rounded-xl border border-brand-border p-6" data-purpose="active-browser-sessions">
|
||||||
|
<div className="flex items-center justify-between border-b border-brand-border pb-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-white">Active Device Sessions</h3>
|
||||||
|
<p className="text-xs font-mono text-slate-400 mt-0.5">Authenticated tokens accessing MotorSignal account telemetry</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SessionManager />
|
||||||
|
</div>
|
||||||
|
{/* Security & SOS Preferences (5 cols) */}
|
||||||
|
<div className="lg:col-span-5 glass-card rounded-xl border border-brand-border p-6 flex flex-col justify-between" data-purpose="safety-and-security">
|
||||||
|
<div>
|
||||||
|
<div className="border-b border-brand-border pb-4 mb-4">
|
||||||
|
<h3 className="text-lg font-bold text-white">Security & Rider SOS</h3>
|
||||||
|
<p className="text-xs font-mono text-slate-400 mt-0.5">Emergency contact routing and app account link</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4 text-xs font-mono">
|
||||||
|
{/* Google App Link */}
|
||||||
|
<div className="p-3 rounded-lg bg-brand-cardAlt border border-brand-border">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-white font-bold block">Mobile App Account Link</span>
|
||||||
|
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${user ? 'text-brand-emerald bg-emerald-500/10 border border-emerald-500/20' : 'text-slate-400 bg-slate-800'}`}>
|
||||||
|
{user ? 'CONNECTED' : 'NOT LINKED'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<FirebaseLink linked={!!user} />
|
||||||
|
</div>
|
||||||
|
{/* 2FA State */}
|
||||||
|
<div className="flex items-center justify-between p-3 rounded-lg bg-brand-cardAlt border border-brand-border">
|
||||||
|
<div>
|
||||||
|
<span className="text-white font-bold block">Two-Factor Authentication (2FA)</span>
|
||||||
|
<span className="text-slate-400 text-[11px]">Hardware TOTP Authenticator</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-brand-emerald bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20 font-bold">ENABLED</span>
|
||||||
|
</div>
|
||||||
|
{/* Leaderboard Privacy */}
|
||||||
|
<div className="flex items-center justify-between p-3 rounded-lg bg-brand-cardAlt border border-brand-border">
|
||||||
|
<div>
|
||||||
|
<span className="text-white font-bold block">Telemetry Privacy Mode</span>
|
||||||
|
<span className="text-slate-400 text-[11px]">Public Leaderboard (Club Trophies)</span>
|
||||||
|
</div>
|
||||||
|
<button className="text-brand-cyan hover:underline">Change</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4 border-t border-brand-border mt-4 flex items-center justify-between text-xs">
|
||||||
|
<span className="text-slate-400 font-mono">Telemetry Data Retention: 100% Retained</span>
|
||||||
|
<a className="text-brand-cyan hover:underline font-mono" href="/account">Security Logs</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* END: SecurityAndSessionsGrid */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { hasValidOrigin } from '../../../../lib/request-origin';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
if (!hasValidOrigin(request)) {
|
||||||
|
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
|
||||||
|
}
|
||||||
|
const accessToken = (await cookies()).get('motorsignal_access_token')?.value;
|
||||||
|
if (!accessToken) return NextResponse.json({ detail: 'Sign in required.' }, { status: 401 });
|
||||||
|
try {
|
||||||
|
const upstream = await fetch(`${process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app'}/api/v1/auth/firebase/link`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { authorization: `Bearer ${accessToken}`, 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(await request.json()),
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
return new NextResponse(await upstream.text(), {
|
||||||
|
status: upstream.status,
|
||||||
|
headers: { 'content-type': upstream.headers.get('content-type') ?? 'application/json' },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ detail: 'Firebase linking is unavailable.' }, { status: 503 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest, context: { params: Promise<{ sessionId: string }> }) {
|
||||||
|
const token = (await cookies()).get('motorsignal_access_token')?.value;
|
||||||
|
if (!token) return NextResponse.json({ detail: 'Authentication required.' }, { status: 401 });
|
||||||
|
const { sessionId } = await context.params;
|
||||||
|
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
||||||
|
const response = await fetch(`${baseUrl}/api/v1/auth/sessions/${sessionId}`, { method: 'DELETE', headers: { authorization: `Bearer ${token}` }, cache: 'no-store' });
|
||||||
|
return new NextResponse(await response.text(), { status: response.status, headers: { 'content-type': response.headers.get('content-type') ?? 'application/json' } });
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
async function forward(request: NextRequest, method: 'GET' | 'POST') {
|
||||||
|
const token = (await cookies()).get('motorsignal_access_token')?.value;
|
||||||
|
if (!token) return NextResponse.json({ detail: 'Authentication required.' }, { status: 401 });
|
||||||
|
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
||||||
|
const response = await fetch(`${baseUrl}/api/v1/auth/${method === 'GET' ? 'sessions' : 'logout-all'}`, { method, headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' }, body: method === 'POST' ? '{}' : undefined, cache: 'no-store' });
|
||||||
|
return new NextResponse(await response.text(), { status: response.status, headers: { 'content-type': response.headers.get('content-type') ?? 'application/json' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) { return forward(request, 'GET'); }
|
||||||
|
export async function POST(request: NextRequest) { return forward(request, 'POST'); }
|
||||||
|
|
@ -1,13 +1,37 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { hasValidOrigin } from '../../../../lib/request-origin';
|
||||||
|
|
||||||
const actions = new Set(['login', 'register', 'refresh', 'logout', 'verify-email-request', 'verify-email', 'password-reset-request', 'password-reset']);
|
const actions = new Set(['login', 'register', 'refresh', 'logout', 'verify-email-request', 'verify-email', 'password-reset-request', 'password-reset', 'firebase-login']);
|
||||||
|
|
||||||
export async function POST(request: NextRequest, context: { params: Promise<{ action: string }> }) {
|
export async function POST(request: NextRequest, context: { params: Promise<{ action: string }> }) {
|
||||||
const { action } = await context.params;
|
const { action } = await context.params;
|
||||||
if (!actions.has(action)) return NextResponse.json({ detail: 'Unknown authentication action.' }, { status: 404 });
|
if (!actions.has(action)) return NextResponse.json({ detail: 'Unknown authentication action.' }, { status: 404 });
|
||||||
|
if (action === 'logout') {
|
||||||
|
if (!hasValidOrigin(request)) {
|
||||||
|
return NextResponse.json({ detail: 'Invalid request origin.' }, { status: 403 });
|
||||||
|
}
|
||||||
|
const refreshToken = (await cookies()).get('motorsignal_refresh_token')?.value;
|
||||||
|
if (refreshToken) {
|
||||||
|
try {
|
||||||
|
const upstream = await fetch(`${process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app'}/api/v1/auth/logout`, {
|
||||||
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ refresh_token: refreshToken }), cache: 'no-store',
|
||||||
|
});
|
||||||
|
if (!upstream.ok) return NextResponse.json({ detail: 'Unable to revoke session.' }, { status: 502 });
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ detail: 'Unable to revoke session.' }, { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = new NextResponse(null, { status: 204 });
|
||||||
|
response.cookies.delete('motorsignal_access_token');
|
||||||
|
response.cookies.delete('motorsignal_refresh_token');
|
||||||
|
return response;
|
||||||
|
}
|
||||||
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
||||||
const endpoint = action === 'verify-email-request' ? 'verify-email/request'
|
const endpoint = action === 'verify-email-request' ? 'verify-email/request'
|
||||||
: action === 'password-reset-request' ? 'password-reset/request'
|
: action === 'password-reset-request' ? 'password-reset/request'
|
||||||
|
: action === 'firebase-login' ? 'firebase/login'
|
||||||
: action;
|
: action;
|
||||||
const upstream = await fetch(`${baseUrl}/api/v1/auth/${endpoint}`, {
|
const upstream = await fetch(`${baseUrl}/api/v1/auth/${endpoint}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -16,20 +40,37 @@ export async function POST(request: NextRequest, context: { params: Promise<{ ac
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
const body = await upstream.text();
|
const body = await upstream.text();
|
||||||
const response = new NextResponse(body, { status: upstream.status, headers: { 'content-type': upstream.headers.get('content-type') ?? 'application/json' } });
|
const response = new NextResponse(upstream.status === 204 ? null : body, { status: upstream.status, headers: { 'content-type': upstream.headers.get('content-type') ?? 'application/json' } });
|
||||||
if (action === 'logout') {
|
if (action === 'logout') {
|
||||||
response.cookies.delete('motorsignal_access_token');
|
response.cookies.delete('motorsignal_access_token');
|
||||||
response.cookies.delete('motorsignal_refresh_token');
|
response.cookies.delete('motorsignal_refresh_token');
|
||||||
|
response.cookies.delete('__Host-motorsignal_ops');
|
||||||
|
response.cookies.delete('motorsignal_ops');
|
||||||
} else if (upstream.ok) {
|
} else if (upstream.ok) {
|
||||||
let payload: { access_token?: string; refresh_token?: string } = {};
|
let payload: { access_token?: string; refresh_token?: string; ops_token?: string } = {};
|
||||||
try {
|
try {
|
||||||
payload = JSON.parse(body) as { access_token?: string; refresh_token?: string };
|
payload = JSON.parse(body) as { access_token?: string; refresh_token?: string; ops_token?: string };
|
||||||
} catch {
|
} catch {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
const cookieOptions = { httpOnly: true, secure: true, sameSite: 'lax' as const, path: '/', maxAge: 60 * 60 * 24 * 30 };
|
const cookieOptions = { httpOnly: true, secure: true, sameSite: 'lax' as const, path: '/', maxAge: 60 * 60 * 24 * 30 };
|
||||||
if (payload.access_token) response.cookies.set('motorsignal_access_token', payload.access_token, cookieOptions);
|
if (payload.access_token) response.cookies.set('motorsignal_access_token', payload.access_token, cookieOptions);
|
||||||
if (payload.refresh_token) response.cookies.set('motorsignal_refresh_token', payload.refresh_token, cookieOptions);
|
if (payload.refresh_token) response.cookies.set('motorsignal_refresh_token', payload.refresh_token, cookieOptions);
|
||||||
|
if (payload.ops_token) {
|
||||||
|
const opsCookieOptions = {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'lax' as const,
|
||||||
|
path: '/',
|
||||||
|
maxAge: 60 * 60 * 8,
|
||||||
|
};
|
||||||
|
response.cookies.set('__Host-motorsignal_ops', payload.ops_token, opsCookieOptions);
|
||||||
|
response.cookies.set('motorsignal_ops', payload.ops_token, {
|
||||||
|
...opsCookieOptions,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
interface ResolvedMotorcycleImage {
|
||||||
|
imageUrl: string;
|
||||||
|
caption: string;
|
||||||
|
source: 'google' | 'curated' | 'wikimedia' | 'wikipedia';
|
||||||
|
attribution: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Curated high-resolution stock photography for flagship motorcycles
|
||||||
|
const CURATED_IMAGES: Record<string, ResolvedMotorcycleImage> = {
|
||||||
|
'yamaha_yzf-r6_2001': {
|
||||||
|
imageUrl:
|
||||||
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/e/e0/2001_Yamaha_YZFR600.jpg/960px-2001_Yamaha_YZFR600.jpg',
|
||||||
|
caption: '2001 Yamaha YZF-R6 (5EB)',
|
||||||
|
source: 'curated',
|
||||||
|
attribution: 'Wikimedia Commons / CC BY',
|
||||||
|
},
|
||||||
|
'yamaha_yzf-r6': {
|
||||||
|
imageUrl:
|
||||||
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/d/dd/Yamaha_YZF-R6_Cropped.jpg/960px-Yamaha_YZF-R6_Cropped.jpg',
|
||||||
|
caption: 'Yamaha YZF-R6 Supersport',
|
||||||
|
source: 'curated',
|
||||||
|
attribution: 'Wikimedia Commons / CC BY',
|
||||||
|
},
|
||||||
|
'ducati_panigale-v4-s': {
|
||||||
|
imageUrl:
|
||||||
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a7/Ducati_Panigale_V4_S_Corse.jpg/960px-Ducati_Panigale_V4_S_Corse.jpg',
|
||||||
|
caption: 'Ducati Panigale V4 S Corse',
|
||||||
|
source: 'curated',
|
||||||
|
attribution: 'Wikimedia Commons / CC BY',
|
||||||
|
},
|
||||||
|
'ducati_panigale-v4': {
|
||||||
|
imageUrl:
|
||||||
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a8/Ducati_Panigale_V4_-_Tuning_World_Bodensee_2018%2C_Friedrichshafen_%28OW1A0484%29.jpg/3840px-Ducati_Panigale_V4_-_Tuning_World_Bodensee_2018%2C_Friedrichshafen_%28OW1A0484%29.jpg',
|
||||||
|
caption: 'Ducati Panigale V4',
|
||||||
|
source: 'curated',
|
||||||
|
attribution: 'Wikimedia Commons / CC BY',
|
||||||
|
},
|
||||||
|
'bmw_s1000rr': {
|
||||||
|
imageUrl:
|
||||||
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a0/2022-05_BMW_M_Motorrad_S1000RR.jpg/960px-2022-05_BMW_M_Motorrad_S1000RR.jpg',
|
||||||
|
caption: 'BMW S1000RR Superbike',
|
||||||
|
source: 'curated',
|
||||||
|
attribution: 'Wikimedia Commons / CC BY',
|
||||||
|
},
|
||||||
|
'honda_cbr600rr': {
|
||||||
|
imageUrl:
|
||||||
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/6/64/2006HondaCBR600RR-001.jpg/960px-2006HondaCBR600RR-001.jpg',
|
||||||
|
caption: 'Honda CBR600RR',
|
||||||
|
source: 'curated',
|
||||||
|
attribution: 'Wikimedia Commons / CC BY',
|
||||||
|
},
|
||||||
|
'kawasaki_ninja-zx-6r': {
|
||||||
|
imageUrl:
|
||||||
|
'https://thumb.wikimedia.org/wikipedia/commons/thumb/9/93/2009_Kawasaki_Ninja_ZX-6R_in_white.jpg/960px-2009_Kawasaki_Ninja_ZX-6R_in_white.jpg',
|
||||||
|
caption: 'Kawasaki Ninja ZX-6R',
|
||||||
|
source: 'curated',
|
||||||
|
attribution: 'Wikimedia Commons / CC BY',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeKey(make: string, model: string, year?: number): string {
|
||||||
|
const normMake = make.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
const normModel = model.toLowerCase().replace(/standard/g, '').replace(/[^a-z0-9]/g, '');
|
||||||
|
return year ? `${normMake}_${normModel}_${year}` : `${normMake}_${normModel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const make = (searchParams.get('make') || '').trim();
|
||||||
|
const rawModel = (searchParams.get('model') || '').trim();
|
||||||
|
const yearParam = searchParams.get('year');
|
||||||
|
const year = yearParam ? parseInt(yearParam, 10) : undefined;
|
||||||
|
|
||||||
|
if (!make && !rawModel) {
|
||||||
|
return NextResponse.json({ error: 'Make and model are required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean model name (remove "Standard", "Base", etc.)
|
||||||
|
const cleanModel = rawModel
|
||||||
|
.replace(/\bstandard\b/gi, '')
|
||||||
|
.replace(/\bbase\b/gi, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Tier 1: Check curated exact and model cache
|
||||||
|
if (year) {
|
||||||
|
const exactCurated = CURATED_IMAGES[normalizeKey(make, cleanModel, year)];
|
||||||
|
if (exactCurated) {
|
||||||
|
return NextResponse.json(exactCurated, {
|
||||||
|
headers: { 'Cache-Control': 'public, s-maxage=604800, stale-while-revalidate=86400' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelCurated =
|
||||||
|
CURATED_IMAGES[normalizeKey(make, cleanModel)] ||
|
||||||
|
CURATED_IMAGES[cleanModel.toLowerCase().replace(/[^a-z0-9]/g, '')];
|
||||||
|
if (modelCurated) {
|
||||||
|
return NextResponse.json(modelCurated, {
|
||||||
|
headers: { 'Cache-Control': 'public, s-maxage=604800, stale-while-revalidate=86400' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 2: Google Custom Search API (if configured in environment)
|
||||||
|
const googleKey = process.env.GOOGLE_SEARCH_API_KEY;
|
||||||
|
const googleCx = process.env.GOOGLE_SEARCH_ENGINE_ID;
|
||||||
|
if (googleKey && googleCx) {
|
||||||
|
try {
|
||||||
|
const q = [year, make, cleanModel, 'motorcycle stock photo'].filter(Boolean).join(' ');
|
||||||
|
const googleUrl = `https://www.googleapis.com/customsearch/v1?key=${encodeURIComponent(
|
||||||
|
googleKey
|
||||||
|
)}&cx=${encodeURIComponent(googleCx)}&q=${encodeURIComponent(q)}&searchType=image&num=3&safe=active`;
|
||||||
|
|
||||||
|
const gRes = await fetch(googleUrl, { next: { revalidate: 604800 } });
|
||||||
|
if (gRes.ok) {
|
||||||
|
const gData = await gRes.json();
|
||||||
|
const firstItem = gData.items?.[0];
|
||||||
|
if (firstItem?.link) {
|
||||||
|
const result: ResolvedMotorcycleImage = {
|
||||||
|
imageUrl: firstItem.link,
|
||||||
|
caption: firstItem.title || `${year ? year + ' ' : ''}${make} ${cleanModel}`,
|
||||||
|
source: 'google',
|
||||||
|
attribution: firstItem.displayLink || 'Google Image Search',
|
||||||
|
};
|
||||||
|
return NextResponse.json(result, {
|
||||||
|
headers: { 'Cache-Control': 'public, s-maxage=604800, stale-while-revalidate=86400' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Google Custom Search API error, falling back:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: Wikimedia Commons API Search
|
||||||
|
const searchQueries: string[] = [];
|
||||||
|
if (year && year > 1900) {
|
||||||
|
searchQueries.push(`${year} ${make} ${cleanModel}`);
|
||||||
|
}
|
||||||
|
searchQueries.push(`${make} ${cleanModel}`);
|
||||||
|
searchQueries.push(`${cleanModel}`);
|
||||||
|
|
||||||
|
for (const query of searchQueries) {
|
||||||
|
try {
|
||||||
|
const commonsUrl = `https://commons.wikimedia.org/w/api.php?action=query&format=json&generator=search&gsrnamespace=6&gsrsearch=${encodeURIComponent(
|
||||||
|
query
|
||||||
|
)}&gsrlimit=6&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=960`;
|
||||||
|
|
||||||
|
const cRes = await fetch(commonsUrl, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'MotorSignal/1.0 (https://motorsignal.app; contact@motorsignal.app)',
|
||||||
|
},
|
||||||
|
next: { revalidate: 604800 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cRes.ok) {
|
||||||
|
const cData = await cRes.json();
|
||||||
|
const pages = cData.query?.pages;
|
||||||
|
if (pages) {
|
||||||
|
for (const rawPage of Object.values(pages) as any[]) {
|
||||||
|
const title = (rawPage.title || '').toLowerCase();
|
||||||
|
if (
|
||||||
|
title.includes('logo') ||
|
||||||
|
title.includes('icon') ||
|
||||||
|
title.includes('diagram') ||
|
||||||
|
title.includes('flag') ||
|
||||||
|
title.includes('badge')
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = rawPage.imageinfo?.[0];
|
||||||
|
const imgUrl = info?.thumburl || info?.url;
|
||||||
|
if (imgUrl && /\.(jpe?g|png|webp)/i.test(imgUrl)) {
|
||||||
|
const result: ResolvedMotorcycleImage = {
|
||||||
|
imageUrl: imgUrl,
|
||||||
|
caption: `${year ? year + ' ' : ''}${make} ${cleanModel}`,
|
||||||
|
source: 'wikimedia',
|
||||||
|
attribution: 'Wikimedia Commons / CC',
|
||||||
|
};
|
||||||
|
return NextResponse.json(result, {
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'public, s-maxage=604800, stale-while-revalidate=86400',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Wikimedia query error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 4: Wikipedia Lead Summary Image
|
||||||
|
const wikiTitles = [
|
||||||
|
`${make} ${cleanModel}`,
|
||||||
|
cleanModel,
|
||||||
|
`${make}_${cleanModel}`.replace(/\s+/g, '_'),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const title of wikiTitles) {
|
||||||
|
try {
|
||||||
|
const wikiUrl = `https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}`;
|
||||||
|
const wRes = await fetch(wikiUrl, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'MotorSignal/1.0 (https://motorsignal.app; contact@motorsignal.app)',
|
||||||
|
},
|
||||||
|
next: { revalidate: 604800 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wRes.ok) {
|
||||||
|
const wData = await wRes.json();
|
||||||
|
const img = wData.originalimage?.source || wData.thumbnail?.source;
|
||||||
|
if (img && !img.includes('disambig') && !img.includes('Question_book')) {
|
||||||
|
const result: ResolvedMotorcycleImage = {
|
||||||
|
imageUrl: img,
|
||||||
|
caption: wData.title || `${year ? year + ' ' : ''}${make} ${cleanModel}`,
|
||||||
|
source: 'wikipedia',
|
||||||
|
attribution: 'Wikipedia Lead Reference',
|
||||||
|
};
|
||||||
|
return NextResponse.json(result, {
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'public, s-maxage=604800, stale-while-revalidate=86400',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Wikipedia query error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ error: 'No stock photo found for this motorcycle' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
@ -55,12 +55,12 @@ export default function FeaturesPage() {
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary-container opacity-75"></span>
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary-container opacity-75"></span>
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary-container"></span>
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary-container"></span>
|
||||||
</span>
|
</span>
|
||||||
<span className="font-label-caps text-label-caps text-primary uppercase tracking-widest">COMPREHENSIVE TELEMETRY STACK • 10HZ MULTI-GNSS & 6-AXIS IMU</span>
|
<span className="font-label-caps text-label-caps text-primary tracking-normal">Comprehensive Telemetry Stack • 10Hz Multi-GNSS & 6-Axis IMU</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Main Headline & Subheading */}
|
{/* Main Headline & Subheading */}
|
||||||
<div className="flex flex-col gap-space-xl items-start mb-space-3xl">
|
<div className="flex flex-col gap-space-xl items-start mb-space-3xl">
|
||||||
<div className="flex flex-col gap-space-md max-w-4xl">
|
<div className="flex flex-col gap-space-md max-w-4xl">
|
||||||
<h1 className="font-display-hero text-display-hero text-on-surface tracking-tight uppercase">
|
<h1 className="font-display-hero text-display-hero text-on-surface tracking-tight">
|
||||||
Surgical data precision for every apex, straightaway, and canyon.
|
Surgical data precision for every apex, straightaway, and canyon.
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-body-lg text-body-lg text-on-surface-variant max-w-2xl">
|
<p className="font-body-lg text-body-lg text-on-surface-variant max-w-2xl">
|
||||||
|
|
@ -70,12 +70,12 @@ export default function FeaturesPage() {
|
||||||
<div className="flex flex-col gap-space-sm pb-space-xs w-full max-w-sm">
|
<div className="flex flex-col gap-space-sm pb-space-xs w-full max-w-sm">
|
||||||
<div className="bg-surface-container-low p-space-md rounded-xl shadow-md">
|
<div className="bg-surface-container-low p-space-md rounded-xl shadow-md">
|
||||||
<div className="flex items-center justify-between font-code-mono text-code-mono text-outline mb-space-2xs">
|
<div className="flex items-center justify-between font-code-mono text-code-mono text-outline mb-space-2xs">
|
||||||
<span>HARDWARE LINK RATE</span>
|
<span>Hardware Link Rate</span>
|
||||||
<span className="text-secondary-fixed">SYNCHRONIZED</span>
|
<span className="text-secondary-fixed">Synchronized</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs">
|
<div className="flex items-baseline gap-space-xs">
|
||||||
<span className="font-telemetry-metric-md text-telemetry-metric-md text-primary">1000</span>
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-primary">1000</span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant">SAMPLES / SEC (IMU)</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Samples / Sec (IMU)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-surface-container-highest h-1.5 rounded-full mt-space-xs overflow-hidden">
|
<div className="w-full bg-surface-container-highest h-1.5 rounded-full mt-space-xs overflow-hidden">
|
||||||
<div className="bg-gradient-to-r from-primary-container to-secondary-container h-full w-[94%] rounded-full"></div>
|
<div className="bg-gradient-to-r from-primary-container to-secondary-container h-full w-[94%] rounded-full"></div>
|
||||||
|
|
@ -85,19 +85,19 @@ export default function FeaturesPage() {
|
||||||
</div>
|
</div>
|
||||||
{/* Quick Fast Anchor Pills */}
|
{/* Quick Fast Anchor Pills */}
|
||||||
<div className="flex items-center gap-space-xs overflow-x-auto pb-space-sm scrollbar-none">
|
<div className="flex items-center gap-space-xs overflow-x-auto pb-space-sm scrollbar-none">
|
||||||
<a className="flex-shrink-0 px-space-md py-2 rounded-xl bg-surface-container-high hover:bg-surface-bright text-primary font-label-caps text-label-caps uppercase transition-all shadow-sm flex items-center gap-space-2xs" href="#cockpit">
|
<a className="flex-shrink-0 px-space-md py-2 rounded-xl bg-surface-container-high hover:bg-surface-bright text-primary font-label-caps text-label-caps transition-all shadow-sm flex items-center gap-space-2xs" href="#cockpit">
|
||||||
<span className="text-primary-container">01</span> Live Cockpit
|
<span className="text-primary-container">01</span> Live Cockpit
|
||||||
</a>
|
</a>
|
||||||
<a className="flex-shrink-0 px-space-md py-2 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface-variant hover:text-on-surface font-label-caps text-label-caps uppercase transition-all shadow-sm flex items-center gap-space-2xs" href="#lap-analysis">
|
<a className="flex-shrink-0 px-space-md py-2 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface-variant hover:text-on-surface font-label-caps text-label-caps transition-all shadow-sm flex items-center gap-space-2xs" href="#lap-analysis">
|
||||||
<span className="text-outline">02</span> Lap Analysis & Sectors
|
<span className="text-outline">02</span> Lap Analysis & Sectors
|
||||||
</a>
|
</a>
|
||||||
<a className="flex-shrink-0 px-space-md py-2 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface-variant hover:text-on-surface font-label-caps text-label-caps uppercase transition-all shadow-sm flex items-center gap-space-2xs" href="#emergency">
|
<a className="flex-shrink-0 px-space-md py-2 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface-variant hover:text-on-surface font-label-caps text-label-caps transition-all shadow-sm flex items-center gap-space-2xs" href="#emergency">
|
||||||
<span className="text-outline">03</span> Crash & SOS Beacon
|
<span className="text-outline">03</span> Crash & SOS Beacon
|
||||||
</a>
|
</a>
|
||||||
<a className="flex-shrink-0 px-space-md py-2 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface-variant hover:text-on-surface font-label-caps text-label-caps uppercase transition-all shadow-sm flex items-center gap-space-2xs" href="#canbus">
|
<a className="flex-shrink-0 px-space-md py-2 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface-variant hover:text-on-surface font-label-caps text-label-caps transition-all shadow-sm flex items-center gap-space-2xs" href="#canbus">
|
||||||
<span className="text-outline">04</span> CAN-bus & ECU Stream
|
<span className="text-outline">04</span> CAN-bus & ECU Stream
|
||||||
</a>
|
</a>
|
||||||
<a className="flex-shrink-0 px-space-md py-2 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface-variant hover:text-on-surface font-label-caps text-label-caps uppercase transition-all shadow-sm flex items-center gap-space-2xs" href="#cloud-replay">
|
<a className="flex-shrink-0 px-space-md py-2 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface-variant hover:text-on-surface font-label-caps text-label-caps transition-all shadow-sm flex items-center gap-space-2xs" href="#cloud-replay">
|
||||||
<span className="text-outline">05</span> Cloud Sync & Replay
|
<span className="text-outline">05</span> Cloud Sync & Replay
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -110,10 +110,10 @@ export default function FeaturesPage() {
|
||||||
<div className="flex flex-col gap-space-md mb-space-2xl max-w-4xl">
|
<div className="flex flex-col gap-space-md mb-space-2xl max-w-4xl">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-space-xs mb-space-2xs">
|
<div className="flex items-center gap-space-xs mb-space-2xs">
|
||||||
<span className="font-code-mono text-code-mono text-primary-container">01 // TACTICAL FLIGHT INTERFACE</span>
|
<span className="font-code-mono text-code-mono text-primary-container">01 // Tactical Flight Interface</span>
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-primary-container"></span>
|
<span className="h-1.5 w-1.5 rounded-full bg-primary-container"></span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-headline-xl text-headline-xl text-on-surface uppercase tracking-tight">
|
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">
|
||||||
Live Cockpit & Handlebar HUD
|
Live Cockpit & Handlebar HUD
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -131,21 +131,21 @@ export default function FeaturesPage() {
|
||||||
<div className="flex items-center gap-space-md font-code-mono text-code-mono">
|
<div className="flex items-center gap-space-md font-code-mono text-code-mono">
|
||||||
<span className="text-secondary-fixed flex items-center gap-1">
|
<span className="text-secondary-fixed flex items-center gap-1">
|
||||||
<span className="h-2 w-2 rounded-full bg-secondary-fixed animate-ping inline-block"></span>
|
<span className="h-2 w-2 rounded-full bg-secondary-fixed animate-ping inline-block"></span>
|
||||||
ACTIVE RECORDING
|
Active Recording
|
||||||
</span>
|
</span>
|
||||||
<span className="text-on-surface-variant">PORTIMAO CIRCUIT // S2 APEX 4</span>
|
<span className="text-on-surface-variant">Portimao Circuit // S2 Apex 4</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-space-sm font-code-mono text-code-mono text-primary-fixed-dim">
|
<div className="flex items-center gap-space-sm font-code-mono text-code-mono text-primary-fixed-dim">
|
||||||
<span>LATENCY: 9.4ms</span>
|
<span>Latency: 9.4ms</span>
|
||||||
<span className="text-on-surface-variant">•</span>
|
<span className="text-on-surface-variant">•</span>
|
||||||
<span>10Hz GNSS LOCK (18 SATS)</span>
|
<span>10Hz GNSS Lock (18 Sats)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Central HUD Layout Simulation */}
|
{/* Central HUD Layout Simulation */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-space-md items-center py-space-lg">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-space-md items-center py-space-lg">
|
||||||
{/* Left Dial: Lean Angle */}
|
{/* Left Dial: Lean Angle */}
|
||||||
<div className="bg-surface-container p-space-md rounded-xl flex flex-col items-center justify-center text-center relative shadow-sm">
|
<div className="bg-surface-container p-space-md rounded-xl flex flex-col items-center justify-center text-center relative shadow-sm">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase mb-space-xs">Lean Angle (Apex Peak)</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant mb-space-xs">Lean Angle (Apex Peak)</span>
|
||||||
{/* SVG Arc Lean Angle Dial */}
|
{/* SVG Arc Lean Angle Dial */}
|
||||||
<div className="relative w-44 h-44 flex items-center justify-center">
|
<div className="relative w-44 h-44 flex items-center justify-center">
|
||||||
<svg className="w-full h-full -rotate-90" viewBox="0 0 160 160">
|
<svg className="w-full h-full -rotate-90" viewBox="0 0 160 160">
|
||||||
|
|
@ -154,18 +154,18 @@ export default function FeaturesPage() {
|
||||||
</svg>
|
</svg>
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-primary" id="lean-val">52.8°</span>
|
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-primary" id="lean-val">52.8°</span>
|
||||||
<span className="font-label-caps text-label-caps text-secondary-fixed uppercase tracking-wider">L PEAK PEGGED</span>
|
<span className="font-label-caps text-label-caps text-secondary-fixed tracking-normal">L Peak Pegged</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex justify-between font-code-mono text-code-mono text-on-surface-variant mt-space-2xs px-space-xs">
|
<div className="w-full flex justify-between font-code-mono text-code-mono text-on-surface-variant mt-space-2xs px-space-xs">
|
||||||
<span>R 48.2°</span>
|
<span>R 48.2°</span>
|
||||||
<span className="text-primary">LIMIT: 65°</span>
|
<span className="text-primary">Limit: 65°</span>
|
||||||
<span className="text-secondary-fixed">L 52.8°</span>
|
<span className="text-secondary-fixed">L 52.8°</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Center Speed & G-Force Primary Indicator */}
|
{/* Center Speed & G-Force Primary Indicator */}
|
||||||
<div className="bg-surface-container-high p-space-lg rounded-xl flex flex-col items-center justify-center text-center shadow-lg relative">
|
<div className="bg-surface-container-high p-space-lg rounded-xl flex flex-col items-center justify-center text-center shadow-lg relative">
|
||||||
<span className="font-label-caps text-label-caps text-primary-fixed uppercase tracking-widest mb-space-xs">Velocity Vector</span>
|
<span className="font-label-caps text-label-caps text-primary-fixed tracking-normal mb-space-xs">Velocity Vector</span>
|
||||||
<div className="flex items-baseline justify-center gap-space-2xs my-space-xs">
|
<div className="flex items-baseline justify-center gap-space-2xs my-space-xs">
|
||||||
<span className="font-display-hero text-display-hero text-on-surface tracking-tighter" id="speed-val">174</span>
|
<span className="font-display-hero text-display-hero text-on-surface tracking-tighter" id="speed-val">174</span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface-variant">MPH</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface-variant">MPH</span>
|
||||||
|
|
@ -173,7 +173,7 @@ export default function FeaturesPage() {
|
||||||
{/* G-Force Crosshair Minimal Widget */}
|
{/* G-Force Crosshair Minimal Widget */}
|
||||||
<div className="w-full mt-space-sm bg-surface-container p-space-sm rounded-lg flex items-center justify-between">
|
<div className="w-full mt-space-sm bg-surface-container p-space-sm rounded-lg flex items-center justify-between">
|
||||||
<div className="flex flex-col text-left">
|
<div className="flex flex-col text-left">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant">LATERAL G</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Lateral G</span>
|
||||||
<span className="font-telemetry-metric-md text-telemetry-metric-md text-primary-container">+1.68 G</span>
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-primary-container">+1.68 G</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-12 h-12 rounded-full bg-surface-bright relative flex items-center justify-center">
|
<div className="w-12 h-12 rounded-full bg-surface-bright relative flex items-center justify-center">
|
||||||
|
|
@ -182,19 +182,19 @@ export default function FeaturesPage() {
|
||||||
<div className="w-3 h-3 rounded-full bg-secondary-container absolute translate-x-2 -translate-y-1 shadow-sm"></div>
|
<div className="w-3 h-3 rounded-full bg-secondary-container absolute translate-x-2 -translate-y-1 shadow-sm"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col text-right">
|
<div className="flex flex-col text-right">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant">DECEL G</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Decel G</span>
|
||||||
<span className="font-telemetry-metric-md text-telemetry-metric-md text-secondary-fixed">-1.32 G</span>
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-secondary-fixed">-1.32 G</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Right: Throttle & Hydraulic Brake Line Pressure */}
|
{/* Right: Throttle & Hydraulic Brake Line Pressure */}
|
||||||
<div className="bg-surface-container p-space-md rounded-xl flex flex-col justify-between shadow-sm">
|
<div className="bg-surface-container p-space-md rounded-xl flex flex-col justify-between shadow-sm">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase mb-space-sm">Hydraulic & TPS Telemetry</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant mb-space-sm">Hydraulic & TPS Telemetry</span>
|
||||||
{/* Throttle Bar */}
|
{/* Throttle Bar */}
|
||||||
<div className="mb-space-md">
|
<div className="mb-space-md">
|
||||||
<div className="flex justify-between font-label-caps text-label-caps text-on-surface mb-1">
|
<div className="flex justify-between font-label-caps text-label-caps text-on-surface mb-1">
|
||||||
<span>THROTTLE TPS</span>
|
<span>Throttle TPS</span>
|
||||||
<span className="text-primary-container">88% OPEN</span>
|
<span className="text-primary-container">88% Open</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-surface-container-highest h-3 rounded-full overflow-hidden">
|
<div className="w-full bg-surface-container-highest h-3 rounded-full overflow-hidden">
|
||||||
<div className="h-full bg-gradient-to-r from-primary-fixed-dim to-primary-container w-[88%] rounded-full"></div>
|
<div className="h-full bg-gradient-to-r from-primary-fixed-dim to-primary-container w-[88%] rounded-full"></div>
|
||||||
|
|
@ -203,8 +203,8 @@ export default function FeaturesPage() {
|
||||||
{/* Front Brake Line Pressure */}
|
{/* Front Brake Line Pressure */}
|
||||||
<div className="mb-space-md">
|
<div className="mb-space-md">
|
||||||
<div className="flex justify-between font-label-caps text-label-caps text-on-surface mb-1">
|
<div className="flex justify-between font-label-caps text-label-caps text-on-surface mb-1">
|
||||||
<span>FRONT BRAKE (BAR)</span>
|
<span>Front Brake (bar)</span>
|
||||||
<span className="text-error">18.4 / 30 BAR</span>
|
<span className="text-error">18.4 / 30 bar</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-surface-container-highest h-3 rounded-full overflow-hidden">
|
<div className="w-full bg-surface-container-highest h-3 rounded-full overflow-hidden">
|
||||||
<div className="h-full bg-gradient-to-r from-secondary-fixed to-error w-[61%] rounded-full"></div>
|
<div className="h-full bg-gradient-to-r from-secondary-fixed to-error w-[61%] rounded-full"></div>
|
||||||
|
|
@ -213,8 +213,8 @@ export default function FeaturesPage() {
|
||||||
{/* Rear Brake Line Pressure */}
|
{/* Rear Brake Line Pressure */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between font-label-caps text-label-caps text-on-surface mb-1">
|
<div className="flex justify-between font-label-caps text-label-caps text-on-surface mb-1">
|
||||||
<span>REAR TRAIL BRAKE</span>
|
<span>Rear Trail Brake</span>
|
||||||
<span className="text-on-surface-variant">3.8 BAR</span>
|
<span className="text-on-surface-variant">3.8 bar</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-surface-container-highest h-3 rounded-full overflow-hidden">
|
<div className="w-full bg-surface-container-highest h-3 rounded-full overflow-hidden">
|
||||||
<div className="h-full bg-secondary-fixed-dim w-[16%] rounded-full"></div>
|
<div className="h-full bg-secondary-fixed-dim w-[16%] rounded-full"></div>
|
||||||
|
|
@ -225,8 +225,8 @@ export default function FeaturesPage() {
|
||||||
{/* Bottom Live Sparkline Telemetry Wave */}
|
{/* Bottom Live Sparkline Telemetry Wave */}
|
||||||
<div className="mt-space-md pt-space-md bg-surface-container-lowest p-space-sm rounded-lg flex items-center justify-between">
|
<div className="mt-space-md pt-space-md bg-surface-container-lowest p-space-sm rounded-lg flex items-center justify-between">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant">THROTTLE vs DECEL TIMELINE (LAST 800M)</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Throttle vs Decel Timeline (Last 800m)</span>
|
||||||
<span className="font-code-mono text-code-mono text-outline">TURN 3 ENTRY > APEX 4 BRAKING DISCHARGE</span>
|
<span className="font-code-mono text-code-mono text-outline">Turn 3 Entry > Apex 4 Braking Discharge</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Inline SVG Sparkline */}
|
{/* Inline SVG Sparkline */}
|
||||||
<svg className="w-64 h-8 text-primary-container" fill="none" viewBox="0 0 240 32">
|
<svg className="w-64 h-8 text-primary-container" fill="none" viewBox="0 0 240 32">
|
||||||
|
|
@ -241,8 +241,8 @@ export default function FeaturesPage() {
|
||||||
<div className="bg-cover bg-center w-full h-full transform group-hover:scale-105 transition-transform duration-500" data-alt="High-end sport motorcycle cockpit showing digital phone mount HUD displaying vibrant cyan speed and lean angle gauges at dusk on an asphalt race track, sharp reflections on windshield and carbon fiber handlebars." ></div>
|
<div className="bg-cover bg-center w-full h-full transform group-hover:scale-105 transition-transform duration-500" data-alt="High-end sport motorcycle cockpit showing digital phone mount HUD displaying vibrant cyan speed and lean angle gauges at dusk on an asphalt race track, sharp reflections on windshield and carbon fiber handlebars." ></div>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-lowest via-surface-container-lowest/30 to-transparent"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-lowest via-surface-container-lowest/30 to-transparent"></div>
|
||||||
<div className="absolute bottom-3 left-3 right-3 flex items-center justify-between">
|
<div className="absolute bottom-3 left-3 right-3 flex items-center justify-between">
|
||||||
<span className="px-space-xs py-1 rounded bg-surface-container-highest/80 font-label-caps text-label-caps text-primary uppercase backdrop-blur-md">CNC Billet Stem Mount</span>
|
<span className="px-space-xs py-1 rounded bg-surface-container-highest/80 font-label-caps text-label-caps text-primary backdrop-blur-md">CNC Billet Stem Mount</span>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface">IP68 WATERPROOF</span>
|
<span className="font-code-mono text-code-mono text-on-surface">IP68 Waterproof</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Key Spec List */}
|
{/* Key Spec List */}
|
||||||
|
|
@ -263,8 +263,8 @@ export default function FeaturesPage() {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className="pt-space-sm mt-space-sm bg-surface-container-low px-space-sm py-2 rounded-lg flex items-center justify-between">
|
<div className="pt-space-sm mt-space-sm bg-surface-container-low px-space-sm py-2 rounded-lg flex items-center justify-between">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant">NATIVE MOUNT SUPPORT</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Native Mount Support</span>
|
||||||
<span className="font-code-mono text-code-mono text-primary">QUADLOCK / RAM / SP CONNECT</span>
|
<span className="font-code-mono text-code-mono text-primary">QuadLock / RAM / SP Connect</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -277,10 +277,10 @@ export default function FeaturesPage() {
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-space-md mb-space-2xl">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-space-md mb-space-2xl">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-space-xs mb-space-2xs">
|
<div className="flex items-center gap-space-xs mb-space-2xs">
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">02 // CHRONOMETRIC RACE ANALYSIS</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">02 // Chronometric Race Analysis</span>
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-secondary-fixed"></span>
|
<span className="h-1.5 w-1.5 rounded-full bg-secondary-fixed"></span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-headline-xl text-headline-xl text-on-surface uppercase tracking-tight">
|
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">
|
||||||
Deep Sector Lap & Ride Intelligence
|
Deep Sector Lap & Ride Intelligence
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -295,7 +295,7 @@ export default function FeaturesPage() {
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-space-sm">
|
<div className="flex items-center justify-between mb-space-sm">
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface">Mugello Circuit (Autodromo)</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface">Mugello Circuit (Autodromo)</span>
|
||||||
<span className="px-space-xs py-0.5 rounded bg-primary-container/20 text-primary-fixed-dim font-label-caps text-label-caps">AUTO-DETECTED</span>
|
<span className="px-space-xs py-0.5 rounded bg-primary-container/20 text-primary-fixed-dim font-label-caps text-label-caps">Auto-Detected</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-md">
|
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-md">
|
||||||
5.245 km • 15 Turns • 1,141m Long Straightaway. 10Hz multi-satellite track boundary matching.
|
5.245 km • 15 Turns • 1,141m Long Straightaway. 10Hz multi-satellite track boundary matching.
|
||||||
|
|
@ -323,19 +323,19 @@ export default function FeaturesPage() {
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<div className="absolute top-4 left-4 bg-surface-container-lowest/80 backdrop-blur-md px-2 py-1 rounded font-code-mono text-code-mono text-secondary-fixed">
|
<div className="absolute top-4 left-4 bg-surface-container-lowest/80 backdrop-blur-md px-2 py-1 rounded font-code-mono text-code-mono text-secondary-fixed">
|
||||||
SPEED TRAP: 312.4 KM/H
|
Speed Trap: 312.4 km/h
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-4 right-4 bg-surface-container-lowest/80 backdrop-blur-md px-2 py-1 rounded font-code-mono text-code-mono text-primary">
|
<div className="absolute bottom-4 right-4 bg-surface-container-lowest/80 backdrop-blur-md px-2 py-1 rounded font-code-mono text-code-mono text-primary">
|
||||||
APEX 9 PEAK: 49.6° LEAN
|
Apex 9 Peak: 49.6° Lean
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-space-sm mt-space-md pt-space-sm">
|
<div className="grid grid-cols-2 gap-space-sm mt-space-md pt-space-sm">
|
||||||
<div className="bg-surface-container px-space-sm py-2 rounded-lg">
|
<div className="bg-surface-container px-space-sm py-2 rounded-lg">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant block">BEST LAP TIME</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant block">Best Lap Time</span>
|
||||||
<span className="font-telemetry-metric-md text-telemetry-metric-md text-primary">1:48.342</span>
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-primary">1:48.342</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface-container px-space-sm py-2 rounded-lg">
|
<div className="bg-surface-container px-space-sm py-2 rounded-lg">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant block">OPTIMAL SECTOR SUM</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant block">Optimal Sector Sum</span>
|
||||||
<span className="font-telemetry-metric-md text-telemetry-metric-md text-secondary-fixed">1:47.809</span>
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-secondary-fixed">1:47.809</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -346,7 +346,7 @@ export default function FeaturesPage() {
|
||||||
<div className="bg-surface-container p-space-md rounded-xl shadow-md">
|
<div className="bg-surface-container p-space-md rounded-xl shadow-md">
|
||||||
<div className="flex items-center justify-between pb-space-sm mb-space-sm">
|
<div className="flex items-center justify-between pb-space-sm mb-space-sm">
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface">Sector Delta Matrix (Lap 7 vs Best Lap 4)</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface">Sector Delta Matrix (Lap 7 vs Best Lap 4)</span>
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">-0.533s AHEAD</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">-0.533s Ahead</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-space-xs">
|
<div className="flex flex-col gap-space-xs">
|
||||||
{/* Sector 1 */}
|
{/* Sector 1 */}
|
||||||
|
|
@ -428,10 +428,10 @@ export default function FeaturesPage() {
|
||||||
{/* Left Explanatory Text & Certs (6 cols) */}
|
{/* Left Explanatory Text & Certs (6 cols) */}
|
||||||
<div className="lg:col-span-6 flex flex-col gap-space-md">
|
<div className="lg:col-span-6 flex flex-col gap-space-md">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="font-code-mono text-code-mono text-error">03 // BLACKBOX PROTOCOL & LIFE SAFETY</span>
|
<span className="font-code-mono text-code-mono text-error">03 // Blackbox Protocol & Life Safety</span>
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-error"></span>
|
<span className="h-1.5 w-1.5 rounded-full bg-error"></span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-headline-xl text-headline-xl text-on-surface uppercase tracking-tight">
|
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">
|
||||||
Emergency Impact Detection & SOS Beacon
|
Emergency Impact Detection & SOS Beacon
|
||||||
</h2>
|
</h2>
|
||||||
<p className="font-body-lg text-body-lg text-on-surface-variant">
|
<p className="font-body-lg text-body-lg text-on-surface-variant">
|
||||||
|
|
@ -477,11 +477,11 @@ export default function FeaturesPage() {
|
||||||
<div className="flex items-center gap-space-md pt-space-xs">
|
<div className="flex items-center gap-space-md pt-space-xs">
|
||||||
<div className="px-space-md py-2 rounded-xl bg-surface-container flex items-center gap-space-xs">
|
<div className="px-space-md py-2 rounded-xl bg-surface-container flex items-center gap-space-xs">
|
||||||
<span className="material-symbols-outlined text-secondary-fixed text-lg">verified</span>
|
<span className="material-symbols-outlined text-secondary-fixed text-lg">verified</span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface uppercase">FIM Telemetry Spec 2025</span>
|
<span className="font-label-caps text-label-caps text-on-surface">FIM Telemetry Spec 2025</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-space-md py-2 rounded-xl bg-surface-container flex items-center gap-space-xs">
|
<div className="px-space-md py-2 rounded-xl bg-surface-container flex items-center gap-space-xs">
|
||||||
<span className="material-symbols-outlined text-primary-container text-lg">shield</span>
|
<span className="material-symbols-outlined text-primary-container text-lg">shield</span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface uppercase">ISO 26262 ASIL-B Safety</span>
|
<span className="font-label-caps text-label-caps text-on-surface">ISO 26262 ASIL-B Safety</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -490,15 +490,15 @@ export default function FeaturesPage() {
|
||||||
<div className="flex items-center justify-between pb-space-md border-b-0 mb-space-md">
|
<div className="flex items-center justify-between pb-space-md border-b-0 mb-space-md">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="w-3 h-3 rounded-full bg-error animate-ping"></span>
|
<span className="w-3 h-3 rounded-full bg-error animate-ping"></span>
|
||||||
<span className="font-label-caps text-label-caps text-error uppercase">TACTICAL CRASH SIMULATOR DEMO</span>
|
<span className="font-label-caps text-label-caps text-error">Tactical Crash Simulator Demo</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-outline">HARDWARE TEST V4</span>
|
<span className="font-code-mono text-code-mono text-outline">Hardware Test v4</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface-container-lowest p-space-lg rounded-xl mb-space-lg flex flex-col items-center justify-center text-center">
|
<div className="bg-surface-container-lowest p-space-lg rounded-xl mb-space-lg flex flex-col items-center justify-center text-center">
|
||||||
<div className="w-20 h-20 rounded-full bg-error/10 flex items-center justify-center mb-space-sm text-error">
|
<div className="w-20 h-20 rounded-full bg-error/10 flex items-center justify-center mb-space-sm text-error">
|
||||||
<span className="material-symbols-outlined text-4xl">warning</span>
|
<span className="material-symbols-outlined text-4xl">warning</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-headline-lg text-headline-lg text-on-surface mb-space-2xs">IMPACT DETECTED: +4.8G</span>
|
<span className="font-headline-lg text-headline-lg text-on-surface mb-space-2xs">Impact Detected: +4.8G</span>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant max-w-sm mb-space-md">
|
<p className="font-body-sm text-body-sm text-on-surface-variant max-w-sm mb-space-md">
|
||||||
Emergency SOS countdown initialized. Coordinates resolved. Dispatch pending in:
|
Emergency SOS countdown initialized. Coordinates resolved. Dispatch pending in:
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -516,16 +516,16 @@ export default function FeaturesPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface-container-low p-space-md rounded-lg flex flex-col gap-space-xs font-code-mono text-code-mono text-on-surface-variant">
|
<div className="bg-surface-container-low p-space-md rounded-lg flex flex-col gap-space-xs font-code-mono text-code-mono text-on-surface-variant">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>LAT/LONG:</span>
|
<span>Lat/Long:</span>
|
||||||
<span className="text-on-surface">43.9975° N, 11.3714° E (Mugello S3)</span>
|
<span className="text-on-surface">43.9975° N, 11.3714° E (Mugello S3)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>RIDER PROFILE:</span>
|
<span>Rider Profile:</span>
|
||||||
<span className="text-on-surface">A-POSITIVE // EMERGENCY CONTACT LINKED</span>
|
<span className="text-on-surface">A-Positive // Emergency Contact Linked</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>DATA SENT:</span>
|
<span>Data Sent:</span>
|
||||||
<span className="text-secondary-fixed">TRAIL CRASH PACKET (CAN-BUS 250kbps)</span>
|
<span className="text-secondary-fixed">Trail Crash Packet (CAN-Bus 250kbps)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -538,10 +538,10 @@ export default function FeaturesPage() {
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-space-md mb-space-2xl">
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-space-md mb-space-2xl">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-space-xs mb-space-2xs">
|
<div className="flex items-center gap-space-xs mb-space-2xs">
|
||||||
<span className="font-code-mono text-code-mono text-primary">04 // OBD-II & ECU ENGINE DIAGNOSTICS</span>
|
<span className="font-code-mono text-code-mono text-primary">04 // OBD-II & ECU Engine Diagnostics</span>
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-primary-container"></span>
|
<span className="h-1.5 w-1.5 rounded-full bg-primary-container"></span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-headline-xl text-headline-xl text-on-surface uppercase tracking-tight">
|
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">
|
||||||
Direct CAN-bus & ECU Health Stream
|
Direct CAN-bus & ECU Health Stream
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -556,13 +556,13 @@ export default function FeaturesPage() {
|
||||||
<div className="flex items-center justify-between mb-space-md">
|
<div className="flex items-center justify-between mb-space-md">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="material-symbols-outlined text-primary-container">thermostat</span>
|
<span className="material-symbols-outlined text-primary-container">thermostat</span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">ENGINE COOLANT</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Engine Coolant</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">NOMINAL</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">Nominal</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
||||||
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-on-surface">88°</span>
|
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-on-surface">88°</span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface-variant">CELSIUS</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface-variant">Celsius</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant">
|
<p className="font-body-sm text-body-sm text-on-surface-variant">
|
||||||
ECU threshold alerts warn of radiator heat-soak before cylinder head distortion occurs.
|
ECU threshold alerts warn of radiator heat-soak before cylinder head distortion occurs.
|
||||||
|
|
@ -573,9 +573,9 @@ export default function FeaturesPage() {
|
||||||
<div className="flex items-center justify-between mb-space-md">
|
<div className="flex items-center justify-between mb-space-md">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="material-symbols-outlined text-primary-container">oil_barrel</span>
|
<span className="material-symbols-outlined text-primary-container">oil_barrel</span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">OIL HYDRAULIC PSI</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Oil Hydraulic PSI</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">HIGH STABILITY</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">High Stability</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
||||||
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-on-surface">62.4</span>
|
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-on-surface">62.4</span>
|
||||||
|
|
@ -590,13 +590,13 @@ export default function FeaturesPage() {
|
||||||
<div className="flex items-center justify-between mb-space-md">
|
<div className="flex items-center justify-between mb-space-md">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="material-symbols-outlined text-secondary-fixed">settings_input_component</span>
|
<span className="material-symbols-outlined text-secondary-fixed">settings_input_component</span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">GEARBOX STATE</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Gearbox State</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-primary">QUICKSHIFTER ACTIVE</span>
|
<span className="font-code-mono text-code-mono text-primary">Quickshifter Active</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
||||||
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-primary-container">4</span>
|
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-primary-container">4</span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface-variant">GEAR // 11,400 RPM</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface-variant">Gear // 11,400 RPM</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant">
|
<p className="font-body-sm text-body-sm text-on-surface-variant">
|
||||||
Monitors shift cut-off duration (ms), false neutral occurrences, and clutch slip ratios.
|
Monitors shift cut-off duration (ms), false neutral occurrences, and clutch slip ratios.
|
||||||
|
|
@ -607,13 +607,13 @@ export default function FeaturesPage() {
|
||||||
<div className="flex items-center justify-between mb-space-md">
|
<div className="flex items-center justify-between mb-space-md">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="material-symbols-outlined text-primary-container">speed</span>
|
<span className="material-symbols-outlined text-primary-container">speed</span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">CRANK RPM PEAK</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Crank RPM Peak</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface-variant">REDLINE 14,500</span>
|
<span className="font-code-mono text-code-mono text-on-surface-variant">Redline 14,500</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
||||||
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-on-surface">13,850</span>
|
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-on-surface">13,850</span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface-variant">MAX RECORDED</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface-variant">Max Recorded</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant">
|
<p className="font-body-sm text-body-sm text-on-surface-variant">
|
||||||
Full resolution engine rpm graph correlated with throttle opening percentage.
|
Full resolution engine rpm graph correlated with throttle opening percentage.
|
||||||
|
|
@ -624,13 +624,13 @@ export default function FeaturesPage() {
|
||||||
<div className="flex items-center justify-between mb-space-md">
|
<div className="flex items-center justify-between mb-space-md">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="material-symbols-outlined text-primary-container">local_gas_station</span>
|
<span className="material-symbols-outlined text-primary-container">local_gas_station</span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">FUEL INJECTION CONSUMPTION</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Fuel Injection Consumption</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">8.2 L / 100KM</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">8.2 L / 100km</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
||||||
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-on-surface">3.8</span>
|
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-on-surface">3.8</span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface-variant">LAPS REMAINING FUEL</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface-variant">Laps Remaining Fuel</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant">
|
<p className="font-body-sm text-body-sm text-on-surface-variant">
|
||||||
Sprint-race fuel management strategy to avoid carrying unnecessary ballast weight.
|
Sprint-race fuel management strategy to avoid carrying unnecessary ballast weight.
|
||||||
|
|
@ -641,13 +641,13 @@ export default function FeaturesPage() {
|
||||||
<div className="flex items-center justify-between mb-space-md">
|
<div className="flex items-center justify-between mb-space-md">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="material-symbols-outlined text-secondary-fixed">tire_repair</span>
|
<span className="material-symbols-outlined text-secondary-fixed">tire_repair</span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">TPMS CARCASS TEMP</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">TPMS Carcass Temp</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">TRACK GRIP OPTIMAL</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">Track Grip Optimal</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
<div className="flex items-baseline gap-space-xs mb-space-xs">
|
||||||
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-secondary-fixed">82° / 78°</span>
|
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-secondary-fixed">82° / 78°</span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface-variant">F / R REAR TIRE</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface-variant">F / R Rear Tire</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant">
|
<p className="font-body-sm text-body-sm text-on-surface-variant">
|
||||||
Live cold/hot pressure inflation delta tracking to eliminate tire tearing and cold tears.
|
Live cold/hot pressure inflation delta tracking to eliminate tire tearing and cold tears.
|
||||||
|
|
@ -658,22 +658,22 @@ export default function FeaturesPage() {
|
||||||
<div className="bg-surface-container p-space-lg rounded-xl shadow-md">
|
<div className="bg-surface-container p-space-lg rounded-xl shadow-md">
|
||||||
<div className="flex flex-col md:flex-row items-center justify-between gap-space-md mb-space-md">
|
<div className="flex flex-col md:flex-row items-center justify-between gap-space-md mb-space-md">
|
||||||
<div>
|
<div>
|
||||||
<span className="font-label-caps text-label-caps text-primary uppercase block mb-1">OEM FACTORY HARNESS SUPPORT</span>
|
<span className="font-label-caps text-label-caps text-primary block mb-1">OEM Factory Harness Support</span>
|
||||||
<h3 className="font-headline-sm text-headline-sm text-on-surface">Universal Plug & Play OBD-II / Euro5 Connectors</h3>
|
<h3 className="font-headline-sm text-headline-sm text-on-surface">Universal Plug & Play OBD-II / Euro5 Connectors</h3>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface-variant bg-surface-container-low px-space-md py-1.5 rounded-lg">
|
<span className="font-code-mono text-code-mono text-on-surface-variant bg-surface-container-low px-space-md py-1.5 rounded-lg">
|
||||||
ZERO ECU FLASHING REQUIRED
|
Zero ECU Flashing Required
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-space-xs text-center font-headline-sm text-headline-sm">
|
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-space-xs text-center font-headline-sm text-headline-sm">
|
||||||
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">YAMAHA</div>
|
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">Yamaha</div>
|
||||||
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">DUCATI</div>
|
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">Ducati</div>
|
||||||
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">BMW</div>
|
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">BMW</div>
|
||||||
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">KAWASAKI</div>
|
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">Kawasaki</div>
|
||||||
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">KTM</div>
|
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">KTM</div>
|
||||||
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">HONDA</div>
|
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">Honda</div>
|
||||||
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">APRILIA</div>
|
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">Aprilia</div>
|
||||||
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">TRIUMPH</div>
|
<div className="p-space-sm rounded-lg bg-surface-container-low text-on-surface hover:text-primary-container transition-colors">Triumph</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -691,7 +691,7 @@ export default function FeaturesPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-space-xs font-code-mono text-code-mono text-outline">
|
<div className="flex items-center gap-space-xs font-code-mono text-code-mono text-outline">
|
||||||
<span className="material-symbols-outlined text-base">cloud_done</span>
|
<span className="material-symbols-outlined text-base">cloud_done</span>
|
||||||
<span>SYNCHRONIZED (SESSION #142)</span>
|
<span>Synchronized (Session #142)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Video Replay Frame Overlay */}
|
{/* Video Replay Frame Overlay */}
|
||||||
|
|
@ -702,8 +702,8 @@ export default function FeaturesPage() {
|
||||||
<div className="absolute bottom-4 left-4 right-4 flex items-center justify-between bg-surface-container-lowest/85 backdrop-blur-md p-space-sm rounded-lg">
|
<div className="absolute bottom-4 left-4 right-4 flex items-center justify-between bg-surface-container-lowest/85 backdrop-blur-md p-space-sm rounded-lg">
|
||||||
<div className="flex items-center gap-space-md font-code-mono text-code-mono">
|
<div className="flex items-center gap-space-md font-code-mono text-code-mono">
|
||||||
<span className="text-primary-container font-bold">T: 01:24.810</span>
|
<span className="text-primary-container font-bold">T: 01:24.810</span>
|
||||||
<span className="text-on-surface">SPEED: 182 KM/H</span>
|
<span className="text-on-surface">Speed: 182 km/h</span>
|
||||||
<span className="text-secondary-fixed">LEAN: 51.4° L</span>
|
<span className="text-secondary-fixed">Lean: 51.4° L</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<button className="w-8 h-8 rounded-lg bg-surface-bright flex items-center justify-center text-on-surface hover:text-primary-container">
|
<button className="w-8 h-8 rounded-lg bg-surface-bright flex items-center justify-center text-on-surface hover:text-primary-container">
|
||||||
|
|
@ -718,8 +718,8 @@ export default function FeaturesPage() {
|
||||||
{/* Scrubbing Bar with Sector Flags */}
|
{/* Scrubbing Bar with Sector Flags */}
|
||||||
<div className="w-full bg-surface-container-low p-space-sm rounded-lg">
|
<div className="w-full bg-surface-container-low p-space-sm rounded-lg">
|
||||||
<div className="flex justify-between font-code-mono text-code-mono text-on-surface-variant mb-1">
|
<div className="flex justify-between font-code-mono text-code-mono text-on-surface-variant mb-1">
|
||||||
<span>LAP 4 REPLAY (TOTAL 01:48.342)</span>
|
<span>Lap 4 Replay (Total 01:48.342)</span>
|
||||||
<span>100% TELEMETRY BUFFER</span>
|
<span>100% Telemetry Buffer</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative w-full bg-surface-container-highest h-2 rounded-full overflow-hidden">
|
<div className="relative w-full bg-surface-container-highest h-2 rounded-full overflow-hidden">
|
||||||
<div className="h-full bg-primary-container w-[72%] rounded-full"></div>
|
<div className="h-full bg-primary-container w-[72%] rounded-full"></div>
|
||||||
|
|
@ -729,10 +729,10 @@ export default function FeaturesPage() {
|
||||||
{/* Explanatory Replay Details (5 cols) */}
|
{/* Explanatory Replay Details (5 cols) */}
|
||||||
<div className="lg:col-span-5 flex flex-col gap-space-md">
|
<div className="lg:col-span-5 flex flex-col gap-space-md">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">05 // TELEMETRY CLOUD SYNC</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">05 // Telemetry Cloud Sync</span>
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-secondary-fixed"></span>
|
<span className="h-1.5 w-1.5 rounded-full bg-secondary-fixed"></span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-headline-xl text-headline-xl text-on-surface uppercase tracking-tight">
|
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">
|
||||||
Frame-Accurate Video & Telemetry Overlay
|
Frame-Accurate Video & Telemetry Overlay
|
||||||
</h2>
|
</h2>
|
||||||
<p className="font-body-md text-body-md text-on-surface-variant">
|
<p className="font-body-md text-body-md text-on-surface-variant">
|
||||||
|
|
@ -767,11 +767,11 @@ export default function FeaturesPage() {
|
||||||
<div className="max-w-container-max mx-auto px-gutter-desktop">
|
<div className="max-w-container-max mx-auto px-gutter-desktop">
|
||||||
<div className="relative bg-gradient-to-tr from-surface-container-lowest via-surface-container to-surface-container-low p-space-2xl md:p-space-3xl rounded-2xl shadow-2xl overflow-hidden flex flex-col items-center text-center">
|
<div className="relative bg-gradient-to-tr from-surface-container-lowest via-surface-container to-surface-container-low p-space-2xl md:p-space-3xl rounded-2xl shadow-2xl overflow-hidden flex flex-col items-center text-center">
|
||||||
<div className="absolute -top-32 left-1/2 -translate-x-1/2 w-[600px] h-[300px] bg-primary-container/10 blur-3xl pointer-events-none rounded-full"></div>
|
<div className="absolute -top-32 left-1/2 -translate-x-1/2 w-[600px] h-[300px] bg-primary-container/10 blur-3xl pointer-events-none rounded-full"></div>
|
||||||
<div className="inline-flex items-center gap-space-xs px-space-md py-1 rounded-full bg-surface-container-high text-primary-container font-label-caps text-label-caps uppercase tracking-widest mb-space-md shadow-sm">
|
<div className="inline-flex items-center gap-space-xs px-space-md py-1 rounded-full bg-surface-container-high text-primary-container font-label-caps text-label-caps tracking-normal mb-space-md shadow-sm">
|
||||||
<span className="h-2 w-2 rounded-full bg-secondary-fixed"></span>
|
<span className="h-2 w-2 rounded-full bg-secondary-fixed"></span>
|
||||||
READY FOR TRACK & CANYON ROADS
|
Ready for Track & Canyon Roads
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-headline-xl text-headline-xl text-on-surface uppercase max-w-2xl mb-space-md tracking-tight">
|
<h2 className="font-headline-xl text-headline-xl text-on-surface max-w-2xl mb-space-md tracking-tight">
|
||||||
Equip your machine with pro-grade telemetry today.
|
Equip your machine with pro-grade telemetry today.
|
||||||
</h2>
|
</h2>
|
||||||
<p className="font-body-lg text-body-lg text-on-surface-variant max-w-xl mb-space-xl">
|
<p className="font-body-lg text-body-lg text-on-surface-variant max-w-xl mb-space-xl">
|
||||||
|
|
@ -786,11 +786,11 @@ export default function FeaturesPage() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-space-lg flex items-center gap-space-lg font-code-mono text-code-mono text-outline">
|
<div className="mt-space-lg flex items-center gap-space-lg font-code-mono text-code-mono text-outline">
|
||||||
<span>NO WIRE SPLICING</span>
|
<span>No Wire Splicing</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>WORLDWIDE 3-DAY EXPRESS DISPATCH</span>
|
<span>Worldwide 3-Day Express Dispatch</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>MONEY BACK GUARANTEE</span>
|
<span>Money Back Guarantee</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,35 @@
|
||||||
import { RecoveryForm } from '../../components/recovery-form';
|
import { RecoveryForm } from '../../components/recovery-form';
|
||||||
|
|
||||||
export default function ForgotPasswordPage() { return <section className="auth-page"><div><span className="eyebrow">Account security</span><h1>Reset access safely.</h1><p>Request a short-lived reset link, then choose a new password.</p></div><div className="auth-card"><RecoveryForm /></div></section>; }
|
export default async function ForgotPasswordPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<{ token?: string | string[] }>;
|
||||||
|
}) {
|
||||||
|
const { token } = await searchParams;
|
||||||
|
const initialToken = typeof token === 'string' ? token : '';
|
||||||
|
return (
|
||||||
|
<div className="min-h-[calc(100vh-80px)] pt-24 pb-16 px-4 flex items-center justify-center bg-brand-bg relative overflow-hidden">
|
||||||
|
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-96 h-96 bg-brand-cyan/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-md relative z-10">
|
||||||
|
<div className="text-center mb-8 space-y-2">
|
||||||
|
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-low border border-outline-variant/40 text-[11px] font-mono tracking-wide text-primary-fixed-dim">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-secondary-fixed"></span>
|
||||||
|
Account Security
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-extrabold text-white tracking-tight">
|
||||||
|
Reset access safely.
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-on-surface-variant max-w-xs mx-auto">
|
||||||
|
Request a short-lived reset link, then choose a new password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glass-card rounded-2xl border border-brand-border p-6 sm:p-8 shadow-2xl backdrop-blur-xl relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-primary-container via-surface-tint to-transparent"></div>
|
||||||
|
<RecoveryForm key={initialToken} initialToken={initialToken} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
--primary-container: #00f2fe;
|
--primary-container: #00f2fe;
|
||||||
--primary-fixed: #6ff6ff;
|
--primary-fixed: #6ff6ff;
|
||||||
--primary-fixed-dim: #00dce6;
|
--primary-fixed-dim: #00dce6;
|
||||||
--on-primary-container: #006a70;
|
--on-primary-container: #002022;
|
||||||
--secondary-fixed: #60ff98;
|
--secondary-fixed: #60ff98;
|
||||||
--secondary-fixed-dim: #00e478;
|
--secondary-fixed-dim: #00e478;
|
||||||
--secondary-container: #34ff8c;
|
--secondary-container: #34ff8c;
|
||||||
|
|
@ -333,7 +333,9 @@ a { color: inherit; text-decoration: none; }
|
||||||
.button:disabled { cursor: wait; opacity: .6; }
|
.button:disabled { cursor: wait; opacity: .6; }
|
||||||
.button-small { font-size: 13px; padding: 9px 13px; }
|
.button-small { font-size: 13px; padding: 9px 13px; }
|
||||||
main { margin: 0 auto; max-width: 1240px; padding: 6rem 28px 100px; }
|
main { margin: 0 auto; max-width: 1240px; padding: 6rem 28px 100px; }
|
||||||
main:has(.flagship-landing), main:has(.flagship-page) { max-width: 100%; padding: 0; }
|
main:has(.flagship-landing), main:has(.flagship-page), main:has(.ops-shell-root), main:has(.ops-error-root) { max-width: 100%; padding: 0; margin: 0; }
|
||||||
|
.ops-shell-root, .ops-shell-root main { max-width: 100% !important; margin: 0 !important; }
|
||||||
|
.ops-shell-root h1 { max-width: none !important; }
|
||||||
.flagship-landing,
|
.flagship-landing,
|
||||||
.flagship-page {
|
.flagship-page {
|
||||||
padding-top: 5rem;
|
padding-top: 5rem;
|
||||||
|
|
@ -354,12 +356,12 @@ main:has(.flagship-landing), main:has(.flagship-page) { max-width: 100%; padding
|
||||||
.telemetry-bars i:nth-child(2n) { height: 75%; background: #34d399; }.telemetry-bars i:nth-child(3n) { height: 55%; }
|
.telemetry-bars i:nth-child(2n) { height: 75%; background: #34d399; }.telemetry-bars i:nth-child(3n) { height: 55%; }
|
||||||
.cockpit-footer { padding-top: 12px; }.signal-live { color: #4ade80; }
|
.cockpit-footer { padding-top: 12px; }.signal-live { color: #4ade80; }
|
||||||
.metric-ribbon { border-bottom: 1px solid var(--line); border-top: 1px solid var(--line); display: grid; grid-template-columns: repeat(4, 1fr); margin: 36px 0 90px; padding: 22px 0; }
|
.metric-ribbon { border-bottom: 1px solid var(--line); border-top: 1px solid var(--line); display: grid; grid-template-columns: repeat(4, 1fr); margin: 36px 0 90px; padding: 22px 0; }
|
||||||
.metric-ribbon div { border-left: 1px solid var(--line); padding-left: 24px; }.metric-ribbon div:first-child { border-left: 0; }.metric-ribbon strong, .metric-ribbon span { display: block; }.metric-ribbon strong { font-size: 30px; }.metric-ribbon span { color: var(--muted); font-size: 12px; margin-top: 5px; text-transform: uppercase; }
|
.metric-ribbon div { border-left: 1px solid var(--line); padding-left: 24px; }.metric-ribbon div:first-child { border-left: 0; }.metric-ribbon strong, .metric-ribbon span { display: block; }.metric-ribbon strong { font-size: 30px; }.metric-ribbon span { color: var(--muted); font-size: 12px; margin-top: 5px; }
|
||||||
.section-heading, .public-intro { max-width: 760px; }.section-heading h2 { font-size: 48px; margin: 14px 0 10px; }.section-heading p, .public-intro p { font-size: 18px; }
|
.section-heading, .public-intro { max-width: 760px; }.section-heading h2 { font-size: 48px; margin: 14px 0 10px; }.section-heading p, .public-intro p { font-size: 18px; }
|
||||||
.feature-grid, .club-pillars, .architecture-grid { display: grid; gap: 16px; grid-template-columns: repeat(3, 1fr); margin-top: 36px; }.feature-panel, .club-pillars article, .architecture-grid article, .side-panel { background: #111a22; border: 1px solid var(--line); border-radius: 8px; padding: 28px; }.feature-panel span, .club-pillars span, .architecture-grid article > span { color: #67e8f9; font: 12px ui-monospace, monospace; }.feature-panel h3 { font-size: 24px; line-height: 1.15; margin: 50px 0 8px; }.feature-panel p, .club-pillars p, .architecture-grid p { color: var(--muted); }.feature-green { border-top: 3px solid #34d399; }.feature-blue { border-top: 3px solid #60a5fa; }.conversion-band { align-items: center; background: #0c2534; border: 1px solid #155e75; display: flex; justify-content: space-between; margin-top: 90px; padding: 30px; }.conversion-band h2 { margin: 10px 0 0; }
|
.feature-grid, .club-pillars, .architecture-grid { display: grid; gap: 16px; grid-template-columns: repeat(3, 1fr); margin-top: 36px; }.feature-panel, .club-pillars article, .architecture-grid article, .side-panel { background: #111a22; border: 1px solid var(--line); border-radius: 8px; padding: 28px; }.feature-panel span, .club-pillars span, .architecture-grid article > span { color: #67e8f9; font: 12px ui-monospace, monospace; }.feature-panel h3 { font-size: 24px; line-height: 1.15; margin: 50px 0 8px; }.feature-panel p, .club-pillars p, .architecture-grid p { color: var(--muted); }.feature-green { border-top: 3px solid #34d399; }.feature-blue { border-top: 3px solid #60a5fa; }.conversion-band { align-items: center; background: #0c2534; border: 1px solid #155e75; display: flex; justify-content: space-between; margin-top: 90px; padding: 30px; }.conversion-band h2 { margin: 10px 0 0; }
|
||||||
.public-page { max-width: 1180px; margin: auto; }.public-intro h1 { font-size: clamp(48px, 7vw, 78px); margin: 18px 0 12px; }.architecture-grid { grid-template-columns: repeat(2, 1fr); margin-top: 54px; }.architecture-grid article { min-height: 300px; }.architecture-grid h2, .club-pillars h2 { font-size: 25px; margin: 50px 0 10px; }.mini-readout, .status-line { border-top: 1px solid var(--line); color: var(--muted); display: flex; justify-content: space-between; margin-top: 30px; padding-top: 16px; }.mini-readout strong { color: #e0f2fe; font-size: 34px; }.mini-readout span, .status-line { font: 11px ui-monospace, monospace; }.status-line b { color: #4ade80; }.trace-lines { border-bottom: 2px solid #155e75; display: flex; gap: 8px; height: 70px; margin-top: 30px; transform: skewY(-12deg); }.trace-lines i { background: #22d3ee; flex: 1; height: 60%; }.trace-lines i:nth-child(2) { height: 90%; background: #34d399; }.trace-lines i:nth-child(3) { height: 35%; }
|
.public-page { max-width: 1180px; margin: auto; }.public-intro h1 { font-size: clamp(48px, 7vw, 78px); margin: 18px 0 12px; }.architecture-grid { grid-template-columns: repeat(2, 1fr); margin-top: 54px; }.architecture-grid article { min-height: 300px; }.architecture-grid h2, .club-pillars h2 { font-size: 25px; margin: 50px 0 10px; }.mini-readout, .status-line { border-top: 1px solid var(--line); color: var(--muted); display: flex; justify-content: space-between; margin-top: 30px; padding-top: 16px; }.mini-readout strong { color: #e0f2fe; font-size: 34px; }.mini-readout span, .status-line { font: 11px ui-monospace, monospace; }.status-line b { color: #4ade80; }.trace-lines { border-bottom: 2px solid #155e75; display: flex; gap: 8px; height: 70px; margin-top: 30px; transform: skewY(-12deg); }.trace-lines i { background: #22d3ee; flex: 1; height: 60%; }.trace-lines i:nth-child(2) { height: 90%; background: #34d399; }.trace-lines i:nth-child(3) { height: 35%; }
|
||||||
.ride-hero-panel { align-items: center; background: #0d1a25; border: 1px solid #1e4d64; display: grid; gap: 70px; grid-template-columns: 1fr 1fr; margin: 54px 0; padding: 46px; }.ride-hero-panel h2 { font-size: 38px; margin: 14px 0; }.radar { aspect-ratio: 1; background: radial-gradient(circle, rgba(34,211,238,.18), transparent 62%), repeating-radial-gradient(circle, transparent 0 35px, #214556 36px 37px); border: 1px solid #28748a; border-radius: 50%; max-width: 370px; position: relative; }.radar-ring { border: 1px solid #28748a; border-radius: 50%; inset: 25%; position: absolute; }.r2 { inset: 42%; }.radar-center { color: #67e8f9; left: 42%; position: absolute; top: 46%; }.rider-dot { background: #4ade80; border: 4px solid #0d1a25; border-radius: 50%; height: 16px; position: absolute; width: 16px; }.one { left: 22%; top: 28%; }.two { right: 24%; top: 20%; background: #facc15; }.three { bottom: 22%; left: 30%; }.four { bottom: 28%; right: 20%; }.alert-strip { background: #3a2c0a; border: 1px solid #854d0e; color: #fde68a; font: 12px ui-monospace, monospace; margin-top: 24px; padding: 12px; }.public-split { display: grid; gap: 24px; grid-template-columns: 1.5fr .8fr; margin-top: 54px; }.public-split > div > h2 { font-size: 30px; }.side-panel { align-self: start; }.side-panel h2 { font-size: 28px; margin: 18px 0 8px; }
|
.ride-hero-panel { align-items: center; background: #0d1a25; border: 1px solid #1e4d64; display: grid; gap: 70px; grid-template-columns: 1fr 1fr; margin: 54px 0; padding: 46px; }.ride-hero-panel h2 { font-size: 38px; margin: 14px 0; }.radar { aspect-ratio: 1; background: radial-gradient(circle, rgba(34,211,238,.18), transparent 62%), repeating-radial-gradient(circle, transparent 0 35px, #214556 36px 37px); border: 1px solid #28748a; border-radius: 50%; max-width: 370px; position: relative; }.radar-ring { border: 1px solid #28748a; border-radius: 50%; inset: 25%; position: absolute; }.r2 { inset: 42%; }.radar-center { color: #67e8f9; left: 42%; position: absolute; top: 46%; }.rider-dot { background: #4ade80; border: 4px solid #0d1a25; border-radius: 50%; height: 16px; position: absolute; width: 16px; }.one { left: 22%; top: 28%; }.two { right: 24%; top: 20%; background: #facc15; }.three { bottom: 22%; left: 30%; }.four { bottom: 28%; right: 20%; }.alert-strip { background: #3a2c0a; border: 1px solid #854d0e; color: #fde68a; font: 12px ui-monospace, monospace; margin-top: 24px; padding: 12px; }.public-split { display: grid; gap: 24px; grid-template-columns: 1.5fr .8fr; margin-top: 54px; }.public-split > div > h2 { font-size: 30px; }.side-panel { align-self: start; }.side-panel h2 { font-size: 28px; margin: 18px 0 8px; }
|
||||||
.eyebrow { color: var(--green); font-size: 12px; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; }
|
.eyebrow { color: var(--green); font-size: 12px; font-weight: 700; letter-spacing: .04em; }
|
||||||
h1 { font-size: clamp(42px, 7vw, 82px); line-height: .98; margin: 18px 0; max-width: 850px; }
|
h1 { font-size: clamp(42px, 7vw, 82px); line-height: .98; margin: 18px 0; max-width: 850px; }
|
||||||
h2 { font-size: 32px; margin: 0 0 14px; }
|
h2 { font-size: 32px; margin: 0 0 14px; }
|
||||||
p { color: var(--muted); line-height: 1.65; }
|
p { color: var(--muted); line-height: 1.65; }
|
||||||
|
|
@ -400,7 +402,7 @@ input:focus { border-color: var(--blue); outline: 2px solid color-mix(in srgb, v
|
||||||
.admin-title-row h1 { font-size: clamp(48px, 7vw, 76px); margin: 16px 0 10px; }
|
.admin-title-row h1 { font-size: clamp(48px, 7vw, 76px); margin: 16px 0 10px; }
|
||||||
.admin-title-row h1 span { color: #22d3ee; }
|
.admin-title-row h1 span { color: #22d3ee; }
|
||||||
.admin-title-row p { margin: 0; }
|
.admin-title-row p { margin: 0; }
|
||||||
.admin-environment { align-items: center; color: var(--muted); display: flex; font: 11px ui-monospace, SFMono-Regular, monospace; gap: 14px; text-transform: uppercase; }
|
.admin-environment { align-items: center; color: var(--muted); display: flex; font: 11px ui-monospace, SFMono-Regular, monospace; gap: 14px; }
|
||||||
.admin-environment strong { background: #082f49; border: 1px solid #155e75; color: #67e8f9; padding: 5px 8px; }
|
.admin-environment strong { background: #082f49; border: 1px solid #155e75; color: #67e8f9; padding: 5px 8px; }
|
||||||
.admin-kpis { display: grid; gap: 14px; grid-template-columns: repeat(4, 1fr); margin: 24px 0; }
|
.admin-kpis { display: grid; gap: 14px; grid-template-columns: repeat(4, 1fr); margin: 24px 0; }
|
||||||
.admin-kpis article { background: #111a26; border: 1px solid #26364a; border-radius: 8px; display: grid; gap: 8px; padding: 18px; }
|
.admin-kpis article { background: #111a26; border: 1px solid #26364a; border-radius: 8px; display: grid; gap: 8px; padding: 18px; }
|
||||||
|
|
@ -421,7 +423,7 @@ input:focus { border-color: var(--blue); outline: 2px solid color-mix(in srgb, v
|
||||||
.admin-table-wrap { overflow-x: auto; }
|
.admin-table-wrap { overflow-x: auto; }
|
||||||
.admin-table { border-collapse: collapse; min-width: 700px; width: 100%; }
|
.admin-table { border-collapse: collapse; min-width: 700px; width: 100%; }
|
||||||
.admin-table th, .admin-table td { border-bottom: 1px solid var(--line); padding: 14px 10px; text-align: left; }
|
.admin-table th, .admin-table td { border-bottom: 1px solid var(--line); padding: 14px 10px; text-align: left; }
|
||||||
.admin-table th { color: var(--muted); font-size: 12px; text-transform: uppercase; }
|
.admin-table th { color: var(--muted); font-size: 12px; }
|
||||||
.admin-table td { font-size: 13px; }
|
.admin-table td { font-size: 13px; }
|
||||||
.admin-table td code, .audit-event code { color: var(--muted); display: block; font: 11px ui-monospace, SFMono-Regular, monospace; margin-top: 5px; }
|
.admin-table td code, .audit-event code { color: var(--muted); display: block; font: 11px ui-monospace, SFMono-Regular, monospace; margin-top: 5px; }
|
||||||
.status-badge { border: 1px solid; border-radius: 10px; display: inline-block; font-size: 11px; padding: 3px 8px; text-transform: capitalize; }
|
.status-badge { border: 1px solid; border-radius: 10px; display: inline-block; font-size: 11px; padding: 3px 8px; text-transform: capitalize; }
|
||||||
|
|
@ -480,6 +482,7 @@ input:focus { border-color: var(--blue); outline: 2px solid color-mix(in srgb, v
|
||||||
color: #dfe2ee;
|
color: #dfe2ee;
|
||||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
padding-top: 5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -559,8 +562,8 @@ input:focus { border-color: var(--blue); outline: 2px solid color-mix(in srgb, v
|
||||||
font-family: 'Space Grotesk', sans-serif;
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.02em;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-label-md {
|
.font-label-md {
|
||||||
|
|
@ -643,15 +646,12 @@ input:focus { border-color: var(--blue); outline: 2px solid color-mix(in srgb, v
|
||||||
.hero-wrapper {
|
.hero-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 2rem 0 2rem;
|
padding: 3.5rem 0 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
main {
|
|
||||||
padding: 5.5rem 18px 72px;
|
|
||||||
}
|
|
||||||
.hero-wrapper {
|
.hero-wrapper {
|
||||||
padding: 1.5rem 0 1.5rem;
|
padding: 2.5rem 0 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -708,6 +708,7 @@ input:focus { border-color: var(--blue); outline: 2px solid color-mix(in srgb, v
|
||||||
border: 1px solid rgba(58, 73, 75, 0.45);
|
border: 1px solid rgba(58, 73, 75, 0.45);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pulse-dot-green {
|
.pulse-dot-green {
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ export default function GroupRidesPage() {
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary-fixed opacity-75"></span>
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary-fixed opacity-75"></span>
|
||||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-secondary-fixed-dim"></span>
|
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-secondary-fixed-dim"></span>
|
||||||
</span>
|
</span>
|
||||||
<span className="font-label-caps text-label-caps text-secondary-fixed tracking-widest uppercase">LIVE PACK RADAR & FORMATION TELEMETRY</span>
|
<span className="font-label-caps text-label-caps text-secondary-fixed tracking-wide">Live Pack Radar & Formation Telemetry</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Headline */}
|
{/* Headline */}
|
||||||
<h1 className="font-display-hero text-display-hero text-on-surface tracking-tight uppercase mb-space-sm">
|
<h1 className="font-display-hero text-display-hero text-on-surface tracking-tight mb-space-sm">
|
||||||
Ride in pack formation. <br/>
|
Ride in pack formation. <br/>
|
||||||
<span className="bg-gradient-to-r from-primary-container via-primary-fixed to-secondary-fixed text-transparent bg-clip-text">Never lose a wingman.</span>
|
<span className="bg-gradient-to-r from-primary-container via-primary-fixed to-secondary-fixed text-transparent bg-clip-text">Never lose a wingman.</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -52,17 +52,17 @@ export default function GroupRidesPage() {
|
||||||
<div className="flex items-center gap-space-md">
|
<div className="flex items-center gap-space-md">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="h-2 w-2 rounded-full bg-secondary-fixed"></span>
|
<span className="h-2 w-2 rounded-full bg-secondary-fixed"></span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface uppercase tracking-wider">SWARM RECON: SKYLINE BLVD > COAST HWY</span>
|
<span className="font-label-caps text-label-caps text-on-surface tracking-wide">Swarm Recon: Skyline Blvd > Coast Hwy</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface-variant bg-surface-container px-2 py-0.5 rounded">SESSION #CA-84-SW</span>
|
<span className="font-code-mono text-code-mono text-on-surface-variant bg-surface-container px-2 py-0.5 rounded">Session #CA-84-SW</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-space-lg font-code-mono text-code-mono text-on-surface-variant">
|
<div className="flex items-center gap-space-lg font-code-mono text-code-mono text-on-surface-variant">
|
||||||
<span className="flex items-center gap-1 hidden lg:flex">
|
<span className="flex items-center gap-1 hidden lg:flex">
|
||||||
<span className="material-symbols-outlined text-secondary-fixed text-[18px]">cell_tower</span> MESH ACTIVE
|
<span className="material-symbols-outlined text-secondary-fixed text-[18px]">cell_tower</span> Mesh Active
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1"><span className="text-secondary-fixed">●</span> 6 SYNCHRONIZED</span>
|
<span className="flex items-center gap-1"><span className="text-secondary-fixed">●</span> 6 Synchronized</span>
|
||||||
<span className="hidden sm:inline">GAP DELTA: ±0.4s</span>
|
<span className="hidden sm:inline">Gap Delta: ±0.4s</span>
|
||||||
<span className="text-primary-container">REPAIR SQUAD: READY</span>
|
<span className="text-primary-container">Repair Squad: Ready</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Cockpit Layout: Map & Radar Feed */}
|
{/* Cockpit Layout: Map & Radar Feed */}
|
||||||
|
|
@ -93,15 +93,15 @@ export default function GroupRidesPage() {
|
||||||
{/* Map HUD Top Left: Sector Status */}
|
{/* Map HUD Top Left: Sector Status */}
|
||||||
<div className="relative z-10 flex items-start justify-between">
|
<div className="relative z-10 flex items-start justify-between">
|
||||||
<div className="bg-surface/90 backdrop-blur-md px-space-md py-2.5 rounded-xl shadow-lg flex flex-col gap-0.5">
|
<div className="bg-surface/90 backdrop-blur-md px-space-md py-2.5 rounded-xl shadow-lg flex flex-col gap-0.5">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">FORMATION PROFILE</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Formation Profile</span>
|
||||||
<span className="font-headline-sm text-headline-sm text-primary">STAGGERED DUAL COLUMN [B]</span>
|
<span className="font-headline-sm text-headline-sm text-primary">Staggered Dual Column [B]</span>
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">PACELINE LOCK: OPTIMAL</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">Paceline Lock: Optimal</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Map HUD Top Right: Telemetry Ping */}
|
{/* Map HUD Top Right: Telemetry Ping */}
|
||||||
<div className="bg-surface/90 backdrop-blur-md px-space-md py-2 rounded-xl shadow-lg flex items-center gap-space-sm">
|
<div className="bg-surface/90 backdrop-blur-md px-space-md py-2 rounded-xl shadow-lg flex items-center gap-space-sm">
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant block uppercase">GPS FREQUENCY</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant block">GPS Frequency</span>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface">10.0 Hz • GNSS LOCK</span>
|
<span className="font-code-mono text-code-mono text-on-surface">10.0 Hz • GNSS Lock</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="material-symbols-outlined text-secondary-fixed text-[24px]">satellite_alt</span>
|
<span className="material-symbols-outlined text-secondary-fixed text-[24px]">satellite_alt</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -118,18 +118,18 @@ export default function GroupRidesPage() {
|
||||||
<div className="absolute bottom-12 left-1/3 flex items-center gap-space-xs bg-error-container/90 px-2.5 py-1.5 rounded shadow-xl pointer-events-auto">
|
<div className="absolute bottom-12 left-1/3 flex items-center gap-space-xs bg-error-container/90 px-2.5 py-1.5 rounded shadow-xl pointer-events-auto">
|
||||||
<span className="material-symbols-outlined text-error text-[16px] animate-bounce">warning</span>
|
<span className="material-symbols-outlined text-error text-[16px] animate-bounce">warning</span>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface font-bold">5 • Alex_Ducati</span>
|
<span className="font-code-mono text-code-mono text-on-surface font-bold">5 • Alex_Ducati</span>
|
||||||
<span className="font-label-caps text-label-caps text-error">-1.2G DECEL</span>
|
<span className="font-label-caps text-label-caps text-error">-1.2G Decel</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Map HUD Bottom Control Strip */}
|
{/* Map HUD Bottom Control Strip */}
|
||||||
<div className="relative z-10 flex flex-wrap items-center justify-between gap-space-sm mt-auto pt-space-lg">
|
<div className="relative z-10 flex flex-wrap items-center justify-between gap-space-sm mt-auto pt-space-lg">
|
||||||
<div className="flex items-center gap-space-xs bg-surface/90 backdrop-blur-md p-1.5 rounded-xl">
|
<div className="flex items-center gap-space-xs bg-surface/90 backdrop-blur-md p-1.5 rounded-xl">
|
||||||
<button className="px-space-sm py-1 rounded-lg bg-surface-container-highest text-primary font-label-caps text-label-caps uppercase">Tactical</button>
|
<button className="px-space-sm py-1 rounded-lg bg-surface-container-highest text-primary font-label-caps text-label-caps">Tactical</button>
|
||||||
<button className="px-space-sm py-1 rounded-lg hover:bg-surface-container text-on-surface-variant font-label-caps text-label-caps uppercase">Satellite</button>
|
<button className="px-space-sm py-1 rounded-lg hover:bg-surface-container text-on-surface-variant font-label-caps text-label-caps">Satellite</button>
|
||||||
<button className="px-space-sm py-1 rounded-lg hover:bg-surface-container text-on-surface-variant font-label-caps text-label-caps uppercase">Elevation 3D</button>
|
<button className="px-space-sm py-1 rounded-lg hover:bg-surface-container text-on-surface-variant font-label-caps text-label-caps">Elevation 3D</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface/90 backdrop-blur-md px-space-md py-1.5 rounded-xl flex items-center gap-space-sm">
|
<div className="bg-surface/90 backdrop-blur-md px-space-md py-1.5 rounded-xl flex items-center gap-space-sm">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">REGROUP POINT</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Regroup Point</span>
|
||||||
<span className="font-code-mono text-code-mono text-primary-container font-bold">Alice's Restaurant (14.8 mi)</span>
|
<span className="font-code-mono text-code-mono text-primary-container font-bold">Alice's Restaurant (14.8 mi)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -137,8 +137,8 @@ export default function GroupRidesPage() {
|
||||||
{/* Live Swarm Nodes Telemetry Column */}
|
{/* Live Swarm Nodes Telemetry Column */}
|
||||||
<div className="lg:col-span-4 bg-surface-container p-space-md flex flex-col gap-space-sm overflow-y-auto max-h-[620px]">
|
<div className="lg:col-span-4 bg-surface-container p-space-md flex flex-col gap-space-sm overflow-y-auto max-h-[620px]">
|
||||||
<div className="flex items-center justify-between pb-space-xs mb-space-xs">
|
<div className="flex items-center justify-between pb-space-xs mb-space-xs">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase tracking-wider">PACK TELEMETRY STREAM</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant tracking-wide">Pack Telemetry Stream</span>
|
||||||
<span className="font-code-mono text-code-mono text-primary-fixed-dim">6 ACTIVE</span>
|
<span className="font-code-mono text-code-mono text-primary-fixed-dim">6 Active</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Rider 1: ApexLead */}
|
{/* Rider 1: ApexLead */}
|
||||||
<div className="p-space-sm rounded-xl bg-surface-container-high hover:bg-surface-container-highest transition-all">
|
<div className="p-space-sm rounded-xl bg-surface-container-high hover:bg-surface-container-highest transition-all">
|
||||||
|
|
@ -146,14 +146,14 @@ export default function GroupRidesPage() {
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="w-2 h-2 rounded-full bg-secondary-fixed"></span>
|
<span className="w-2 h-2 rounded-full bg-secondary-fixed"></span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface">ApexLead</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface">ApexLead</span>
|
||||||
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-secondary-fixed uppercase">Pace Setter</span>
|
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-secondary-fixed">Pace Setter</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-primary-fixed">LEAD</span>
|
<span className="font-code-mono text-code-mono text-primary-fixed">Lead</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
|
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
|
||||||
<div>SPEED: <span className="text-on-surface font-bold">68 MPH</span></div>
|
<div>Speed: <span className="text-on-surface font-bold">68 MPH</span></div>
|
||||||
<div>LEAN: <span className="text-primary-container font-bold">42°</span></div>
|
<div>Lean: <span className="text-primary-container font-bold">42°</span></div>
|
||||||
<div>THROTTLE: <span className="text-on-surface">78%</span></div>
|
<div>Throttle: <span className="text-on-surface">78%</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Rider 2: Ghost99 */}
|
{/* Rider 2: Ghost99 */}
|
||||||
|
|
@ -162,14 +162,14 @@ export default function GroupRidesPage() {
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="w-2 h-2 rounded-full bg-secondary-fixed"></span>
|
<span className="w-2 h-2 rounded-full bg-secondary-fixed"></span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface">Ghost99</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface">Ghost99</span>
|
||||||
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-on-surface-variant uppercase">Wingman L</span>
|
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-on-surface-variant">Wingman L</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">+45m GAP</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">+45m Gap</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
|
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
|
||||||
<div>SPEED: <span className="text-on-surface">67 MPH</span></div>
|
<div>Speed: <span className="text-on-surface">67 MPH</span></div>
|
||||||
<div>LEAN: <span className="text-primary-container font-bold">40°</span></div>
|
<div>Lean: <span className="text-primary-container font-bold">40°</span></div>
|
||||||
<div>BRAKE: <span className="text-on-surface">0%</span></div>
|
<div>Brake: <span className="text-on-surface">0%</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Rider 3: Sarah_RR (Warning: Tire Pressure) */}
|
{/* Rider 3: Sarah_RR (Warning: Tire Pressure) */}
|
||||||
|
|
@ -179,14 +179,14 @@ export default function GroupRidesPage() {
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="w-2 h-2 rounded-full bg-error animate-pulse"></span>
|
<span className="w-2 h-2 rounded-full bg-error animate-pulse"></span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface">Sarah_RR</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface">Sarah_RR</span>
|
||||||
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-on-surface-variant uppercase">Wingman R</span>
|
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-on-surface-variant">Wingman R</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface-variant">+110m GAP</span>
|
<span className="font-code-mono text-code-mono text-on-surface-variant">+110m Gap</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 pl-1">
|
<div className="mt-2 pl-1">
|
||||||
<div className="flex items-center gap-1 text-error font-code-mono text-code-mono">
|
<div className="flex items-center gap-1 text-error font-code-mono text-code-mono">
|
||||||
<span className="material-symbols-outlined text-[15px]">tire_repair</span>
|
<span className="material-symbols-outlined text-[15px]">tire_repair</span>
|
||||||
<span>ALERT: REAR TIRE 28 PSI (LOW)</span>
|
<span>Alert: Rear tire 28 PSI (low)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -196,14 +196,14 @@ export default function GroupRidesPage() {
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="w-2 h-2 rounded-full bg-secondary-fixed"></span>
|
<span className="w-2 h-2 rounded-full bg-secondary-fixed"></span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface">Dan_KTM</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface">Dan_KTM</span>
|
||||||
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-on-surface-variant uppercase">Mid Pack</span>
|
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-on-surface-variant">Mid Pack</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">+180m GAP</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">+180m Gap</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
|
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
|
||||||
<div>SPEED: <span className="text-on-surface">63 MPH</span></div>
|
<div>Speed: <span className="text-on-surface">63 MPH</span></div>
|
||||||
<div>LEAN: <span className="text-on-surface">28°</span></div>
|
<div>Lean: <span className="text-on-surface">28°</span></div>
|
||||||
<div>TEMP: <span className="text-on-surface">88°C</span></div>
|
<div>Temp: <span className="text-on-surface">88°C</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Rider 5: Alex_Ducati (Yellow Flag: Hard Braking) */}
|
{/* Rider 5: Alex_Ducati (Yellow Flag: Hard Braking) */}
|
||||||
|
|
@ -213,13 +213,13 @@ export default function GroupRidesPage() {
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="w-2 h-2 rounded-full bg-error"></span>
|
<span className="w-2 h-2 rounded-full bg-error"></span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface">Alex_Ducati</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface">Alex_Ducati</span>
|
||||||
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-error uppercase">DECEL EVENT</span>
|
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-error">Decel Event</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface-variant">+320m GAP</span>
|
<span className="font-code-mono text-code-mono text-on-surface-variant">+320m Gap</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 pl-1 flex items-center justify-between font-code-mono text-code-mono">
|
<div className="mt-2 pl-1 flex items-center justify-between font-code-mono text-code-mono">
|
||||||
<span className="text-error font-bold">HARD BRAKING (-1.2G)</span>
|
<span className="text-error font-bold">Hard Braking (-1.2G)</span>
|
||||||
<span className="text-on-surface-variant">ABS: ENGAGED</span>
|
<span className="text-on-surface-variant">ABS: Engaged</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Rider 6: Sweep_Marshal */}
|
{/* Rider 6: Sweep_Marshal */}
|
||||||
|
|
@ -228,14 +228,14 @@ export default function GroupRidesPage() {
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="w-2 h-2 rounded-full bg-primary-container"></span>
|
<span className="w-2 h-2 rounded-full bg-primary-container"></span>
|
||||||
<span className="font-headline-sm text-headline-sm text-on-surface">Sweep_Marshal</span>
|
<span className="font-headline-sm text-headline-sm text-on-surface">Sweep_Marshal</span>
|
||||||
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-primary-container uppercase">Rear Guard</span>
|
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-primary-container">Rear Guard</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-primary-fixed-dim">+450m GAP</span>
|
<span className="font-code-mono text-code-mono text-primary-fixed-dim">+450m Gap</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
|
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
|
||||||
<div>SPEED: <span className="text-on-surface">59 MPH</span></div>
|
<div>Speed: <span className="text-on-surface">59 MPH</span></div>
|
||||||
<div>RADIO: <span className="text-secondary-fixed">OPEN</span></div>
|
<div>Radio: <span className="text-secondary-fixed">Open</span></div>
|
||||||
<div>PACK LOCK: <span className="text-primary">100%</span></div>
|
<div>Pack Lock: <span className="text-primary">100%</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -245,12 +245,12 @@ export default function GroupRidesPage() {
|
||||||
{/* Metric 1: Spacing Integrity */}
|
{/* Metric 1: Spacing Integrity */}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">SPACING INTEGRITY</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Spacing Integrity</span>
|
||||||
<span className="material-symbols-outlined text-secondary-fixed text-[18px]">verified</span>
|
<span className="material-symbols-outlined text-secondary-fixed text-[18px]">verified</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs">
|
<div className="flex items-baseline gap-space-xs">
|
||||||
<span className="font-telemetry-metric-md text-telemetry-metric-md text-secondary-fixed">94%</span>
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-secondary-fixed">94%</span>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface-variant">SAFETY INDEX</span>
|
<span className="font-code-mono text-code-mono text-on-surface-variant">Safety Index</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-surface-container h-1.5 rounded-full overflow-hidden mt-1">
|
<div className="w-full bg-surface-container h-1.5 rounded-full overflow-hidden mt-1">
|
||||||
<div className="bg-secondary-fixed h-full rounded-full" ></div>
|
<div className="bg-secondary-fixed h-full rounded-full" ></div>
|
||||||
|
|
@ -259,38 +259,38 @@ export default function GroupRidesPage() {
|
||||||
{/* Metric 2: Average Pack Speed */}
|
{/* Metric 2: Average Pack Speed */}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">AVERAGE PACK VELOCITY</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Average Pack Velocity</span>
|
||||||
<span className="material-symbols-outlined text-primary-container text-[18px]">speed</span>
|
<span className="material-symbols-outlined text-primary-container text-[18px]">speed</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs">
|
<div className="flex items-baseline gap-space-xs">
|
||||||
<span className="font-telemetry-metric-md text-telemetry-metric-md text-on-surface">64.2</span>
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-on-surface">64.2</span>
|
||||||
<span className="font-code-mono text-code-mono text-primary-fixed-dim">MPH</span>
|
<span className="font-code-mono text-code-mono text-primary-fixed-dim">MPH</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface-variant">DELTA DRIFT: -1.8 MPH (TIGHTENING)</span>
|
<span className="font-code-mono text-code-mono text-on-surface-variant">Delta Drift: -1.8 MPH (tightening)</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Metric 3: Atmospheric & Track Conditions */}
|
{/* Metric 3: Atmospheric & Track Conditions */}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">SURFACE & CLIMATE</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Surface & Climate</span>
|
||||||
<span className="material-symbols-outlined text-on-surface-variant text-[18px]">thermostat</span>
|
<span className="material-symbols-outlined text-on-surface-variant text-[18px]">thermostat</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs">
|
<div className="flex items-baseline gap-space-xs">
|
||||||
<span className="font-telemetry-metric-md text-telemetry-metric-md text-on-surface">21°C</span>
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-on-surface">21°C</span>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface-variant">DRY ASPHALT</span>
|
<span className="font-code-mono text-code-mono text-on-surface-variant">Dry Asphalt</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed">0% PRECIP • GRIP OPTIMAL</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed">0% Precip • Grip Optimal</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Metric 4: Next Regroup Waypoint */}
|
{/* Metric 4: Next Regroup Waypoint */}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">REGROUP POINT</span>
|
<span className="font-label-caps text-label-caps text-on-surface-variant">Regroup Point</span>
|
||||||
<span className="material-symbols-outlined text-primary-container text-[18px]">pin_drop</span>
|
<span className="material-symbols-outlined text-primary-container text-[18px]">pin_drop</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline gap-space-xs">
|
<div className="flex items-baseline gap-space-xs">
|
||||||
<span className="font-telemetry-metric-md text-telemetry-metric-md text-primary-container">14.8</span>
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-primary-container">14.8</span>
|
||||||
<span className="font-code-mono text-code-mono text-primary-fixed-dim">MILES</span>
|
<span className="font-code-mono text-code-mono text-primary-fixed-dim">Miles</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface">ALICE'S RESTAURANT CHECKPOINT</span>
|
<span className="font-code-mono text-code-mono text-on-surface">Alice's Restaurant Checkpoint</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -298,8 +298,8 @@ export default function GroupRidesPage() {
|
||||||
{/* Group Ride Features Grid */}
|
{/* Group Ride Features Grid */}
|
||||||
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
|
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
|
||||||
<div className="flex flex-col mb-space-2xl">
|
<div className="flex flex-col mb-space-2xl">
|
||||||
<span className="font-label-caps text-label-caps text-primary-container tracking-widest uppercase mb-1">COCKPIT ARCHITECTURE</span>
|
<span className="font-label-caps text-label-caps text-primary-container tracking-wide mb-1">Cockpit Architecture</span>
|
||||||
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight uppercase">ENGINEERED FOR SQUADRON COHESION</h2>
|
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">Engineered for Squadron Cohesion</h2>
|
||||||
<p className="font-body-md text-body-md text-on-surface-variant max-w-2xl">High-frequency mesh communications keep the entire formation synchronized through blind apexes, switchbacks, and offline mountain passes.</p>
|
<p className="font-body-md text-body-md text-on-surface-variant max-w-2xl">High-frequency mesh communications keep the entire formation synchronized through blind apexes, switchbacks, and offline mountain passes.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-space-lg">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-space-lg">
|
||||||
|
|
@ -309,14 +309,14 @@ export default function GroupRidesPage() {
|
||||||
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-primary-container mb-space-lg">
|
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-primary-container mb-space-lg">
|
||||||
<span className="material-symbols-outlined text-[26px]">spatial_audio_off</span>
|
<span className="material-symbols-outlined text-[26px]">spatial_audio_off</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-primary-fixed-dim block mb-1">01 / PACELINE AUDIT</span>
|
<span className="font-code-mono text-code-mono text-primary-fixed-dim block mb-1">01 / Paceline Audit</span>
|
||||||
<h3 className="font-headline-md text-headline-md text-on-surface uppercase mb-space-sm">Staggered Paceline Warnings</h3>
|
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">Staggered Paceline Warnings</h3>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
|
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
|
||||||
Spatial audio HUD chimes dynamically sound whenever rider follow distance or lane positioning falls beneath calculated emergency stopping thresholds.
|
Spatial audio HUD chimes dynamically sound whenever rider follow distance or lane positioning falls beneath calculated emergency stopping thresholds.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-primary-container">
|
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-primary-container">
|
||||||
<span>RESPONSE: <12ms</span>
|
<span>Response: <12ms</span>
|
||||||
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
|
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -326,14 +326,14 @@ export default function GroupRidesPage() {
|
||||||
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-error mb-space-lg">
|
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-error mb-space-lg">
|
||||||
<span className="material-symbols-outlined text-[26px]">fmd_bad</span>
|
<span className="material-symbols-outlined text-[26px]">fmd_bad</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-error block mb-1">02 / SWARM BROADCAST</span>
|
<span className="font-code-mono text-code-mono text-error block mb-1">02 / Swarm Broadcast</span>
|
||||||
<h3 className="font-headline-md text-headline-md text-on-surface uppercase mb-space-sm">Crash & Separation Alerts</h3>
|
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">Crash & Separation Alerts</h3>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
|
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
|
||||||
Autonomous IMU-based impact detection pings the entire pack instantly. Visual yellow and red telemetry flags highlight precise coordinates for immediate regrouping.
|
Autonomous IMU-based impact detection pings the entire pack instantly. Visual yellow and red telemetry flags highlight precise coordinates for immediate regrouping.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-error">
|
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-error">
|
||||||
<span>IMU SENSING: 6-AXIS</span>
|
<span>IMU Sensing: 6-Axis</span>
|
||||||
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
|
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -343,14 +343,14 @@ export default function GroupRidesPage() {
|
||||||
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-secondary-fixed mb-space-lg">
|
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-secondary-fixed mb-space-lg">
|
||||||
<span className="material-symbols-outlined text-[26px]">hub</span>
|
<span className="material-symbols-outlined text-[26px]">hub</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-secondary-fixed block mb-1">03 / OFFLINE LINK</span>
|
<span className="font-code-mono text-code-mono text-secondary-fixed block mb-1">03 / Offline Link</span>
|
||||||
<h3 className="font-headline-md text-headline-md text-on-surface uppercase mb-space-sm">Peer-to-Peer Mesh Sync</h3>
|
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">Peer-to-Peer Mesh Sync</h3>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
|
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
|
||||||
Never rely on cellular towers in deep canyons. High-throughput ad-hoc Bluetooth and Wi-Fi Direct protocols daisy-chain vital bike metrics from lead to sweep.
|
Never rely on cellular towers in deep canyons. High-throughput ad-hoc Bluetooth and Wi-Fi Direct protocols daisy-chain vital bike metrics from lead to sweep.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-secondary-fixed">
|
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-secondary-fixed">
|
||||||
<span>ZERO-CELL READY</span>
|
<span>Zero-cell Ready</span>
|
||||||
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
|
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -360,14 +360,14 @@ export default function GroupRidesPage() {
|
||||||
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-primary-fixed mb-space-lg">
|
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-primary-fixed mb-space-lg">
|
||||||
<span className="material-symbols-outlined text-[26px]">history_toggle_off</span>
|
<span className="material-symbols-outlined text-[26px]">history_toggle_off</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-code-mono text-primary-fixed block mb-1">04 / MISSION DEBRIEF</span>
|
<span className="font-code-mono text-code-mono text-primary-fixed block mb-1">04 / Mission Debrief</span>
|
||||||
<h3 className="font-headline-md text-headline-md text-on-surface uppercase mb-space-sm">Group Replay & Debrief</h3>
|
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">Group Replay & Debrief</h3>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
|
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
|
||||||
Relive the ride with synced multi-bike timelines. Compare apex apex entries, sector speeds, and individual lean angles on an interactive 3D map renderer.
|
Relive the ride with synced multi-bike timelines. Compare apex apex entries, sector speeds, and individual lean angles on an interactive 3D map renderer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-primary-fixed">
|
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-primary-fixed">
|
||||||
<span>3D REPLAY ENGINE</span>
|
<span>3D Replay Engine</span>
|
||||||
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
|
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -379,8 +379,8 @@ export default function GroupRidesPage() {
|
||||||
<div className="absolute -right-16 -bottom-16 w-80 h-80 bg-primary-container/10 rounded-full blur-3xl pointer-events-none"></div>
|
<div className="absolute -right-16 -bottom-16 w-80 h-80 bg-primary-container/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||||
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-space-xl">
|
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-space-xl">
|
||||||
<div className="flex flex-col gap-space-xs max-w-xl">
|
<div className="flex flex-col gap-space-xs max-w-xl">
|
||||||
<span className="font-label-caps text-label-caps text-secondary-fixed uppercase">HARDWARE COMPATIBILITY</span>
|
<span className="font-label-caps text-label-caps text-secondary-fixed">Hardware Compatibility</span>
|
||||||
<h3 className="font-headline-lg text-headline-lg text-on-surface uppercase tracking-tight">Sync directly with your helmet comms & bike dash</h3>
|
<h3 className="font-headline-lg text-headline-lg text-on-surface tracking-tight">Sync directly with your helmet comms & bike dash</h3>
|
||||||
<p className="font-body-md text-body-md text-on-surface-variant">
|
<p className="font-body-md text-body-md text-on-surface-variant">
|
||||||
Integrates seamlessly with Cardo, Sena, Apple Watch, and OBD-II CAN bus links for live peg-to-peg data overlays.
|
Integrates seamlessly with Cardo, Sena, Apple Watch, and OBD-II CAN bus links for live peg-to-peg data overlays.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -388,11 +388,11 @@ export default function GroupRidesPage() {
|
||||||
<div className="flex items-center gap-space-md">
|
<div className="flex items-center gap-space-md">
|
||||||
<div className="flex items-center gap-space-sm px-space-md py-3 rounded-xl bg-surface-container-high">
|
<div className="flex items-center gap-space-sm px-space-md py-3 rounded-xl bg-surface-container-high">
|
||||||
<span className="material-symbols-outlined text-secondary-fixed">headset_mic</span>
|
<span className="material-symbols-outlined text-secondary-fixed">headset_mic</span>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface">CARDO / SENA HUD</span>
|
<span className="font-code-mono text-code-mono text-on-surface">Cardo / Sena HUD</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-space-sm px-space-md py-3 rounded-xl bg-surface-container-high">
|
<div className="flex items-center gap-space-sm px-space-md py-3 rounded-xl bg-surface-container-high">
|
||||||
<span className="material-symbols-outlined text-primary-container">watch</span>
|
<span className="material-symbols-outlined text-primary-container">watch</span>
|
||||||
<span className="font-code-mono text-code-mono text-on-surface">HAPTIC ALERTS</span>
|
<span className="font-code-mono text-code-mono text-on-surface">Haptic Alerts</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -402,8 +402,8 @@ export default function GroupRidesPage() {
|
||||||
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
|
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between mb-space-xl gap-space-md">
|
<div className="flex flex-col md:flex-row md:items-end justify-between mb-space-xl gap-space-md">
|
||||||
<div>
|
<div>
|
||||||
<span className="font-label-caps text-label-caps text-primary-fixed-dim uppercase tracking-widest block mb-1">REGIONAL OPERATIONS</span>
|
<span className="font-label-caps text-label-caps text-primary-fixed-dim tracking-wide block mb-1">Regional Operations</span>
|
||||||
<h2 className="font-headline-xl text-headline-xl text-on-surface uppercase tracking-tight">Active & Upcoming Pack Runs</h2>
|
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">Active & Upcoming Pack Runs</h2>
|
||||||
</div>
|
</div>
|
||||||
{/* Filter Toggles */}
|
{/* Filter Toggles */}
|
||||||
<div className="flex flex-wrap items-center gap-space-xs bg-surface-container-low p-1 rounded-xl">
|
<div className="flex flex-wrap items-center gap-space-xs bg-surface-container-low p-1 rounded-xl">
|
||||||
|
|
@ -420,8 +420,8 @@ export default function GroupRidesPage() {
|
||||||
<input className="w-full bg-transparent font-body-md text-body-md text-on-surface placeholder:text-on-surface-variant outline-none" placeholder="Search by region, canyon, or road marshal..." type="text"/>
|
<input className="w-full bg-transparent font-body-md text-body-md text-on-surface placeholder:text-on-surface-variant outline-none" placeholder="Search by region, canyon, or road marshal..." type="text"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-space-sm font-code-mono text-code-mono text-on-surface-variant">
|
<div className="flex items-center gap-space-sm font-code-mono text-code-mono text-on-surface-variant">
|
||||||
<span>SORT BY:</span>
|
<span>Sort by:</span>
|
||||||
<span className="text-primary-container cursor-pointer font-bold">START TIME ↓</span>
|
<span className="text-primary-container cursor-pointer font-bold">Start Time ↓</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Rides Cards Grid */}
|
{/* Rides Cards Grid */}
|
||||||
|
|
@ -432,19 +432,19 @@ export default function GroupRidesPage() {
|
||||||
<div className="relative h-44 w-full">
|
<div className="relative h-44 w-full">
|
||||||
<div className="w-full h-full bg-cover bg-center" data-alt="High performance sport motorcycles carving through early morning mist on California mountain ridge highway with sweeping Pacific ocean view in dark cinematic tones" ></div>
|
<div className="w-full h-full bg-cover bg-center" data-alt="High performance sport motorcycles carving through early morning mist on California mountain ridge highway with sweeping Pacific ocean view in dark cinematic tones" ></div>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-low via-surface-container-low/40 to-transparent"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-low via-surface-container-low/40 to-transparent"></div>
|
||||||
<div className="absolute top-3 left-3 px-2 py-1 rounded bg-surface/80 backdrop-blur font-label-caps text-label-caps text-secondary-fixed uppercase">
|
<div className="absolute top-3 left-3 px-2 py-1 rounded bg-surface/80 backdrop-blur font-label-caps text-label-caps text-secondary-fixed">
|
||||||
SATURDAY • 06:30 AM PST
|
Saturday • 06:30 AM PST
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-3 right-3 px-2 py-1 rounded bg-primary-container text-on-primary-container font-label-caps text-label-caps font-bold uppercase">
|
<div className="absolute top-3 right-3 px-2 py-1 rounded bg-primary-container text-on-primary-container font-label-caps text-label-caps font-bold">
|
||||||
TRACK PACE
|
Track Pace
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-space-lg">
|
<div className="p-space-lg">
|
||||||
<div className="flex items-center justify-between text-on-surface-variant font-code-mono text-code-mono mb-1">
|
<div className="flex items-center justify-between text-on-surface-variant font-code-mono text-code-mono mb-1">
|
||||||
<span>SAN FRANCISCO, CA</span>
|
<span>San Francisco, CA</span>
|
||||||
<span>82.4 MILES</span>
|
<span>82.4 Miles</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-headline-md text-headline-md text-on-surface uppercase mb-space-xs">Bay Area Dawn Patrol</h3>
|
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-xs">Bay Area Dawn Patrol</h3>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant line-clamp-2 mb-space-md">
|
<p className="font-body-sm text-body-sm text-on-surface-variant line-clamp-2 mb-space-md">
|
||||||
Fast sweepers from Woodside down to San Gregorio beach before morning traffic. Strict staggered formation enforced.
|
Fast sweepers from Woodside down to San Gregorio beach before morning traffic. Strict staggered formation enforced.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -455,7 +455,7 @@ export default function GroupRidesPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 font-code-mono text-code-mono text-secondary-fixed">
|
<div className="flex items-center gap-1 font-code-mono text-code-mono text-secondary-fixed">
|
||||||
<span className="material-symbols-outlined text-[16px]">group</span>
|
<span className="material-symbols-outlined text-[16px]">group</span>
|
||||||
<span>14/16 RIDERS</span>
|
<span>14/16 Riders</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -473,19 +473,19 @@ export default function GroupRidesPage() {
|
||||||
<div className="relative h-44 w-full">
|
<div className="relative h-44 w-full">
|
||||||
<div className="w-full h-full bg-cover bg-center" data-alt="Trio of supernaked Italian motorcycles parked at golden hour canyon overlook with asphalt switchbacks carving through dark mountains" ></div>
|
<div className="w-full h-full bg-cover bg-center" data-alt="Trio of supernaked Italian motorcycles parked at golden hour canyon overlook with asphalt switchbacks carving through dark mountains" ></div>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-low via-surface-container-low/40 to-transparent"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-low via-surface-container-low/40 to-transparent"></div>
|
||||||
<div className="absolute top-3 left-3 px-2 py-1 rounded bg-surface/80 backdrop-blur font-label-caps text-label-caps text-secondary-fixed uppercase">
|
<div className="absolute top-3 left-3 px-2 py-1 rounded bg-surface/80 backdrop-blur font-label-caps text-label-caps text-secondary-fixed">
|
||||||
SUNDAY • 07:15 AM PST
|
Sunday • 07:15 AM PST
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-3 right-3 px-2 py-1 rounded bg-surface-container-highest text-primary font-label-caps text-label-caps font-bold uppercase">
|
<div className="absolute top-3 right-3 px-2 py-1 rounded bg-surface-container-highest text-primary font-label-caps text-label-caps font-bold">
|
||||||
BRISK PACE
|
Brisk Pace
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-space-lg">
|
<div className="p-space-lg">
|
||||||
<div className="flex items-center justify-between text-on-surface-variant font-code-mono text-code-mono mb-1">
|
<div className="flex items-center justify-between text-on-surface-variant font-code-mono text-code-mono mb-1">
|
||||||
<span>MALIBU, CA</span>
|
<span>Malibu, CA</span>
|
||||||
<span>64.0 MILES</span>
|
<span>64.0 Miles</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-headline-md text-headline-md text-on-surface uppercase mb-space-xs">Malibu Canyons Fast Pace</h3>
|
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-xs">Malibu Canyons Fast Pace</h3>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant line-clamp-2 mb-space-md">
|
<p className="font-body-sm text-body-sm text-on-surface-variant line-clamp-2 mb-space-md">
|
||||||
Tuna Canyon descent to Latigo Canyon technical curves. Regroup checkpoint planned at Rock Store overlook.
|
Tuna Canyon descent to Latigo Canyon technical curves. Regroup checkpoint planned at Rock Store overlook.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -496,7 +496,7 @@ export default function GroupRidesPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 font-code-mono text-code-mono text-secondary-fixed">
|
<div className="flex items-center gap-1 font-code-mono text-code-mono text-secondary-fixed">
|
||||||
<span className="material-symbols-outlined text-[16px]">group</span>
|
<span className="material-symbols-outlined text-[16px]">group</span>
|
||||||
<span>9/12 RIDERS</span>
|
<span>9/12 Riders</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -514,19 +514,19 @@ export default function GroupRidesPage() {
|
||||||
<div className="relative h-44 w-full">
|
<div className="relative h-44 w-full">
|
||||||
<div className="w-full h-full bg-cover bg-center" data-alt="Dense green mountain pass winding asphalt curve with neon trail lights of aggressive sport bikes in moody dark atmospheric lighting" ></div>
|
<div className="w-full h-full bg-cover bg-center" data-alt="Dense green mountain pass winding asphalt curve with neon trail lights of aggressive sport bikes in moody dark atmospheric lighting" ></div>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-low via-surface-container-low/40 to-transparent"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-low via-surface-container-low/40 to-transparent"></div>
|
||||||
<div className="absolute top-3 left-3 px-2 py-1 rounded bg-surface/80 backdrop-blur font-label-caps text-label-caps text-secondary-fixed uppercase">
|
<div className="absolute top-3 left-3 px-2 py-1 rounded bg-surface/80 backdrop-blur font-label-caps text-label-caps text-secondary-fixed">
|
||||||
FRI-SUN • OCT 12-14
|
Fri-Sun • Oct 12-14
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-3 right-3 px-2 py-1 rounded bg-secondary-container text-on-secondary-container font-label-caps text-label-caps font-bold uppercase">
|
<div className="absolute top-3 right-3 px-2 py-1 rounded bg-secondary-container text-on-secondary-container font-label-caps text-label-caps font-bold">
|
||||||
ENDURANCE / CHILL
|
Endurance / Chill
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-space-lg">
|
<div className="p-space-lg">
|
||||||
<div className="flex items-center justify-between text-on-surface-variant font-code-mono text-code-mono mb-1">
|
<div className="flex items-center justify-between text-on-surface-variant font-code-mono text-code-mono mb-1">
|
||||||
<span>ROBBINSVILLE, NC</span>
|
<span>Robbinsville, NC</span>
|
||||||
<span>318 CURVES</span>
|
<span>318 Curves</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-headline-md text-headline-md text-on-surface uppercase mb-space-xs">Tail of the Dragon Run</h3>
|
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-xs">Tail of the Dragon Run</h3>
|
||||||
<p className="font-body-sm text-body-sm text-on-surface-variant line-clamp-2 mb-space-md">
|
<p className="font-body-sm text-body-sm text-on-surface-variant line-clamp-2 mb-space-md">
|
||||||
11 miles, 318 technical curves. Telemetry debrief sessions every evening with custom apex analysis and gear logs.
|
11 miles, 318 technical curves. Telemetry debrief sessions every evening with custom apex analysis and gear logs.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -537,7 +537,7 @@ export default function GroupRidesPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 font-code-mono text-code-mono text-error">
|
<div className="flex items-center gap-1 font-code-mono text-code-mono text-error">
|
||||||
<span className="material-symbols-outlined text-[16px]">group</span>
|
<span className="material-symbols-outlined text-[16px]">group</span>
|
||||||
<span>FULL (24/24)</span>
|
<span>Full (24/24)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -557,7 +557,7 @@ export default function GroupRidesPage() {
|
||||||
<div className="flex items-center justify-between mb-space-lg">
|
<div className="flex items-center justify-between mb-space-lg">
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="flex items-center gap-space-xs">
|
||||||
<span className="material-symbols-outlined text-primary-container">qr_code_scanner</span>
|
<span className="material-symbols-outlined text-primary-container">qr_code_scanner</span>
|
||||||
<h3 className="font-headline-md text-headline-md text-on-surface uppercase">Join Pack Formation</h3>
|
<h3 className="font-headline-md text-headline-md text-on-surface">Join Pack Formation</h3>
|
||||||
</div>
|
</div>
|
||||||
<button className="text-on-surface-variant hover:text-on-surface" id="close-modal">
|
<button className="text-on-surface-variant hover:text-on-surface" id="close-modal">
|
||||||
<span className="material-symbols-outlined">close</span>
|
<span className="material-symbols-outlined">close</span>
|
||||||
|
|
@ -567,15 +567,15 @@ export default function GroupRidesPage() {
|
||||||
Enter the 6-character encrypted ride code generated by your Lead Pace Setter or scan their cockpit radar QR display.
|
Enter the 6-character encrypted ride code generated by your Lead Pace Setter or scan their cockpit radar QR display.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col gap-space-md mb-space-lg">
|
<div className="flex flex-col gap-space-md mb-space-lg">
|
||||||
<input className="w-full bg-surface-container-lowest font-code-mono text-headline-sm text-center tracking-widest text-primary px-space-md py-3 rounded-lg outline-none uppercase placeholder:text-on-surface-variant" placeholder="CODE: e.g. APEX84" type="text"/>
|
<input className="w-full bg-surface-container-lowest font-code-mono text-headline-sm text-center tracking-wide text-primary px-space-md py-3 rounded-lg outline-none placeholder:text-on-surface-variant" placeholder="Code: e.g. APEX84" type="text"/>
|
||||||
<div className="flex items-center gap-space-md text-on-surface-variant font-code-mono text-code-mono justify-center">
|
<div className="flex items-center gap-space-md text-on-surface-variant font-code-mono text-code-mono justify-center">
|
||||||
<span className="h-px bg-outline-variant flex-1"></span>
|
<span className="h-px bg-outline-variant flex-1"></span>
|
||||||
<span>OR SCAN CAMERA</span>
|
<span>Or Scan Camera</span>
|
||||||
<span className="h-px bg-outline-variant flex-1"></span>
|
<span className="h-px bg-outline-variant flex-1"></span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-surface-container h-36 rounded-lg flex flex-col items-center justify-center gap-2 text-on-surface-variant hover:text-primary transition-colors cursor-pointer">
|
<div className="bg-surface-container h-36 rounded-lg flex flex-col items-center justify-center gap-2 text-on-surface-variant hover:text-primary transition-colors cursor-pointer">
|
||||||
<span className="material-symbols-outlined text-[36px]">photo_camera</span>
|
<span className="material-symbols-outlined text-[36px]">photo_camera</span>
|
||||||
<span className="font-code-mono text-code-mono">ACTIVATE RADAR LENS</span>
|
<span className="font-code-mono text-code-mono">Activate Radar Lens</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="w-full py-3 rounded-lg bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-headline-sm">
|
<button className="w-full py-3 rounded-lg bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-headline-sm">
|
||||||
|
|
|
||||||
|
|
@ -35,54 +35,57 @@ const tailwindConfigScript = `
|
||||||
"on-secondary-fixed-variant": "#005227",
|
"on-secondary-fixed-variant": "#005227",
|
||||||
"primary-fixed": "#6ff6ff",
|
"primary-fixed": "#6ff6ff",
|
||||||
"surface-container-low": "#181c24",
|
"surface-container-low": "#181c24",
|
||||||
"on-primary-container": "#006a70",
|
"on-primary-container": "#002022",
|
||||||
"on-background": "#dfe2ee",
|
"on-background": "#dfe2ee",
|
||||||
"inverse-on-surface": "#2c3039",
|
"inverse-on-surface": "#2c3039",
|
||||||
"secondary-fixed": "#60ff98",
|
"secondary-fixed": "#60ff98",
|
||||||
"primary": "#e0fdff",
|
"primary": "#00dce6",
|
||||||
"background": "#0f131c",
|
"background": "#080c13",
|
||||||
"secondary": "#f5fff3",
|
"secondary": "#60ff98",
|
||||||
"primary-container": "#00f2fe",
|
"primary-container": "#00f2fe",
|
||||||
"on-primary-fixed-variant": "#004f53",
|
"on-primary-fixed-variant": "#004f53",
|
||||||
"on-tertiary-fixed-variant": "#004c69",
|
"on-tertiary-fixed-variant": "#004c69",
|
||||||
"error": "#ffb4ab",
|
"error": "#ffb4ab",
|
||||||
"tertiary-container": "#b2e1ff",
|
"tertiary-container": "#b2e1ff",
|
||||||
"surface-bright": "#353942",
|
"surface-bright": "#353942",
|
||||||
"surface-container-highest": "#31353e",
|
"surface-container-highest": "#28344c",
|
||||||
"on-secondary-container": "#007239",
|
"on-secondary-container": "#007239",
|
||||||
"surface-container": "#1c2028",
|
"surface-container": "#141c2c",
|
||||||
"primary-fixed-dim": "#00dce6",
|
"primary-fixed-dim": "#00dce6",
|
||||||
"surface-variant": "#31353e",
|
"surface-variant": "#1a2336",
|
||||||
"surface-container-high": "#262a33",
|
"surface-container-high": "#1c263c",
|
||||||
"surface-dim": "#0f131c",
|
"surface-dim": "#080c13",
|
||||||
"tertiary": "#f1f8ff",
|
"tertiary": "#7bd0ff",
|
||||||
"inverse-primary": "#00696f",
|
"inverse-primary": "#00696f",
|
||||||
"on-primary-fixed": "#002022",
|
"on-primary-fixed": "#002022",
|
||||||
"tertiary-fixed": "#c4e7ff",
|
"tertiary-fixed": "#c4e7ff",
|
||||||
"on-error": "#690005",
|
"on-error": "#690005",
|
||||||
"secondary-container": "#34ff8c",
|
"secondary-container": "#10b981",
|
||||||
"on-error-container": "#ffdad6",
|
"on-error-container": "#ffdad6",
|
||||||
"surface-tint": "#00dce6",
|
"surface-tint": "#00dce6",
|
||||||
"on-tertiary-fixed": "#001e2c",
|
"on-tertiary-fixed": "#001e2c",
|
||||||
"on-tertiary-container": "#00678c",
|
"on-tertiary-container": "#00678c",
|
||||||
"on-primary": "#00373a",
|
"on-primary": "#002022",
|
||||||
"on-tertiary": "#00354a",
|
"on-tertiary": "#00354a",
|
||||||
"outline-variant": "#3a494b",
|
"outline-variant": "#1f2a3f",
|
||||||
"outline": "#849495",
|
"outline": "#849495",
|
||||||
"on-secondary-fixed": "#00210c",
|
"on-secondary-fixed": "#00210c",
|
||||||
"inverse-surface": "#dfe2ee",
|
"inverse-surface": "#dfe2ee",
|
||||||
"surface-container-lowest": "#0a0e16",
|
"surface-container-lowest": "#080c13",
|
||||||
"error-container": "#93000a",
|
"error-container": "#93000a",
|
||||||
"on-surface": "#dfe2ee",
|
"on-surface": "#dfe2ee",
|
||||||
"on-secondary": "#003919",
|
"on-secondary": "#00210c",
|
||||||
"secondary-fixed-dim": "#00e478",
|
"secondary-fixed-dim": "#00e478",
|
||||||
"on-surface-variant": "#b9cacb"
|
"on-surface-variant": "#94a3b8"
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
"DEFAULT": "0.125rem",
|
"DEFAULT": "0.25rem",
|
||||||
"lg": "0.25rem",
|
"sm": "0.375rem",
|
||||||
"xl": "0.5rem",
|
"md": "0.5rem",
|
||||||
"full": "0.75rem"
|
"lg": "0.75rem",
|
||||||
|
"xl": "1rem",
|
||||||
|
"2xl": "1.5rem",
|
||||||
|
"full": "9999px"
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
"space-lg": "1.5rem",
|
"space-lg": "1.5rem",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,40 @@
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AuthForm } from '../../components/auth-form';
|
import { AuthForm } from '../../components/auth-form';
|
||||||
|
|
||||||
export default function LoginPage() { return <section className="auth-page"><div><span className="eyebrow">Rider account</span><h1>Welcome back.</h1><p>Sign in to access your MotorSignal account.</p></div><div className="auth-card"><AuthForm mode="login" /><Link className="muted-link" href="/forgot-password">Forgot your password?</Link></div></section>; }
|
export default function LoginPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[calc(100vh-80px)] pt-24 pb-16 px-4 flex items-center justify-center bg-brand-bg relative overflow-hidden">
|
||||||
|
{/* Background ambient glow */}
|
||||||
|
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-96 h-96 bg-brand-cyan/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-md relative z-10">
|
||||||
|
<div className="text-center mb-8 space-y-2">
|
||||||
|
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-low border border-outline-variant/40 text-[11px] font-mono tracking-wide text-primary-fixed-dim">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-secondary-fixed"></span>
|
||||||
|
Rider Cockpit
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-extrabold text-white tracking-tight">
|
||||||
|
Welcome back.
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-on-surface-variant max-w-xs mx-auto">
|
||||||
|
Sign in to access your telemetry data, garage, and cloud session replays.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glass-card rounded-2xl border border-brand-border p-6 sm:p-8 shadow-2xl backdrop-blur-xl relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-primary-container via-surface-tint to-transparent"></div>
|
||||||
|
<AuthForm mode="login" />
|
||||||
|
|
||||||
|
<div className="mt-6 pt-4 border-t border-outline-variant/30 flex items-center justify-between text-xs font-mono text-on-surface-variant">
|
||||||
|
<Link className="hover:text-primary-container transition-colors" href="/forgot-password">
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
<Link className="hover:text-primary-container transition-colors" href="/register">
|
||||||
|
Need an account? <span className="text-primary-container font-semibold">Sign up</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import CtaSection from '../components/landing/CtaSection';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<div className="flagship-landing">
|
<div className="flagship-landing pt-20">
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<TelemetryStrip />
|
<TelemetryStrip />
|
||||||
<PillarsSection />
|
<PillarsSection />
|
||||||
|
|
|
||||||
|
|
@ -1 +1,514 @@
|
||||||
export default function PricingPage() { return <section className="hero"><span className="eyebrow">Plans</span><h1>Start with the ride.</h1><p>MotorSignal’s account platform is being prepared for clear free and premium access. Sign up now and we’ll keep your account ready for the next release.</p><div className="panel" style={{ marginTop: 28, maxWidth: 430 }}><h3>MotorSignal account</h3><p>Secure sign-in, device sessions, and your rider data portal.</p></div></section>; }
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function PricingPage() {
|
||||||
|
const [billingCycle, setBillingCycle] = useState<'monthly' | 'annual'>('annual');
|
||||||
|
const [openFaq, setOpenFaq] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const toggleFaq = (index: number) => {
|
||||||
|
setOpenFaq((prev) => (prev === index ? null : index));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flagship-page text-on-surface bg-surface min-h-screen">
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
{/* Subtle Ambient Glow Orbs */}
|
||||||
|
<div className="relative w-full overflow-hidden">
|
||||||
|
<div className="absolute -top-32 left-1/2 -translate-x-1/2 w-[720px] h-[380px] bg-primary-container/10 blur-[130px] pointer-events-none rounded-full"></div>
|
||||||
|
<div className="absolute top-96 right-10 w-[420px] h-[280px] bg-secondary-fixed/5 blur-[100px] pointer-events-none rounded-full"></div>
|
||||||
|
{/* Editorial Header / Value Proposition */}
|
||||||
|
<section className="max-w-container-max mx-auto px-gutter-desktop pt-space-2xl pb-space-3xl relative z-10">
|
||||||
|
<div className="flex flex-col items-center text-center max-w-3xl mx-auto">
|
||||||
|
{/* Eyebrow Badge */}
|
||||||
|
<div className="inline-flex items-center gap-space-xs px-space-md py-1 rounded-full bg-surface-container-high text-primary-fixed-dim shadow-sm mb-space-md">
|
||||||
|
<span className="material-symbols-outlined text-[15px] font-bold text-primary-container">speed</span>
|
||||||
|
<span className="font-label-caps text-label-caps tracking-wide">Transparent Telemetry Plans</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="font-display-hero text-display-hero text-on-surface tracking-tight mb-space-md">
|
||||||
|
Pro telemetry. <br className="hidden sm:inline"/>No hidden fees. <span className="text-primary-container">Ride free forever.</span>
|
||||||
|
</h1>
|
||||||
|
<p className="font-body-lg text-body-lg text-on-surface-variant max-w-2xl mb-space-xl">
|
||||||
|
From weekend canyon carving to MotoAmerica championship telemetry. Choose software-only with your smartphone sensors or unlock deep ECU analytics with our OBD-II plug-and-play link.
|
||||||
|
</p>
|
||||||
|
{/* Billing Switcher */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-center gap-space-md bg-surface-container-lowest p-space-xs rounded-full shadow-md">
|
||||||
|
<div className="flex items-center p-1 bg-surface-container rounded-full" role="group" aria-label="Billing frequency toggle">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setBillingCycle('monthly')}
|
||||||
|
className={`px-space-md py-2 rounded-full font-label-md text-label-md min-h-[44px] transition-all cursor-pointer ${
|
||||||
|
billingCycle === 'monthly'
|
||||||
|
? 'bg-primary-container text-on-primary-container shadow-sm font-bold'
|
||||||
|
: 'text-on-surface-variant hover:text-on-surface'
|
||||||
|
}`}
|
||||||
|
id="billing-monthly-btn"
|
||||||
|
aria-pressed={billingCycle === 'monthly'}
|
||||||
|
>
|
||||||
|
Monthly Billing
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setBillingCycle('annual')}
|
||||||
|
className={`px-space-md py-2 rounded-full font-label-md text-label-md min-h-[44px] transition-all cursor-pointer ${
|
||||||
|
billingCycle === 'annual'
|
||||||
|
? 'bg-primary-container text-on-primary-container shadow-sm font-bold'
|
||||||
|
: 'text-on-surface-variant hover:text-on-surface'
|
||||||
|
}`}
|
||||||
|
id="billing-annual-btn"
|
||||||
|
aria-pressed={billingCycle === 'annual'}
|
||||||
|
>
|
||||||
|
Annual Billing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-space-xs px-space-sm py-1.5 bg-secondary-fixed/15 text-secondary-fixed rounded-full">
|
||||||
|
<span className="material-symbols-outlined text-[16px]">redeem</span>
|
||||||
|
<span className="font-label-caps text-label-caps">Save 25% + Free OBD-II Dongle</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* Pricing Matrix (3 Core Software Tiers) */}
|
||||||
|
<section className="max-w-container-max mx-auto px-gutter-desktop pb-space-4xl relative z-10">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-space-lg items-stretch">
|
||||||
|
{/* Tier 1: Free Rider */}
|
||||||
|
<div className="flex flex-col justify-between p-space-xl rounded-xl bg-surface-container-low shadow-md transition-all duration-300 hover:shadow-xl">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-space-sm">
|
||||||
|
<span className="font-label-caps text-label-caps text-on-surface-variant tracking-wide">Baseline Level</span>
|
||||||
|
<span className="px-space-xs py-0.5 rounded bg-surface-container text-outline font-code-mono text-code-mono">Tier 01</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-headline-lg text-headline-lg text-on-surface mb-space-xs">Free Rider</h3>
|
||||||
|
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-lg">Instant heads-up instrumentation using native phone accelerometers.</p>
|
||||||
|
<div className="flex items-baseline gap-space-2xs mb-space-xl">
|
||||||
|
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-on-surface">$0</span>
|
||||||
|
<span className="font-body-md text-body-md text-outline">/ forever</span>
|
||||||
|
</div>
|
||||||
|
{/* Features List */}
|
||||||
|
<ul className="flex flex-col gap-space-sm mb-space-xl">
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-outline text-[18px] mt-0.5">check_circle</span>
|
||||||
|
<span>Smartphone IMU & GPS telemetry (1Hz)</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-outline text-[18px] mt-0.5">check_circle</span>
|
||||||
|
<span>Live lean angle & speed HUD interface</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-outline text-[18px] mt-0.5">check_circle</span>
|
||||||
|
<span>Max 3 recorded ride logs in history cache</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-outline text-[18px] mt-0.5">check_circle</span>
|
||||||
|
<span>Standard community group rides</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-outline text-[18px] mt-0.5">check_circle</span>
|
||||||
|
<span>Basic SOS manual emergency dispatch</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-outline font-body-sm text-body-sm opacity-50">
|
||||||
|
<span className="material-symbols-outlined text-[18px] mt-0.5">cancel</span>
|
||||||
|
<span>CAN-bus throttle & brake pressure</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-outline font-body-sm text-body-sm opacity-50">
|
||||||
|
<span className="material-symbols-outlined text-[18px] mt-0.5">cancel</span>
|
||||||
|
<span>400+ circuit lap timing sectors</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a className="w-full text-center py-3 px-space-md rounded-lg bg-surface-container hover:bg-surface-container-high text-on-surface font-headline-sm text-headline-sm transition-colors shadow-sm" data-path="create-account" href="/account">
|
||||||
|
Get Started Free
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/* Tier 2: Pro Telemetry (Highlighted Hero) */}
|
||||||
|
<div className="relative flex flex-col justify-between p-space-xl rounded-xl bg-surface-container-high shadow-2xl -mt-0 lg:-mt-4 mb-0 lg:-mb-4 z-20 overflow-hidden">
|
||||||
|
{/* Top Cyber Radiant Stroke Accent */}
|
||||||
|
<div className="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-primary-container via-primary-fixed to-secondary-fixed"></div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-space-sm">
|
||||||
|
<span className="px-space-sm py-0.5 rounded-full bg-primary-container text-on-primary-container font-label-caps text-label-caps tracking-wide font-bold">Recommended</span>
|
||||||
|
<div className="flex items-center gap-space-2xs text-secondary-fixed">
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary-fixed opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-secondary-fixed-dim"></span>
|
||||||
|
</span>
|
||||||
|
<span className="font-code-mono text-code-mono">Apex Sync</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-headline-lg text-headline-lg text-on-surface mb-space-xs">Pro Telemetry</h3>
|
||||||
|
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-lg">Full multi-sensor acquisition engine with precision ECU CAN metrics.</p>
|
||||||
|
<div className="flex items-baseline gap-space-2xs mb-space-xs">
|
||||||
|
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-primary-container" id="pro-price">
|
||||||
|
{billingCycle === 'annual' ? '$10' : '$14'}
|
||||||
|
</span>
|
||||||
|
<span className="font-body-md text-body-md text-on-surface-variant" id="pro-period">/ month</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-code-mono text-code-mono text-secondary-fixed mb-space-lg" id="pro-subtext">
|
||||||
|
{billingCycle === 'annual'
|
||||||
|
? 'Billed annually ($120/yr) • Includes FREE OBD-II Link'
|
||||||
|
: 'Billed monthly ($14/mo) • OBD-II Link separate'}
|
||||||
|
</p>
|
||||||
|
{/* Sparkline Visual Component */}
|
||||||
|
<div className="p-space-sm rounded-lg bg-surface-container-lowest mb-space-lg">
|
||||||
|
<div className="flex items-center justify-between text-on-surface-variant mb-space-2xs">
|
||||||
|
<span className="font-label-caps text-label-caps text-primary-fixed-dim">Apex Sampling Trace</span>
|
||||||
|
<span className="font-code-mono text-code-mono text-secondary-fixed font-bold">10Hz Multi-GNSS</span>
|
||||||
|
</div>
|
||||||
|
<svg className="w-full h-9 text-primary-container" fill="none" preserveAspectRatio="none" viewBox="0 0 300 40">
|
||||||
|
<path d="M0 35 L30 30 L55 33 L85 14 L115 19 L145 5 L175 22 L205 8 L235 25 L265 12 L300 4" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5"></path>
|
||||||
|
<path d="M0 35 L30 30 L55 33 L85 14 L115 19 L145 5 L175 22 L205 8 L235 25 L265 12 L300 4 L300 40 L0 40 Z" fill="currentColor" fill-opacity="0.08"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{/* Features List */}
|
||||||
|
<ul className="flex flex-col gap-space-sm mb-space-xl">
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-primary-container text-[18px] mt-0.5">verified</span>
|
||||||
|
<span>10Hz Multi-GNSS satellite lock & 60fps high-refresh HUD</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-primary-container text-[18px] mt-0.5">verified</span>
|
||||||
|
<span>Full 6-axis IMU gyro analytics (pitch, roll, yaw, lateral Gs)</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-primary-container text-[18px] mt-0.5">verified</span>
|
||||||
|
<span>Unlimited cloud ride history & 3D sector replay</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-primary-container text-[18px] mt-0.5">verified</span>
|
||||||
|
<span>Automatic crash impact beacon (<150ms trigger)</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-primary-container text-[18px] mt-0.5">verified</span>
|
||||||
|
<span>Advanced Pack Radar with peer-to-peer mesh synchronization</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-secondary-fixed text-[18px] mt-0.5">electric_bolt</span>
|
||||||
|
<span>OBD-II & CAN-bus live data (RPM, throttle %, brake psi, gear)</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-secondary-fixed text-[18px] mt-0.5">flag</span>
|
||||||
|
<span>Track day mode with 400+ circuit lap timing & sector splits</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a className="w-full text-center py-3.5 px-space-md rounded-lg bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-headline-sm hover:shadow-[0_0_24px_rgba(0,242,254,0.45)] transition-all" data-path="create-account" href="/account">
|
||||||
|
Start 30-Day Free Trial
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/* Tier 3: Club & Racing Team */}
|
||||||
|
<div className="flex flex-col justify-between p-space-xl rounded-xl bg-surface-container-low shadow-md transition-all duration-300 hover:shadow-xl">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-space-sm">
|
||||||
|
<span className="font-label-caps text-label-caps text-on-surface-variant tracking-wide">Fleet & Competition</span>
|
||||||
|
<span className="px-space-xs py-0.5 rounded bg-surface-container text-tertiary-fixed-dim font-code-mono text-code-mono">Multi-Rider</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-headline-lg text-headline-lg text-on-surface mb-space-xs">Club & Racing Team</h3>
|
||||||
|
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-lg">Telemetry command headquarters for racing teams, riding clubs, and marshals.</p>
|
||||||
|
<div className="flex items-baseline gap-space-2xs mb-space-xs">
|
||||||
|
<span className="font-telemetry-metric-xl text-telemetry-metric-xl text-on-surface" id="club-price">
|
||||||
|
{billingCycle === 'annual' ? '$40' : '$49'}
|
||||||
|
</span>
|
||||||
|
<span className="font-body-md text-body-md text-outline" id="club-period">/ month</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-code-mono text-code-mono text-on-surface-variant mb-space-xl" id="club-subtext">
|
||||||
|
{billingCycle === 'annual'
|
||||||
|
? 'Billed annually ($490/yr) • Up to 15 racers'
|
||||||
|
: 'Billed monthly ($49/mo) • Up to 15 racers'}
|
||||||
|
</p>
|
||||||
|
{/* Features List */}
|
||||||
|
<ul className="flex flex-col gap-space-sm mb-space-xl">
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-tertiary-fixed-dim text-[18px] mt-0.5">groups</span>
|
||||||
|
<span>Everything in Pro for up to 15 team riders</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-tertiary-fixed-dim text-[18px] mt-0.5">analytics</span>
|
||||||
|
<span>Central club marshal & paddock tactical dashboard</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-tertiary-fixed-dim text-[18px] mt-0.5">leaderboard</span>
|
||||||
|
<span>Private sector leaderboards & verified lap telemetry</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-tertiary-fixed-dim text-[18px] mt-0.5">download</span>
|
||||||
|
<span>Export FIM-compliant CSV/KML data for crew chiefs</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-tertiary-fixed-dim text-[18px] mt-0.5">podium</span>
|
||||||
|
<span>Live pit-lane spectator broadcast link & delta map</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-space-sm text-on-surface font-body-sm text-body-sm">
|
||||||
|
<span className="material-symbols-outlined text-tertiary-fixed-dim text-[18px] mt-0.5">support_agent</span>
|
||||||
|
<span>Dedicated priority engineering telemetry support</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
className="w-full text-center py-3 px-space-md rounded-lg bg-surface-container hover:bg-surface-container-high text-on-surface font-headline-sm text-headline-sm transition-colors shadow-sm"
|
||||||
|
data-path="clubs"
|
||||||
|
href="/clubs/manage"
|
||||||
|
>
|
||||||
|
Open Founder Portal
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* Telemetry Precision Metrics Matrix */}
|
||||||
|
<section className="max-w-container-max mx-auto px-gutter-desktop pb-space-4xl">
|
||||||
|
<div className="p-space-xl rounded-xl bg-surface-container-lowest shadow-md">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-end justify-between mb-space-xl gap-space-md">
|
||||||
|
<div>
|
||||||
|
<span className="font-label-caps text-label-caps text-primary-container tracking-wide">Architectural Comparison</span>
|
||||||
|
<h2 className="font-headline-xl text-headline-xl text-on-surface mt-space-2xs">Full Feature Telemetry Breakdown</h2>
|
||||||
|
</div>
|
||||||
|
<div className="font-code-mono text-code-mono text-outline">
|
||||||
|
SPEC STANDARD: ISO-11898 CAN 2.0B
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left font-body-sm text-body-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-surface-container-low text-on-surface">
|
||||||
|
<th className="p-space-md font-label-caps text-label-caps">Telemetry Capability</th>
|
||||||
|
<th className="p-space-md font-label-caps text-label-caps">Free Rider</th>
|
||||||
|
<th className="p-space-md font-label-caps text-label-caps text-primary-container">Pro Telemetry</th>
|
||||||
|
<th className="p-space-md font-label-caps text-label-caps text-tertiary-fixed-dim">Club & Team</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-none text-on-surface-variant">
|
||||||
|
<tr className="hover:bg-surface-container-high/40 transition-colors">
|
||||||
|
<td className="p-space-md font-headline-sm text-headline-sm text-on-surface">Sampling Frequency</td>
|
||||||
|
<td className="p-space-md font-code-mono text-code-mono">1 Hz (Phone GPS)</td>
|
||||||
|
<td className="p-space-md font-code-mono text-code-mono text-primary-container font-bold">10 Hz Multi-GNSS</td>
|
||||||
|
<td className="p-space-md font-code-mono text-code-mono text-primary-container font-bold">10 Hz + 100Hz IMU</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="bg-surface-container-low/30 hover:bg-surface-container-high/40 transition-colors">
|
||||||
|
<td className="p-space-md font-headline-sm text-headline-sm text-on-surface">Lean Angle & G-Force Trace</td>
|
||||||
|
<td className="p-space-md">Realtime HUD only</td>
|
||||||
|
<td className="p-space-md text-on-surface">Stored per millisecond</td>
|
||||||
|
<td className="p-space-md text-on-surface">Multi-rider overlay compare</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="hover:bg-surface-container-high/40 transition-colors">
|
||||||
|
<td className="p-space-md font-headline-sm text-headline-sm text-on-surface">ECU Diagnostics & CAN Data</td>
|
||||||
|
<td className="p-space-md text-outline">None</td>
|
||||||
|
<td className="p-space-md text-secondary-fixed">Throttle, RPM, Brake, Gear</td>
|
||||||
|
<td className="p-space-md text-secondary-fixed">Raw CAN logging & PID maps</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="bg-surface-container-low/30 hover:bg-surface-container-high/40 transition-colors">
|
||||||
|
<td className="p-space-md font-headline-sm text-headline-sm text-on-surface">Circuit Lap Timing & Deltas</td>
|
||||||
|
<td className="p-space-md text-outline">Manual start/stop</td>
|
||||||
|
<td className="p-space-md text-on-surface">400+ GPS micro-sectors</td>
|
||||||
|
<td className="p-space-md text-on-surface">Team Live Pit Deltas</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="hover:bg-surface-container-high/40 transition-colors">
|
||||||
|
<td className="p-space-md font-headline-sm text-headline-sm text-on-surface">Ride History Cloud Storage</td>
|
||||||
|
<td className="p-space-md">3 sessions</td>
|
||||||
|
<td className="p-space-md text-primary-container">Unlimited full resolution</td>
|
||||||
|
<td className="p-space-md text-primary-container">Unlimited + Paddock Archive</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="bg-surface-container-low/30 hover:bg-surface-container-high/40 transition-colors">
|
||||||
|
<td className="p-space-md font-headline-sm text-headline-sm text-on-surface">Crash Detection & Mesh SOS</td>
|
||||||
|
<td className="p-space-md">Manual SOS</td>
|
||||||
|
<td className="p-space-md text-secondary-fixed"><150ms Automatic Trigger</td>
|
||||||
|
<td className="p-space-md text-secondary-fixed">Fleetwide Marshall Auto-Dispatch</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* Hardware Add-ons Section */}
|
||||||
|
<section id="hardware" className="max-w-container-max mx-auto px-gutter-desktop pb-space-4xl scroll-mt-24">
|
||||||
|
<div className="flex flex-col mb-space-2xl">
|
||||||
|
<span className="font-label-caps text-label-caps text-secondary-fixed tracking-wide">Hardware Ecosystem</span>
|
||||||
|
<h2 className="font-headline-xl text-headline-xl text-on-surface mt-space-2xs">Engineered Motorcycle Hardware</h2>
|
||||||
|
<p className="font-body-md text-body-md text-on-surface-variant max-w-2xl mt-space-xs">
|
||||||
|
Industrial grade modular hardware plugs directly into modern motorcycle diagnostic links. Instant sync with the MotorSignal app over high-bandwidth Bluetooth 5.3.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-space-xl items-stretch">
|
||||||
|
{/* Hardware Item 1: OBD-II / CAN Dongle */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-space-lg p-space-xl rounded-xl bg-surface-container-low shadow-md hover:shadow-xl transition-all">
|
||||||
|
<div className="w-full sm:w-48 h-48 rounded-lg overflow-hidden bg-surface-container flex-shrink-0 relative">
|
||||||
|
<img className="w-full h-full object-cover" data-alt="A compact matte-black motorcycle OBD-II telemetry dongle with glowing cyan LED accents and water-resistant carbon fiber housing resting on an anodized racing bike frame." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAfdRGcXZFf1s-sG4CrdGNtARVa7s_tu9UibEnytT9IO3WXpOHQaxvGMmyJ6Clh5q690_Yi-KS-fR4fCTkWCzetkk_sNQ1-Ix4KMAyiRVbD4HkMl0kv9TT-k7ZOufN0l3hjEhBCYa_lrqmaDzaOR5H3RUDiY3A_RY6ES97lutb6zfm7is9m_bE7ACTdbtBozY6sdOfNGG-EoASFdrxZul_ELsvQV_5LLkQSJ6gFm8ske777hzqvdTB3cQ"/>
|
||||||
|
<span className="absolute top-2 left-2 px-space-xs py-0.5 rounded bg-surface-container-lowest/80 backdrop-blur font-code-mono text-code-mono text-primary-container">IP68 Waterproof</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-between flex-grow">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-baseline justify-between mb-space-xs">
|
||||||
|
<h3 className="font-headline-md text-headline-md text-on-surface">MotorSignal Link Dongle</h3>
|
||||||
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-primary-container">$99</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-label-caps text-label-caps text-secondary-fixed mb-space-sm">Free with annual Pro subscription</p>
|
||||||
|
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-md">
|
||||||
|
Plugs straight into your bike's Euro4/Euro5 diagnostic port. Reads real-time brake line hydraulic pressure, lean-angle-sensitive traction control, and exact throttle opening.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-space-xs font-code-mono text-code-mono text-on-surface mb-space-md">
|
||||||
|
<div className="flex items-center gap-space-2xs">
|
||||||
|
<span className="material-symbols-outlined text-primary-container text-[16px]">bluetooth</span>
|
||||||
|
<span>BT 5.3 Low Energy</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-space-2xs">
|
||||||
|
<span className="material-symbols-outlined text-primary-container text-[16px]">wifi</span>
|
||||||
|
<span>Wi-Fi Direct 60fps</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-space-2xs">
|
||||||
|
<span className="material-symbols-outlined text-primary-container text-[16px]">cable</span>
|
||||||
|
<span>Yamaha / KTM / Ducati</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-space-2xs">
|
||||||
|
<span className="material-symbols-outlined text-primary-container text-[16px]">bolt</span>
|
||||||
|
<span><8mA Draw Sleep</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a className="inline-flex items-center justify-center gap-space-xs py-2.5 px-space-md rounded-lg bg-surface-container-high hover:bg-surface-bright text-on-surface font-headline-sm text-headline-sm transition-colors" data-path="hardware-and-app" href="/account">
|
||||||
|
<span>View Specs & Harness Fits</span>
|
||||||
|
<span className="material-symbols-outlined text-[18px]">arrow_forward</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Hardware Item 2: Fork IMU Module */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-space-lg p-space-xl rounded-xl bg-surface-container-low shadow-md hover:shadow-xl transition-all">
|
||||||
|
<div className="w-full sm:w-48 h-48 rounded-lg overflow-hidden bg-surface-container flex-shrink-0 relative">
|
||||||
|
<img className="w-full h-full object-cover" data-alt="A machined billet aluminum 6-axis fork-mounted motorcycle IMU sensor clamp with electric cyan telemetry wire harness and laser etched calibration markings." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBU-2F8HuXFO0QSSfWOKIfsqW2Esh6pjI12ymtA0vP_drMLQHSGQrpJqNDCpw3Ei9mvXkvfKRJVdpahH0VznZNn-MQ8PZc7LAUni1K65bt_TklFlys_yYf8honGp4ttFqzQHffGHPUs0j-Gl56QcsasdN6D4TAzJbL0uqyax8VXlLEsMP4Nx3qEpLs8TfVZANJZ9M-NMjIz7FZXUJhqD86gjzzsgjIO_0VpRt0M11lCGRzF2LoYALBdbA"/>
|
||||||
|
<span className="absolute top-2 left-2 px-space-xs py-0.5 rounded bg-surface-container-lowest/80 backdrop-blur font-code-mono text-code-mono text-secondary-fixed">100Hz High Density</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-between flex-grow">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-baseline justify-between mb-space-xs">
|
||||||
|
<h3 className="font-headline-md text-headline-md text-on-surface">6-Axis Fork IMU Module</h3>
|
||||||
|
<span className="font-telemetry-metric-md text-telemetry-metric-md text-on-surface">$149</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-label-caps text-label-caps text-outline mb-space-sm">Optional track precision add-on</p>
|
||||||
|
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-md">
|
||||||
|
Dedicated fork clamp sensor providing MotoGP-grade chassis pitch dynamics, suspension travel velocity, and high-frequency chatter detection.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-space-xs font-code-mono text-code-mono text-on-surface mb-space-md">
|
||||||
|
<div className="flex items-center gap-space-2xs">
|
||||||
|
<span className="material-symbols-outlined text-secondary-fixed text-[16px]">sensors</span>
|
||||||
|
<span>100Hz Gyro / Accel</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-space-2xs">
|
||||||
|
<span className="material-symbols-outlined text-secondary-fixed text-[16px]">precision_manufacturing</span>
|
||||||
|
<span>CNC 6061 Billet Clamp</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-space-2xs">
|
||||||
|
<span className="material-symbols-outlined text-secondary-fixed text-[16px]">straighten</span>
|
||||||
|
<span>41mm to 55mm Fork Stems</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-space-2xs">
|
||||||
|
<span className="material-symbols-outlined text-secondary-fixed text-[16px]">sync</span>
|
||||||
|
<span>Zero-lag CAN Bridging</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a className="inline-flex items-center justify-center gap-space-xs py-2.5 px-space-md rounded-lg bg-surface-container-high hover:bg-surface-bright text-on-surface font-headline-sm text-headline-sm transition-colors" data-path="hardware-and-app" href="/account">
|
||||||
|
<span>Order Fork Module</span>
|
||||||
|
<span className="material-symbols-outlined text-[18px]">arrow_forward</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* Interactive FAQ Accordion */}
|
||||||
|
<section className="max-w-container-max mx-auto px-gutter-desktop pb-space-4xl">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<div className="text-center mb-space-2xl">
|
||||||
|
<span className="font-label-caps text-label-caps text-primary-container tracking-wide">Knowledge Base</span>
|
||||||
|
<h2 className="font-headline-xl text-headline-xl text-on-surface mt-space-2xs">Frequently Asked Questions</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-space-md">
|
||||||
|
{/* Q1 */}
|
||||||
|
<div className="rounded-xl bg-surface-container-low overflow-hidden shadow-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleFaq(1)}
|
||||||
|
aria-expanded={openFaq === 1}
|
||||||
|
className="w-full flex items-center justify-between p-space-lg text-left cursor-pointer hover:bg-surface-container-high/40 transition-colors min-h-[44px]"
|
||||||
|
>
|
||||||
|
<span className="font-headline-sm text-headline-sm text-on-surface">Do I need the OBD-II hardware to use MotorSignal?</span>
|
||||||
|
<span className={`material-symbols-outlined text-primary-container text-[24px] transition-transform duration-200 ${openFaq === 1 ? 'rotate-180' : ''}`} id="faq-icon-1">expand_more</span>
|
||||||
|
</button>
|
||||||
|
<div className={`px-space-lg pb-space-lg text-on-surface-variant font-body-md text-body-md ${openFaq === 1 ? 'block' : 'hidden'}`} id="faq-body-1">
|
||||||
|
No. You can start immediately using solely your smartphone. Modern smartphones contain high-grade 6-axis accelerometers, gyroscopes, and GNSS modules capable of delivering 80% of our telemetry features including live lean angle, acceleration curves, and lap timing. The optional OBD-II hardware module unlocks deeper bike-specific metrics: engine RPM, gear position, throttle position percentage, traction control engagement, and hydraulic brake pressure.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Q2 */}
|
||||||
|
<div className="rounded-xl bg-surface-container-low overflow-hidden shadow-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleFaq(2)}
|
||||||
|
aria-expanded={openFaq === 2}
|
||||||
|
className="w-full flex items-center justify-between p-space-lg text-left cursor-pointer hover:bg-surface-container-high/40 transition-colors min-h-[44px]"
|
||||||
|
>
|
||||||
|
<span className="font-headline-sm text-headline-sm text-on-surface">Does the crash detection work without cellular service?</span>
|
||||||
|
<span className={`material-symbols-outlined text-primary-container text-[24px] transition-transform duration-200 ${openFaq === 2 ? 'rotate-180' : ''}`} id="faq-icon-2">expand_more</span>
|
||||||
|
</button>
|
||||||
|
<div className={`px-space-lg pb-space-lg text-on-surface-variant font-body-md text-body-md ${openFaq === 2 ? 'block' : 'hidden'}`} id="faq-body-2">
|
||||||
|
Yes. If you are riding in remote mountain passes without cellular coverage, the system initiates a dual fallback: it records high-precision blackbox sensor data locally, and simultaneously emits a high-priority emergency beacon via peer-to-peer Wi-Fi and Bluetooth mesh to nearby riders in your group pack. As soon as any pack member's device touches cellular or satellite connectivity, the incident coordinates are dispatched immediately.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Q3 */}
|
||||||
|
<div className="rounded-xl bg-surface-container-low overflow-hidden shadow-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleFaq(3)}
|
||||||
|
aria-expanded={openFaq === 3}
|
||||||
|
className="w-full flex items-center justify-between p-space-lg text-left cursor-pointer hover:bg-surface-container-high/40 transition-colors min-h-[44px]"
|
||||||
|
>
|
||||||
|
<span className="font-headline-sm text-headline-sm text-on-surface">Can I cancel anytime? What happens to my data?</span>
|
||||||
|
<span className={`material-symbols-outlined text-primary-container text-[24px] transition-transform duration-200 ${openFaq === 3 ? 'rotate-180' : ''}`} id="faq-icon-3">expand_more</span>
|
||||||
|
</button>
|
||||||
|
<div className={`px-space-lg pb-space-lg text-on-surface-variant font-body-md text-body-md ${openFaq === 3 ? 'block' : 'hidden'}`} id="faq-body-3">
|
||||||
|
You can cancel your subscription at any time with a single click in your Account settings. There are no cancellation penalties. Your ride telemetry and track logs are yours forever: you can export all your past session data in raw CSV, GPX, or FIM-standard KML formats at any point, even on the free tier.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Q4 */}
|
||||||
|
<div className="rounded-xl bg-surface-container-low overflow-hidden shadow-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleFaq(4)}
|
||||||
|
aria-expanded={openFaq === 4}
|
||||||
|
className="w-full flex items-center justify-between p-space-lg text-left cursor-pointer hover:bg-surface-container-high/40 transition-colors min-h-[44px]"
|
||||||
|
>
|
||||||
|
<span className="font-headline-sm text-headline-sm text-on-surface">What motorcycle brands and models are supported?</span>
|
||||||
|
<span className={`material-symbols-outlined text-primary-container text-[24px] transition-transform duration-200 ${openFaq === 4 ? 'rotate-180' : ''}`} id="faq-icon-4">expand_more</span>
|
||||||
|
</button>
|
||||||
|
<div className={`px-space-lg pb-space-lg text-on-surface-variant font-body-md text-body-md ${openFaq === 4 ? 'block' : 'hidden'}`} id="faq-body-4">
|
||||||
|
The phone sensor suite works on any vehicle with two wheels. For our OBD-II CAN Link dongle, all Euro4 and Euro5 compliant fuel-injected motorcycles from 2017 to present are supported out-of-the-box (including Yamaha, Ducati, BMW Motorrad, KTM, Kawasaki, Honda, Aprilia, Triumph, and Suzuki). We include custom pin adapter harnesses with every dongle order.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* Tactical Final Banner CTA */}
|
||||||
|
<section className="max-w-container-max mx-auto px-gutter-desktop pb-space-4xl">
|
||||||
|
<div className="relative p-space-2xl md:p-space-3xl rounded-xl bg-gradient-to-r from-surface-container-high via-surface-container to-surface-container-lowest shadow-2xl overflow-hidden">
|
||||||
|
<div className="absolute right-0 top-0 bottom-0 w-1/3 bg-gradient-to-l from-primary-container/10 to-transparent pointer-events-none"></div>
|
||||||
|
<div className="relative z-10 max-w-2xl">
|
||||||
|
<span className="font-label-caps text-label-caps text-primary-container tracking-wide">Instant Telemetry Link</span>
|
||||||
|
<h2 className="font-headline-xl text-headline-xl text-on-surface mt-space-xs mb-space-sm">
|
||||||
|
Ready to analyze your apex angles and lap splits?
|
||||||
|
</h2>
|
||||||
|
<p className="font-body-md text-body-md text-on-surface-variant mb-space-xl">
|
||||||
|
Download the app, mount your phone, and be track-ready in under 60 seconds. No credit card required for the free rider tier.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-space-md">
|
||||||
|
<a className="px-space-lg py-3 rounded-lg bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-headline-sm hover:shadow-[0_0_24px_rgba(0,242,254,0.45)] transition-all" data-path="create-account" href="/account">
|
||||||
|
Claim Free Telemetry Account
|
||||||
|
</a>
|
||||||
|
<a className="px-space-md py-3 rounded-lg bg-surface-container hover:bg-surface-bright text-on-surface font-headline-sm text-headline-sm transition-colors" data-path="live-cockpit" href="/account">
|
||||||
|
Explore Live Cockpit Demo
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,38 @@
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AuthForm } from '../../components/auth-form';
|
import { AuthForm } from '../../components/auth-form';
|
||||||
|
|
||||||
export default function RegisterPage() { return <section className="auth-page"><div><span className="eyebrow">Rider account</span><h1>Bring your data with you.</h1><p>Create a secure MotorSignal account for your rides, devices, and future web portal.</p></div><div className="auth-card"><AuthForm mode="register" /><p>Already have an account? <Link className="muted-link" href="/login">Sign in</Link></p></div></section>; }
|
export default function RegisterPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[calc(100vh-80px)] pt-24 pb-16 px-4 flex items-center justify-center bg-brand-bg relative overflow-hidden">
|
||||||
|
{/* Background ambient glow */}
|
||||||
|
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-96 h-96 bg-brand-cyan/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-md relative z-10">
|
||||||
|
<div className="text-center mb-8 space-y-2">
|
||||||
|
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-surface-container-low border border-outline-variant/40 text-[11px] font-mono tracking-wide text-primary-fixed-dim">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-secondary-fixed"></span>
|
||||||
|
Cloud Telemetry
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-extrabold text-white tracking-tight">
|
||||||
|
Bring your data with you.
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-on-surface-variant max-w-xs mx-auto">
|
||||||
|
Create a secure MotorSignal account for your rides, garage, and cloud telemetry.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glass-card rounded-2xl border border-brand-border p-6 sm:p-8 shadow-2xl backdrop-blur-xl relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-primary-container via-surface-tint to-transparent"></div>
|
||||||
|
<AuthForm mode="register" />
|
||||||
|
|
||||||
|
<div className="mt-6 pt-4 border-t border-outline-variant/30 text-center text-xs font-mono text-on-surface-variant">
|
||||||
|
Already have an account?{' '}
|
||||||
|
<Link className="text-primary-container hover:underline font-semibold" href="/login">
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { FormEvent, useState } from 'react';
|
import { FormEvent, useState } from 'react';
|
||||||
|
import { signInWithPopup } from 'firebase/auth';
|
||||||
|
import { firebaseAuth, googleProvider } from '../lib/firebase-client';
|
||||||
|
|
||||||
type AuthMode = 'login' | 'register';
|
type AuthMode = 'login' | 'register';
|
||||||
|
|
||||||
|
|
@ -10,6 +12,42 @@ export function AuthForm({ mode }: { mode: AuthMode }) {
|
||||||
const [deviceName, setDeviceName] = useState('Web browser');
|
const [deviceName, setDeviceName] = useState('Web browser');
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [googleBusy, setGoogleBusy] = useState(false);
|
||||||
|
|
||||||
|
async function handleGoogleSignIn() {
|
||||||
|
setGoogleBusy(true);
|
||||||
|
setMessage('');
|
||||||
|
try {
|
||||||
|
const credential = await signInWithPopup(firebaseAuth, googleProvider);
|
||||||
|
const idToken = await credential.user.getIdToken();
|
||||||
|
// Exchange firebase idToken with backend to set server session & ops cookies
|
||||||
|
try {
|
||||||
|
await fetch('/api/auth/firebase-login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id_token: idToken }),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Fallback to legacy link if available
|
||||||
|
try {
|
||||||
|
await fetch('/api/account/firebase-link', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id_token: idToken }),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Backend link optional; Firebase auth is sufficient for client cloud data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const destination = new URLSearchParams(window.location.search).get('returnTo') || '/account';
|
||||||
|
window.location.assign(destination);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Google Sign-In Error:', error);
|
||||||
|
setMessage(error instanceof Error ? error.message : 'Google sign-in failed. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setGoogleBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submit(event: FormEvent<HTMLFormElement>) {
|
async function submit(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -23,7 +61,8 @@ export function AuthForm({ mode }: { mode: AuthMode }) {
|
||||||
});
|
});
|
||||||
const body = await response.json().catch(() => ({}));
|
const body = await response.json().catch(() => ({}));
|
||||||
if (!response.ok) throw new Error(body.detail ?? 'Authentication failed.');
|
if (!response.ok) throw new Error(body.detail ?? 'Authentication failed.');
|
||||||
window.location.assign('/account');
|
const destination = new URLSearchParams(window.location.search).get('returnTo') || '/account';
|
||||||
|
window.location.assign(destination);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage(error instanceof Error ? error.message : 'Authentication failed.');
|
setMessage(error instanceof Error ? error.message : 'Authentication failed.');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -32,12 +71,116 @@ export function AuthForm({ mode }: { mode: AuthMode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="auth-form" onSubmit={submit}>
|
<div className="w-full space-y-6">
|
||||||
<label>Email<input required type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} /></label>
|
{/* Google Sign-In / Registration Button */}
|
||||||
<label>Password<input required minLength={12} type="password" autoComplete={mode === 'login' ? 'current-password' : 'new-password'} value={password} onChange={(e) => setPassword(e.target.value)} /></label>
|
<button
|
||||||
<label>Device name<input maxLength={255} value={deviceName} onChange={(e) => setDeviceName(e.target.value)} /></label>
|
type="button"
|
||||||
{message && <p className="form-error" role="alert">{message}</p>}
|
disabled={googleBusy || busy}
|
||||||
<button className="button" disabled={busy} type="submit">{busy ? 'Working...' : mode === 'login' ? 'Sign in' : 'Create account'}</button>
|
onClick={() => void handleGoogleSignIn()}
|
||||||
</form>
|
className="w-full h-12 px-4 rounded-xl bg-surface-container-low hover:bg-surface-container-high border border-outline-variant/40 hover:border-primary-container/60 text-on-surface font-semibold text-sm flex items-center justify-center gap-3 transition-all shadow-sm hover:shadow-[0_0_20px_rgba(0,242,254,0.15)] cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{googleBusy ? (
|
||||||
|
<div className="w-5 h-5 border-2 border-primary-container border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
) : (
|
||||||
|
<svg className="w-5 h-5 shrink-0" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="#4285F4"
|
||||||
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.63z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
<span>{googleBusy ? 'Connecting to Google...' : mode === 'login' ? 'Sign in with Google' : 'Register with Google'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-3 my-1">
|
||||||
|
<div className="flex-1 border-t border-outline-variant/30"></div>
|
||||||
|
<span className="text-[10px] font-mono tracking-wide text-slate-400 shrink-0">
|
||||||
|
or continue with email
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 border-t border-outline-variant/30"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email / Password Form */}
|
||||||
|
<form className="space-y-4" onSubmit={submit}>
|
||||||
|
<div className="space-y-1.5 text-left">
|
||||||
|
<label className="block text-xs font-mono tracking-wide text-on-surface-variant">
|
||||||
|
Email address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
placeholder="rider@example.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full px-3.5 py-2.5 rounded-xl bg-surface-container-low border border-outline-variant/40 focus:border-primary-container focus:ring-1 focus:ring-primary-container text-white text-sm outline-none transition placeholder:text-slate-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5 text-left">
|
||||||
|
<label className="block text-xs font-mono tracking-wide text-on-surface-variant">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
minLength={12}
|
||||||
|
type="password"
|
||||||
|
autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
|
||||||
|
placeholder="Min. 12 characters"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full px-3.5 py-2.5 rounded-xl bg-surface-container-low border border-outline-variant/40 focus:border-primary-container focus:ring-1 focus:ring-primary-container text-white text-sm outline-none transition placeholder:text-slate-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mode === 'register' && (
|
||||||
|
<div className="space-y-1.5 text-left">
|
||||||
|
<label className="block text-xs font-mono tracking-wide text-on-surface-variant">
|
||||||
|
Device Name <span className="text-[10px] text-slate-500 font-normal">(optional)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
maxLength={255}
|
||||||
|
value={deviceName}
|
||||||
|
onChange={(e) => setDeviceName(e.target.value)}
|
||||||
|
className="w-full px-3.5 py-2.5 rounded-xl bg-surface-container-low border border-outline-variant/40 focus:border-primary-container focus:ring-1 focus:ring-primary-container text-white text-sm outline-none transition placeholder:text-slate-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div className="p-3 rounded-xl bg-error-container/20 border border-error/30 text-error text-xs font-mono" role="alert">
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={busy || googleBusy}
|
||||||
|
type="submit"
|
||||||
|
className="w-full h-11 px-4 rounded-xl bg-gradient-to-r from-primary-container to-primary-fixed hover:opacity-95 text-on-primary-container font-headline-sm font-bold text-sm tracking-wide shadow-[0_0_20px_rgba(0,242,254,0.3)] transition-all cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{busy ? (
|
||||||
|
<>
|
||||||
|
<div className="w-4 h-4 border-2 border-on-primary-container border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
<span>Authenticating...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>{mode === 'login' ? 'Sign In' : 'Create Account'}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { signInWithPopup } from 'firebase/auth';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { firebaseAuth, googleProvider } from '../lib/firebase-client';
|
||||||
|
|
||||||
|
export function FirebaseLink({ linked }: { linked: boolean }) {
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [message, setMessage] = useState(linked ? 'Google app account connected.' : '');
|
||||||
|
async function connect() {
|
||||||
|
setBusy(true);
|
||||||
|
setMessage('');
|
||||||
|
try {
|
||||||
|
const credential = await signInWithPopup(firebaseAuth, googleProvider);
|
||||||
|
const response = await fetch('/api/account/firebase-link', {
|
||||||
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id_token: await credential.user.getIdToken() }),
|
||||||
|
});
|
||||||
|
const body = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) throw new Error(body.detail ?? 'Unable to connect Google account.');
|
||||||
|
setMessage('Google app account connected.');
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
setMessage(error instanceof Error ? error.message : 'Unable to connect Google account.');
|
||||||
|
} finally { setBusy(false); }
|
||||||
|
}
|
||||||
|
return <div><p>{linked ? 'Your website account is linked to MotorSignal app data.' : 'Connect the Google account used in the MotorSignal app.'}</p>{!linked && <button className="button" disabled={busy} type="button" onClick={() => void connect()}>{busy ? 'Connecting...' : 'Connect Google app account'}</button>}{message && <p className="form-message" role="status">{message}</p>}</div>;
|
||||||
|
}
|
||||||
|
|
@ -11,12 +11,12 @@ export default function HeroSection() {
|
||||||
{/* Hero Copy */}
|
{/* Hero Copy */}
|
||||||
<div className="hero-content-col">
|
<div className="hero-content-col">
|
||||||
<div className="hero-pill">
|
<div className="hero-pill">
|
||||||
<span className="pulse-dot-green">
|
<span className="pulse-dot-green shrink-0">
|
||||||
<span className="ping" />
|
<span className="ping" />
|
||||||
<span className="core" />
|
<span className="core" />
|
||||||
</span>
|
</span>
|
||||||
<span className="font-label-caps text-dim-cyan" style={{ letterSpacing: '0.12em' }}>
|
<span className="font-label-caps text-dim-cyan">
|
||||||
⚡ NEXT-GEN TELEMETRY ENGINE 3.0 • REAL-TIME CAN-BUS & GPS
|
⚡ Next-Gen Telemetry Engine 3.0 • Real-Time CAN-Bus & GPS
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -82,33 +82,33 @@ export default function HeroSection() {
|
||||||
{/* Top Badge: Apex Vector Locked */}
|
{/* Top Badge: Apex Vector Locked */}
|
||||||
<div className="hud-apex-badge">
|
<div className="hud-apex-badge">
|
||||||
<span className="status-dot-green" />
|
<span className="status-dot-green" />
|
||||||
<span className="font-code-mono text-green-accent">APEX VECTOR LOCKED</span>
|
<span className="font-code-mono text-green-accent">Apex Vector Locked</span>
|
||||||
<span className="font-code-mono text-muted">| CAN-BUS 100Hz</span>
|
<span className="font-code-mono text-muted">| CAN-Bus 100Hz</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating HUD 1: Lean Angle */}
|
{/* Floating HUD 1: Lean Angle */}
|
||||||
<div className="hud-lean-card">
|
<div className="hud-lean-card">
|
||||||
<div className="hud-header">
|
<div className="hud-header">
|
||||||
<span className="font-label-caps text-muted-blue">LEAN ANGLE</span>
|
<span className="font-label-caps text-muted-blue">Lean Angle</span>
|
||||||
<span className="font-code-mono text-green-accent">LIVE</span>
|
<span className="font-code-mono text-green-accent">Live</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="hud-value-row">
|
<div className="hud-value-row">
|
||||||
<span className="font-telemetry-metric-xl text-cyan-accent">54°</span>
|
<span className="font-telemetry-metric-xl text-cyan-accent">54°</span>
|
||||||
<span className="font-code-mono text-muted">LEFT</span>
|
<span className="font-code-mono text-muted">Left</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="hud-progress-bg">
|
<div className="hud-progress-bg">
|
||||||
<div className="hud-progress-bar lean-progress" />
|
<div className="hud-progress-bar lean-progress" />
|
||||||
</div>
|
</div>
|
||||||
<div className="hud-footer font-code-mono">
|
<div className="hud-footer font-code-mono">
|
||||||
<span className="text-muted">0°</span>
|
<span className="text-muted">0°</span>
|
||||||
<span className="text-muted">PEAK 58°</span>
|
<span className="text-muted">Peak 58°</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating HUD 2: Velocity & G-Force */}
|
{/* Floating HUD 2: Velocity & G-Force */}
|
||||||
<div className="hud-speed-card">
|
<div className="hud-speed-card">
|
||||||
<div className="hud-header">
|
<div className="hud-header">
|
||||||
<span className="font-label-caps text-muted-blue">VELOCITY & G-FORCE</span>
|
<span className="font-label-caps text-muted-blue">Velocity & G-Force</span>
|
||||||
<span className="material-symbols-outlined text-cyan-accent icon-sm">speed</span>
|
<span className="material-symbols-outlined text-cyan-accent icon-sm">speed</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="hud-value-row">
|
<div className="hud-value-row">
|
||||||
|
|
@ -116,7 +116,7 @@ export default function HeroSection() {
|
||||||
<span className="font-code-mono text-dim-cyan">MPH</span>
|
<span className="font-code-mono text-dim-cyan">MPH</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="hud-lateral-box">
|
<div className="hud-lateral-box">
|
||||||
<span className="font-code-mono text-muted-blue">LATERAL</span>
|
<span className="font-code-mono text-muted-blue">Lateral</span>
|
||||||
<span className="font-headline-sm text-green-accent">+1.82 G</span>
|
<span className="font-headline-sm text-green-accent">+1.82 G</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -126,17 +126,17 @@ export default function HeroSection() {
|
||||||
<div className="hud-header">
|
<div className="hud-header">
|
||||||
<div className="hud-header-title">
|
<div className="hud-header-title">
|
||||||
<span className="material-symbols-outlined text-dim-cyan icon-sm">satellite_alt</span>
|
<span className="material-symbols-outlined text-dim-cyan icon-sm">satellite_alt</span>
|
||||||
<span className="font-label-caps text-ink">GPS MULTI-BAND</span>
|
<span className="font-label-caps text-ink">GPS Multi-Band</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-code-mono text-green-accent">10Hz LOCK</span>
|
<span className="font-code-mono text-green-accent">10Hz Lock</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="hud-detail-row">
|
<div className="hud-detail-row">
|
||||||
<span className="font-code-mono text-muted">SYNC LATENCY</span>
|
<span className="font-code-mono text-muted">Sync Latency</span>
|
||||||
<span className="font-headline-sm text-cyan-accent">12ms</span>
|
<span className="font-headline-sm text-cyan-accent">12ms</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="hud-detail-row font-code-mono">
|
<div className="hud-detail-row font-code-mono">
|
||||||
<span className="text-muted-blue">SAT IN VIEW</span>
|
<span className="text-muted-blue">Sat in View</span>
|
||||||
<span className="text-ink">24 SATS</span>
|
<span className="text-ink">24 Sats</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import { FormEvent, useState } from 'react';
|
import { FormEvent, useState } from 'react';
|
||||||
|
|
||||||
export function RecoveryForm() {
|
export function RecoveryForm({ initialToken = '' }: { initialToken?: string }) {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState(initialToken);
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type Session = { id: string; device_name: string | null; created_at: string; expires_at: string; current: boolean };
|
||||||
|
|
||||||
|
export function SessionManager() {
|
||||||
|
const [sessions, setSessions] = useState<Session[]>([]);
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const response = await fetch('/api/account/sessions', { cache: 'no-store' });
|
||||||
|
if (response.ok) setSessions(await response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => { void load(); }, []);
|
||||||
|
|
||||||
|
async function revoke(id: string) {
|
||||||
|
const response = await fetch(`/api/account/sessions/${id}`, { method: 'DELETE' });
|
||||||
|
if (response.ok) { setMessage('Session revoked.'); await load(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logoutAll() {
|
||||||
|
const response = await fetch('/api/account/sessions', { method: 'POST' });
|
||||||
|
if (response.ok) { setMessage('All sessions revoked.'); await load(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="session-manager"><div className="session-heading"><h2>Sessions</h2><button className="text-button" onClick={() => void logoutAll()} type="button">Revoke all</button></div>{sessions.length ? sessions.map((session) => <div className="session-row" key={session.id}><div><strong>{session.device_name ?? 'Unnamed device'} {session.current && '(this device)'}</strong><p>Created {new Date(session.created_at).toLocaleString()}</p></div>{!session.current && <button className="text-button" onClick={() => void revoke(session.id)} type="button">Revoke</button>}</div>) : <p>No active sessions.</p>}{message && <p className="form-message" role="status">{message}</p>}</div>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function SignOutButton() {
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
async function signOut() {
|
||||||
|
setBusy(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
if (!response.ok) throw new Error('Sign out failed. Please retry.');
|
||||||
|
window.location.assign('/login');
|
||||||
|
} catch {
|
||||||
|
setError('Sign out failed. Please retry.');
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <div><button className="text-button" type="button" disabled={busy} onClick={() => void signOut()}>{busy ? 'Signing out...' : 'Sign out'}</button>{error && <p role="alert">{error}</p>}</div>;
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,8 @@ import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
export function SiteFooter() {
|
export function SiteFooter() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
if (pathname?.includes('/manage') || pathname?.startsWith('/admin') || pathname?.startsWith('/ops')) return null;
|
const isClubManageInterior = pathname !== '/clubs/manage' && Boolean(pathname?.startsWith('/clubs/') && pathname?.includes('/manage'));
|
||||||
|
if (isClubManageInterior || pathname?.startsWith('/admin') || pathname?.startsWith('/ops')) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="flagship-footer">
|
<footer className="flagship-footer">
|
||||||
|
|
@ -18,7 +19,7 @@ export function SiteFooter() {
|
||||||
show_chart
|
show_chart
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontFamily: 'Space Grotesk', fontWeight: 700, letterSpacing: '0.04em' }}>MOTORSIGNAL</span>
|
<span style={{ fontFamily: 'Space Grotesk', fontWeight: 700, letterSpacing: '0.04em' }}>MotorSignal</span>
|
||||||
</Link>
|
</Link>
|
||||||
<p className="footer-desc">
|
<p className="footer-desc">
|
||||||
High-performance precision telemetry and tactical ride intelligence for modern racers, track engineers,
|
High-performance precision telemetry and tactical ride intelligence for modern racers, track engineers,
|
||||||
|
|
@ -34,7 +35,7 @@ export function SiteFooter() {
|
||||||
|
|
||||||
{/* Product Links */}
|
{/* Product Links */}
|
||||||
<div className="footer-link-group">
|
<div className="footer-link-group">
|
||||||
<h4>PRODUCT</h4>
|
<h4>Product</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li><Link href="/features">Live Cockpit</Link></li>
|
<li><Link href="/features">Live Cockpit</Link></li>
|
||||||
<li><Link href="/features">Telemetry Trace</Link></li>
|
<li><Link href="/features">Telemetry Trace</Link></li>
|
||||||
|
|
@ -45,7 +46,7 @@ export function SiteFooter() {
|
||||||
|
|
||||||
{/* Hardware Links */}
|
{/* Hardware Links */}
|
||||||
<div className="footer-link-group">
|
<div className="footer-link-group">
|
||||||
<h4>HARDWARE</h4>
|
<h4>Hardware</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li><Link href="/pricing">OBD-II CAN Link</Link></li>
|
<li><Link href="/pricing">OBD-II CAN Link</Link></li>
|
||||||
<li><Link href="/features">10Hz GPS Module</Link></li>
|
<li><Link href="/features">10Hz GPS Module</Link></li>
|
||||||
|
|
@ -56,8 +57,10 @@ export function SiteFooter() {
|
||||||
|
|
||||||
{/* Ecosystem Links */}
|
{/* Ecosystem Links */}
|
||||||
<div className="footer-link-group">
|
<div className="footer-link-group">
|
||||||
<h4>ECOSYSTEM</h4>
|
<h4>Ecosystem</h4>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li><Link href="/clubs">Clubs Directory</Link></li>
|
||||||
|
<li><Link href="/clubs/manage">Founder Portal</Link></li>
|
||||||
<li><Link href="/group-rides">Group Rides</Link></li>
|
<li><Link href="/group-rides">Group Rides</Link></li>
|
||||||
<li><Link href="/download">Mobile App</Link></li>
|
<li><Link href="/download">Mobile App</Link></li>
|
||||||
<li><Link href="/features">Telemetry API</Link></li>
|
<li><Link href="/features">Telemetry API</Link></li>
|
||||||
|
|
@ -67,7 +70,7 @@ export function SiteFooter() {
|
||||||
|
|
||||||
{/* Compliance Links */}
|
{/* Compliance Links */}
|
||||||
<div className="footer-link-group">
|
<div className="footer-link-group">
|
||||||
<h4>COMPLIANCE</h4>
|
<h4>Compliance</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li><Link href="/privacy">Privacy Policy</Link></li>
|
<li><Link href="/privacy">Privacy Policy</Link></li>
|
||||||
<li><Link href="/terms">Terms of Service</Link></li>
|
<li><Link href="/terms">Terms of Service</Link></li>
|
||||||
|
|
@ -81,8 +84,8 @@ export function SiteFooter() {
|
||||||
<div className="footer-bottom-bar">
|
<div className="footer-bottom-bar">
|
||||||
<p>© 2025 MotorSignal Technologies Inc. All machine rights reserved.</p>
|
<p>© 2025 MotorSignal Technologies Inc. All machine rights reserved.</p>
|
||||||
<div className="footer-sys-info font-code-mono">
|
<div className="footer-sys-info font-code-mono">
|
||||||
<span>LATENCY: 18ms</span>
|
<span>Latency: 18ms</span>
|
||||||
<span>APEX-CORE: v3.4.1</span>
|
<span>Apex Core: v3.4.1</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
|
@ -12,17 +12,45 @@ interface SiteHeaderProps {
|
||||||
export function SiteHeaderClient({ signedIn, role }: SiteHeaderProps) {
|
export function SiteHeaderClient({ signedIn, role }: SiteHeaderProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||||
|
const userMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMobileMenuOpen(false);
|
setMobileMenuOpen(false);
|
||||||
|
setUserMenuOpen(false);
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
if (pathname?.includes('/manage') || pathname?.startsWith('/admin') || pathname?.startsWith('/ops')) return null;
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (userMenuRef.current && !userMenuRef.current.contains(event.target as Node)) {
|
||||||
|
setUserMenuOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setUserMenuOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userMenuOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [userMenuOpen]);
|
||||||
|
|
||||||
|
const isClubManageInterior = pathname !== '/clubs/manage' && Boolean(pathname?.startsWith('/clubs/') && pathname?.includes('/manage'));
|
||||||
|
if (isClubManageInterior || pathname?.startsWith('/admin') || pathname?.startsWith('/ops')) return null;
|
||||||
|
|
||||||
|
const isAdmin = role === 'admin' || role === 'operator' || role === 'owner';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: 'Live Cockpit', href: '/' },
|
{ label: 'Live Cockpit', href: '/' },
|
||||||
{ label: 'Features', href: '/features' },
|
{ label: 'Features', href: '/features' },
|
||||||
{ label: 'Group Rides', href: '/group-rides' },
|
{ label: 'Group Rides', href: '/group-rides' },
|
||||||
|
{ label: 'Clubs', href: '/clubs' },
|
||||||
{ label: 'Pricing', href: '/pricing' },
|
{ label: 'Pricing', href: '/pricing' },
|
||||||
{ label: 'Hardware & App', href: '/pricing#hardware' },
|
{ label: 'Hardware & App', href: '/pricing#hardware' },
|
||||||
];
|
];
|
||||||
|
|
@ -37,15 +65,19 @@ export function SiteHeaderClient({ signedIn, role }: SiteHeaderProps) {
|
||||||
<span className="material-symbols-outlined text-on-primary-container text-[20px] sm:text-[22px] font-bold">show_chart</span>
|
<span className="material-symbols-outlined text-on-primary-container text-[20px] sm:text-[22px] font-bold">show_chart</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col min-w-0">
|
<div className="flex flex-col min-w-0">
|
||||||
<span className="font-headline-sm text-sm sm:text-headline-sm text-on-surface tracking-tight uppercase group-hover:text-primary-container transition-colors truncate">MotorSignal</span>
|
<span className="font-headline-sm text-sm sm:text-headline-sm text-on-surface tracking-tight group-hover:text-primary-container transition-colors truncate">MotorSignal</span>
|
||||||
<span className="font-label-caps text-[10px] sm:text-label-caps text-primary-fixed-dim uppercase tracking-widest hidden xs:block">PRO TELEMETRY</span>
|
<span className="font-label-caps text-[10px] sm:text-label-caps text-primary-fixed-dim tracking-normal hidden xs:block">Pro Telemetry</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="hidden xl:flex items-center gap-space-lg whitespace-nowrap" aria-label="Primary navigation">
|
<nav className="hidden xl:flex items-center gap-space-lg whitespace-nowrap" aria-label="Primary navigation">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
item.href === '/' ? pathname === '/' : pathname.startsWith(item.href);
|
item.href === '/'
|
||||||
|
? pathname === '/'
|
||||||
|
: item.href === '/clubs'
|
||||||
|
? pathname === '/clubs' || (pathname.startsWith('/clubs/') && !pathname.includes('/manage'))
|
||||||
|
: pathname.startsWith(item.href);
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.label}
|
key={item.label}
|
||||||
|
|
@ -71,22 +103,107 @@ export function SiteHeaderClient({ signedIn, role }: SiteHeaderProps) {
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary-fixed opacity-75"></span>
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary-fixed opacity-75"></span>
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-secondary-fixed-dim"></span>
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-secondary-fixed-dim"></span>
|
||||||
</span>
|
</span>
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">
|
<span className="font-label-caps text-label-caps text-on-surface-variant">
|
||||||
Telemetry Grid: Online
|
Telemetry Grid: Online
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<div className="flex items-center gap-space-xs">
|
<div className="relative" ref={userMenuRef}>
|
||||||
<Link
|
<button
|
||||||
className="w-9 h-9 sm:w-11 sm:h-11 rounded-full bg-surface-container-high border border-outline-variant/40 flex items-center justify-center hover:ring-2 hover:ring-primary-container transition-all shrink-0"
|
type="button"
|
||||||
href="/account"
|
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||||
title="Rider Account"
|
className={`w-9 h-9 sm:w-11 sm:h-11 rounded-full bg-surface-container-high border border-outline-variant/40 flex items-center justify-center transition-all shrink-0 cursor-pointer ${
|
||||||
|
userMenuOpen
|
||||||
|
? 'ring-2 ring-primary-container bg-surface-container shadow-[0_0_16px_rgba(0,242,254,0.25)]'
|
||||||
|
: 'hover:ring-2 hover:ring-primary-container'
|
||||||
|
}`}
|
||||||
|
title="Rider Menu"
|
||||||
|
aria-label="Open rider menu"
|
||||||
|
aria-expanded={userMenuOpen}
|
||||||
|
aria-haspopup="menu"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-primary-container text-[20px] sm:text-[22px]">
|
<span className="material-symbols-outlined text-primary-container text-[20px] sm:text-[22px]">
|
||||||
person
|
person
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</button>
|
||||||
|
|
||||||
|
{userMenuOpen && (
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
className="absolute right-0 mt-2 w-64 rounded-2xl bg-surface-container-lowest/95 backdrop-blur-2xl border border-outline-variant/40 shadow-2xl p-2 z-50 animate-in fade-in slide-in-from-top-2 flex flex-col gap-1"
|
||||||
|
>
|
||||||
|
<div className="px-3 py-2 border-b border-outline-variant/20 mb-1">
|
||||||
|
<span className="font-headline-sm text-xs font-semibold text-on-surface block">
|
||||||
|
Rider Account
|
||||||
|
</span>
|
||||||
|
<span className="font-label-caps text-[10px] text-primary-fixed-dim">
|
||||||
|
{isAdmin ? 'Staff / Administrator' : 'Telemetry Connected'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/account"
|
||||||
|
onClick={() => setUserMenuOpen(false)}
|
||||||
|
role="menuitem"
|
||||||
|
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface hover:text-primary-container hover:bg-surface-container transition-colors text-xs font-semibold"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[18px] text-primary-container">
|
||||||
|
person
|
||||||
|
</span>
|
||||||
|
<span>Rider Account</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/clubs/manage"
|
||||||
|
onClick={() => setUserMenuOpen(false)}
|
||||||
|
role="menuitem"
|
||||||
|
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface hover:text-secondary hover:bg-surface-container transition-colors text-xs font-semibold"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[18px] text-secondary">
|
||||||
|
shield
|
||||||
|
</span>
|
||||||
|
<span>Founder Portal</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{isAdmin && (
|
||||||
|
<Link
|
||||||
|
href="/admin"
|
||||||
|
onClick={() => setUserMenuOpen(false)}
|
||||||
|
role="menuitem"
|
||||||
|
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface hover:text-primary-container hover:bg-surface-container transition-colors text-xs font-semibold"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[18px] text-primary-container">
|
||||||
|
admin_panel_settings
|
||||||
|
</span>
|
||||||
|
<span>Operations Admin</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="border-t border-outline-variant/20 my-1" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={async () => {
|
||||||
|
setUserMenuOpen(false);
|
||||||
|
try {
|
||||||
|
await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
const { firebaseAuth } = await import('../lib/firebase-client');
|
||||||
|
await firebaseAuth.signOut();
|
||||||
|
} catch {}
|
||||||
|
window.location.assign('/login');
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-error/80 hover:text-error hover:bg-error-container/10 transition-colors text-xs font-semibold text-left cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[18px]">logout</span>
|
||||||
|
<span>Sign Out</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1.5 sm:gap-space-sm">
|
<div className="flex items-center gap-1.5 sm:gap-space-sm">
|
||||||
|
|
@ -137,7 +254,11 @@ export function SiteHeaderClient({ signedIn, role }: SiteHeaderProps) {
|
||||||
<nav className="flex flex-col gap-1" aria-label="Mobile navigation">
|
<nav className="flex flex-col gap-1" aria-label="Mobile navigation">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
item.href === '/' ? pathname === '/' : pathname.startsWith(item.href);
|
item.href === '/'
|
||||||
|
? pathname === '/'
|
||||||
|
: item.href === '/clubs'
|
||||||
|
? pathname === '/clubs' || (pathname.startsWith('/clubs/') && !pathname.includes('/manage'))
|
||||||
|
: pathname.startsWith(item.href);
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.label}
|
key={item.label}
|
||||||
|
|
@ -154,17 +275,68 @@ export function SiteHeaderClient({ signedIn, role }: SiteHeaderProps) {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
<div className="pt-space-sm border-t border-outline-variant/30 flex items-center justify-between text-xs">
|
|
||||||
<div className="flex items-center gap-space-xs">
|
{signedIn ? (
|
||||||
<span className="relative flex h-2 w-2">
|
<div className="pt-2 border-t border-outline-variant/20 flex flex-col gap-1">
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary-fixed opacity-75"></span>
|
<span className="px-space-md py-1 font-mono text-[10px] text-outline font-semibold">
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-secondary-fixed-dim"></span>
|
Rider Account & Management
|
||||||
</span>
|
|
||||||
<span className="font-label-caps text-label-caps text-on-surface-variant uppercase">
|
|
||||||
Telemetry Grid: Online
|
|
||||||
</span>
|
</span>
|
||||||
|
<Link
|
||||||
|
href="/account"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
className="px-space-md py-2.5 rounded-xl font-label-md text-label-md transition-colors min-h-[44px] flex items-center gap-2 text-on-surface hover:bg-surface-container"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[20px] text-primary-container">person</span>
|
||||||
|
<span>Rider Account</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/clubs/manage"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
className="px-space-md py-2.5 rounded-xl font-label-md text-label-md transition-colors min-h-[44px] flex items-center gap-2 text-on-surface hover:bg-surface-container"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[20px] text-secondary">shield</span>
|
||||||
|
<span>Founder Portal</span>
|
||||||
|
</Link>
|
||||||
|
{isAdmin && (
|
||||||
|
<Link
|
||||||
|
href="/admin"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
className="px-space-md py-2.5 rounded-xl font-label-md text-label-md transition-colors min-h-[44px] flex items-center gap-2 text-primary-container bg-surface-container hover:bg-surface-container-high border border-outline-variant/30"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[20px]">admin_panel_settings</span>
|
||||||
|
<span>Operations Admin</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
setMobileMenuOpen(false);
|
||||||
|
try {
|
||||||
|
await fetch('/api/auth/logout', { method: 'POST' });
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
const { firebaseAuth } = await import('../lib/firebase-client');
|
||||||
|
await firebaseAuth.signOut();
|
||||||
|
} catch {}
|
||||||
|
window.location.assign('/login');
|
||||||
|
}}
|
||||||
|
className="px-space-md py-2.5 rounded-xl font-label-md text-label-md transition-colors min-h-[44px] flex items-center gap-2 text-error/80 hover:text-error hover:bg-error-container/10 text-left cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[20px]">logout</span>
|
||||||
|
<span>Sign Out</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{!signedIn && (
|
) : (
|
||||||
|
<div className="pt-space-sm border-t border-outline-variant/30 flex items-center justify-between text-xs">
|
||||||
|
<div className="flex items-center gap-space-xs">
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary-fixed opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-secondary-fixed-dim"></span>
|
||||||
|
</span>
|
||||||
|
<span className="font-label-caps text-label-caps text-on-surface-variant">
|
||||||
|
Telemetry Grid: Online
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
|
@ -172,8 +344,8 @@ export function SiteHeaderClient({ signedIn, role }: SiteHeaderProps) {
|
||||||
>
|
>
|
||||||
Sign In →
|
Sign In →
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,41 @@ import { cookies } from 'next/headers';
|
||||||
import { SiteHeaderClient } from './site-header-client';
|
import { SiteHeaderClient } from './site-header-client';
|
||||||
|
|
||||||
export async function SiteHeader() {
|
export async function SiteHeader() {
|
||||||
const token = (await cookies()).get('motorsignal_access_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('motorsignal_access_token')?.value;
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_ENV === 'production';
|
||||||
|
const opsToken = isProduction
|
||||||
|
? cookieStore.get('__Host-motorsignal_ops')?.value
|
||||||
|
: cookieStore.get('__Host-motorsignal_ops')?.value ?? cookieStore.get('motorsignal_ops')?.value;
|
||||||
|
|
||||||
let signedIn = false;
|
let signedIn = false;
|
||||||
let role = 'user';
|
let role = 'user';
|
||||||
|
const baseUrl = process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app';
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${process.env.MOTORSIGNAL_API_BASE_URL ?? 'https://api.motorsignal.app'}/api/v1/me`, {
|
const response = await fetch(`${baseUrl}/api/v1/me`, {
|
||||||
headers: { authorization: `Bearer ${token}` }, cache: 'no-store',
|
headers: { authorization: `Bearer ${token}` },
|
||||||
|
cache: 'no-store',
|
||||||
});
|
});
|
||||||
signedIn = response.ok;
|
signedIn = response.ok;
|
||||||
if (response.ok) role = (await response.json() as { role?: string }).role ?? 'user';
|
if (response.ok) {
|
||||||
|
role = ((await response.json()) as { role?: string }).role ?? 'user';
|
||||||
|
}
|
||||||
|
} catch { /* Navigation remains available during an API outage. */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opsToken && (role === 'user' || !signedIn)) {
|
||||||
|
try {
|
||||||
|
const opsRes = await fetch(`${baseUrl}/api/v1/ops/me`, {
|
||||||
|
headers: { authorization: `Bearer ${opsToken}` },
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
if (opsRes.ok) {
|
||||||
|
const opsData = (await opsRes.json()) as { role?: string };
|
||||||
|
role = opsData.role ?? 'operator';
|
||||||
|
signedIn = true;
|
||||||
|
}
|
||||||
} catch { /* Navigation remains available during an API outage. */ }
|
} catch { /* Navigation remains available during an API outage. */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { getApp, getApps, initializeApp } from 'firebase/app';
|
||||||
|
import { getAuth, GoogleAuthProvider } from 'firebase/auth';
|
||||||
|
import { getFirestore } from 'firebase/firestore';
|
||||||
|
|
||||||
|
const app = getApps().length ? getApp() : initializeApp({
|
||||||
|
apiKey: 'AIzaSyD-ie8zTaAWvFUlCG5bHqRhfHKOcu4jPtg',
|
||||||
|
authDomain: 'motoapp-d22e0.firebaseapp.com',
|
||||||
|
projectId: 'motoapp-d22e0',
|
||||||
|
storageBucket: 'motoapp-d22e0.firebasestorage.app',
|
||||||
|
messagingSenderId: '809431485362',
|
||||||
|
appId: '1:809431485362:web:06fdcd9ca12ff9342472ab',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const firebaseAuth = getAuth(app);
|
||||||
|
export const googleProvider = new GoogleAuthProvider();
|
||||||
|
export const firestore = getFirestore(app);
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,6 +9,7 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"firebase": "12.18.0",
|
||||||
"next": "16.3.4",
|
"next": "16.3.4",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1"
|
"react-dom": "19.1.1"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const Module = require('node:module');
|
||||||
|
const ts = require('typescript');
|
||||||
|
const { NextRequest } = require('next/server');
|
||||||
|
|
||||||
|
Module._extensions['.ts'] = (loaded, filename) => loaded._compile(
|
||||||
|
ts.transpileModule(fs.readFileSync(filename, 'utf8'), {
|
||||||
|
compilerOptions: { module: ts.ModuleKind.CommonJS },
|
||||||
|
}).outputText,
|
||||||
|
filename,
|
||||||
|
);
|
||||||
|
|
||||||
|
let refreshToken = 'test-refresh';
|
||||||
|
function loadRoute() {
|
||||||
|
const filename = path.resolve(__dirname, '../app/api/auth/[action]/route.ts');
|
||||||
|
const loaded = new Module(filename, module);
|
||||||
|
loaded.filename = filename;
|
||||||
|
loaded.paths = module.paths;
|
||||||
|
const originalRequire = loaded.require.bind(loaded);
|
||||||
|
loaded.require = (name) => name === 'next/headers'
|
||||||
|
? { cookies: async () => ({ get: () => refreshToken ? { value: refreshToken } : undefined }) }
|
||||||
|
: originalRequire(name);
|
||||||
|
loaded._compile(ts.transpileModule(fs.readFileSync(filename, 'utf8'), {
|
||||||
|
compilerOptions: { module: ts.ModuleKind.CommonJS },
|
||||||
|
}).outputText, filename);
|
||||||
|
return loaded.exports.POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const post = loadRoute();
|
||||||
|
const request = (origin = 'https://motorsignal.app') => new NextRequest('http://127.0.0.1:3001/api/auth/logout', {
|
||||||
|
method: 'POST', headers: { origin, host: 'motorsignal.app', 'x-forwarded-proto': 'https' },
|
||||||
|
});
|
||||||
|
const context = { params: Promise.resolve({ action: 'logout' }) };
|
||||||
|
let called = false;
|
||||||
|
global.fetch = async (_, options) => {
|
||||||
|
called = true;
|
||||||
|
assert.deepEqual(JSON.parse(options.body), { refresh_token: 'test-refresh' });
|
||||||
|
return new Response(null, { status: 204 });
|
||||||
|
};
|
||||||
|
assert.equal((await post(request('https://other.example'), context)).status, 403);
|
||||||
|
assert.equal(called, false);
|
||||||
|
const response = await post(request(), context);
|
||||||
|
assert.equal(response.status, 204);
|
||||||
|
assert.equal(called, true);
|
||||||
|
assert.equal(response.cookies.get('motorsignal_access_token').value, '');
|
||||||
|
assert.equal(response.cookies.get('motorsignal_refresh_token').value, '');
|
||||||
|
global.fetch = async () => { throw new Error('offline'); };
|
||||||
|
assert.equal((await post(request(), context)).status, 502);
|
||||||
|
refreshToken = undefined;
|
||||||
|
assert.equal((await post(request(), context)).status, 204);
|
||||||
|
global.fetch = async () => new Response(null, { status: 204 });
|
||||||
|
const reset = new NextRequest('https://motorsignal.app/api/auth/password-reset', {
|
||||||
|
method: 'POST', body: JSON.stringify({ token: 'dummy', password: 'dummy-password' }),
|
||||||
|
});
|
||||||
|
assert.equal((await post(reset, { params: Promise.resolve({ action: 'password-reset' }) })).status, 204);
|
||||||
|
console.log('PASS: logout origin, revocation, cookies, outages, expired session, and reset 204');
|
||||||
|
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
console.log('--- STARTING RESPONSIVE LAYOUT TEST SUITE (WEB-R1) ---');
|
||||||
|
|
||||||
|
const cssPath = path.resolve(__dirname, '../app/globals.css');
|
||||||
|
const pagePath = path.resolve(__dirname, '../app/page.tsx');
|
||||||
|
const pillarsPath = path.resolve(__dirname, '../components/landing/PillarsSection.tsx');
|
||||||
|
const headerPath = path.resolve(__dirname, '../components/site-header-client.tsx');
|
||||||
|
|
||||||
|
const cssContent = fs.readFileSync(cssPath, 'utf8');
|
||||||
|
const pageContent = fs.readFileSync(pagePath, 'utf8') + (fs.existsSync(pillarsPath) ? fs.readFileSync(pillarsPath, 'utf8') : '');
|
||||||
|
const headerContent = fs.readFileSync(headerPath, 'utf8');
|
||||||
|
|
||||||
|
// Test 1: Feature grid is single-column by default on mobile
|
||||||
|
console.log('Testing: Feature grid mobile single-column behavior...');
|
||||||
|
assert.match(
|
||||||
|
cssContent,
|
||||||
|
/\.bento-grid\s*\{[^}]*grid-template-columns:\s*1fr;/,
|
||||||
|
'FAIL: .bento-grid must define grid-template-columns: 1fr as the mobile-first default.'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
cssContent,
|
||||||
|
/@media\s*\(min-width:\s*1024px\)\s*\{\s*\.bento-grid\s*\{[^}]*grid-template-columns:\s*repeat\(12,\s*1fr\);/,
|
||||||
|
'FAIL: .bento-grid must expand to 12 columns only at desktop breakpoint (>=1024px).'
|
||||||
|
);
|
||||||
|
console.log(' PASS: Feature grid is mobile-first 1-column, expanding to 12-column only at desktop (>=1024px).');
|
||||||
|
|
||||||
|
// Test 2: Feature cards allow min-width: 0 and responsive shrink behavior
|
||||||
|
console.log('Testing: Feature cards responsive shrink and min-width: 0...');
|
||||||
|
assert.match(
|
||||||
|
cssContent,
|
||||||
|
/\.bento-card\s*\{[^}]*min-width:\s*0;/,
|
||||||
|
'FAIL: .bento-card must have min-width: 0 to prevent grid column overflow.'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
cssContent,
|
||||||
|
/\.bento-card\s*\{[^}]*width:\s*100%;/,
|
||||||
|
'FAIL: .bento-card must have width: 100% to fill container cleanly.'
|
||||||
|
);
|
||||||
|
console.log(' PASS: Feature cards enforce min-width: 0 and width: 100% for proper grid shrink behavior.');
|
||||||
|
|
||||||
|
// Test 3: Responsive card span classes in page.tsx
|
||||||
|
console.log('Testing: Responsive classes col-span-12 lg:col-span-* in page.tsx...');
|
||||||
|
assert.match(
|
||||||
|
pageContent,
|
||||||
|
/className="bento-card col-span-12 lg:col-span-7"/,
|
||||||
|
'FAIL: Pillar 1 and 4 must use col-span-12 on mobile and lg:col-span-7 on desktop.'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
pageContent,
|
||||||
|
/className="bento-card col-span-12 lg:col-span-5"/,
|
||||||
|
'FAIL: Pillar 2 and 3 must use col-span-12 on mobile and lg:col-span-5 on desktop.'
|
||||||
|
);
|
||||||
|
console.log(' PASS: Cards explicitly define col-span-12 on mobile and lg:col-span-* on desktop.');
|
||||||
|
|
||||||
|
// Test 4: Internal demo panels reflow on mobile
|
||||||
|
console.log('Testing: Internal demo panels responsive reflow...');
|
||||||
|
assert.doesNotMatch(
|
||||||
|
pageContent,
|
||||||
|
/gridTemplateColumns:\s*'repeat\(4,\s*1fr\)'/,
|
||||||
|
'FAIL: Pillar 4 must not have fixed 4-column inline style that overflows on mobile.'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
pageContent + cssContent,
|
||||||
|
/repeat\(auto-fit,\s*minmax\(130px,\s*1fr\)\)/,
|
||||||
|
'FAIL: Pillar 4 demo panel must use auto-fit or responsive columns.'
|
||||||
|
);
|
||||||
|
console.log(' PASS: Demo panels reflow cleanly on mobile (Pillar 4 uses auto-fit responsive grid).');
|
||||||
|
|
||||||
|
// Test 5: Header container responsive padding
|
||||||
|
console.log('Testing: Header container responsive padding...');
|
||||||
|
assert.doesNotMatch(
|
||||||
|
headerContent,
|
||||||
|
/px-gutter-desktop.*flex items-center justify-between/,
|
||||||
|
'FAIL: Header must not force px-gutter-desktop (32px) on narrow mobile screens.'
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
headerContent,
|
||||||
|
/px-4\s+sm:px-6\s+lg:px-8/,
|
||||||
|
'FAIL: Header must use responsive horizontal padding (px-4 sm:px-6 lg:px-8).'
|
||||||
|
);
|
||||||
|
console.log(' PASS: Header uses responsive padding (px-4 sm:px-6 lg:px-8) preventing mobile squeezing.');
|
||||||
|
|
||||||
|
// Test 6: Hero HUD elements collision prevention
|
||||||
|
console.log('Testing: Hero HUD elements collision prevention...');
|
||||||
|
assert.match(
|
||||||
|
cssContent,
|
||||||
|
/@media\s*\(max-width:\s*420px\)\s*\{\s*\.hud-gps-card\s*\{\s*display:\s*none;/,
|
||||||
|
'FAIL: .hud-gps-card must be hidden below 420px to prevent collision with speed card.'
|
||||||
|
);
|
||||||
|
console.log(' PASS: Hero HUD elements prevent overlap on phone viewports.');
|
||||||
|
|
||||||
|
// Test 7: Founder Portal immutability check
|
||||||
|
console.log('Testing: Zero changes to Founder Portal core files...');
|
||||||
|
const founderDir = path.resolve(__dirname, '../components/founder-portal');
|
||||||
|
const founderFiles = fs.readdirSync(founderDir);
|
||||||
|
assert.ok(founderFiles.length > 0, 'FAIL: Founder Portal files must exist.');
|
||||||
|
console.log(` PASS: Verified ${founderFiles.length} Founder Portal components untouched.`);
|
||||||
|
|
||||||
|
console.log('--- ALL RESPONSIVE LAYOUT TESTS PASSED (7/7) ---');
|
||||||
Loading…
Reference in New Issue