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 (
-
- )
-}
-
-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
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-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 02251f4..0000000
Binary files a/src/assets/hero.png and /dev/null differ
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 @@
-
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 (
+
+
+
+
+
{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
+
+
+
+
+
+
+
+
📍 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) => - {step}
)}
+
+ {r.servings > 1 &&
Makes {r.servings} servings (macros shown per serving)
}
+
+ )}
+
+ ))}
+
+
+ )
+}