import React, { useState, useEffect, useCallback } from "react"; import { Dumbbell, UtensilsCrossed, Droplet, CheckCircle2, Circle, Flame, RotateCcw, ChevronRight, } from "lucide-react"; // ---------- Design tokens ---------- const C = { bg: "#111C27", panel: "#1B2A38", panelSoft: "#20313F", line: "#2E4356", amber: "#E8A23D", amberDim: "#8A6127", green: "#6FA96A", text: "#F2EEE4", muted: "#8DA0B0", mutedSoft: "#5D7182", }; const FONT_IMPORT = ` @import url('https://fonts.googleapis.com/css2?family=Oswald:wght@500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500;700&display=swap'); `; // ---------- Plan data ---------- const DAYS = [ { id: "d1", label: "Dia 1", focus: "Cardio + Educativos de Corrida", sections: [ { title: "Aquecimento", time: "5 min", items: ["Marcha estacionária", "Rotação de articulações"] }, { title: "Parte principal", time: "20 min", items: [ "Round 1 — 2min caminhada rápida", "Round 1 — 1min trote leve / marcha acelerada", "Round 2 — 2min caminhada rápida", "Round 2 — 1min trote leve / marcha acelerada", "Round 3 — 2min caminhada rápida", "Round 3 — 1min trote leve / marcha acelerada", "Repetir até 6 rounds (30s descanso entre eles)", ], }, { title: "Desaquecimento", time: "5 min", items: ["Caminhada lenta", "Alongamento de pernas"] }, ], }, { id: "d2", label: "Dia 2", focus: "Força Superior + Core (flexão)", sections: [ { title: "Aquecimento", time: "5 min", items: ["Polichinelos leves", "Rotação de ombros"] }, { title: "Parte principal", time: "20 min · 3 voltas", items: [ "Flexão apoiada (joelho ou parede) — 8 a 10 reps", "Prancha apoiada no joelho — 15 a 20s", "Remada com toalha, elástico ou garrafa — 12 reps", "Descanso 45s entre exercícios", ], }, { title: "Desaquecimento", time: "5 min", items: ["Alongamento de peito", "Alongamento de ombros e core"] }, ], }, { id: "d3", label: "Dia 3", focus: "HIIT Cardio", sections: [ { title: "Aquecimento", time: "5 min", items: ["Mobilidade geral"] }, { title: "Parte principal", time: "20 min · 8 rounds", items: [ "30s intenso — agachamento com salto ou polichinelo", "30s descanso", "Repetir por 8 rounds completos", ], }, { title: "Desaquecimento", time: "5 min", items: ["Respiração guiada", "Alongamento geral"] }, ], }, { id: "d4", label: "Dia 4", focus: "Força Inferior + Core", sections: [ { title: "Aquecimento", time: "5 min", items: ["Mobilidade geral"] }, { title: "Parte principal", time: "20 min · 3 voltas", items: [ "Agachamento livre — 12 reps", "Abdominal curto (crunch) — 10 a 12 reps", "Afundo estático, cada perna — 8 reps", "Prancha — 15 a 20s", "Descanso 45s entre exercícios", ], }, { title: "Desaquecimento", time: "5 min", items: ["Alongamento de pernas", "Alongamento lombar"] }, ], }, { id: "d5", label: "Dia 5", focus: "Circuito Simulado TAF", sections: [ { title: "Aquecimento", time: "5 min", items: ["Mobilidade geral"] }, { title: "Parte principal", time: "20 min · circuito", items: [ "2min corrida ou caminhada rápida", "Flexões facilitadas — o máximo possível", "Abdominais — o máximo possível", "Descanso entre blocos, repetir o circuito", ], }, { title: "Desaquecimento", time: "5 min", items: ["Alongamento completo"] }, ], }, ]; const MEALS = { cafe: { label: "Café da manhã", options: [ "3 ovos mexidos + 1 fatia de pão integral + 1 fruta", "Aveia (4 col. sopa) com leite desnatado + banana + canela", "Tapioca com ovo mexido + queijo cottage", ], }, almoco: { label: "Almoço", options: [ "Arroz integral + feijão + frango grelhado + salada crua", "Batata-doce + carne moída magra + legumes refogados", "Arroz + ovo + brócolis + cenoura no vapor", ], }, lanche: { label: "Lanche", options: [ "Iogurte natural + granola + fruta", "2 ovos cozidos + 1 fruta", "Queijo cottage + torradas integrais", ], }, jantar: { label: "Jantar", options: [ "Sopa de legumes com frango desfiado", "Omelete de claras com legumes", "Peixe grelhado (sardinha/tilápia) + salada verde", ], }, }; const MACROS = [ { key: "prot", label: "Proteína", grams: 150, kcal: 600, pct: 34, color: C.amber }, { key: "carb", label: "Carboidrato", grams: 150, kcal: 600, pct: 34, color: C.green }, { key: "gord", label: "Gordura", grams: 58, kcal: 550, pct: 32, color: "#7FA7C9" }, ]; const WATER_GOAL_ML = 3500; const WATER_CUP_ML = 250; const STORAGE_KEY = "taf-tracker-state-v1"; const defaultState = () => ({ checks: {}, // { d1: [bool,...] } water: 0, // ml meals: {}, // { cafe: optionIndex } }); export default function TafTracker() { const [tab, setTab] = useState("treino"); const [dayIdx, setDayIdx] = useState(0); const [mealKey, setMealKey] = useState("cafe"); const [state, setState] = useState(defaultState()); const [loaded, setLoaded] = useState(false); useEffect(() => { (async () => { try { const res = await window.storage.get(STORAGE_KEY, false); if (res && res.value) setState(JSON.parse(res.value)); } catch (e) { // no saved state yet } finally { setLoaded(true); } })(); }, []); const persist = useCallback(async (next) => { setState(next); try { await window.storage.set(STORAGE_KEY, JSON.stringify(next), false); } catch (e) { console.error("Falha ao salvar progresso", e); } }, []); if (!loaded) { return ( <div style={{ background: C.bg, minHeight: "100vh" }} className="flex items-center justify-center"> <style>{FONT_IMPORT}</style> <p style={{ color: C.muted, fontFamily: "Inter, sans-serif" }}>Carregando…</p> </div> ); } const day = DAYS[dayIdx]; const dayChecks = state.checks[day.id] || flatItemCount(day) && new Array(flatItemCount(day)).fill(false); const totalItemsInDay = flatItemCount(day); function flatItemCount(d) { return d.sections.reduce((acc, s) => acc + s.items.length, 0); } function toggleCheck(flatIndex) { const arr = (state.checks[day.id] ? [...state.checks[day.id]] : new Array(totalItemsInDay).fill(false)); arr[flatIndex] = !arr[flatIndex]; const nextChecks = { ...state.checks, [day.id]: arr }; persist({ ...state, checks: nextChecks }); } function resetDay() { const nextChecks = { ...state.checks, [day.id]: new Array(totalItemsInDay).fill(false) }; persist({ ...state, checks: nextChecks }); } function addWater() { const next = Math.min(WATER_GOAL_ML + WATER_CUP_ML * 2, state.water + WATER_CUP_ML); persist({ ...state, water: next }); } function removeWater() { const next = Math.max(0, state.water - WATER_CUP_ML); persist({ ...state, water: next }); } function resetWater() { persist({ ...state, water: 0 }); } function selectMeal(key, idx) { const nextMeals = { ...state.meals, [key]: state.meals[key] === idx ? undefined : idx }; persist({ ...state, meals: nextMeals }); } // Weekly completion for the badge ring const weekPct = computeWeekPct(state.checks); return ( <div style={{ background: C.bg, minHeight: "100vh", fontFamily: "Inter, sans-serif" }}> <style>{FONT_IMPORT}</style> <div className="max-w-md mx-auto pb-12"> <Header weekPct={weekPct} /> {/* Segmented tabs */} <div className="px-5 mt-5"> <div className="flex rounded-full p-1" style={{ background: C.panel, border: `1px solid ${C.line}` }} > <TabButton active={tab === "treino"} onClick={() => setTab("treino")} icon={<Dumbbell size={16} />} label="Treino" /> <TabButton active={tab === "nutricao"} onClick={() => setTab("nutricao")} icon={<UtensilsCrossed size={16} />} label="Nutrição" /> </div> </div> {tab === "treino" ? ( <TreinoTab day={day} dayIdx={dayIdx} setDayIdx={setDayIdx} checks={state.checks[day.id] || new Array(totalItemsInDay).fill(false)} toggleCheck={toggleCheck} resetDay={resetDay} /> ) : ( <NutricaoTab water={state.water} addWater={addWater} removeWater={removeWater} resetWater={resetWater} mealKey={mealKey} setMealKey={setMealKey} selectedMeals={state.meals} selectMeal={selectMeal} /> )} </div> </div> ); } function computeWeekPct(checks) { let done = 0; let total = 0; DAYS.forEach((d) => { const items = d.sections.reduce((acc, s) => acc + s.items.length, 0); total += items; const arr = checks[d.id] || []; done += arr.filter(Boolean).length; }); if (total === 0) return 0; return Math.round((done / total) * 100); } function Header({ weekPct }) { const r = 22; const circ = 2 * Math.PI * r; const offset = circ - (weekPct / 100) * circ; return ( <div className="px-5 pt-6 flex items-center justify-between"> <div> <div style={{ color: C.amber, fontFamily: "JetBrains Mono, monospace", fontSize: "11px", letterSpacing: "0.18em", }} > PREPARAÇÃO GCM </div> <div style={{ color: C.text, fontFamily: "Oswald, sans-serif", fontWeight: 600, fontSize: "26px", letterSpacing: "0.03em", lineHeight: 1.1, marginTop: "2px", }} > OPERAÇÃO TAF </div> </div> <div className="relative flex items-center justify-center" style={{ width: 56, height: 56 }}> <svg width="56" height="56" viewBox="0 0 56 56"> <circle cx="28" cy="28" r={r} fill="none" stroke={C.line} strokeWidth="4" /> <circle cx="28" cy="28" r={r} fill="none" stroke={C.amber} strokeWidth="4" strokeLinecap="round" strokeDasharray={circ} strokeDashoffset={offset} transform="rotate(-90 28 28)" /> </svg> <span className="absolute" style={{ fontFamily: "JetBrains Mono, monospace", fontSize: "12px", fontWeight: 700, color: C.text, }} > {weekPct}% </span> </div> </div> ); } function TabButton({ active, onClick, icon, label }) { return ( <button onClick={onClick} className="flex-1 flex items-center justify-center gap-2 rounded-full py-2 transition-colors" style={{ background: active ? C.amber : "transparent", color: active ? "#1B140A" : C.muted, fontFamily: "Inter, sans-serif", fontWeight: 600, fontSize: "14px", }} > {icon} {label} </button> ); } function TreinoTab({ day, dayIdx, setDayIdx, checks, toggleCheck, resetDay }) { let flatIndex = -1; const doneCount = checks.filter(Boolean).length; const total = checks.length; const dayPct = total ? Math.round((doneCount / total) * 100) : 0; return ( <div className="px-5 mt-5"> {/* Day chips */} <div className="flex gap-2 overflow-x-auto pb-1" style={{ scrollbarWidth: "none" }}> {DAYS.map((d, i) => ( <button key={d.id} onClick={() => setDayIdx(i)} className="shrink-0 rounded-xl px-3 py-2 text-left" style={{ background: i === dayIdx ? C.panelSoft : C.panel, border: `1px solid ${i === dayIdx ? C.amber : C.line}`, minWidth: "92px", }} > <div style={{ fontFamily: "JetBrains Mono, monospace", fontSize: "10px", color: i === dayIdx ? C.amber : C.mutedSoft, letterSpacing: "0.08em", }} > {d.label.toUpperCase()} </div> <div style={{ fontFamily: "Inter, sans-serif", fontSize: "11px", color: i === dayIdx ? C.text : C.muted, marginTop: "2px", lineHeight: 1.25, }} > {d.focus} </div> </button> ))} </div> {/* Day header */} <div className="flex items-center justify-between mt-5"> <div> <div style={{ fontFamily: "Oswald, sans-serif", color: C.text, fontSize: "19px", fontWeight: 600, letterSpacing: "0.02em", }} > {day.label} — {day.focus} </div> <div style={{ fontFamily: "JetBrains Mono, monospace", color: C.muted, fontSize: "11px", marginTop: "3px", }} > {doneCount}/{total} concluídos · {dayPct}% </div> </div> <button onClick={resetDay} title="Reiniciar dia" style={{ color: C.mutedSoft }}> <RotateCcw size={18} /> </button> </div> {/* Progress bar */} <div className="mt-2 rounded-full overflow-hidden" style={{ background: C.line, height: "6px" }}> <div style={{ width: `${dayPct}%`, background: C.amber, height: "100%", transition: "width 0.3s ease", }} /> </div> {/* Sections */} <div className="mt-5 flex flex-col gap-4"> {day.sections.map((section) => ( <div key={section.title} className="rounded-2xl p-4" style={{ background: C.panel, border: `1px solid ${C.line}` }} > <div className="flex items-center justify-between mb-3"> <div style={{ fontFamily: "Oswald, sans-serif", color: C.amber, fontSize: "13px", letterSpacing: "0.08em", fontWeight: 600, }} > {section.title.toUpperCase()} </div> <div style={{ fontFamily: "JetBrains Mono, monospace", color: C.mutedSoft, fontSize: "11px", }} > {section.time} </div> </div> <div className="flex flex-col gap-2"> {section.items.map((item) => { flatIndex += 1; const idx = flatIndex; const checked = !!checks[idx]; return ( <button key={idx} onClick={() => toggleCheck(idx)} className="flex items-start gap-2 text-left rounded-lg py-1" > {checked ? ( <CheckCircle2 size={18} color={C.green} className="shrink-0 mt-0.5" /> ) : ( <Circle size={18} color={C.mutedSoft} className="shrink-0 mt-0.5" /> )} <span style={{ fontFamily: "Inter, sans-serif", fontSize: "14px", color: checked ? C.mutedSoft : C.text, textDecoration: checked ? "line-through" : "none", lineHeight: 1.35, }} > {item} </span> </button> ); })} </div> </div> ))} </div> </div> ); } function NutricaoTab({ water, addWater, removeWater, resetWater, mealKey, setMealKey, selectedMeals, selectMeal }) { const cups = Math.round(WATER_GOAL_ML / WATER_CUP_ML); const filledCups = Math.round(water / WATER_CUP_ML); const waterPct = Math.min(100, Math.round((water / WATER_GOAL_ML) * 100)); return ( <div className="px-5 mt-5 flex flex-col gap-4"> {/* Calorie / macro card */} <div className="rounded-2xl p-4" style={{ background: C.panel, border: `1px solid ${C.line}` }}> <div className="flex items-center gap-2 mb-1"> <Flame size={16} color={C.amber} /> <span style={{ fontFamily: "Oswald, sans-serif", color: C.text, fontSize: "13px", letterSpacing: "0.08em", fontWeight: 600, }} > META CALÓRICA </span> </div> <div style={{ fontFamily: "JetBrains Mono, monospace", color: C.amber, fontSize: "28px", fontWeight: 700, marginTop: "4px", }} > 1.750 <span style={{ fontSize: "13px", color: C.muted, fontWeight: 500 }}>kcal/dia</span> </div> <div className="flex flex-col gap-3 mt-4"> {MACROS.map((m) => ( <div key={m.key}> <div className="flex items-center justify-between mb-1"> <span style={{ fontFamily: "Inter, sans-serif", color: C.text, fontSize: "13px" }}> {m.label} </span> <span style={{ fontFamily: "JetBrains Mono, monospace", color: C.muted, fontSize: "11px", }} > {m.grams}g · {m.pct}% </span> </div> <div className="rounded-full overflow-hidden" style={{ background: C.line, height: "6px" }}> <div style={{ width: `${m.pct}%`, background: m.color, height: "100%" }} /> </div> </div> ))} </div> </div> {/* Water tracker */} <div className="rounded-2xl p-4" style={{ background: C.panel, border: `1px solid ${C.line}` }}> <div className="flex items-center justify-between mb-3"> <div className="flex items-center gap-2"> <Droplet size={16} color="#7FB6D9" /> <span style={{ fontFamily: "Oswald, sans-serif", color: C.text, fontSize: "13px", letterSpacing: "0.08em", fontWeight: 600, }} > HIDRATAÇÃO </span> </div> <button onClick={resetWater} style={{ color: C.mutedSoft }}> <RotateCcw size={16} /> </button> </div> <div className="flex items-baseline gap-2"> <span style={{ fontFamily: "JetBrains Mono, monospace", color: C.text, fontSize: "22px", fontWeight: 700 }}> {(water / 1000).toFixed(2)}L </span> <span style={{ fontFamily: "Inter, sans-serif", color: C.muted, fontSize: "12px" }}> de {(WATER_GOAL_ML / 1000).toFixed(1)}L · {waterPct}% </span> </div> <div className="flex flex-wrap gap-1.5 mt-3"> {Array.from({ length: cups }).map((_, i) => ( <Droplet key={i} size={20} color={i < filledCups ? "#7FB6D9" : C.line} fill={i < filledCups ? "#7FB6D9" : "none"} /> ))} </div> <div className="flex gap-2 mt-4"> <button onClick={removeWater} className="flex-1 rounded-xl py-2" style={{ background: C.panelSoft, color: C.text, fontFamily: "Inter, sans-serif", fontSize: "13px", fontWeight: 600 }} > − 250ml </button> <button onClick={addWater} className="flex-1 rounded-xl py-2" style={{ background: "#7FB6D9", color: "#0E2733", fontFamily: "Inter, sans-serif", fontSize: "13px", fontWeight: 600 }} > + 250ml </button> </div> </div> {/* Meals */} <div className="rounded-2xl p-4" style={{ background: C.panel, border: `1px solid ${C.line}` }}> <div style={{ fontFamily: "Oswald, sans-serif", color: C.text, fontSize: "13px", letterSpacing: "0.08em", fontWeight: 600, marginBottom: "12px", }} > REFEIÇÕES </div> <div className="flex gap-1.5 overflow-x-auto pb-1" style={{ scrollbarWidth: "none" }}> {Object.entries(MEALS).map(([key, meal]) => ( <button key={key} onClick={() => setMealKey(key)} className="shrink-0 rounded-full px-3 py-1.5" style={{ background: mealKey === key ? C.amber : C.panelSoft, color: mealKey === key ? "#1B140A" : C.muted, fontFamily: "Inter, sans-serif", fontSize: "12px", fontWeight: 600, }} > {meal.label} </button> ))} </div> <div className="flex flex-col gap-2 mt-3"> {MEALS[mealKey].options.map((opt, idx) => { const selected = selectedMeals[mealKey] === idx; return ( <button key={idx} onClick={() => selectMeal(mealKey, idx)} className="flex items-center gap-2 text-left rounded-xl p-3" style={{ background: selected ? C.panelSoft : "transparent", border: `1px solid ${selected ? C.amber : C.line}`, }} > {selected ? ( <CheckCircle2 size={16} color={C.amber} className="shrink-0" /> ) : ( <ChevronRight size={16} color={C.mutedSoft} className="shrink-0" /> )} <span style={{ fontFamily: "Inter, sans-serif", fontSize: "13px", color: selected ? C.text : C.muted, lineHeight: 1.35, }} > {opt} </span> </button> ); })} </div> </div> </div> ); }