From 175043a9a3c279adf23c06a1ae26da5cc34dc753 Mon Sep 17 00:00:00 2001 From: sidadrian3 Date: Fri, 31 Jul 2026 11:59:58 +0800 Subject: [PATCH 1/9] feat: implement muscle volume calculation logic and add anatomy UI design specification --- .../2026-07-31-human-anatomy-ui-design.md | 71 +++++++++++++++++++ .../domain/__tests__/muscle-evaluator.test.ts | 54 ++++++++++++++ src/lib/domain/muscle-evaluator.ts | 38 ++++++++++ 3 files changed, 163 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-31-human-anatomy-ui-design.md create mode 100644 src/lib/domain/__tests__/muscle-evaluator.test.ts create mode 100644 src/lib/domain/muscle-evaluator.ts diff --git a/docs/superpowers/specs/2026-07-31-human-anatomy-ui-design.md b/docs/superpowers/specs/2026-07-31-human-anatomy-ui-design.md new file mode 100644 index 0000000..0ce6ce7 --- /dev/null +++ b/docs/superpowers/specs/2026-07-31-human-anatomy-ui-design.md @@ -0,0 +1,71 @@ +# Interactive Human Anatomy UI - Design Spec + +## 1. Goal and Purpose +Implement an interactive, visual representation of the human muscular system mapping to the `TargetMuscle` enum. This gamifies the workout experience by providing immediate visual feedback during workouts and tracking recovery/volume over time. + +## 2. Design System & Aesthetics (UI-UX-Pro-Max) +- **Theme**: Dark Mode (OLED) - Deep blacks (`#020617`, `#0F172A`) with high-contrast glowing accents. +- **Typography**: `Barlow Condensed` for labels, athletic and energetic. +- **Visual Effects**: + - Minimal glow (`text-shadow: 0 0 10px`, `drop-shadow` on SVGs) for "pumped" or active muscles. + - Smooth transitions (300ms `ease-in-out`) for color shifts. +- **Color Scale (Heatmap / Recovery)**: + - **Exhausted / High Volume (Active)**: Neon Red (`#EF4444`) with glow. + - **Recovering / Medium Volume**: Amber/Orange (`#F59E0B`). + - **Recovered / Low Volume**: Neon Green (`#22C55E`). + - **Inactive / Baseline**: Muted Blue/Slate (`#1E293B` or `#334155`). + +## 3. Core Architecture: Inline SVG Component +We will build a single, highly controllable React component: ``. +- **Implementation**: An inline SVG containing `` elements for each major muscle group. +- **Mapping**: Each path will have an `id` or `data-muscle` attribute mapping directly to the `TargetMuscle` enum (`Chest`, `Back`, `Legs`, `Arms`, `Core`). +- **Handling Non-Muscles**: + - `Cardio`: Represented as a glowing heart icon in the center of the chest or an aura around the body. + - `FullBody`: Triggers an overarching aura or simultaneous glow of all muscle groups. +- **State Injection**: The component accepts a dictionary of muscle states, determining the fill color and glow intensity of each SVG path. + +```tsx +// Proposed Interface +type MuscleIntensity = "inactive" | "low" | "medium" | "high"; + +interface AnatomyModelProps { + frontIntensities: Record; + backIntensities: Record; + view: "front" | "back" | "both"; // Determines which SVG to render +} +``` + +## 4. Contextual Behaviors + +### A. The 7-Day Heatmap (Dashboard) +- **Data Source**: Aggregates `Workout[]` from the past 7 days. +- **Logic**: Calculates total sets/reps or XP per `TargetMuscle`. Maps the volume to the `MuscleIntensity` scale. +- **Visuals**: A static but glowing representation showing where the user has focused their training. + +### B. Live Session "Pump" Tracker (Workout View) +- **Data Source**: The active workout session state (Zustand or React Context). +- **Logic**: As the user adds an `Exercise` (e.g., Bench Press -> `Chest`), the `Chest` intensity immediately spikes to "high". +- **Visuals**: Employs a pulsing CSS animation (`animate-pulse` or custom keyframes) to indicate an active "pump". + +### C. Recovery Monitor (Profile/Dashboard) +- **Data Source**: Historical workouts, computing time elapsed since a muscle was last trained. +- **Logic**: + - < 24 hours: Red (Exhausted) + - 24 - 48 hours: Orange (Recovering) + - > 72 hours: Green (Fully Recovered / Ready) +- **Visuals**: Smooth gradient colors based on the exact timestamp, allowing users to visually see their recovery progress. + +## 5. Implementation Steps +1. **SVG Sourcing & Prep**: Acquire a clean, minimalist vector of human anatomy (front and back). Group and name paths strictly according to `TargetMuscle`. +2. **Component Scaffold**: Create the `` in `src/components/ui/`. +3. **Domain Evaluators**: Create pure functions in `src/lib/domain/` to calculate: + - `calcMuscleVolume(workouts, days)` -> Heatmap data. + - `calcMuscleRecovery(workouts, now)` -> Recovery data. +4. **Integration**: Embed the component into the Dashboard and Workout entry forms. + +## 6. Open Questions / Grilling +*I need your input on the following to finalize the plan:* + +1. **SVG Views**: Do we want the UI to automatically flip between Front and Back views on a timer, or should the user toggle it manually with a button? +2. **"Cardio" & "Full Body"**: Do you agree with the "Aura" approach for these non-muscle targets, or would you prefer a different visual indicator (like a separate stat bar)? +3. **Animations**: Should the "Live Pump" tracker use a heartbeat/pulse animation, or a steady bright glow? diff --git a/src/lib/domain/__tests__/muscle-evaluator.test.ts b/src/lib/domain/__tests__/muscle-evaluator.test.ts new file mode 100644 index 0000000..da4eb9f --- /dev/null +++ b/src/lib/domain/__tests__/muscle-evaluator.test.ts @@ -0,0 +1,54 @@ +import { calcMuscleVolume } from "../muscle-evaluator"; +import { TargetMuscle, Workout } from "../../types"; +import { describe, it, expect } from "vitest"; + +describe("muscle-evaluator", () => { + describe("calcMuscleVolume", () => { + it("should calculate correct muscle intensity based on pure sets and reps", () => { + const mockWorkouts: Workout[] = [{ + id: "1", + title: "Push Day", + duration: 60, + xpEarned: 100, + date: new Date().toISOString(), + exercises: [ + { name: "Bench Press", targetMuscle: TargetMuscle.Chest, sets: 5, reps: 10, weight: 100 }, // 50 reps -> medium + { name: "Tricep Pushdown", targetMuscle: TargetMuscle.Arms, sets: 3, reps: 15, weight: 50 }, // 45 reps -> medium + { name: "Pushups", targetMuscle: TargetMuscle.Chest, sets: 5, reps: 20, weight: null } // 100 reps + 50 above = 150 -> high + ] + }]; + + const result = calcMuscleVolume(mockWorkouts, 7); + + // Chest is 150 total reps -> high + expect(result[TargetMuscle.Chest]).toBe("high"); + + // Arms is 45 total reps -> medium + expect(result[TargetMuscle.Arms]).toBe("medium"); + + // Legs is 0 reps -> inactive + expect(result[TargetMuscle.Legs]).toBe("inactive"); + }); + + it("should filter out workouts older than the specified days", () => { + const oldDate = new Date(); + oldDate.setDate(oldDate.getDate() - 10); // 10 days ago + + const mockWorkouts: Workout[] = [{ + id: "1", + title: "Leg Day (Old)", + duration: 60, + xpEarned: 100, + date: oldDate.toISOString(), + exercises: [ + { name: "Squats", targetMuscle: TargetMuscle.Legs, sets: 10, reps: 20, weight: 100 } // 200 reps + ] + }]; + + const result = calcMuscleVolume(mockWorkouts, 7); + + // Should be inactive because the workout is 10 days old, but we only asked for 7 days + expect(result[TargetMuscle.Legs]).toBe("inactive"); + }); + }); +}); diff --git a/src/lib/domain/muscle-evaluator.ts b/src/lib/domain/muscle-evaluator.ts new file mode 100644 index 0000000..f111dd4 --- /dev/null +++ b/src/lib/domain/muscle-evaluator.ts @@ -0,0 +1,38 @@ +import { TargetMuscle, Workout } from "../types"; + +export type MuscleIntensity = "inactive" | "low" | "medium" | "high"; + +export function calcMuscleVolume(workouts: Workout[], days: number): Record { + const now = new Date(); + const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1000); + + const recentWorkouts = workouts.filter(w => new Date(w.date) >= cutoff); + + const counts: Record = { + [TargetMuscle.Chest]: 0, + [TargetMuscle.Back]: 0, + [TargetMuscle.Legs]: 0, + [TargetMuscle.Arms]: 0, + [TargetMuscle.Core]: 0, + [TargetMuscle.Cardio]: 0, + [TargetMuscle.FullBody]: 0, + }; + + for (const w of recentWorkouts) { + for (const ex of w.exercises) { + counts[ex.targetMuscle] += (ex.sets * ex.reps); + } + } + + const intensities = {} as Record; + for (const [muscle, count] of Object.entries(counts)) { + let intensity: MuscleIntensity = "inactive"; + if (count > 0 && count <= 30) intensity = "low"; + else if (count > 30 && count <= 100) intensity = "medium"; + else if (count > 100) intensity = "high"; + + intensities[muscle as TargetMuscle] = intensity; + } + + return intensities; +} From 1e1d0f846b14f16853641f6279c7489ad333bb8c Mon Sep 17 00:00:00 2001 From: sidadrian3 Date: Fri, 31 Jul 2026 12:02:45 +0800 Subject: [PATCH 2/9] feat(ui): add Base AnatomyModel Component for interactive SVG rendering --- src/components/ui/AnatomyModel.tsx | 85 ++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/components/ui/AnatomyModel.tsx diff --git a/src/components/ui/AnatomyModel.tsx b/src/components/ui/AnatomyModel.tsx new file mode 100644 index 0000000..f9694ad --- /dev/null +++ b/src/components/ui/AnatomyModel.tsx @@ -0,0 +1,85 @@ +"use client"; +import React from "react"; +import { TargetMuscle } from "@/lib/types"; +import { MuscleIntensity } from "@/lib/domain/muscle-evaluator"; + +interface Props { + intensities: Record; + activeMuscle?: TargetMuscle | null; + view?: "front" | "back"; +} + +const intensityStyles: Record = { + inactive: "fill-slate-800 drop-shadow-none", + low: "fill-green-500 drop-shadow-[0_0_8px_rgba(34,197,94,0.6)]", + medium: "fill-orange-500 drop-shadow-[0_0_12px_rgba(249,115,22,0.6)]", + high: "fill-red-500 drop-shadow-[0_0_15px_rgba(239,68,68,0.8)]", +}; + +export function AnatomyModel({ intensities, activeMuscle, view = "front" }: Props) { + const getStyle = (muscle: TargetMuscle) => { + const isLive = activeMuscle === muscle; + const baseStyle = intensityStyles[intensities[muscle]] || intensityStyles.inactive; + + let style = `${baseStyle} transition-all duration-300 stroke-slate-900 stroke-[3px]`; + if (isLive) { + style += " animate-pulse scale-[1.02] origin-center z-10 relative"; + } + return style; + }; + + const hasAura = activeMuscle === TargetMuscle.FullBody || intensities[TargetMuscle.FullBody] !== "inactive"; + const containerStyle = hasAura ? "filter drop-shadow-[0_0_20px_#22C55E]" : ""; + + return ( +
+ {/* + Stylized placeholder paths for the muscle groups mapping to the front/back references. + A designer can drop in precise SVG d="..." data later while keeping these IDs and classes. + */} + + {view === "front" ? ( + + {/* Head/Neck (Inactive base) */} + + + {/* Chest */} + + + {/* Core / Abs */} + + + {/* Arms (Left & Right) */} + + + + {/* Legs (Quads) */} + + + + {/* Cardio Aura (if active) */} + {(activeMuscle === TargetMuscle.Cardio || intensities[TargetMuscle.Cardio] !== "inactive") && ( + + )} + + ) : ( + + {/* Head/Neck (Inactive base) */} + + + {/* Back (Lats & Traps) */} + + + {/* Arms (Triceps/Shoulders from back) */} + + + + {/* Legs (Hamstrings/Calves) */} + + + + )} + +
+ ); +} From 92623a777ee781b90c88a4a0a6250354e10ee1e5 Mon Sep 17 00:00:00 2001 From: sidadrian3 Date: Fri, 31 Jul 2026 12:07:08 +0800 Subject: [PATCH 3/9] feat(dashboard): add MuscleHeatmap widget using TanStack Query --- src/components/dashboard/MuscleHeatmap.tsx | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/components/dashboard/MuscleHeatmap.tsx diff --git a/src/components/dashboard/MuscleHeatmap.tsx b/src/components/dashboard/MuscleHeatmap.tsx new file mode 100644 index 0000000..bad8143 --- /dev/null +++ b/src/components/dashboard/MuscleHeatmap.tsx @@ -0,0 +1,53 @@ +"use client"; +import React, { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { AnatomyModel } from "../ui/AnatomyModel"; +import { calcMuscleVolume } from "@/lib/domain/muscle-evaluator"; + +import { getWorkouts } from "@/lib/data/api-client"; + +export function MuscleHeatmap() { + const [view, setView] = useState<"front" | "back">("front"); + + const { data, isLoading, error } = useQuery({ + queryKey: ["workouts", { page: 1, limit: 30 }], + queryFn: () => getWorkouts(1, 30), + }); + + if (isLoading || !data) { + return
; + } + + if (error) { + return ( +
+ Failed to load heatmap data. +
+ ); + } + + const intensities = calcMuscleVolume(data.data, 7); + + return ( +
+

7-Day Volume Heatmap

+
+ + +
+
+ +
+
+ ); +} From 44d55c855cbc6b2e3765504218018094421cd3b8 Mon Sep 17 00:00:00 2001 From: sidadrian3 Date: Fri, 31 Jul 2026 12:09:24 +0800 Subject: [PATCH 4/9] feat(dashboard): integrate MuscleHeatmap into dashboard layout --- src/app/(app)/dashboard/page.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index 8df1af1..fb2c636 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -5,6 +5,7 @@ import { XPCard } from "@/components/dashboard/XPCard"; import { ActivityTracker } from "@/components/dashboard/ActivityTracker"; import { DailyQuests } from "@/components/dashboard/DailyQuests"; import { RecentActivity } from "@/components/dashboard/RecentActivity"; +import { MuscleHeatmap } from "@/components/dashboard/MuscleHeatmap"; import { StatCard } from "@/components/ui/StatCard"; import { getAuthUserId } from "@/lib/auth/auth-helpers"; import { UserStateService } from "@/lib/services/users/user-state.service"; @@ -107,10 +108,11 @@ export default async function DashboardPage() { {/* Middle row: Activity Tracker Grid */} - {/* Bottom row: Quests & Recent Activity */} -
+ {/* Bottom row: Quests, Recent Activity & Heatmap */} +
+
); From c4b19fb2598a5a4d30a9c91bbfc467c7e78192c8 Mon Sep 17 00:00:00 2001 From: sidadrian3 Date: Fri, 31 Jul 2026 12:10:57 +0800 Subject: [PATCH 5/9] feat(workouts): integrate AnatomyModel Live Pump tracker into WorkoutForm --- src/components/workouts/WorkoutForm.tsx | 35 +++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/workouts/WorkoutForm.tsx b/src/components/workouts/WorkoutForm.tsx index 07c7899..793f509 100644 --- a/src/components/workouts/WorkoutForm.tsx +++ b/src/components/workouts/WorkoutForm.tsx @@ -12,6 +12,8 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { PREDEFINED_EXERCISES } from "@/lib/constants/exercises"; import { mergeAndSortExercises } from "@/lib/domain/exercise-rules"; import { CreateExerciseModal } from "./CreateExerciseModal"; +import { AnatomyModel } from "../ui/AnatomyModel"; +import { MuscleIntensity } from "@/lib/domain/muscle-evaluator"; const emptyExercise: Exercise = { name: "", @@ -37,8 +39,15 @@ export function WorkoutForm({ onCancel?: () => void; }) { const [isModalOpen, setIsModalOpen] = useState(false); + const [activeMuscle, setActiveMuscle] = useState(null); const queryClient = useQueryClient(); + const baseIntensities = useMemo(() => { + const obj = {} as Record; + Object.values(TargetMuscle).forEach(m => obj[m as TargetMuscle] = "inactive"); + return obj; + }, []); + // Fetch custom exercises const { data: customExercises = [] } = useQuery({ queryKey: ["customExercises"], @@ -148,7 +157,8 @@ export function WorkoutForm({ "w-full bg-background border border-border rounded-lg px-4 py-2.5 text-foreground text-sm placeholder:text-muted focus:border-accent-green focus:ring-1 focus:ring-accent-green/50 focus:outline-none transition-default"; return ( - <> +
+

@@ -201,11 +211,19 @@ export function WorkoutForm({ >
exercise.name && setActiveMuscle(exercise.targetMuscle)} value={exercise.sets} onChange={(e) => updateExercise( @@ -261,6 +280,7 @@ export function WorkoutForm({ exercise.name && setActiveMuscle(exercise.targetMuscle)} value={exercise.reps} onChange={(e) => updateExercise( @@ -282,6 +302,7 @@ export function WorkoutForm({ type="number" min={0} placeholder="BW" + onFocus={() => exercise.name && setActiveMuscle(exercise.targetMuscle)} value={ exercise.weight !== null ? exercise.weight @@ -392,6 +413,16 @@ export function WorkoutForm({ })); }} /> - +
+ +
+

Live Pump Tracker

+ +
+

); } From 14f464342b8a68d59d7ae1b3abcff67034d4fbdc Mon Sep 17 00:00:00 2001 From: sidadrian3 Date: Fri, 31 Jul 2026 12:18:24 +0800 Subject: [PATCH 6/9] fix(workouts): compute real-time intensities for Live Pump Tracker --- src/components/workouts/WorkoutForm.tsx | 28 +++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/components/workouts/WorkoutForm.tsx b/src/components/workouts/WorkoutForm.tsx index 793f509..fd1f327 100644 --- a/src/components/workouts/WorkoutForm.tsx +++ b/src/components/workouts/WorkoutForm.tsx @@ -13,7 +13,7 @@ import { PREDEFINED_EXERCISES } from "@/lib/constants/exercises"; import { mergeAndSortExercises } from "@/lib/domain/exercise-rules"; import { CreateExerciseModal } from "./CreateExerciseModal"; import { AnatomyModel } from "../ui/AnatomyModel"; -import { MuscleIntensity } from "@/lib/domain/muscle-evaluator"; +import { MuscleIntensity, calcMuscleVolume } from "@/lib/domain/muscle-evaluator"; const emptyExercise: Exercise = { name: "", @@ -42,12 +42,6 @@ export function WorkoutForm({ const [activeMuscle, setActiveMuscle] = useState(null); const queryClient = useQueryClient(); - const baseIntensities = useMemo(() => { - const obj = {} as Record; - Object.values(TargetMuscle).forEach(m => obj[m as TargetMuscle] = "inactive"); - return obj; - }, []); - // Fetch custom exercises const { data: customExercises = [] } = useQuery({ queryKey: ["customExercises"], @@ -111,6 +105,18 @@ export function WorkoutForm({ onSuccess: onWorkoutLogged, }); + const currentIntensities = useMemo(() => { + const mockWorkout: Workout = { + id: "temp", + title: "temp", + duration: 1, + xpEarned: 0, + date: new Date().toISOString(), + exercises: fields.exercises.filter(ex => ex.name.trim() !== "") + }; + return calcMuscleVolume([mockWorkout], 1); + }, [fields.exercises]); + const addExercise = () => { setFields((prev) => ({ ...prev, exercises: [...prev.exercises, { ...emptyExercise }] })); }; @@ -157,9 +163,9 @@ export function WorkoutForm({ "w-full bg-background border border-border rounded-lg px-4 py-2.5 text-foreground text-sm placeholder:text-muted focus:border-accent-green focus:ring-1 focus:ring-accent-green/50 focus:outline-none transition-default"; return ( -
-
- +
+
+

{isEditMode ? "Edit Workout" : "Log Workout"} @@ -418,7 +424,7 @@ export function WorkoutForm({

Live Pump Tracker

From c4733cc6fc6715f85d84b5e1e69eb236644cf91d Mon Sep 17 00:00:00 2001 From: sidadrian3 Date: Fri, 31 Jul 2026 12:25:58 +0800 Subject: [PATCH 7/9] style: use Card component for Anatomy UIs instead of slate backgrounds --- src/components/dashboard/MuscleHeatmap.tsx | 19 ++++++++++--------- src/components/workouts/WorkoutForm.tsx | 6 +++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/dashboard/MuscleHeatmap.tsx b/src/components/dashboard/MuscleHeatmap.tsx index bad8143..ae0fddd 100644 --- a/src/components/dashboard/MuscleHeatmap.tsx +++ b/src/components/dashboard/MuscleHeatmap.tsx @@ -4,6 +4,7 @@ import { useQuery } from "@tanstack/react-query"; import { AnatomyModel } from "../ui/AnatomyModel"; import { calcMuscleVolume } from "@/lib/domain/muscle-evaluator"; +import { Card } from "@/components/ui/Card"; import { getWorkouts } from "@/lib/data/api-client"; export function MuscleHeatmap() { @@ -15,32 +16,32 @@ export function MuscleHeatmap() { }); if (isLoading || !data) { - return
; + return Loading...; } if (error) { return ( -
+ Failed to load heatmap data. -
+ ); } const intensities = calcMuscleVolume(data.data, 7); return ( -
-

7-Day Volume Heatmap

-
+ +

7-Day Volume Heatmap

+
@@ -48,6 +49,6 @@ export function MuscleHeatmap() {
-
+
); } diff --git a/src/components/workouts/WorkoutForm.tsx b/src/components/workouts/WorkoutForm.tsx index fd1f327..a4b8297 100644 --- a/src/components/workouts/WorkoutForm.tsx +++ b/src/components/workouts/WorkoutForm.tsx @@ -421,14 +421,14 @@ export function WorkoutForm({ />
-
-

Live Pump Tracker

+ +

Live Pump Tracker

-
+
); } From 34f4cf9b3c8e0863899fcb179a161af7ee3e7550 Mon Sep 17 00:00:00 2001 From: sidadrian3 Date: Fri, 31 Jul 2026 12:35:29 +0800 Subject: [PATCH 8/9] feat(ui): redesign layout for zero-scrolling using tabs and compact sizing --- src/app/(app)/dashboard/page.tsx | 10 +-- src/components/dashboard/DashboardTabs.tsx | 42 ++++++++++ src/components/dashboard/MuscleHeatmap.tsx | 2 +- src/components/ui/AnatomyModel.tsx | 2 +- src/components/workouts/WorkoutForm.tsx | 95 ++++++++++++---------- 5 files changed, 97 insertions(+), 54 deletions(-) create mode 100644 src/components/dashboard/DashboardTabs.tsx diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index fb2c636..175b23f 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -5,7 +5,7 @@ import { XPCard } from "@/components/dashboard/XPCard"; import { ActivityTracker } from "@/components/dashboard/ActivityTracker"; import { DailyQuests } from "@/components/dashboard/DailyQuests"; import { RecentActivity } from "@/components/dashboard/RecentActivity"; -import { MuscleHeatmap } from "@/components/dashboard/MuscleHeatmap"; +import { DashboardTabs } from "@/components/dashboard/DashboardTabs"; import { StatCard } from "@/components/ui/StatCard"; import { getAuthUserId } from "@/lib/auth/auth-helpers"; import { UserStateService } from "@/lib/services/users/user-state.service"; @@ -108,12 +108,8 @@ export default async function DashboardPage() { {/* Middle row: Activity Tracker Grid */} - {/* Bottom row: Quests, Recent Activity & Heatmap */} -
- - - -
+ {/* Bottom row: Tabbed Quests, Activity, Heatmap */} +
); } \ No newline at end of file diff --git a/src/components/dashboard/DashboardTabs.tsx b/src/components/dashboard/DashboardTabs.tsx new file mode 100644 index 0000000..62aa0c9 --- /dev/null +++ b/src/components/dashboard/DashboardTabs.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useState } from "react"; +import { DailyQuests } from "./DailyQuests"; +import { RecentActivity } from "./RecentActivity"; +import { MuscleHeatmap } from "./MuscleHeatmap"; +import { Quest, Workout } from "@/lib/types"; + +export function DashboardTabs({ quests, recentWorkouts }: { quests: Quest[], recentWorkouts: Workout[] }) { + const [activeTab, setActiveTab] = useState<"activity" | "quests" | "heatmap">("activity"); + + return ( +
+
+ + + +
+ +
+ {activeTab === "activity" && } + {activeTab === "quests" && } + {activeTab === "heatmap" && } +
+
+ ); +} diff --git a/src/components/dashboard/MuscleHeatmap.tsx b/src/components/dashboard/MuscleHeatmap.tsx index ae0fddd..68a6a29 100644 --- a/src/components/dashboard/MuscleHeatmap.tsx +++ b/src/components/dashboard/MuscleHeatmap.tsx @@ -46,7 +46,7 @@ export function MuscleHeatmap() { Back

-
+
diff --git a/src/components/ui/AnatomyModel.tsx b/src/components/ui/AnatomyModel.tsx index f9694ad..5f469c0 100644 --- a/src/components/ui/AnatomyModel.tsx +++ b/src/components/ui/AnatomyModel.tsx @@ -32,7 +32,7 @@ export function AnatomyModel({ intensities, activeMuscle, view = "front" }: Prop const containerStyle = hasAura ? "filter drop-shadow-[0_0_20px_#22C55E]" : ""; return ( -
+
{/* Stylized placeholder paths for the muscle groups mapping to the front/back references. A designer can drop in precise SVG d="..." data later while keeping these IDs and classes. diff --git a/src/components/workouts/WorkoutForm.tsx b/src/components/workouts/WorkoutForm.tsx index a4b8297..e08559d 100644 --- a/src/components/workouts/WorkoutForm.tsx +++ b/src/components/workouts/WorkoutForm.tsx @@ -163,39 +163,41 @@ export function WorkoutForm({ "w-full bg-background border border-border rounded-lg px-4 py-2.5 text-foreground text-sm placeholder:text-muted focus:border-accent-green focus:ring-1 focus:ring-accent-green/50 focus:outline-none transition-default"; return ( -
-
- -
-

- {isEditMode ? "Edit Workout" : "Log Workout"} -

- {isEditMode && onCancel && ( - - )} -
- -
-
- - setFields((prev) => ({ ...prev, title: e.target.value }))} - className={inputBase} - /> +
+ + + {/* Left side: Form Inputs */} +
+
+

+ {isEditMode ? "Edit Workout" : "Log Workout"} +

+ {isEditMode && onCancel && ( + + )}
+ +
+ + setFields((prev) => ({ ...prev, title: e.target.value }))} + className={inputBase} + /> +
+
- {error && ( -

{error}

- )} - + {error && ( +

{error}

+ )} + +
+ + {/* Right side: Compact Live Pump Tracker */} +
+

Live Pump Tracker

+
+ +
+
-
- - -

Live Pump Tracker

- -
); } From cd7d68286e5acc7203f72f301927bf3c2ecf15a8 Mon Sep 17 00:00:00 2001 From: sidadrian3 Date: Fri, 31 Jul 2026 12:40:37 +0800 Subject: [PATCH 9/9] docs: update project briefing and architecture review to reflect completed Interactive Anatomy UI features --- architecture_review.md | 2 +- project_briefing.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/architecture_review.md b/architecture_review.md index f95fddd..2a1f538 100644 --- a/architecture_review.md +++ b/architecture_review.md @@ -330,7 +330,7 @@ There is no complex Role-Based Access Control (RBAC) yet. Authorization is handl Based on our architectural review and brainstorming session, the following frontend-heavy gamification features are approved for the next development cycle before the Vercel deployment: -### 1. Interactive Human Anatomy UI +### 1. Interactive Human Anatomy UI [INITIALLY DONE ✅ ] A visual representation of the human muscular system mapping to the `TargetMuscle` enum (`Chest`, `Back`, `Legs`, etc.). It will be implemented across 3 contexts: diff --git a/project_briefing.md b/project_briefing.md index 475f792..d4f7829 100644 --- a/project_briefing.md +++ b/project_briefing.md @@ -187,6 +187,9 @@ POST /api/workouts - Custom exercises (create, reuse across workouts) - Full CRUD for workouts and runs (create, list with pagination, edit, delete) - Profile page with personal records +- **Interactive Human Anatomy UI:** + - 7-Day Heatmap (Dashboard widget) to visualize trained vs neglected muscles + - Live Session "Pump" Tracker (Workout Form) with dynamic SVG highlighting - **Friend System (fully implemented):** - Send/accept/decline friend requests by User ID - Remove friends