From daf7297a014de792c559f39eea82a2041c3f59bd Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Sat, 6 Jun 2026 22:47:28 +0100 Subject: [PATCH] v2: Full Family Life OS - Dashboard, Meal Planner, Projections, Recipes --- src/App.css | 184 ------------------------------ src/App.jsx | 241 ++++----------------------------------- src/assets/hero.png | Bin 13057 -> 0 bytes src/assets/react.svg | 1 - src/assets/vite.svg | 1 - src/index.css | 68 ----------- src/main.jsx | 2 +- src/styles.css | 138 ++++++++++++++++++++++ src/tabs/Dashboard.jsx | 156 +++++++++++++++++++++++++ src/tabs/MealPlanner.jsx | 126 ++++++++++++++++++++ src/tabs/Projections.jsx | 125 ++++++++++++++++++++ src/tabs/Recipes.jsx | 130 +++++++++++++++++++++ 12 files changed, 700 insertions(+), 472 deletions(-) delete mode 100644 src/App.css delete mode 100644 src/assets/hero.png delete mode 100644 src/assets/react.svg delete mode 100644 src/assets/vite.svg delete mode 100644 src/index.css create mode 100644 src/styles.css create mode 100644 src/tabs/Dashboard.jsx create mode 100644 src/tabs/MealPlanner.jsx create mode 100644 src/tabs/Projections.jsx create mode 100644 src/tabs/Recipes.jsx diff --git a/src/App.css b/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/src/App.jsx b/src/App.jsx index 9d94387..d842a6f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,228 +1,35 @@ import { useState, useEffect, useCallback } from 'react' +import { Dashboard } from './tabs/Dashboard.jsx' +import { MealPlanner } from './tabs/MealPlanner.jsx' +import { Projections } from './tabs/Projections.jsx' +import { Recipes } from './tabs/Recipes.jsx' -const API = 'http://localhost:4003' - -// Meal rotation: Mon=0, Sun=6 -const MEALS = [ - { day: 'Monday', meal: 'Thai Red Curry', icon: '🍛', desc: 'Chicken breast (160g) + Thai Red sauce + boil-in-bag rice + broccoli', protein: 45, cals: 520 }, - { day: 'Tuesday', meal: 'Rice Bowl', icon: '🍚', desc: 'Chicken breast (150g) + rice + broccoli + Nando\'s peri-peri', protein: 42, cals: 480 }, - { day: 'Wednesday', meal: 'Chow Mein', icon: '🍜', desc: 'Chicken breast (200g) + noodles + chow mein sauce + red pepper + sesame oil', protein: 50, cals: 530 }, - { day: 'Thursday', meal: 'Sweet & Sour', icon: '🍗', desc: 'Chicken breast (160g) + Ben\'s S&S sauce + boil-in-bag rice + broccoli', protein: 43, cals: 490 }, - { day: 'Friday', meal: 'Curry (Takeaway)', icon: '🥘', desc: 'Half butter chicken + half pilau rice', protein: 30, cals: 650 }, - { day: 'Saturday', meal: 'Curry (Takeaway)', icon: '🥘', desc: 'Half butter chicken + half pilau rice (+ half naan on run days)', protein: 30, cals: 650 }, - { day: 'Sunday', meal: 'Meatballs', icon: '🍝', desc: 'SuperValu meatballs + spaghetti (60g) + pizza sauce + cheddar + broccoli', protein: 38, cals: 510 }, +const TABS = [ + { id: 'dash', label: '📊', name: 'Today' }, + { id: 'meals', label: '🍽️', name: 'Meals' }, + { id: 'project', label: '📈', name: 'Goals' }, + { id: 'recipes', label: '📖', name: 'Recipes' }, ] -const DAILY_CONSTANTS = { breakfast: { cals: 348, protein: 10, fat: 12, fibre: 9 }, shakes: { cals: 330, protein: 81, fat: 0, fibre: 0 } } -const TARGETS = { cals: [1800, 2000], protein: [140, 160], fat: [55, 70], fibre: [28, 35], sodium: 2300 } - -function useFetch(url) { - const [data, setData] = useState(null) - const [loading, setLoading] = useState(true) - const refresh = useCallback(() => { - setLoading(true) - fetch(url).then(r => r.json()).then(d => { setData(d); setLoading(false) }).catch(() => setLoading(false)) - }, [url]) - useEffect(() => { refresh() }, [refresh]) - return { data, loading, refresh } -} - -function MetricCard({ label, value, unit, target, icon, color }) { - const pct = target ? Math.min((value / target) * 100, 100) : null - const over = target && value > target - return ( -
-
{icon}{label}
-
{value}{unit}
- {pct !== null && ( -
- )} - {target &&
{over ? '⚠️ ' : ''}{value} / {target}{unit}
} -
- ) -} - -function WeightChart({ data }) { - if (!data || data.length === 0) return null - const vals = data.map(d => d.value) - const min = Math.min(...vals) - 1 - const max = Math.max(...vals) + 1 - const W = 300, H = 80, P = 5 - const pts = vals.map((v, i) => ({ - x: P + (i / (vals.length - 1 || 1)) * (W - P * 2), - y: P + (1 - (v - min) / (max - min)) * (H - P * 2) - })) - const line = pts.map((p, i) => `${i ? 'L' : 'M'}${p.x},${p.y}`).join(' ') - return ( - - - - - {vals.at(-1).toFixed(1)} - - ) -} - -function TodaysMeal() { - const dow = new Date().getDay() - const idx = dow === 0 ? 6 : dow - 1 - const meal = MEALS[idx] - return ( -
-
{meal.icon}

Tonight's Dinner

{meal.day}'s rotation

-
{meal.meal}
-
{meal.desc}
-
~{meal.cals} cal{meal.protein}g protein
-
- ) -} - -function MacroBudget({ todayCals }) { - const eaten = todayCals || 0 - const budget = TARGETS.cals[1] - const remaining = Math.max(0, budget - eaten) - const dinnerMeal = MEALS[new Date().getDay() === 0 ? 6 : new Date().getDay() - 1] - - const alreadyPlanned = DAILY_CONSTANTS.breakfast.cals + DAILY_CONSTANTS.shakes.cals + dinnerMeal.cals - const snackBudget = budget - alreadyPlanned - - return ( -
-

📊 Today's Budget

-
-
Breakfast (porridge+chia){DAILY_CONSTANTS.breakfast.cals} cal
-
3× NXR Shakes{DAILY_CONSTANTS.shakes.cals} cal
-
Dinner ({dinnerMeal.meal})~{dinnerMeal.cals} cal
-
Snack allowance~{snackBudget} cal
-
Day total (planned)~{alreadyPlanned + snackBudget} cal
-
- {eaten > 0 &&
Logged so far: {eaten} cal • {remaining} remaining
} -
- ) -} - -function QuickActions({ onSync }) { - const [syncing, setSyncing] = useState(false) - const sync = async () => { - setSyncing(true) - try { await fetch(`${API}/fetch-loseit`, { method: 'POST', headers: { Authorization: 'my-health-data' } }) } - catch {} - setSyncing(false) - onSync() - } - return ( -
- - -
- ) -} - export default function App() { - const today = new Date().toISOString().slice(0, 10) - const weekAgo = new Date(Date.now() - 14 * 86400000).toISOString().slice(0, 10) - - const weight = useFetch(`${API}/health-data?metric=weight_body_mass&from=${weekAgo}`) - const bodyFat = useFetch(`${API}/health-data?metric=body_fat_percentage&from=${weekAgo}`) - const hr = useFetch(`${API}/health-data?metric=resting_heart_rate&from=${weekAgo}`) - const steps = useFetch(`${API}/health-data?metric=step_count&from=${today}`) - const energy = useFetch(`${API}/health-data?metric=dietary_energy&from=${today}`) - const protein = useFetch(`${API}/health-data?metric=protein&from=${today}`) - - // Aggregate steps for today - const todaySteps = steps.data?.data?.reduce((sum, d) => sum + (d.value || 0), 0) || 0 - // Aggregate cals (kJ → kcal) - const todayCalsKj = energy.data?.data?.reduce((sum, d) => sum + (d.value || 0), 0) || 0 - const todayCals = Math.round(todayCalsKj / 4.184) - // Aggregate protein - const todayProtein = Math.round(protein.data?.data?.reduce((sum, d) => sum + (d.value || 0), 0) || 0) - - // Latest weight - const latestWeight = weight.data?.data?.[0]?.value?.toFixed(1) || '—' - const latestBf = bodyFat.data?.data?.[0]?.value?.toFixed(1) || '—' - const latestHr = hr.data?.data?.[0]?.value || '—' - - // Weight chart data (dedupe by date, take first reading per day) - const weightByDay = {} - weight.data?.data?.forEach(d => { - const day = d.date.slice(0, 10) - if (!weightByDay[day]) weightByDay[day] = d.value - }) - const weightChartData = Object.entries(weightByDay).sort().map(([date, value]) => ({ date, value })) - - // Goal calc - const paulStart = 114.9, paulTarget = 83.5 - const current = parseFloat(latestWeight) || 108.5 - const lost = paulStart - current - const pct = ((lost / (paulStart - paulTarget)) * 100).toFixed(0) - const daysIn = Math.floor((Date.now() - new Date('2026-04-27')) / 86400000) - - const refreshAll = () => { weight.refresh(); energy.refresh(); protein.refresh(); steps.refresh() } + const [tab, setTab] = useState('dash') return (
-
-

🏠 Home Hub

-

{new Date().toLocaleDateString('en-IE', { weekday: 'long', day: 'numeric', month: 'long' })} • Day {daysIn}

-
- - - -
-
-

Weight Journey

{latestWeight}kg • {pct}% to goal • {(current - paulTarget).toFixed(1)}kg remaining

-
{latestBf}% BF
-
- -
- -
- - - - -
- - - - -
-

✅ Daily Checklist

- -
- -
Home Hub • Refreshes from diet-coach-api (localhost:4003)
-
- ) -} - -function Checklist() { - const key = `checklist-${new Date().toISOString().slice(0, 10)}` - const [checked, setChecked] = useState(() => JSON.parse(localStorage.getItem(key) || '{}')) - const toggle = id => { - const next = { ...checked, [id]: !checked[id] } - setChecked(next) - localStorage.setItem(key, JSON.stringify(next)) - } - const items = [ - { id: 'porridge', label: 'Breakfast: Porridge + chia + seeds' }, - { id: 'shake1', label: 'NXR Shake #1 (+ creatine + psyllium)' }, - { id: 'shake2', label: 'NXR Shake #2' }, - { id: 'shake3', label: 'NXR Shake #3' }, - { id: 'water', label: 'Water: 2L minimum' }, - { id: 'steps', label: 'Steps: 8,000+ target' }, - { id: 'veg', label: 'Broccoli / veg with dinner' }, - ] - const done = Object.values(checked).filter(Boolean).length - return ( -
-
{done}/{items.length} complete
- {items.map(it => ( - - ))} +
+ {tab === 'dash' && } + {tab === 'meals' && } + {tab === 'project' && } + {tab === 'recipes' && } +
+
) } diff --git a/src/assets/hero.png b/src/assets/hero.png deleted file mode 100644 index 02251f4b956c55af2d76fd0788124d7eee2b45eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13057 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 P00000NkvXXu0mjft=yBf diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 4316262..0000000 --- a/src/index.css +++ /dev/null @@ -1,68 +0,0 @@ -*{margin:0;padding:0;box-sizing:border-box} -:root{--bg:#0f172a;--s:#1e293b;--s2:#334155;--t:#f8fafc;--tm:#94a3b8;--b:rgba(148,163,184,.12)} -body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--t);min-height:100vh;-webkit-font-smoothing:antialiased} -.app{max-width:600px;margin:0 auto;padding:20px 16px 80px} - -header{text-align:center;padding:24px 0 16px} -header h1{font-size:1.6rem;font-weight:800} -.date{font-size:.85rem;color:var(--tm);margin-top:4px} - -.actions{display:flex;gap:10px;margin-bottom:18px} -.actions button{flex:1;padding:12px;border:1px solid var(--b);border-radius:10px;background:var(--s);color:var(--t);font-size:.85rem;font-weight:600;cursor:pointer;transition:background .2s} -.actions button:hover{background:var(--s2)} -.actions button:disabled{opacity:.5} -.actions a{flex:1;text-decoration:none} -.actions a button{width:100%} - -.weight-section{background:var(--s);border:1px solid var(--b);border-radius:14px;padding:18px;margin-bottom:16px} -.weight-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px} -.weight-header h2{font-size:1rem;font-weight:700} -.weight-sub{font-size:.75rem;color:var(--tm);margin-top:2px} -.weight-badge{background:rgba(99,102,241,.15);color:#a78bfa;font-size:.75rem;font-weight:700;padding:4px 10px;border-radius:8px} -.weight-chart{width:100%;height:70px;display:block} - -.metrics-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px} -.metric-card{background:var(--s);border:1px solid var(--b);border-radius:12px;padding:14px} -.mc-top{display:flex;align-items:center;gap:6px;margin-bottom:6px} -.mc-icon{font-size:1rem} -.mc-label{font-size:.7rem;color:var(--tm);text-transform:uppercase;letter-spacing:.3px;font-weight:600} -.mc-value{font-size:1.4rem;font-weight:800} -.mc-value small{font-size:.7rem;font-weight:500;color:var(--tm)} -.mc-bar{height:5px;background:var(--s2);border-radius:3px;margin-top:8px;overflow:hidden} -.mc-fill{height:100%;border-radius:3px;transition:width .8s ease} -.mc-target{font-size:.65rem;color:var(--tm);margin-top:4px} - -.meal-card{background:var(--s);border:1px solid var(--b);border-radius:14px;padding:18px;margin-bottom:16px} -.meal-header{display:flex;align-items:center;gap:12px;margin-bottom:10px} -.meal-icon{font-size:2rem} -.meal-header h3{font-size:.9rem;font-weight:700} -.meal-day{font-size:.7rem;color:var(--tm)} -.meal-name{font-size:1.2rem;font-weight:800;margin-bottom:6px} -.meal-desc{font-size:.8rem;color:var(--tm);line-height:1.4;margin-bottom:8px} -.meal-macros{display:flex;gap:12px;font-size:.75rem;font-weight:600;color:#10b981} - -.budget-card{background:var(--s);border:1px solid var(--b);border-radius:14px;padding:18px;margin-bottom:16px} -.budget-card h3{font-size:.9rem;margin-bottom:12px} -.budget-rows{display:flex;flex-direction:column;gap:6px} -.budget-row{display:flex;justify-content:space-between;font-size:.8rem;padding:6px 0;border-bottom:1px solid var(--b)} -.budget-row:last-child{border:none} -.budget-row span:first-child{color:var(--tm)} -.budget-row span:last-child{font-weight:600} -.budget-row.highlight{background:rgba(245,158,11,.08);margin:4px -8px;padding:8px;border-radius:8px;border:none} -.budget-row.highlight span{color:#f59e0b} -.budget-row.total{border:none;padding-top:10px} -.budget-row.total span{font-weight:800} -.budget-eaten{font-size:.75rem;color:var(--tm);margin-top:10px;text-align:center} - -.reminders{background:var(--s);border:1px solid var(--b);border-radius:14px;padding:18px;margin-bottom:16px} -.reminders h3{font-size:.9rem;margin-bottom:12px} -.checklist{display:flex;flex-direction:column;gap:6px} -.cl-progress{font-size:.7rem;color:var(--tm);margin-bottom:6px;font-weight:600} -.cl-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;cursor:pointer;transition:background .2s;font-size:.85rem} -.cl-item:hover{background:var(--s2)} -.cl-item.done span{text-decoration:line-through;color:var(--tm)} -.cl-item input{width:18px;height:18px;accent-color:#6366f1} - -footer{text-align:center;padding-top:16px;font-size:.7rem;color:var(--tm)} - -@media(max-width:400px){.metrics-grid{grid-template-columns:1fr}} diff --git a/src/main.jsx b/src/main.jsx index 124a0ac..5688706 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App.jsx' -import './index.css' +import './styles.css' createRoot(document.getElementById('root')).render() diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..77f0c02 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,138 @@ +*{margin:0;padding:0;box-sizing:border-box} +:root{--bg:#0f172a;--s:#1e293b;--s2:#334155;--t:#f8fafc;--tm:#94a3b8;--b:rgba(148,163,184,.1);--r:12px} +body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--t);min-height:100vh;-webkit-font-smoothing:antialiased;overscroll-behavior:none} +.app{max-width:500px;margin:0 auto;padding-bottom:72px} +.tab-content{padding:16px} +.loading{text-align:center;padding:80px 0;color:var(--tm)} + +/* Tab bar */ +.tab-bar{position:fixed;bottom:0;left:0;right:0;background:var(--s);border-top:1px solid var(--b);display:flex;justify-content:center;padding:8px 0 env(safe-area-inset-bottom,8px);z-index:100;backdrop-filter:blur(12px)} +.tab-btn{flex:1;max-width:100px;display:flex;flex-direction:column;align-items:center;gap:2px;background:none;border:none;color:var(--tm);cursor:pointer;padding:6px} +.tab-btn.active{color:#6366f1} +.tab-icon{font-size:1.3rem} +.tab-name{font-size:.6rem;font-weight:600;text-transform:uppercase;letter-spacing:.3px} + +/* Dashboard */ +.dash-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px} +.dash-header h1{font-size:1.4rem;font-weight:800} +.dash-date{font-size:.75rem;color:var(--tm);margin-top:2px} +.sync-btn{width:40px;height:40px;border-radius:50%;background:var(--s);border:1px solid var(--b);font-size:1.1rem;cursor:pointer} +.sync-btn:disabled{opacity:.5} + +.vitals-row{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:18px} +.vital{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:12px 8px;text-align:center} +.vital-val{display:block;font-size:1.2rem;font-weight:800} +.vital-val small{font-size:.6rem;color:var(--tm);font-weight:500} +.vital-label{font-size:.6rem;color:var(--tm);text-transform:uppercase;letter-spacing:.3px} + +.macros-row{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:18px} +.macro-ring{text-align:center} +.mr-ring-wrap{position:relative;width:54px;height:54px;margin:0 auto 4px} +.ring{display:block} +.mr-pct{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:.65rem;font-weight:700} +.mr-val{font-size:.9rem;font-weight:800} +.mr-val small{font-size:.55rem;color:var(--tm)} +.mr-label{font-size:.55rem;color:var(--tm);text-transform:uppercase;letter-spacing:.2px} +.mr-target{font-size:.55rem;color:var(--tm)} + +.extras-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:18px} +.extra{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:10px 12px;display:flex;align-items:center;gap:8px;font-size:.8rem;font-weight:600} +.extra-icon{font-size:1rem} + +.checklist-section{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px} +.cl-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px} +.cl-header h3{font-size:.9rem} +.cl-count{font-size:.75rem;color:var(--tm);font-weight:700} +.cl-item{display:flex;align-items:center;gap:10px;padding:10px;border-radius:8px;cursor:pointer;transition:background .15s} +.cl-item:hover{background:var(--s2)} +.cl-item.done span{text-decoration:line-through;opacity:.5} +.cl-check{width:22px;height:22px;border-radius:6px;border:2px solid var(--s2);display:flex;align-items:center;justify-content:center;font-size:.7rem;color:#6366f1;font-weight:800;flex-shrink:0} +.cl-check.checked{background:#6366f1;border-color:#6366f1;color:white} +.cl-item span{font-size:.85rem} + +/* Meal Planner */ +.meal-planner h1,.projections h1,.recipes h1{font-size:1.4rem;font-weight:800;margin-bottom:4px} +.mp-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px} +.mp-sub{font-size:.8rem;color:var(--tm);margin-bottom:16px} +.shop-btn,.mp-back{background:var(--s);border:1px solid var(--b);border-radius:8px;padding:8px 14px;color:var(--t);font-size:.8rem;font-weight:600;cursor:pointer} +.week-summary{font-size:.75rem;color:var(--tm);margin-bottom:12px;padding:8px 12px;background:var(--s);border-radius:8px} +.week-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;margin-bottom:16px} +.day-card{background:var(--s);border:1px solid var(--b);border-radius:10px;padding:10px 6px;text-align:center;cursor:pointer;transition:all .2s} +.day-card:hover,.day-card.selecting{border-color:#6366f1;transform:translateY(-2px)} +.day-card.today{border-color:#f59e0b;box-shadow:0 0 12px rgba(245,158,11,.15)} +.day-label{font-size:.6rem;font-weight:700;color:var(--tm);text-transform:uppercase;margin-bottom:4px} +.day-icon{font-size:1.5rem;margin:4px 0} +.day-meal{font-size:.6rem;font-weight:600;line-height:1.2;margin-bottom:2px} +.day-macros{font-size:.5rem;color:var(--tm)} + +.swap-panel{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:16px;margin-bottom:12px} +.swap-panel h3{font-size:.85rem;margin-bottom:10px} +.swap-options{display:grid;grid-template-columns:1fr 1fr;gap:6px} +.swap-opt{display:flex;align-items:center;gap:8px;background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:10px;cursor:pointer;color:var(--t);font-size:.8rem;font-weight:600} +.swap-opt.current{border-color:#6366f1;background:rgba(99,102,241,.1)} +.swap-opt small{color:var(--tm);font-size:.65rem;margin-left:auto} +.reset-btn{width:100%;background:none;border:1px solid var(--b);border-radius:8px;padding:10px;color:var(--tm);font-size:.75rem;cursor:pointer} + +.shop-list,.shop-extras{margin-bottom:16px} +.shop-extras h4{font-size:.8rem;color:var(--tm);margin-bottom:8px} +.shop-item{display:flex;justify-content:space-between;padding:8px 12px;background:var(--s);border:1px solid var(--b);border-radius:8px;margin-bottom:4px;font-size:.85rem} +.shop-count{color:var(--tm);font-size:.75rem} + +/* Projections */ +.proj-sub{font-size:.8rem;color:var(--tm);margin-bottom:16px} +.proj-controls{margin-bottom:18px} +.control-group{margin-bottom:14px} +.control-group label{font-size:.75rem;font-weight:700;color:var(--tm);text-transform:uppercase;letter-spacing:.3px;display:block;margin-bottom:6px} +.slider-row{display:flex;align-items:center;gap:12px} +.slider-row input[type=range]{flex:1;accent-color:#6366f1;height:6px} +.slider-val{font-size:1rem;font-weight:800;min-width:70px;text-align:right} +.rate-note{font-size:.7rem;color:var(--tm);margin-top:4px} + +.proj-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:16px} +.ps-item{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:12px;text-align:center} +.ps-val{display:block;font-size:1.1rem;font-weight:800} +.ps-label{font-size:.6rem;color:var(--tm);text-transform:uppercase} + +.proj-chart{margin-bottom:18px} +.proj-chart svg{width:100%;height:120px;background:var(--s);border-radius:var(--r);display:block} + +.milestones-list{margin-bottom:18px} +.milestones-list h3{font-size:.9rem;margin-bottom:10px} +.ms-row{display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid var(--b);font-size:.85rem} +.ms-row:last-child{border:none} +.ms-icon{font-size:.85rem} +.ms-name{flex:1;font-weight:600} +.ms-row.reached .ms-name{color:var(--tm)} +.ms-eta{font-size:.75rem;color:var(--tm)} + +.scenarios h3{font-size:.9rem;margin-bottom:10px} +.scenario-btns{display:flex;gap:6px} +.scenario-btns button{flex:1;padding:10px;background:var(--s);border:1px solid var(--b);border-radius:8px;color:var(--t);font-size:.7rem;font-weight:600;cursor:pointer} +.scenario-btns button.active{border-color:#6366f1;background:rgba(99,102,241,.1)} + +/* Recipes */ +.recipe-filters{display:flex;gap:6px;margin-bottom:16px;overflow-x:auto} +.filter-btn{padding:8px 14px;border-radius:20px;border:1px solid var(--b);background:var(--s);color:var(--tm);font-size:.75rem;font-weight:600;cursor:pointer;white-space:nowrap} +.filter-btn.active{background:#6366f1;border-color:#6366f1;color:white} +.recipe-list{display:flex;flex-direction:column;gap:8px} +.recipe-card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:hidden} +.rc-header{display:flex;align-items:center;gap:12px;padding:14px;cursor:pointer} +.rc-icon{font-size:1.5rem} +.rc-info{flex:1} +.rc-name{font-size:.9rem;font-weight:700} +.rc-meta{font-size:.7rem;color:var(--tm)} +.rc-arrow{color:var(--tm);font-size:.7rem} +.rc-body{padding:0 14px 14px;border-top:1px solid var(--b)} +.rc-macros-full{display:flex;flex-wrap:wrap;gap:8px;padding:12px 0;font-size:.75rem;font-weight:600} +.rc-body h4{font-size:.8rem;color:var(--tm);margin:12px 0 6px;text-transform:uppercase;letter-spacing:.3px} +.rc-ingredients{padding-left:16px;font-size:.85rem;line-height:1.8} +.rc-method{padding-left:16px;font-size:.85rem;line-height:1.8} +.rc-method li{margin-bottom:4px} +.rc-serves{font-size:.7rem;color:var(--tm);margin-top:12px;font-style:italic} + +@media(max-width:400px){ + .vitals-row{grid-template-columns:repeat(2,1fr)} + .macros-row{grid-template-columns:repeat(2,1fr);gap:16px} + .week-grid{grid-template-columns:repeat(4,1fr)} + .proj-summary{grid-template-columns:repeat(2,1fr)} +} diff --git a/src/tabs/Dashboard.jsx b/src/tabs/Dashboard.jsx new file mode 100644 index 0000000..adb8b4e --- /dev/null +++ b/src/tabs/Dashboard.jsx @@ -0,0 +1,156 @@ +import { useState, useEffect } from 'react' + +const API = 'http://localhost:4003' +const TARGETS = { cals: 2000, protein: 160, fat: 70, fibre: 35, sodium: 2300, steps: 8000 } + +function useLiveData() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + const refresh = () => { + const today = new Date().toISOString().slice(0, 10) + const week = new Date(Date.now() - 14 * 86400000).toISOString().slice(0, 10) + Promise.all([ + fetch(`${API}/health-data?metric=weight_body_mass&from=${week}`).then(r => r.json()), + fetch(`${API}/health-data?metric=body_fat_percentage&from=${week}`).then(r => r.json()), + fetch(`${API}/health-data?metric=step_count&from=${today}`).then(r => r.json()), + fetch(`${API}/health-data?metric=dietary_energy&from=${today}`).then(r => r.json()), + fetch(`${API}/health-data?metric=protein&from=${today}`).then(r => r.json()), + fetch(`${API}/health-data?metric=total_fat&from=${today}`).then(r => r.json()), + fetch(`${API}/health-data?metric=fiber&from=${today}`).then(r => r.json()), + fetch(`${API}/health-data?metric=sodium&from=${today}`).then(r => r.json()), + fetch(`${API}/health-data?metric=resting_heart_rate&from=${week}`).then(r => r.json()), + fetch(`${API}/health-data?metric=active_energy&from=${today}`).then(r => r.json()), + ]).then(([wt, bf, steps, energy, prot, fat, fib, sod, hr, active]) => { + setData({ wt, bf, steps, energy, prot, fat, fib, sod, hr, active }) + setLoading(false) + }).catch(() => setLoading(false)) + } + + useEffect(() => { refresh() }, []) + return { data, loading, refresh } +} + +function Ring({ pct, size = 54, stroke = 5, color }) { + const r = (size - stroke) / 2, c = 2 * Math.PI * r + return ( + + + + + ) +} + +function MacroRing({ label, value, target, unit, color }) { + const pct = target ? (value / target) * 100 : 0 + const over = value > target + return ( +
+
+ + {Math.round(pct)}% +
+
{value}{unit}
+
{label}
+
/ {target}{unit}
+
+ ) +} + +export function Dashboard() { + const { data, loading, refresh } = useLiveData() + const [syncing, setSyncing] = useState(false) + + const sync = async () => { + setSyncing(true) + await fetch(`${API}/fetch-loseit`, { method: 'POST', headers: { Authorization: 'my-health-data' } }).catch(() => {}) + setTimeout(() => { refresh(); setSyncing(false) }, 2000) + } + + if (loading) return
Loading...
+ + const sum = (arr) => (arr || []).reduce((a, d) => a + (d.value || 0), 0) + const latest = (obj) => obj?.data?.[0]?.value + + const steps = Math.round(sum(data?.steps?.data)) + const calsKj = sum(data?.energy?.data) + const cals = Math.round(calsKj / 4.184) + const protein = Math.round(sum(data?.prot?.data)) + const fat = Math.round(sum(data?.fat?.data)) + const fibre = Math.round(sum(data?.fib?.data)) + const sodium = Math.round(sum(data?.sod?.data)) + const activeKj = sum(data?.active?.data) + const activeCals = Math.round(activeKj / 4.184) + const weight = latest(data?.wt)?.toFixed(1) || '—' + const bf = latest(data?.bf)?.toFixed(1) || '—' + const hr = latest(data?.hr) || '—' + const netCals = cals - activeCals + + const daysIn = Math.floor((Date.now() - new Date('2026-04-27')) / 86400000) + const lost = (114.9 - parseFloat(weight || 114.9)).toFixed(1) + + return ( +
+
+
+

Home Hub

+

{new Date().toLocaleDateString('en-IE', { weekday: 'long', day: 'numeric', month: 'long' })} • Day {daysIn}

+
+ +
+ +
+
{weight}kgWeight
+
{bf}%Body Fat
+
{lost}kgLost
+
{hr}bpmRHR
+
+ +
+ + + + +
+ +
+
👣{steps.toLocaleString()} / {TARGETS.steps.toLocaleString()}
+
🔥{activeCals} burned
+
📉Net: {netCals} cal
+
🧂{sodium} / {TARGETS.sodium}mg
+
+ + +
+ ) +} + +function Checklist() { + const key = `checklist-${new Date().toISOString().slice(0, 10)}` + const [checked, setChecked] = useState(() => JSON.parse(localStorage.getItem(key) || '{}')) + const toggle = id => { const n = { ...checked, [id]: !checked[id] }; setChecked(n); localStorage.setItem(key, JSON.stringify(n)) } + const items = [ + { id: 'bk', label: '🥣 Porridge + chia + seeds' }, + { id: 's1', label: '🥤 NXR Shake #1 + creatine + psyllium' }, + { id: 's2', label: '🥤 NXR Shake #2' }, + { id: 's3', label: '🥤 NXR Shake #3' }, + { id: 'wt', label: '💧 2L+ water' }, + { id: 'vg', label: '🥦 Veg with dinner' }, + { id: 'wk', label: '🚶 8000+ steps' }, + { id: 'lg', label: '📝 Log all food in Lose It' }, + ] + const done = Object.values(checked).filter(Boolean).length + return ( +
+

Daily Checklist

{done}/{items.length}
+ {items.map(it => ( + + ))} +
+ ) +} diff --git a/src/tabs/MealPlanner.jsx b/src/tabs/MealPlanner.jsx new file mode 100644 index 0000000..d90afa0 --- /dev/null +++ b/src/tabs/MealPlanner.jsx @@ -0,0 +1,126 @@ +import { useState } from 'react' + +const ROTATION = [ + { id: 'thai', name: 'Thai Red Curry', icon: '🍛', protein: 45, cals: 520, fat: 14, fibre: 6, ingredients: ['Chicken breast 160g', 'Blue Dragon Thai Red 120g', 'Boil-in-bag rice 62.5g', 'Broccoli 100g', 'Spray oil'] }, + { id: 'rice', name: 'Rice Bowl', icon: '🍚', protein: 42, cals: 480, fat: 8, fibre: 5, ingredients: ['Chicken breast 150g', 'Rice 0.7 cup', 'Broccoli 100g', "Nando's hot peri-peri", 'Sesame oil 1 tsp'] }, + { id: 'chow', name: 'Chow Mein', icon: '🍜', protein: 50, cals: 530, fat: 12, fibre: 4, ingredients: ['Chicken breast 200g', 'Noodles (Amoy pack)', 'Blue Dragon Chow Mein 60g', 'Red pepper 1', 'Sesame oil 1 tsp'] }, + { id: 'sweet', name: 'Sweet & Sour', icon: '🍗', protein: 43, cals: 490, fat: 6, fibre: 5, ingredients: ["Chicken breast 160g", "Ben's S&S No Added Sugar 120g", 'Boil-in-bag rice 62.5g', 'Broccoli 100g'] }, + { id: 'curry', name: 'Curry (Takeaway)', icon: '🥘', protein: 30, cals: 650, fat: 28, fibre: 3, ingredients: ['Half butter chicken', 'Half pilau rice', '(Optional: half peshwari naan on run days)'] }, + { id: 'meatballs', name: 'Meatballs', icon: '🍝', protein: 38, cals: 510, fat: 18, fibre: 5, ingredients: ['SuperValu meatballs 2.5 srv', 'Spaghetti 60g', 'Pizza sauce 35g', 'Cheddar 10g', 'Broccoli 100g'] }, + { id: 'satay', name: 'Satay', icon: '🥜', protein: 48, cals: 560, fat: 16, fibre: 5, ingredients: ['Chicken breast 160g', 'Noodles (Amoy pack)', 'Blue Dragon Satay 120g', 'Broccoli 100g'] }, + { id: 'pizza', name: 'Pizza Night', icon: '🍕', protein: 35, cals: 580, fat: 22, fibre: 4, ingredients: ["Piero's base 105g", 'Mutti sauce', 'Pepperoni', 'Ham', 'Mozzarella/cheddar 68g'] }, + { id: 'katsu', name: 'Katsu', icon: '🍱', protein: 44, cals: 500, fat: 10, fibre: 5, ingredients: ['Chicken breast 160g', 'Blue Dragon Katsu 120g', 'Noodles (Amoy 150g)', 'Broccoli 100g'] }, +] + +const DEFAULT_WEEK = ['thai', 'rice', 'chow', 'sweet', 'curry', 'curry', 'meatballs'] +const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + +export function MealPlanner() { + const [week, setWeek] = useState(() => JSON.parse(localStorage.getItem('meal-week') || 'null') || DEFAULT_WEEK) + const [selected, setSelected] = useState(null) + const [showShop, setShowShop] = useState(false) + + const save = (w) => { setWeek(w); localStorage.setItem('meal-week', JSON.stringify(w)) } + + const swap = (dayIdx, mealId) => { + const next = [...week] + next[dayIdx] = mealId + save(next) + setSelected(null) + } + + const getMeal = id => ROTATION.find(m => m.id === id) + const today = new Date().getDay() + const todayIdx = today === 0 ? 6 : today - 1 + + // Generate shopping list + const ingredientCounts = {} + week.forEach(id => { + const meal = getMeal(id) + if (meal) meal.ingredients.forEach(ing => { ingredientCounts[ing] = (ingredientCounts[ing] || 0) + 1 }) + }) + + // Weekly macro summary + const weekMacros = week.reduce((acc, id) => { + const m = getMeal(id) + if (m) { acc.cals += m.cals; acc.protein += m.protein; acc.fat += m.fat } + return acc + }, { cals: 0, protein: 0, fat: 0 }) + + if (showShop) { + return ( +
+
+

🛒 Shopping List

+ +
+

Based on this week's plan

+
+ {Object.entries(ingredientCounts).sort().map(([item, count]) => ( +
+ {item} + {count > 1 && ×{count}} +
+ ))} +
+
+

Weekly Constants

+
Flahavan's oats
+
Chia seeds
+
Sunflower seeds
+
Honey
+
NXR Beef Protein (21 scoops)
+
Psyllium husk
+
Creatine
+
+
+ ) + } + + return ( +
+
+

🍽️ Meal Planner

+ +
+ +
+ Week totals (dinners): {weekMacros.cals} cal • {weekMacros.protein}g protein • {weekMacros.fat}g fat +
+ +
+ {DAYS.map((day, i) => { + const meal = getMeal(week[i]) + const isToday = i === todayIdx + return ( +
setSelected(selected === i ? null : i)}> +
{day}{isToday && ' ⭐'}
+
{meal?.icon}
+
{meal?.name}
+
{meal?.cals} cal • {meal?.protein}g P
+
+ ) + })} +
+ + {selected !== null && ( +
+

Swap {DAYS[selected]}'s meal:

+
+ {ROTATION.map(m => ( + + ))} +
+
+ )} + + +
+ ) +} diff --git a/src/tabs/Projections.jsx b/src/tabs/Projections.jsx new file mode 100644 index 0000000..e2b479f --- /dev/null +++ b/src/tabs/Projections.jsx @@ -0,0 +1,125 @@ +import { useState } from 'react' + +const START = 114.9 +const START_DATE = new Date('2026-04-27') +const CURRENT = 108.4 // latest from API +const TARGET = 83.5 + +export function Projections() { + const [rate, setRate] = useState(0.8) + const [customTarget, setCustomTarget] = useState(TARGET) + + const daysIn = Math.floor((Date.now() - START_DATE) / 86400000) + const lost = START - CURRENT + const actualRate = lost / (daysIn / 7) + const remaining = CURRENT - customTarget + const weeksLeft = Math.ceil(remaining / rate) + const goalDate = new Date(Date.now() + weeksLeft * 7 * 86400000) + const pct = ((lost / (START - customTarget)) * 100).toFixed(0) + + // Generate projection data points + const projections = [] + let w = CURRENT + for (let i = 0; i <= weeksLeft && i < 60; i++) { + projections.push({ week: i, weight: Math.max(w, customTarget) }) + w -= rate + } + + // Milestones + const milestones = [ + { label: 'Under 105kg', target: 105 }, + { label: 'Under 100kg 🎉', target: 100 }, + { label: '20kg lost', target: START - 20 }, + { label: 'Under 90kg', target: 90 }, + { label: '25kg lost', target: START - 25 }, + { label: 'Goal weight', target: customTarget }, + ].map(m => { + const kgToGo = CURRENT - m.target + const weeks = kgToGo > 0 ? Math.ceil(kgToGo / rate) : 0 + const date = kgToGo > 0 ? new Date(Date.now() + weeks * 7 * 86400000) : null + return { ...m, kgToGo, weeks, date, reached: CURRENT <= m.target } + }) + + // Chart + const W = 320, H = 140, P = 10 + const minW = customTarget - 2, maxW = START + const rangeW = maxW - minW + const pts = projections.map((p, i) => ({ + x: P + (i / (projections.length - 1 || 1)) * (W - P * 2), + y: P + (1 - (p.weight - minW) / rangeW) * (H - P * 2) + })) + const line = pts.map((p, i) => `${i ? 'L' : 'M'}${p.x},${p.y}`).join(' ') + const targetY = P + (1 - (customTarget - minW) / rangeW) * (H - P * 2) + const currentY = P + (1 - (CURRENT - minW) / rangeW) * (H - P * 2) + + return ( +
+

