<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Montserrat:wght@700;800&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<title>CARla Desk — Dealer Intelligence</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body,#root{height:100%;min-height:600px}
body{font-family:'DM Sans',sans-serif;background:#f0f4f4;color:#0f2e2e;-webkit-font-smoothing:antialiased}
:root{
--teal:#1a7070;--teal-dark:#0f4a4a;--teal-darker:#0a3232;
--teal-light:#2a9090;--teal-pale:#e8f4f4;--teal-mid:#d0e8e8;
--teal-dim:rgba(26,112,112,0.12);--teal-border:rgba(26,112,112,0.25);
--surface:#ffffff;--surface-2:#f5fafa;--surface-3:#eaf3f3;
--bg:#f0f4f4;--bg-dark:#e4eeee;
--border:rgba(26,112,112,0.1);--border-2:rgba(26,112,112,0.2);
--text:#0f2e2e;--muted:#4a7070;--muted-2:#8aacac;
--green:#16a34a;--green-dim:rgba(22,163,74,0.1);
--red:#dc2626;--red-dim:rgba(220,38,38,0.08);
--amber:#d97706;--amber-dim:rgba(217,119,6,0.1);
--radius:10px;--radius-sm:6px;--radius-lg:14px;
}
input,select,textarea{background:var(--surface-3);border:1.5px solid var(--border-2);border-radius:var(--radius-sm);color:var(--text);font-family:'DM Sans',sans-serif;font-size:0.9rem;padding:10px 14px;outline:none;width:100%;transition:border-color 0.2s,box-shadow 0.2s}
input:focus,select:focus{border-color:var(--teal);box-shadow:0 0 0 3px rgba(26,112,112,0.12);background:#fff}
input::placeholder{color:var(--muted-2)}
select option{background:#fff;color:var(--text)}
button{cursor:pointer;font-family:'DM Sans',sans-serif}
::-webkit-scrollbar{width:4px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--teal-border);border-radius:2px}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const {useState,useRef,useCallback}=React;
/* ============================================================
CONFIGURATION
Replace these with your actual API keys before deploying.
NEVER share these keys publicly or commit them to GitHub.
============================================================ */
const MARKETCHECK_KEY = 'mc_live_YQkXyiYQSHXE3SGxID7JRlFZdVdQGiJh';
const ANTHROPIC_KEY = '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';
/* ============================================================
DEALER ACCOUNTS
Add your dealer accounts here. Each dealer only sees
their own session data. For production, replace this
with a proper database-backed auth system.
============================================================ */
const USERS = [
{ email: 'dealer@thedealership.co.uk', password: 'demo2024', name: 'The Dealership', postcode: 'SS72PD' },
{ email: 'demo@carla.ai', password: 'carla2024', name: 'Demo Dealer', postcode: 'SW1A1AA' },
];
/* ============================================================
HELPERS
============================================================ */
const fmt = n => (!n || isNaN(n)) ? '—' : '£' + Math.round(n).toLocaleString('en-GB');
const fmtNum = n => (!n || isNaN(n)) ? '—' : Math.round(n).toLocaleString('en-GB');
/* ============================================================
BADGE COMPONENT
============================================================ */
function Badge({ type, children }) {
const styles = {
high: { bg: 'var(--green-dim)', color: 'var(--green)' },
medium: { bg: 'var(--amber-dim)', color: 'var(--amber)' },
low: { bg: 'var(--red-dim)', color: 'var(--red)' },
teal: { bg: 'var(--teal-dim)', color: 'var(--teal)' },
};
const s = styles[type] || styles.teal;
return (
<span style={{ background: s.bg, color: s.color, fontSize: '0.68rem', fontWeight: 700,
letterSpacing: '0.07em', textTransform: 'uppercase', padding: '3px 9px',
borderRadius: 99, whiteSpace: 'nowrap' }}>
{children}
</span>
);
}
/* ============================================================
LOGIN SCREEN
============================================================ */
function LoginScreen({ onLogin }) {
const [email, setEmail] = useState('');
const [pw, setPw] = useState('');
const [err, setErr] = useState('');
const [loading, setLoading] = useState(false);
const submit = e => {
e.preventDefault(); setLoading(true); setErr('');
setTimeout(() => {
const u = USERS.find(u => u.email === email.toLowerCase().trim() && u.password === pw);
if (u) onLogin(u);
else { setErr('Incorrect email or password.'); setLoading(false); }
}, 600);
};
return (
<div style={{ minHeight: '100vh', display: 'flex', background: 'var(--bg)' }}>
{/* Left panel */}
<div style={{ width: 420, background: 'var(--teal-dark)', display: 'flex',
flexDirection: 'column', padding: '48px 40px', flexShrink: 0 }}>
<div style={{ marginBottom: 'auto' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 48 }}>
<div style={{ width: 38, height: 38, background: 'rgba(255,255,255,0.15)',
borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center',
border: '1.5px solid rgba(255,255,255,0.2)' }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#fff">
<path d="M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99z"/>
</svg>
</div>
<div>
<p style={{ fontFamily: 'Montserrat', fontWeight: 800, fontSize: '1.1rem',
color: '#fff', letterSpacing: '-0.01em' }}>CARla.ai</p>
<p style={{ fontSize: '0.68rem', color: 'rgba(255,255,255,0.5)',
letterSpacing: '0.08em', textTransform: 'uppercase' }}>Desk</p>
</div>
</div>
<h1 style={{ fontFamily: 'Montserrat', fontWeight: 800, fontSize: '1.6rem',
color: '#fff', lineHeight: 1.2, marginBottom: 12 }}>
Your dealer intelligence platform
</h1>
<p style={{ color: 'rgba(255,255,255,0.6)', fontSize: '0.88rem', lineHeight: 1.65 }}>
Live market data, AI-powered buy recommendations, and full stock analysis in seconds.
</p>
</div>
<div style={{ marginTop: 48 }}>
{[
['Live UK market pricing', 'Powered by MarketCheck ML'],
['AI buy recommendations', 'Claude-powered analysis'],
['Full stock dashboard', 'Track every vehicle'],
].map(([t, s]) => (
<div key={t} style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 16 }}>
<div style={{ width: 20, height: 20, borderRadius: '50%',
background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0, marginTop: 1 }}>
<svg width="10" height="10" fill="none" viewBox="0 0 24 24"
stroke="#fff" strokeWidth="3">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7"/>
</svg>
</div>
<div>
<p style={{ fontSize: '0.82rem', color: '#fff', fontWeight: 500, marginBottom: 1 }}>{t}</p>
<p style={{ fontSize: '0.75rem', color: 'rgba(255,255,255,0.45)' }}>{s}</p>
</div>
</div>
))}
</div>
</div>
{/* Right panel */}
<div style={{ flex: 1, display: 'flex', alignItems: 'center',
justifyContent: 'center', padding: 40 }}>
<div style={{ width: '100%', maxWidth: 380 }}>
<h2 style={{ fontFamily: 'Montserrat', fontWeight: 800, fontSize: '1.4rem',
color: 'var(--teal-dark)', marginBottom: 6 }}>Sign in</h2>
<p style={{ color: 'var(--muted)', fontSize: '0.84rem', marginBottom: 28 }}>
Enter your dealership credentials to continue
</p>
<form onSubmit={submit}>
<div style={{ marginBottom: 14 }}>
<label style={{ fontSize: '0.7rem', fontWeight: 700, letterSpacing: '0.07em',
textTransform: 'uppercase', color: 'var(--muted)', display: 'block', marginBottom: 6 }}>
Email address
</label>
<input type="email" value={email} onChange={e => setEmail(e.target.value)}
placeholder="dealer@yourgarage.co.uk" required/>
</div>
<div style={{ marginBottom: 20 }}>
<label style={{ fontSize: '0.7rem', fontWeight: 700, letterSpacing: '0.07em',
textTransform: 'uppercase', color: 'var(--muted)', display: 'block', marginBottom: 6 }}>
Password
</label>
<input type="password" value={pw} onChange={e => setPw(e.target.value)}
placeholder="••••••••" required/>
</div>
{err && (
<p style={{ color: 'var(--red)', fontSize: '0.8rem', marginBottom: 14,
background: 'var(--red-dim)', padding: '8px 12px', borderRadius: 'var(--radius-sm)',
border: '1px solid rgba(220,38,38,0.15)' }}>{err}</p>
)}
<button type="submit" disabled={loading} style={{ width: '100%', padding: '13px',
background: 'var(--teal)', color: '#fff', border: 'none',
borderRadius: 'var(--radius-sm)', fontFamily: 'Montserrat', fontWeight: 800,
fontSize: '0.88rem', letterSpacing: '0.06em', textTransform: 'uppercase',
transition: 'background 0.2s', opacity: loading ? 0.75 : 1 }}>
{loading ? 'Signing in…' : 'Sign In →'}
</button>
</form>
<div style={{ marginTop: 24, padding: '14px 16px', background: 'var(--surface-3)',
borderRadius: 'var(--radius-sm)', borderLeft: '3px solid var(--teal)' }}>
<p style={{ fontSize: '0.72rem', color: 'var(--muted)', fontWeight: 700,
marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Demo credentials
</p>
<p style={{ fontSize: '0.78rem', color: 'var(--teal)', fontFamily: 'DM Mono' }}>
dealer@thedealership.co.uk
</p>
<p style={{ fontSize: '0.78rem', color: 'var(--muted)', fontFamily: 'DM Mono' }}>
demo2024
</p>
</div>
</div>
</div>
</div>
);
}
/* ============================================================
LOOKUP FORM
============================================================ */
function LookupForm({ onSubmit, loading }) {
const [reg, setReg] = useState('');
const [mileage, setMileage] = useState('');
const [condition, setCondition] = useState('');
const submit = e => {
e.preventDefault();
if (!reg || !mileage || !condition) return;
onSubmit({ reg: reg.trim().toUpperCase(), mileage: parseInt(mileage), condition });
setReg(''); setMileage(''); setCondition('');
};
return (
<form onSubmit={submit}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr auto',
gap: 10, alignItems: 'end' }}>
<div>
<label style={{ fontSize: '0.69rem', fontWeight: 700, letterSpacing: '0.07em',
textTransform: 'uppercase', color: 'var(--muted)', display: 'block', marginBottom: 6 }}>
Registration
</label>
<input value={reg} onChange={e => setReg(e.target.value.toUpperCase())}
placeholder="AB12 CDE" style={{ letterSpacing: '0.1em', fontWeight: 600,
fontFamily: 'DM Mono', fontSize: '0.95rem' }} required/>
</div>
<div>
<label style={{ fontSize: '0.69rem', fontWeight: 700, letterSpacing: '0.07em',
textTransform: 'uppercase', color: 'var(--muted)', display: 'block', marginBottom: 6 }}>
Mileage
</label>
<input type="number" value={mileage} onChange={e => setMileage(e.target.value)}
placeholder="e.g. 45000" min="0" required/>
</div>
<div>
<label style={{ fontSize: '0.69rem', fontWeight: 700, letterSpacing: '0.07em',
textTransform: 'uppercase', color: 'var(--muted)', display: 'block', marginBottom: 6 }}>
Condition
</label>
<select value={condition} onChange={e => setCondition(e.target.value)}
required style={{ appearance: 'none', WebkitAppearance: 'none' }}>
<option value="">Select…</option>
<option>Excellent</option>
<option>Good</option>
<option>Fair</option>
<option>Poor</option>
</select>
</div>
<button type="submit" disabled={loading} style={{ height: 42, padding: '0 20px',
background: 'var(--teal)', color: '#fff', border: 'none',
borderRadius: 'var(--radius-sm)', fontFamily: 'Montserrat', fontWeight: 800,
fontSize: '0.78rem', letterSpacing: '0.06em', textTransform: 'uppercase',
whiteSpace: 'nowrap', opacity: loading ? 0.7 : 1, transition: 'background 0.2s' }}>
{loading ? 'Checking…' : 'Run Lookup'}
</button>
</div>
</form>
);
}
/* ============================================================
STAT BOX
============================================================ */
function StatBox({ label, value, accent }) {
return (
<div style={{ background: 'var(--surface-2)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)', padding: '10px 12px',
borderLeft: accent ? '2px solid var(--teal)' : 'none' }}>
<p style={{ fontSize: '0.66rem', color: 'var(--muted)', fontWeight: 700,
letterSpacing: '0.05em', textTransform: 'uppercase', marginBottom: 4 }}>{label}</p>
<p style={{ fontSize: '1.05rem', fontWeight: 600,
color: accent ? 'var(--teal)' : 'var(--text)' }}>{value}</p>
</div>
);
}
/* ============================================================
CAR CARD
============================================================ */
function CarCard({ car, onRemove, onImageUpload }) {
const fileRef = useRef();
const [expanded, setExpanded] = useState(true);
const domColor = car.avgDom < 21 ? 'var(--green)' : car.avgDom < 45 ? 'var(--amber)' : 'var(--red)';
const domBg = car.avgDom < 21 ? 'var(--green-dim)' : car.avgDom < 45 ? 'var(--amber-dim)' : 'var(--red-dim)';
const domLabel = car.avgDom < 21 ? 'Fast mover' : car.avgDom < 45 ? 'Normal pace' : 'Slow mover';
return (
<div style={{ background: 'var(--surface)', border: '1px solid var(--border-2)',
borderRadius: 'var(--radius-lg)', overflow: 'hidden', marginBottom: 14,
animation: 'fadeIn 0.35s ease both', boxShadow: '0 2px 12px rgba(26,112,112,0.07)' }}>
{/* Card top row */}
<div style={{ display: 'flex', alignItems: 'stretch' }}>
{/* Photo upload area */}
<div onClick={() => fileRef.current.click()} style={{ width: 150, minHeight: 110,
background: car.image ? 'transparent' : 'var(--surface-3)', flexShrink: 0,
cursor: 'pointer', position: 'relative', display: 'flex', alignItems: 'center',
justifyContent: 'center', borderRight: '1px solid var(--border)' }}>
{car.image
? <img src={car.image} alt="" style={{ width: '100%', height: '100%',
objectFit: 'cover', position: 'absolute', inset: 0 }}/>
: <div style={{ textAlign: 'center', padding: 14 }}>
<svg width="24" height="24" fill="none" viewBox="0 0 24 24"
stroke="var(--muted-2)" strokeWidth="1.5"
style={{ display: 'block', margin: '0 auto 5px' }}>
<path strokeLinecap="round" strokeLinejoin="round"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 20M14 8h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<p style={{ fontSize: '0.65rem', color: 'var(--muted-2)', lineHeight: 1.3 }}>
Add photo
</p>
</div>
}
<input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }}
onChange={e => {
const f = e.target.files[0];
if (f) { const r = new FileReader(); r.onload = ev => onImageUpload(car.id, ev.target.result); r.readAsDataURL(f); }
}}/>
</div>
{/* Card main content */}
<div style={{ flex: 1, padding: '14px 18px' }}>
<div style={{ display: 'flex', alignItems: 'flex-start',
justifyContent: 'space-between', marginBottom: 10 }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}>
<span style={{ fontFamily: 'DM Mono', fontWeight: 500, fontSize: '1rem',
color: 'var(--teal-dark)', letterSpacing: '0.08em' }}>{car.reg}</span>
<Badge type={car.confidence}>{car.confidence} confidence</Badge>
<span style={{ fontSize: '0.7rem', color: 'var(--muted-2)' }}>
Added {car.addedAt}
</span>
</div>
<p style={{ color: 'var(--muted)', fontSize: '0.81rem' }}>
{[car.year, car.make, car.model, car.trim].filter(Boolean).join(' ')}
{' · '}{fmtNum(car.mileage)} miles · {car.condition}
</p>
</div>
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
<button onClick={() => setExpanded(v => !v)} style={{ background: 'var(--surface-3)',
border: '1px solid var(--border-2)', borderRadius: 'var(--radius-sm)',
padding: '5px 10px', color: 'var(--muted)', fontSize: '0.73rem', fontWeight: 600 }}>
{expanded ? 'Collapse' : 'Expand'}
</button>
<button onClick={() => onRemove(car.id)} style={{ background: 'var(--red-dim)',
border: '1px solid rgba(220,38,38,0.2)', borderRadius: 'var(--radius-sm)',
padding: '5px 10px', color: 'var(--red)', fontSize: '0.73rem', fontWeight: 600 }}>
Remove
</button>
</div>
</div>
{/* Key metrics */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 8 }}>
<StatBox label="Predicted retail" value={fmt(car.marketPrice)}/>
<StatBox label="Buy price" value={fmt(car.buyPrice)} accent/>
<StatBox label="Est. margin" value={fmt(car.marketPrice - car.buyPrice)}/>
<StatBox label="Comparables" value={car.numFound || '—'}/>
</div>
</div>
</div>
{/* Expanded detail section */}
{expanded && (
<div style={{ borderTop: '1px solid var(--border)', padding: '16px 18px',
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
{/* Market stats */}
<div>
<p style={{ fontSize: '0.69rem', fontWeight: 700, letterSpacing: '0.07em',
textTransform: 'uppercase', color: 'var(--muted)', marginBottom: 10 }}>
Market statistics
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, marginBottom: 8 }}>
{[
{ label: 'Mean price', value: fmt(car.meanPrice) },
{ label: 'Median price', value: fmt(car.medianPrice) },
{ label: 'Min price', value: fmt(car.minPrice) },
{ label: 'Max price', value: fmt(car.maxPrice) },
{ label: 'Avg mileage', value: `${fmtNum(car.meanMiles)} mi` },
].map(s => (
<div key={s.label} style={{ background: 'var(--surface-2)',
border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)',
padding: '8px 10px' }}>
<p style={{ fontSize: '0.64rem', color: 'var(--muted)', fontWeight: 700,
letterSpacing: '0.04em', textTransform: 'uppercase', marginBottom: 2 }}>
{s.label}
</p>
<p style={{ fontSize: '0.88rem', color: 'var(--text)', fontWeight: 500 }}>
{s.value}
</p>
</div>
))}
<div style={{ background: domBg, border: `1px solid ${domColor}22`,
borderRadius: 'var(--radius-sm)', padding: '8px 10px',
display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 7, height: 7, borderRadius: '50%',
background: domColor, flexShrink: 0 }}/>
<div>
<p style={{ fontSize: '0.64rem', color: domColor, fontWeight: 700,
letterSpacing: '0.04em', textTransform: 'uppercase', marginBottom: 1 }}>
Days on market
</p>
<p style={{ fontSize: '0.88rem', color: domColor, fontWeight: 600 }}>
{Math.round(car.avgDom)} days · {domLabel}
</p>
</div>
</div>
</div>
</div>
{/* AI recommendation */}
<div>
<p style={{ fontSize: '0.69rem', fontWeight: 700, letterSpacing: '0.07em',
textTransform: 'uppercase', color: 'var(--muted)', marginBottom: 10 }}>
AI buying recommendation
</p>
{car.aiLoading
? <div style={{ display: 'flex', alignItems: 'center', gap: 10,
color: 'var(--muted)', fontSize: '0.83rem', background: 'var(--surface-2)',
padding: '14px', borderRadius: 'var(--radius-sm)' }}>
<div style={{ width: 14, height: 14, border: '2px solid var(--teal)',
borderTopColor: 'transparent', borderRadius: '50%',
animation: 'spin 0.7s linear infinite', flexShrink: 0 }}/>
Generating AI analysis…
</div>
: <div style={{ background: 'var(--teal-pale)',
border: '1px solid var(--teal-mid)', borderLeft: '3px solid var(--teal)',
borderRadius: 'var(--radius-sm)', padding: '12px 14px' }}>
<p style={{ fontSize: '0.82rem', color: 'var(--teal-dark)', lineHeight: 1.7 }}>
{car.aiSummary || 'No AI summary available.'}
</p>
</div>
}
</div>
</div>
)}
</div>
);
}
/* ============================================================
MAIN DASHBOARD
============================================================ */
function Dashboard({ user, onLogout }) {
const [cars, setCars] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
/* Generate AI buying recommendation via Anthropic API */
const getAI = useCallback(async c => {
try {
const r = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': ANTHROPIC_KEY,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 280,
messages: [{
role: 'user',
content: `UK trade car buyer for independent dealership. Write 3-4 sentences: buy/negotiate/walk away recommendation for this vehicle. Be direct and practical. End with a clear verdict.
${c.year} ${c.make} ${c.model} ${c.trim || ''} | ${c.mileage} miles | ${c.condition}
Retail: £${c.marketPrice} | Buy offer: £${c.buyPrice} | Avg DOM: ${Math.round(c.avgDom)} days | Comparables: ${c.numFound}
Mean: £${Math.round(c.meanPrice)} | Range: £${Math.round(c.minPrice)}–£${Math.round(c.maxPrice)}`
}]
})
});
const d = await r.json();
return d.content?.[0]?.text || 'AI analysis unavailable.';
} catch {
return 'AI analysis unavailable — check your Anthropic API key.';
}
}, []);
/* Run MarketCheck lookup then generate AI summary */
const handleLookup = useCallback(async ({ reg, mileage, condition }) => {
setLoading(true); setError('');
try {
const p = new URLSearchParams({
api_key: MARKETCHECK_KEY, vrm: reg, miles: mileage,
dealer_type: 'independent', postal_code: user.postcode,
});
const r = await fetch(`https://api.marketcheck.com/v2/predict/car/uk/marketcheck_price/comparables?${p}`);
if (!r.ok) { const e = await r.json(); throw new Error(e.message || 'MarketCheck error'); }
const d = await r.json();
const mp = d.marketcheck_price || 0;
const stats = d.comparables?.stats || {};
const listings = d.comparables?.listings || [];
const numFound = d.comparables?.num_found || 0;
const fl = listings[0] || {};
const avgDom = stats.dos_active?.mean || 0;
/* Calculate trade discount */
let disc = 1500;
if (condition === 'Fair') disc += 800;
if (condition === 'Poor') disc += 1500;
if (avgDom > 45) disc += 500;
const car = {
id: Date.now(), reg, mileage, condition,
make: fl.make || 'Unknown', model: fl.model || 'Unknown',
trim: fl.trim || '', year: fl.year || '',
marketPrice: mp, buyPrice: Math.max(mp - disc, 0),
numFound,
meanPrice: stats.price?.mean || 0,
medianPrice: stats.price?.median || 0,
minPrice: stats.price?.min || 0,
maxPrice: stats.price?.max || 0,
meanMiles: stats.miles?.mean || 0,
avgDom,
confidence: numFound >= 5 ? 'high' : numFound >= 2 ? 'medium' : 'low',
aiLoading: true, aiSummary: '', image: null,
addedAt: new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }),
};
setCars(prev => [car, ...prev]);
setLoading(false);
/* Generate AI summary in background */
getAI(car).then(txt =>
setCars(prev => prev.map(c => c.id === car.id ? { ...c, aiLoading: false, aiSummary: txt } : c))
);
} catch (e) {
setError(e.message || 'Lookup failed. Check API key and try again.');
setLoading(false);
}
}, [user, getAI]);
const totalBuy = cars.reduce((a, c) => a + c.buyPrice, 0);
const totalMargin = cars.reduce((a, c) => a + (c.marketPrice - c.buyPrice), 0);
return (
<div style={{ minHeight: '100vh', background: 'var(--bg)', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<header style={{ background: 'var(--teal-dark)', padding: '0 24px', height: 54,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
flexShrink: 0, boxShadow: '0 1px 0 rgba(255,255,255,0.08)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 28, height: 28, background: 'rgba(255,255,255,0.15)',
borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center',
border: '1px solid rgba(255,255,255,0.2)' }}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="#fff">
<path d="M18.92 6.01C18.72 5.42 18.16 5 17.5 5h-11c-.66 0-1.21.42-1.42 1.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99z"/>
</svg>
</div>
<span style={{ fontFamily: 'Montserrat', fontWeight: 800, fontSize: '0.95rem',
color: '#fff', letterSpacing: '-0.01em' }}>CARla.ai</span>
<span style={{ fontSize: '0.68rem', color: 'rgba(255,255,255,0.45)',
borderLeft: '1px solid rgba(255,255,255,0.15)', paddingLeft: 10,
letterSpacing: '0.08em', textTransform: 'uppercase' }}>Desk</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<div style={{ textAlign: 'right' }}>
<p style={{ fontSize: '0.78rem', fontWeight: 600, color: '#fff' }}>{user.name}</p>
<p style={{ fontSize: '0.68rem', color: 'rgba(255,255,255,0.45)' }}>{user.email}</p>
</div>
<button onClick={onLogout} style={{ background: 'rgba(255,255,255,0.1)',
border: '1px solid rgba(255,255,255,0.15)', borderRadius: 'var(--radius-sm)',
padding: '6px 12px', color: 'rgba(255,255,255,0.7)',
fontSize: '0.75rem', fontWeight: 600 }}>
Sign out
</button>
</div>
</header>
{/* Session summary bar */}
{cars.length > 0 && (
<div style={{ background: 'var(--teal)', padding: '9px 24px',
display: 'flex', gap: 24, alignItems: 'center' }}>
<p style={{ fontSize: '0.75rem', color: 'rgba(255,255,255,0.7)', fontWeight: 500 }}>
{cars.length} vehicle{cars.length !== 1 ? 's' : ''} in session
</p>
<p style={{ fontSize: '0.75rem', color: 'rgba(255,255,255,0.7)' }}>
Total buy value: <strong style={{ color: '#fff' }}>{fmt(totalBuy)}</strong>
</p>
<p style={{ fontSize: '0.75rem', color: 'rgba(255,255,255,0.7)' }}>
Est. total margin: <strong style={{ color: '#fff' }}>{fmt(totalMargin)}</strong>
</p>
</div>
)}
{/* Main content */}
<div style={{ flex: 1, padding: '20px 24px', maxWidth: 1080, width: '100%', margin: '0 auto' }}>
{/* Lookup form card */}
<div style={{ background: 'var(--surface)', border: '1px solid var(--border-2)',
borderTop: '3px solid var(--teal)', borderRadius: 'var(--radius-lg)',
padding: '18px 20px', marginBottom: 18,
boxShadow: '0 2px 12px rgba(26,112,112,0.06)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>
<svg width="15" height="15" fill="none" viewBox="0 0 24 24"
stroke="var(--teal)" strokeWidth="2.5">
<circle cx="11" cy="11" r="8"/>
<path strokeLinecap="round" d="M21 21l-4.35-4.35"/>
</svg>
<p style={{ fontFamily: 'Montserrat', fontWeight: 800, fontSize: '0.82rem',
color: 'var(--teal-dark)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
Vehicle lookup
</p>
</div>
<LookupForm onSubmit={handleLookup} loading={loading}/>
{error && (
<p style={{ color: 'var(--red)', fontSize: '0.8rem', marginTop: 10,
background: 'var(--red-dim)', padding: '8px 12px',
borderRadius: 'var(--radius-sm)', border: '1px solid rgba(220,38,38,0.15)' }}>
{error}
</p>
)}
</div>
{/* Loading indicator */}
{loading && (
<div style={{ background: 'var(--surface)', border: '1px solid var(--border-2)',
borderRadius: 'var(--radius-lg)', padding: '20px',
display: 'flex', alignItems: 'center', gap: 14, marginBottom: 14,
boxShadow: '0 2px 12px rgba(26,112,112,0.06)' }}>
<div style={{ width: 18, height: 18, border: '2px solid var(--teal)',
borderTopColor: 'transparent', borderRadius: '50%',
animation: 'spin 0.7s linear infinite', flexShrink: 0 }}/>
<div>
<p style={{ fontWeight: 600, fontSize: '0.88rem', color: 'var(--text)', marginBottom: 2 }}>
Analysing vehicle…
</p>
<p style={{ color: 'var(--muted)', fontSize: '0.78rem' }}>
Calling MarketCheck · Calculating trade offer · Generating AI report
</p>
</div>
</div>
)}
{/* Empty state */}
{cars.length === 0 && !loading && (
<div style={{ background: 'var(--surface)', border: '1.5px dashed var(--border-2)',
borderRadius: 'var(--radius-lg)', padding: '48px 24px', textAlign: 'center' }}>
<svg width="36" height="36" fill="none" viewBox="0 0 24 24"
stroke="var(--muted-2)" strokeWidth="1.2"
style={{ display: 'block', margin: '0 auto 12px' }}>
<path strokeLinecap="round" strokeLinejoin="round"
d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"/>
</svg>
<p style={{ color: 'var(--muted)', fontSize: '0.88rem', fontWeight: 500, marginBottom: 4 }}>
No vehicles added yet
</p>
<p style={{ color: 'var(--muted-2)', fontSize: '0.8rem' }}>
Enter a registration above to run your first market lookup
</p>
</div>
)}
{/* Car cards */}
<div style={{ marginTop: 4 }}>
{cars.map(car => (
<CarCard key={car.id} car={car}
onRemove={id => setCars(prev => prev.filter(c => c.id !== id))}
onImageUpload={(id, src) => setCars(prev => prev.map(c => c.id === id ? { ...c, image: src } : c))}
/>
))}
</div>
</div>
</div>
);
}
/* ============================================================
APP ROOT
============================================================ */
function App() {
const [user, setUser] = useState(null);
return user
? <Dashboard user={user} onLogout={() => setUser(null)}/>
: <LoginScreen onLogin={setUser}/>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
</body>
</html>