From c133a463ecde40f9dd386b66d3b5eaf20aa911b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 14:40:34 +0000 Subject: [PATCH] Deliver Step 9: agent-card.json, SKILL.md, USDA lookup, integrations dynamic config, Welcome/Integrations/MealPlan pages, API methods, README, routes --- README.md | 85 ++++++---- backend/app/routers/integrations.py | 114 +++++++++++++ backend/app/services/usda_lookup.py | 59 +++++++ frontend/public/.well-known/agent-card.json | 102 ++++++++++++ .../skills/diligence-fitness/SKILL.md | 50 ++++++ frontend/src/App.jsx | 3 + frontend/src/api.js | 17 ++ frontend/src/pages/MealPlan.jsx | 156 ++++++++++++++++++ frontend/src/pages/SettingsIntegrations.jsx | 137 +++++++++++++++ frontend/src/pages/Welcome.jsx | 61 +++++++ 10 files changed, 751 insertions(+), 33 deletions(-) create mode 100644 backend/app/services/usda_lookup.py create mode 100644 frontend/public/.well-known/agent-card.json create mode 100644 frontend/public/.well-known/skills/diligence-fitness/SKILL.md create mode 100644 frontend/src/pages/MealPlan.jsx create mode 100644 frontend/src/pages/SettingsIntegrations.jsx create mode 100644 frontend/src/pages/Welcome.jsx diff --git a/README.md b/README.md index 297a113..63dd685 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,61 @@ -# Fitness Rewards +# Diligence — Self-Hosted Fitness Rewards with AI Agent Support -Personal fitness web app with a points-based behavioral economy. Earn points through fitness activities, spend them on real-world rewards. +Points-based fitness accountability app. Log workouts and food, earn points, unlock rewards. Connect any AI agent via MCP for logging, coaching, and meal planning. Self-hosted, open source, zero cloud dependencies. -## Stack -- **Backend**: FastAPI + PostgreSQL + SQLAlchemy (async) -- **Frontend**: React + Vite -- **Deployment**: Docker Compose via Coolify +Built by [DiligenceWorks](https://diligenceworks.online). -## Quick Start (Development) +## Quick Start ```bash -# Backend -cd backend -python -m venv .venv && source .venv/bin/activate -pip install -r requirements.txt -uvicorn app.main:app --reload - -# Frontend -cd frontend -npm install -npm run dev +git clone https://github.com/diligenceworks/diligence +cd diligence +./setup.sh +docker compose up -d ``` -## Deploy (Production) - -```bash -cp .env.example .env -# Edit .env with real secrets -docker compose up -d --build -``` +Open http://localhost and create your account. ## Features -- Science-based onboarding (PAR-Q+, TTM Stages of Change, BREQ-2) -- Points economy with configurable earning rules and rewards -- Daily gate: earn your daily minimum before unlocking rewards -- Weekly targets with reset -- Strava + Polar integration (OAuth 2.0) -- Open Food Facts barcode scanning for food logging -- Curated external fitness resources (Darebee, StrongLifts, YouTube) -- 90-day program commitment tracking -- Mobile-first PWA design + +- **Points economy** — earn from workouts, food logging, step goals. Daily gate locks rewards until you earn enough. Weekly reset. +- **Science-based onboarding** — PAR-Q+ safety screening, TTM stages of change, BREQ-2 motivation profiling. +- **AI agent integration** — 14 MCP tools for logging, coaching, meal planning, and device configuration. +- **Meal plans** — AI-generated plans with compliance tracking and points integration. +- **Activity sync** — Strava, Polar (OAuth 2.0). Garmin, Fitbit, Withings, WHOOP, Oura configurable in-app. +- **Food search** — Open Food Facts (4M+ products) + USDA FoodData Central (400K+ research-grade). +- **Program tracking** — 90-day structured programs (StrongLifts, Darebee, etc.) with day-by-day progression. +- **Configurable rewards** — you define what's worth earning. Gaming time, screen time, treats — your rules. + +## Connecting an AI Agent + +Point your agent's MCP config to `http://localhost:3001/sse` (development) or `https://your-domain/mcp` (production behind reverse proxy). + +Works with Claude Desktop, Cursor, Windsurf, and any MCP-compatible agent. + +Copy the contents of [AGENT_GUIDE.md](AGENT_GUIDE.md) into your agent's system instructions for motivation-aware coaching. + +## Configuring Integrations + +All integrations are configured through the app UI (Settings → Integrations) or through your AI agent. No `.env` file editing or container restarts needed. + +## Data Sovereignty + +Diligence is self-hosted software. Your data never leaves your server. No cloud dependency, no vendor lock-in, no telemetry. MIT license — fork it, modify it, keep it forever. + +Your fitness data — heart rate, body composition, food intake, GPS traces — is biometric data that deserves sovereignty. + +## Stack + +- Python 3.12, FastAPI, SQLAlchemy +- React 18, Vite +- PostgreSQL 16 +- FastMCP (Streamable HTTP/SSE) +- Docker Compose + +## Contributing + +Contributions welcome. Open an issue to discuss before submitting a PR. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/backend/app/routers/integrations.py b/backend/app/routers/integrations.py index 5fa91de..8562a7b 100644 --- a/backend/app/routers/integrations.py +++ b/backend/app/routers/integrations.py @@ -153,3 +153,117 @@ async def disconnect( if token: await db.delete(token) return {"status": "disconnected", "provider": provider} + + +# --- Dynamic Integration Config (v3) --- + +from pydantic import BaseModel +from app.models.integration_config import IntegrationConfig +from app.services.provider_registry import PROVIDER_REGISTRY +from app.services.crypto import encrypt_value, decrypt_value +from app.config import settings + + +class ConfigureRequest(BaseModel): + provider: str + credentials: dict[str, str] + + +@router.get("/status") +async def full_integration_status( + user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +): + """Return connection status for all providers. Never returns actual credentials.""" + # OAuth tokens (Strava, Polar) + oauth_result = await db.execute(select(OAuthToken).where(OAuthToken.user_id == user.id)) + oauth_tokens = {t.provider: True for t in oauth_result.scalars().all()} + + # Dynamic config entries + config_result = await db.execute( + select(IntegrationConfig.provider) + .where(IntegrationConfig.user_id == user.id) + .distinct() + ) + configured_providers = {row[0] for row in config_result.all()} + + # Also check env vars for backward compatibility + env_providers = set() + if settings.strava_client_id: + env_providers.add("strava") + if settings.polar_client_id: + env_providers.add("polar") + if settings.telegram_bot_token: + env_providers.add("telegram") + if settings.groq_api_key: + env_providers.add("groq") + + status = {} + for key, info in PROVIDER_REGISTRY.items(): + if key in oauth_tokens: + status[key] = "connected" + elif key in configured_providers or key in env_providers: + status[key] = "configured" + else: + status[key] = "not_configured" + + return status + + +@router.get("/providers") +async def list_providers(): + """Return the provider registry with setup instructions.""" + result = {} + for key, info in PROVIDER_REGISTRY.items(): + result[key] = { + "name": info["name"], + "type": info["type"], + "fields": info["fields"], + "help_url": info.get("help_url", ""), + "help_text": info.get("help_text", "").replace("{BASE_URL}", settings.base_url), + } + return result + + +@router.post("/configure") +async def configure_integration( + body: ConfigureRequest, + user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +): + """Store encrypted integration credentials. Write-only — values can never be read back.""" + if body.provider not in PROVIDER_REGISTRY: + raise HTTPException(400, f"Unknown provider: {body.provider}") + + info = PROVIDER_REGISTRY[body.provider] + expected_fields = set(info["fields"]) + provided_fields = set(body.credentials.keys()) + + missing = expected_fields - provided_fields + if missing: + raise HTTPException(400, f"Missing required fields: {', '.join(missing)}") + + for key, value in body.credentials.items(): + encrypted = encrypt_value(settings.secret_key, value) + + # Upsert + existing = await db.execute( + select(IntegrationConfig).where( + IntegrationConfig.user_id == user.id, + IntegrationConfig.provider == body.provider, + IntegrationConfig.config_key == key, + ) + ) + row = existing.scalar_one_or_none() + if row: + row.config_value = encrypted + row.updated_at = datetime.now(timezone.utc) + else: + db.add(IntegrationConfig( + user_id=user.id, + provider=body.provider, + config_key=key, + config_value=encrypted, + )) + + return {"status": "configured", "provider": body.provider} diff --git a/backend/app/services/usda_lookup.py b/backend/app/services/usda_lookup.py new file mode 100644 index 0000000..fb00238 --- /dev/null +++ b/backend/app/services/usda_lookup.py @@ -0,0 +1,59 @@ +"""USDA FoodData Central lookup service. + +Free API with 400K+ foods and research-grade nutrition data. +API key required (free, instant signup at https://fdc.nal.usda.gov/api-key-signup). +""" +from __future__ import annotations + +import logging +import httpx + +logger = logging.getLogger(__name__) + +USDA_BASE = "https://api.nal.usda.gov/fdc/v1" + + +async def search_usda(query: str, api_key: str, page_size: int = 10) -> list[dict]: + """Search USDA FoodData Central for food items. + + Returns a list of dicts with: fdcId, description, brandName, calories, + protein_g, carbs_g, fat_g, serving_size. + """ + if not api_key: + return [] + + try: + async with httpx.AsyncClient(timeout=15) as client: + resp = await client.get( + f"{USDA_BASE}/foods/search", + params={ + "api_key": api_key, + "query": query, + "pageSize": page_size, + "dataType": "Foundation,SR Legacy,Branded", + }, + ) + resp.raise_for_status() + data = resp.json() + + results = [] + for food in data.get("foods", []): + nutrients = {n["nutrientName"]: n["value"] for n in food.get("foodNutrients", [])} + results.append({ + "fdcId": food.get("fdcId"), + "description": food.get("description", ""), + "brandName": food.get("brandName"), + "source": "usda", + "calories": nutrients.get("Energy"), + "protein_g": nutrients.get("Protein"), + "carbs_g": nutrients.get("Carbohydrate, by difference"), + "fat_g": nutrients.get("Total lipid (fat)"), + "fiber_g": nutrients.get("Fiber, total dietary"), + "serving_size": food.get("servingSize"), + "serving_unit": food.get("servingSizeUnit"), + }) + return results + + except Exception as e: + logger.warning(f"USDA search failed: {e}") + return [] diff --git a/frontend/public/.well-known/agent-card.json b/frontend/public/.well-known/agent-card.json new file mode 100644 index 0000000..d9b04f7 --- /dev/null +++ b/frontend/public/.well-known/agent-card.json @@ -0,0 +1,102 @@ +{ + "name": "Diligence Fitness Rewards", + "description": "Self-hosted fitness rewards platform with points-based behavioral economy. Your fitness data stays on your hardware. AI agents can log activities, query progress, manage meal plans, and configure device integrations via MCP.", + "url": "{BASE_URL}/mcp", + "provider": { + "organization": "DiligenceWorks Pte. Ltd.", + "url": "https://diligenceworks.online" + }, + "version": "1.0.0", + "documentationUrl": "https://github.com/diligenceworks/diligence", + "capabilities": { + "streaming": true, + "pushNotifications": false, + "stateTransitionHistory": false + }, + "authentication": { + "schemes": ["apiKey"], + "credentials": null + }, + "defaultInputModes": ["text/plain", "application/json"], + "defaultOutputModes": ["application/json"], + "skills": [ + { + "id": "context-status", + "name": "Context & Status", + "description": "Get full user profile with motivation type (BREQ-2), active program, points status, rewards, and integration connections. Includes get_context (full brief), get_today (daily points/gate), and get_week (weekly summary).", + "tags": ["fitness", "status", "profile", "motivation"], + "examples": [ + "How am I doing today?", + "What's my weekly progress?", + "Show me my fitness dashboard" + ] + }, + { + "id": "activity-logging", + "name": "Activity Logging", + "description": "Record workouts, runs, walks, cycling, yoga, or any physical activity. Awards points based on activity type and duration. Supports structured program workouts via program_day parameter.", + "tags": ["fitness", "tracking", "points", "workout"], + "examples": [ + "I just did a 30-minute run", + "Log my StrongLifts Workout B today", + "I did 45 minutes of yoga this morning" + ] + }, + { + "id": "nutrition-tracking", + "name": "Food & Nutrition", + "description": "Log food intake with calorie and macro tracking. Search Open Food Facts (4M+ products) and USDA FoodData Central (400K+ research-grade items). Awards points for consistent food logging.", + "tags": ["nutrition", "food", "tracking", "barcode", "search"], + "examples": [ + "I had 2 eggs and toast for breakfast", + "Search for chicken breast nutrition", + "Log a salad for lunch, about 400 calories" + ] + }, + { + "id": "program-schedule", + "name": "Program Schedule", + "description": "View today's scheduled workout from the active program including exercises, sets, reps, and weight progression. Shows upcoming workouts and overall program completion percentage.", + "tags": ["program", "schedule", "workout", "planning"], + "examples": [ + "What's my workout today?", + "Show me this week's program schedule", + "How far through my program am I?" + ] + }, + { + "id": "rewards", + "name": "Rewards", + "description": "View available rewards with point costs and spend earned points. Rewards are user-configurable (gaming time, screen time, treats, etc.). Points gate must be passed before redemption.", + "tags": ["rewards", "points", "spending", "gamification"], + "examples": [ + "What rewards can I unlock?", + "Spend points on 30 minutes of gaming", + "Do I have enough points for a movie?" + ] + }, + { + "id": "meal-planning", + "name": "Meal Planning", + "description": "Create, view, and track AI-generated meal plans. The agent generates plan content; the app stores and tracks compliance. Includes load_meal_plan, get_meal_plan, update_meal_compliance, and get_plan_progress.", + "tags": ["nutrition", "meal-plan", "diet", "compliance"], + "examples": [ + "Create a 7-day keto meal plan", + "What am I supposed to eat today?", + "I followed my breakfast plan", + "How's my meal plan compliance?" + ] + }, + { + "id": "device-integration", + "name": "Device Integration", + "description": "Configure connections to fitness devices and services (Strava, Polar, Garmin, Fitbit, WHOOP, Oura, Withings). Write-only credential storage — agent can set but never read credentials back.", + "tags": ["integration", "strava", "garmin", "fitbit", "sync"], + "examples": [ + "Connect my Strava account", + "What integrations are available?", + "I want to set up my Garmin" + ] + } + ] +} diff --git a/frontend/public/.well-known/skills/diligence-fitness/SKILL.md b/frontend/public/.well-known/skills/diligence-fitness/SKILL.md new file mode 100644 index 0000000..1597825 --- /dev/null +++ b/frontend/public/.well-known/skills/diligence-fitness/SKILL.md @@ -0,0 +1,50 @@ +# Diligence — Agent Skill Definition + +## What Is Diligence? + +Diligence is a self-hosted fitness rewards platform. Users earn points through fitness activities (workouts, food logging, step goals) and spend them on configurable rewards (gaming time, screen time, etc.). A daily gate keeps rewards locked until enough points are earned. Points reset weekly. + +## Architecture + +- **Backend:** FastAPI (Python 3.12) + PostgreSQL 16 +- **Frontend:** React 18 + Vite, served by nginx +- **MCP Connector:** FastMCP (Streamable HTTP/SSE) on port 3001, proxied via nginx at `/mcp` +- **Deployment:** Docker Compose (4 services: frontend, backend, mcp-connector, fitness-db) + +## Connecting + +Point your MCP client to: +- **Development:** `http://localhost:3001/sse` +- **Production:** `https://your-domain/mcp` + +## Available Tools (14) + +| Tool | Category | Description | +|------|----------|-------------| +| `get_context()` | Status | Full profile, motivation, program, rules, rewards | +| `get_today(date?)` | Status | Daily points, gate status, activities | +| `get_week(start_date?)` | Status | Weekly summary, active days | +| `log_activity(category, title, duration_minutes, ...)` | Activity | Log workout, earn points | +| `log_food(meal_type, food_name, ...)` | Food | Log food with macros | +| `search_food(query)` | Food | Search Open Food Facts + USDA | +| `get_program_schedule(date?)` | Programs | Today's workout from active program | +| `redeem_reward(reward_name)` | Rewards | Spend points on a reward | +| `load_meal_plan(name, duration_days, meals, ...)` | Meal Plans | Create a full meal plan | +| `get_meal_plan(date?)` | Meal Plans | View today's planned meals | +| `update_meal_compliance(plan_item_id, status, ...)` | Meal Plans | Mark meal followed/skipped | +| `get_plan_progress(plan_id?)` | Meal Plans | Compliance statistics | +| `configure_integration(provider, credentials)` | Integrations | Store encrypted credentials (write-only) | +| `get_integration_status()` | Integrations | Check provider connection status | + +## Key Design Decisions + +- The **agent generates meal plans** — the app only stores and tracks them. Zero AI API cost. +- Integration credentials are **write-only through MCP** — agents can set but never read them back. +- The app collects **BREQ-2 motivation data** during onboarding. Use `get_context()` to read the user's motivation profile and calibrate tone accordingly. +- Points reset weekly. Each week is a fresh start. No guilt carry-over. + +## Source + +- **Repository:** https://github.com/diligenceworks/diligence +- **License:** MIT +- **Built by:** DiligenceWorks Pte. Ltd. (https://diligenceworks.online) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index bb67945..327f3a8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,6 +10,9 @@ import Rewards from './pages/Rewards' import Settings from './pages/Settings' import Onboarding from './pages/Onboarding' import WeekView from './pages/WeekView' +import Welcome from './pages/Welcome' +import SettingsIntegrations from './pages/SettingsIntegrations' +import MealPlan from './pages/MealPlan' import ProgramSearch from './pages/ProgramSearch' import ProgramDetail from './pages/ProgramDetail' import CatalogDetail from './pages/CatalogDetail' diff --git a/frontend/src/api.js b/frontend/src/api.js index 398f0a2..93db0f1 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -122,6 +122,23 @@ export const api = { polarSync: () => request('/integrations/polar/sync', { method: 'POST' }), disconnect: (provider) => request(`/integrations/${provider}`, { method: 'DELETE' }), + + // Meal Plans (v3) + listMealPlans: () => request('/meal-plans'), + createMealPlan: (data) => request('/meal-plans', { method: 'POST', body: JSON.stringify(data) }), + getMealPlanToday: () => request('/meal-plans/today'), + getMealPlan: (id) => request('/meal-plans/' + id), + updateMealPlanStatus: (id, status) => + request('/meal-plans/' + id, { method: 'PATCH', body: JSON.stringify({ status }) }), + logMealCompliance: (data) => + request('/meal-plans/compliance', { method: 'POST', body: JSON.stringify(data) }), + getMealPlanProgress: (id) => request('/meal-plans/' + id + '/progress'), + + // Dynamic Integrations (v3) + fullIntegrationStatus: () => request('/integrations/status'), + listProviders: () => request('/integrations/providers'), + configureIntegration: (provider, credentials) => + request('/integrations/configure', { method: 'POST', body: JSON.stringify({ provider, credentials }) }), // Resources getResourceRecommendations: () => request('/onboarding/recommendations'), }; diff --git a/frontend/src/pages/MealPlan.jsx b/frontend/src/pages/MealPlan.jsx new file mode 100644 index 0000000..d3c75f7 --- /dev/null +++ b/frontend/src/pages/MealPlan.jsx @@ -0,0 +1,156 @@ +import { useState, useEffect } from 'react' +import { api } from '../api' + +const MEAL_ICONS = { breakfast: '🌅', lunch: '🌞', dinner: '🌙', snack: '🍎' } + +export default function MealPlan() { + const [plan, setPlan] = useState(null) + const [plans, setPlans] = useState([]) + const [loading, setLoading] = useState(true) + const [compliance, setCompliance] = useState({}) // plan_item_id → status + + useEffect(() => { loadAll() }, []) + + async function loadAll() { + try { + const [today, all] = await Promise.all([api.getMealPlanToday(), api.listMealPlans()]) + setPlan(today) + setPlans(all) + } catch (err) { console.error(err) } + finally { setLoading(false) } + } + + async function markCompliance(itemId, status) { + try { + await api.logMealCompliance({ plan_item_id: itemId, status }) + setCompliance(prev => ({ ...prev, [itemId]: status })) + } catch (err) { alert(`Failed: ${err.message}`) } + } + + if (loading) return
Loading...
+ + return ( +
+

Meal Plan

+ + {!plan?.active_plan ? ( +
+
🍽️
+

No active meal plan.

+

+ Ask your AI agent to create one: "Generate a 7-day keto meal plan for me" +

+
+ ) : ( + <> +
+ {plan.active_plan} + + Day {plan.day} / {plan.duration_days} + +
+ + {plan.daily_calories && ( +
+ Target: {plan.daily_calories} cal · {plan.diet_type || 'balanced'} +
+ )} + + {plan.meals?.length > 0 ? ( + plan.meals.map(meal => { + const itemStatus = compliance[meal.id] + return ( +
+
+
+
+ {MEAL_ICONS[meal.meal_type] || '🍽️'} {meal.meal_type} +
+
{meal.food_name}
+ {meal.description && ( +
{meal.description}
+ )} +
+ {meal.calories && ( +
+ {meal.calories} cal +
+ )} +
+ + {meal.protein_g && ( +
+ P: {meal.protein_g}g · C: {meal.carbs_g}g · F: {meal.fat_g}g +
+ )} + + {!itemStatus && ( +
+ {['followed', 'substituted', 'skipped'].map(s => ( + + ))} +
+ )} +
+ ) + }) + ) : ( +

{plan.message || 'No meals for today.'}

+ )} + + )} + + {plans.length > 0 && ( +
+

All Plans

+ {plans.map(p => ( +
+
+
{p.name}
+
+ {p.diet_type} · {p.duration_days} days · started {p.start_date} +
+
+ + {p.status} + +
+ ))} +
+ )} +
+ ) +} diff --git a/frontend/src/pages/SettingsIntegrations.jsx b/frontend/src/pages/SettingsIntegrations.jsx new file mode 100644 index 0000000..c274e9c --- /dev/null +++ b/frontend/src/pages/SettingsIntegrations.jsx @@ -0,0 +1,137 @@ +import { useState, useEffect } from 'react' +import { api } from '../api' + +const STATUS_COLORS = { + connected: '#4CAF50', + configured: '#FF9800', + not_configured: '#9E9E9E', +} + +const STATUS_LABELS = { + connected: 'Connected', + configured: 'Configured', + not_configured: 'Not configured', +} + +export default function SettingsIntegrations() { + const [providers, setProviders] = useState({}) + const [status, setStatus] = useState({}) + const [loading, setLoading] = useState(true) + const [configuring, setConfiguring] = useState(null) // provider key being configured + const [formData, setFormData] = useState({}) + const [saving, setSaving] = useState(false) + + useEffect(() => { loadAll() }, []) + + async function loadAll() { + try { + const [p, s] = await Promise.all([api.listProviders(), api.fullIntegrationStatus()]) + setProviders(p) + setStatus(s) + } catch (err) { console.error(err) } + finally { setLoading(false) } + } + + function startConfigure(key) { + setConfiguring(key) + const fields = providers[key]?.fields || [] + setFormData(Object.fromEntries(fields.map(f => [f, '']))) + } + + async function handleSave(key) { + setSaving(true) + try { + await api.configureIntegration(key, formData) + setConfiguring(null) + await loadAll() + } catch (err) { alert(`Failed: ${err.message}`) } + finally { setSaving(false) } + } + + if (loading) return
Loading...
+ + return ( +
+

Integrations

+

+ Configure your fitness devices and services. Credentials are encrypted and stored locally — never sent to any external server. +

+ + {Object.entries(providers).map(([key, info]) => ( +
+
+ {info.name} + + {STATUS_LABELS[status[key] || 'not_configured']} + +
+ +

+ {info.help_text} +

+ + {info.help_url && ( + + Developer portal → + + )} + + {configuring === key ? ( +
+ {info.fields.map(field => ( +
+ + setFormData({ ...formData, [field]: e.target.value })} + style={{ + width: '100%', padding: '8px 12px', borderRadius: 'var(--r-sm)', + border: '1px solid var(--border)', fontSize: '0.9rem', boxSizing: 'border-box', + }} + placeholder={`Enter ${field.replace(/_/g, ' ')}`} + /> +
+ ))} +
+ + +
+
+ ) : ( + + )} +
+ ))} +
+ ) +} diff --git a/frontend/src/pages/Welcome.jsx b/frontend/src/pages/Welcome.jsx new file mode 100644 index 0000000..29d4411 --- /dev/null +++ b/frontend/src/pages/Welcome.jsx @@ -0,0 +1,61 @@ +import { useNavigate } from 'react-router-dom' + +export default function Welcome() { + const navigate = useNavigate() + + return ( +
+
💪
+

Earn Before You Spend

+

+ This app runs on a simple idea: you earn points by doing healthy things + (workouts, logging meals, hitting step goals), and you spend points on + guilty pleasures you configure yourself. +

+ +
+
+ 🔒 +
+ Daily Gate +

+ Until you earn enough points today, your rewards stay locked. Hit your target, and the gate opens. +

+
+
+
+ 🎮 +
+ Your Rules +

+ You set the rewards — gaming time, takeout, screen time, whatever you want. You hold yourself accountable. +

+
+
+
+ 🤖 +
+ AI Agent (Optional) +

+ Connect an AI agent to log activities by voice, get nudged when you're behind, and have a fitness companion that knows your goals. +

+
+
+
+ + + +

+ Points reset weekly. Each week is a fresh start. +

+
+ ) +}