// dashboard.jsx β€” BeanScriptor home screen const TINTS = ['#F5E6D3', '#EAD7C2', '#E2CDB4', '#D9C0A4', '#EFDFC8']; function formatDate(iso) { if (!iso) return ''; const d = new Date(iso); const now = new Date(); const yes = new Date(now); yes.setDate(now.getDate() - 1); if (d.toDateString() === now.toDateString()) return 'Today'; if (d.toDateString() === yes.toDateString()) return 'Yesterday'; return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } const SCORE_STEPS = [0, 70, 75, 80, 85, 90]; function deriveScore(log) { if (log.cva_total_score != null) return Math.round(log.cva_total_score); const vals = [log.acidity, log.body, log.balance, log.sweetness].filter(v => v != null); if (!vals.length) return 0; return Math.round(vals.reduce((a, b) => a + b, 0) / vals.length * 10); } function mapLog(log) { return { id: log.id, name: log.coffee_name, roaster: log.roaster || 'β€”', method: log.brew_method || 'β€”', date: formatDate(log.created_at), score: deriveScore(log), tint: TINTS[log.id % TINTS.length], }; } function greeting() { const h = new Date().getHours(); if (h < 12) return 'Morning'; if (h < 18) return 'Afternoon'; return 'Evening'; } function Achievement({ icon, title, sub, color, locked }) { return (
{icon}
{title}
{sub}
); } function StatPill({ label, value, accent }) { return (
{label}
{value}
); } function ScoreRing({ score, size = 44 }) { const r = size / 2 - 3; const c = 2 * Math.PI * r; const p = score / 100; const tone = score >= 88 ? PALETTE.sageDeep : score >= 80 ? PALETTE.caramel : PALETTE.cocoa; return (
{score || 'β€”'}
); } function LogRow({ log, onOpen }) { return (
{log.name}
{log.roaster} {log.method} {log.date}
); } const ACHIEVEMENTS = [ { icon: 'πŸ’―', title: '100 Cups', sub: 'logged this season', color: '#F4E0C2' }, { icon: 'πŸ†', title: 'Brew Master', sub: 'rated 90+ Γ— 5', color: '#E5D4BC' }, { icon: 'πŸ‘ƒ', title: 'Sensory Pro', sub: 'all 8 flavors used', color: '#DCC8AE' }, { icon: 'πŸ”₯', title: '14-day Streak',sub: 'keep it going', color: '#F0D9BC' }, { icon: '🌍', title: 'Origin Hunter',sub: '12 origins tasted', color: '#D8C5A8', locked: true }, { icon: '⏱', title: 'Timer Hero', sub: 'log 50 timers', color: '#CDB89C', locked: true }, ]; function Dashboard({ onNewLog, onOpenLog }) { const [logs, setLogs] = React.useState([]); const [loading, setLoading] = React.useState(true); const [loadError, setLoadError] = React.useState(false); const [minScoreIdx, setMinScoreIdx] = React.useState(0); const API_BASE = window.APP_API_BASE ?? ''; React.useEffect(() => { let cancelled = false; fetch(`${API_BASE}/api/logs`) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then(data => { if (cancelled) return; setLogs(Array.isArray(data) ? data : []); setLoading(false); }) .catch(() => { if (cancelled) return; setLoadError(true); setLoading(false); }); return () => { cancelled = true; }; }, []); const now = new Date(); const { weekCount, originsThisWeek, dayCounts, maxDay, avgScore, bestScore, avgTds } = React.useMemo(() => { // Week stats const weekStart = new Date(now); weekStart.setDate(now.getDate() - 6); weekStart.setHours(0,0,0,0); const weekLogs = logs.filter(l => new Date(l.created_at) >= weekStart); // Bar chart β€” last 7 days const dayCounts = Array.from({ length: 7 }, (_, i) => { const d = new Date(now); d.setDate(now.getDate() - (6 - i)); const ds = d.toDateString(); return logs.filter(l => new Date(l.created_at).toDateString() === ds).length; }); // Aggregate stats const scores = logs.map(deriveScore).filter(s => s > 0); const tdsVals = logs.map(l => l.tds).filter(v => v != null); return { weekCount: weekLogs.length, originsThisWeek: new Set(weekLogs.map(l => l.roaster).filter(Boolean)).size, dayCounts, maxDay: Math.max(...dayCounts, 1), avgScore: scores.length ? (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1) : 'β€”', bestScore: scores.length ? Math.max(...scores) : 'β€”', avgTds: tdsVals.length ? (tdsVals.reduce((a, b) => a + b, 0) / tdsVals.length).toFixed(2) + '%' : 'β€”', }; }, [logs]); const tg = window.Telegram?.WebApp; const firstName = tg?.initDataUnsafe?.user?.first_name || 'there'; const minScore = SCORE_STEPS[minScoreIdx]; const displayLogs = React.useMemo(() => logs .filter(l => !minScore || deriveScore(l) >= minScore) .slice(0, 10) .map(mapLog), [logs, minScore]); return (
{/* top action bar */}
{/* greeting + avatar */}
{now.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
{greeting()}, {firstName}.
{firstName.slice(0, 2).toUpperCase()}
{/* hero stats */}
This week
{weekCount} {weekCount === 1 ? 'cup' : 'cups'}{originsThisWeek > 0 ? ` Β· ${originsThisWeek} origin${originsThisWeek > 1 ? 's' : ''}` : ''}
{/* mini bar chart */}
{dayCounts.map((v, i) => (
{['S','M','T','W','T','F','S'][(new Date(now.getTime() - (6 - i) * 86400000)).getDay()]}
))}
{/* achievements */} Achievements
{ACHIEVEMENTS.map((a, i) => )}
{/* logs */} 0 ? `${logs.length} total` : ''} style={{ padding: '4px 24px 8px' }}> Active logs
{SCORE_STEPS.map((s, i) => ( ))}
{loading && (
Loading…
)} {!loading && loadError && (
Couldn't load logs β€” check the backend and refresh.
)} {!loading && !loadError && displayLogs.length === 0 && (
No logs yet β€” tap New tasting to start.
)} {displayLogs.map((l, i) => ( onOpenLog && onOpenLog(l)} /> {i < displayLogs.length - 1 && (
)} ))}
); } Object.assign(window, { Dashboard });