📈 Goal Projections

+

Model your path to goal weight

+ +
+
+ +
+ setRate(+e.target.value)} /> + {rate} kg/wk +
+
+ {rate < 0.5 ? '🐢 Conservative' : rate < 0.9 ? '👍 Sustainable' : rate < 1.2 ? '🔥 Aggressive' : '⚠️ Very aggressive'} + {' • '}Actual rate: {actualRate.toFixed(2)} kg/wk +
+
+
+ +
+ setCustomTarget(+e.target.value)} /> + {customTarget} kg +
+
+
+ +
+
{remaining.toFixed(1)}kgto go
+
{weeksLeft}weeks
+
{goalDate.toLocaleDateString('en-IE', { month: 'short', year: 'numeric' })}ETA
+
{pct}%complete
+
+ +
+ + + + + + + {customTarget}kg + {CURRENT}kg + +
+ +
+

📍 Milestone Estimates

+ {milestones.map(m => ( +
+ {m.reached ? '✅' : '⏳'} + {m.label} + + {m.reached ? 'Done!' : m.date ? `${m.date.toLocaleDateString('en-IE', { day: 'numeric', month: 'short' })} (~${m.weeks} wks)` : '—'} + +
+ ))} +
+ +
+

⚡ Quick Scenarios

+
+ {[{ label: 'Conservative (0.5)', r: 0.5 }, { label: 'Current pace', r: +actualRate.toFixed(1) }, { label: 'Aggressive (1.0)', r: 1.0 }].map(s => ( + + ))} +
+
+
+ ) +} diff --git a/src/tabs/Recipes.jsx b/src/tabs/Recipes.jsx new file mode 100644 index 0000000..aff355e --- /dev/null +++ b/src/tabs/Recipes.jsx @@ -0,0 +1,130 @@ +import { useState } from 'react' + +const RECIPES = [ + { + id: 'porridge', name: 'Morning Porridge', icon: '🥣', category: 'breakfast', + servings: 1, time: '5 min', + macros: { cals: 348, protein: 10, fat: 12, fibre: 9, carbs: 48 }, + ingredients: ['45g Flahavan\'s oats', '15g chia seeds', '10g sunflower seeds', '15g honey', 'Water/milk'], + method: ['Add oats + water to bowl, microwave 2.5 min', 'Stir in chia seeds and sunflower seeds', 'Drizzle honey on top', 'Serve with tea'] + }, + { + id: 'cake', name: 'Protein Drizzle Cake', icon: '🍰', category: 'baking', + servings: 6, time: '35 min', + macros: { cals: 144, protein: 9, fat: 3, fibre: 3, carbs: 19 }, + ingredients: ['90g oats', '30g NXR Strawberry protein', '2 eggs', '1 banana (~120g)', '100g strawberries', '15g chia seeds', '10g honey (batter)', '1 tsp vanilla', '20g honey + 1 tbsp warm water (drizzle)'], + method: ['Preheat oven to 180°C. Line a loaf tin.', 'Blend oats, banana, eggs, 50g strawberries, 10g honey, vanilla until smooth.', 'Stir in NXR protein and chia seeds (don\'t blend).', 'Pour into tin. Lay sliced strawberries on top.', 'Bake 25–30 min until firm and golden.', 'Poke holes with skewer. Pour honey+water drizzle over.', 'Leave 10 min to soak, then cool on rack.'] + }, + { + id: 'satay', name: 'Chicken Satay Noodles', icon: '🥜', category: 'dinner', + servings: 1, time: '20 min', + macros: { cals: 560, protein: 48, fat: 16, fibre: 5, carbs: 52 }, + ingredients: ['160g chicken breast', '1 Amoy noodle pack', '120g Blue Dragon Satay sauce', '100g broccoli', 'Spray oil'], + method: ['Slice chicken, air-fry at 200°C for 12 min.', 'Cook noodles per pack instructions.', 'Steam/microwave broccoli 3 min.', 'Heat satay sauce in pan. Add chicken + noodles, toss.', 'Serve with broccoli on side.'] + }, + { + id: 'chowmein', name: 'Chicken Chow Mein', icon: '🍜', category: 'dinner', + servings: 1, time: '20 min', + macros: { cals: 530, protein: 50, fat: 12, fibre: 4, carbs: 48 }, + ingredients: ['200g chicken breast', '1 Amoy noodle pack', '60g Blue Dragon Chow Mein sauce', '1 red pepper, sliced', '1 tsp sesame oil'], + method: ['Slice chicken, air-fry at 200°C for 12 min.', 'Cook noodles. Slice pepper.', 'Heat sesame oil in wok, stir-fry pepper 2 min.', 'Add noodles, chicken, sauce. Toss 1 min.', 'Serve immediately.'] + }, + { + id: 'thaicurry', name: 'Thai Red Chicken', icon: '🍛', category: 'dinner', + servings: 1, time: '20 min', + macros: { cals: 520, protein: 45, fat: 14, fibre: 6, carbs: 50 }, + ingredients: ['160g chicken breast', '120g Blue Dragon Thai Red sauce', '62.5g boil-in-bag rice (dry)', '100g broccoli', 'Spray oil'], + method: ['Put rice on to boil (12 min).', 'Slice chicken, air-fry at 200°C for 12 min.', 'Steam broccoli 3 min.', 'Heat Thai Red sauce, add chicken, simmer 2 min.', 'Serve curry over rice with broccoli.'] + }, + { + id: 'meatballs', name: 'Meatball Spaghetti', icon: '🍝', category: 'dinner', + servings: 1, time: '15 min', + macros: { cals: 510, protein: 38, fat: 18, fibre: 5, carbs: 48 }, + ingredients: ['SuperValu meatballs (2.5 srv)', '60g spaghetti', '35g pizza sauce', '10g cheddar, grated', '100g broccoli'], + method: ['Cook spaghetti per pack (10 min).', 'Heat meatballs in oven/microwave.', 'Steam broccoli.', 'Drain pasta, top with meatballs + pizza sauce + cheddar.', 'Serve with broccoli on side.'] + }, + { + id: 'katsu', name: 'Chicken Katsu Noodles', icon: '🍱', category: 'dinner', + servings: 1, time: '20 min', + macros: { cals: 500, protein: 44, fat: 10, fibre: 5, carbs: 50 }, + ingredients: ['160g chicken breast', '120g Blue Dragon Katsu sauce', '150g Amoy noodles', '100g broccoli'], + method: ['Slice chicken, air-fry at 200°C for 12 min.', 'Cook noodles per pack.', 'Steam broccoli.', 'Heat katsu sauce, add chicken.', 'Serve over noodles with broccoli.'] + }, + { + id: 'sweetnsour', name: 'Sweet & Sour Chicken', icon: '🍗', category: 'dinner', + servings: 1, time: '20 min', + macros: { cals: 490, protein: 43, fat: 6, fibre: 5, carbs: 55 }, + ingredients: ['160g chicken breast', "120g Ben's Original No Added Sugar Sweet & Sour", '62.5g boil-in-bag rice (dry)', '100g broccoli'], + method: ['Put rice on to boil (12 min).', 'Slice chicken, air-fry at 200°C for 12 min.', 'Steam broccoli.', 'Heat sauce, add chicken, simmer 2 min.', 'Serve over rice with broccoli.'] + }, + { + id: 'pizza', name: 'Pizza Night', icon: '🍕', category: 'dinner', + servings: 1, time: '15 min', + macros: { cals: 580, protein: 35, fat: 22, fibre: 4, carbs: 55 }, + ingredients: ["Piero's base 105g", 'Mutti pizza sauce', 'Pepperoni', 'Ham', 'Mozzarella/cheddar 68g'], + method: ['Preheat oven to 220°C.', 'Spread Mutti sauce on base.', 'Top with pepperoni, ham, cheese.', 'Bake 10–12 min until cheese golden.', 'Snack: tuna + light mayo + carrot batons (swap Huel).'] + }, + { + id: 'shake', name: 'NXR Protein Shake', icon: '🥤', category: 'snack', + servings: 1, time: '1 min', + macros: { cals: 110, protein: 27, fat: 0, fibre: 0, carbs: 0 }, + ingredients: ['30g NXR Beef Protein Isolate', '300ml cold water', '(Optional: 5g creatine)', '(Optional: 4g psyllium husk)'], + method: ['Add water to shaker.', 'Add protein powder (+ creatine + psyllium for shake #1).', 'Shake 15 seconds.', 'Drink immediately.'] + }, +] + +export function Recipes() { + const [filter, setFilter] = useState('all') + const [expanded, setExpanded] = useState(null) + + const categories = ['all', 'dinner', 'breakfast', 'snack', 'baking'] + const filtered = filter === 'all' ? RECIPES : RECIPES.filter(r => r.category === filter) + + return ( +
+

📖 Recipe Book

+
+ {categories.map(c => ( + + ))} +
+ +
+ {filtered.map(r => ( +
+
setExpanded(expanded === r.id ? null : r.id)}> + {r.icon} +
+
{r.name}
+
{r.macros.cals} cal • {r.macros.protein}g protein • {r.time}
+
+ {expanded === r.id ? '▼' : '▶'} +
+ {expanded === r.id && ( +
+
+ 🔥 {r.macros.cals} cal + 🥩 {r.macros.protein}g protein + 🧈 {r.macros.fat}g fat + 🌾 {r.macros.carbs}g carbs + 🌿 {r.macros.fibre}g fibre +
+

Ingredients

+
    + {r.ingredients.map((ing, i) =>
  • {ing}
  • )} +
+

Method

+
    + {r.method.map((step, i) =>
  1. {step}
  2. )} +
+ {r.servings > 1 &&
Makes {r.servings} servings (macros shown per serving)
} +
+ )} +
+ ))} +
+
+ ) +}