Feature: expanded equipment, integration-aware sync, tooltips

1. Equipment (onboarding step 5):
   - Changed from 3 radio buttons to 14 multi-select chips
   - Bicycle, pool, free weights, squat rack, bench press, machines,
     resistance bands, pull-up bar, kettlebell, jump rope, yoga mat,
     treadmill, stationary bike, rowing machine
   - Backend: equipment_list JSONB field on profile model
   - Resource matcher updated to filter by specific equipment
   - Legacy equipment_access auto-derived for backward compat

2. Sync Strava/Polar (dashboard):
   - Fetches integration status on load
   - Shows 'Connect Strava/Polar' (dashed border) when not connected
   - Shows 'Sync Strava/Polar' only when connected
   - Connect button triggers OAuth flow
   - Sync shows loading state and result count

3. Tooltips:
   - Tip component: hover/click to show explanation bubble
   - Added to: goals, TTM stages, PAR-Q+, BREQ-2 items,
     equipment, rewards gate, daily points, activity checklist,
     weekly progress, integrations section
   - Science references: TTM, PAR-Q+, BREQ-2/RAI explained
This commit is contained in:
claude 2026-03-15 22:16:55 +00:00
parent 434db38f0d
commit 82c4533a96
6 changed files with 270 additions and 66 deletions

View file

@ -42,7 +42,8 @@ class UserProfile(Base):
# Preferences
activity_preferences: Mapped[dict] = mapped_column(JSONB, default=list)
equipment_access: Mapped[str | None] = mapped_column(String(50))
equipment_access: Mapped[str | None] = mapped_column(String(50)) # legacy — kept for compat
equipment_list: Mapped[dict] = mapped_column(JSONB, default=list) # new: list of equipment strings
days_per_week: Mapped[int] = mapped_column(Integer, default=3)
minutes_per_session: Mapped[int] = mapped_column(Integer, default=30)

View file

@ -119,6 +119,7 @@ async def save_phase2(
# Preferences
profile.activity_preferences = req.activity_preferences
profile.equipment_access = req.equipment_access
profile.equipment_list = req.equipment_list
profile.days_per_week = req.days_per_week
profile.minutes_per_session = req.minutes_per_session
profile.phase2_completed = True

View file

@ -30,7 +30,8 @@ class Phase2Request(BaseModel):
# Preferences
activity_preferences: list[str] = []
equipment_access: str | None = None # none, basic_home, full_gym
equipment_access: str | None = None # legacy single value
equipment_list: list[str] = [] # new: list of equipment tags
days_per_week: int = 3
minutes_per_session: int = 30

View file

@ -8,6 +8,14 @@ from app.models.resource import Resource
from app.models.profile import UserProfile
# Equipment categories that satisfy each resource requirement
EQUIPMENT_SATISFIES = {
"none": set(), # bodyweight — always available
"basic_home": {"dumbbells", "resistance_bands", "pull_up_bar", "kettlebell"},
"full_gym": {"barbell", "squat_rack", "bench_press", "machines", "cable_machine", "dumbbells"},
}
async def get_recommendations(db: AsyncSession, user_id: uuid.UUID) -> list[dict]:
profile_result = await db.execute(
select(UserProfile).where(UserProfile.user_id == user_id)
@ -17,10 +25,21 @@ async def get_recommendations(db: AsyncSession, user_id: uuid.UUID) -> list[dict
return []
query = select(Resource).where(Resource.is_active == True)
if profile.equipment_access == "none":
# Filter by equipment: if user has specific equipment, determine what resource levels they can do
user_equipment = set(profile.equipment_list or [])
if not user_equipment:
# No equipment selected — only show bodyweight programs
query = query.where(Resource.equipment_needed == "none")
elif profile.equipment_access == "basic_home":
elif user_equipment & EQUIPMENT_SATISFIES["full_gym"]:
# Has gym-level equipment — show everything
pass
elif user_equipment & EQUIPMENT_SATISFIES["basic_home"]:
# Has some home equipment — show none + basic_home
query = query.where(Resource.equipment_needed.in_(["none", "basic_home"]))
else:
# Only has non-gym items (bicycle, pool, etc.) — show bodyweight
query = query.where(Resource.equipment_needed == "none")
result = await db.execute(query)
resources = result.scalars().all()