File size: 14,438 Bytes
542c765
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee7023b
542c765
 
 
61b86c9
 
 
 
 
 
542c765
 
 
ee7023b
 
61b86c9
 
 
 
542c765
61b86c9
 
 
 
 
 
 
 
 
 
 
542c765
 
 
 
 
 
 
 
 
ee7023b
542c765
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
"use client";

import { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useGUCStore } from "@/lib/store";
import { PageShell } from "@/components/ui";

interface ExerciseDay {
  day: string;
  activity: string;
  duration_minutes: number;
  intensity: string;
  notes: string;
}

interface ExerciseResponse {
  tier: string;
  tier_description: string;
  weekly_plan: ExerciseDay[];
  general_advice: string;
  avoid: string[];
}

const TIER_CONFIG: Record<string, { color: string; bg: string; icon: string; badge: string }> = {
  LIGHT_WALKING_ONLY: { color: "#22C55E", bg: "#22C55E15", icon: "🚢", badge: "Light Recovery" },
  CARDIO_RESTRICTED:  { color: "#06B6D4", bg: "#06B6D415", icon: "🧘", badge: "Low Intensity" },
  NORMAL_ACTIVITY:    { color: "#FF9933", bg: "#FF993315", icon: "πŸƒ", badge: "Moderate" },
  ACTIVE_ENCOURAGED:  { color: "#EF4444", bg: "#EF444415", icon: "πŸ’ͺ", badge: "Active" },
};

const INTENSITY_COLORS: Record<string, string> = {
  "Very Low": "#64748B", "Low": "#22C55E", "Low-Moderate": "#84CC16",
  "Moderate": "#FF9933", "Moderate-High": "#F97316", "High": "#EF4444",
  "Very High": "#DC2626", "Rest": "#334155",
};

const DAY_ABBR: Record<string, string> = {
  Monday: "Mon", Tuesday: "Tue", Wednesday: "Wed", Thursday: "Thu",
  Friday: "Fri", Saturday: "Sat", Sunday: "Sun",
};

const FALLBACK_PLAN: ExerciseResponse = {
  tier: "NORMAL_ACTIVITY",
  tier_description: "Standard moderate activity plan. 30-minute sessions, 5 days a week.",
  general_advice: "Stay consistent. 5 days of 30 minutes beats 1 day of 2 hours. Drink water before and after.",
  avoid: ["Exercising on an empty stomach", "Skipping warm-up and cool-down", "Pushing through sharp pain"],
  weekly_plan: [
    { day: "Monday",    activity: "Brisk walking 30 min",                        duration_minutes: 30, intensity: "Moderate", notes: "Comfortable pace, slightly breathless." },
    { day: "Tuesday",   activity: "Bodyweight squats, push-ups, lunges",          duration_minutes: 30, intensity: "Moderate", notes: "3 sets of 12 reps each." },
    { day: "Wednesday", activity: "Yoga + stretching",                            duration_minutes: 30, intensity: "Low",      notes: "Active recovery. Focus on flexibility." },
    { day: "Thursday",  activity: "Brisk walk + light jog intervals",             duration_minutes: 35, intensity: "Moderate", notes: "3 min walk, 2 min jog. Repeat 5 times." },
    { day: "Friday",    activity: "Resistance band strength training",             duration_minutes: 30, intensity: "Moderate", notes: "Focus on compound movements." },
    { day: "Saturday",  activity: "Recreational activity β€” badminton or cycling", duration_minutes: 45, intensity: "Moderate", notes: "Make it fun and social!" },
    { day: "Sunday",    activity: "Rest day",                                     duration_minutes: 0,  intensity: "Rest",     notes: "Full rest. Light household activity fine." },
  ],
};

export default function ExercisePage() {
  const exerciseLevel  = useGUCStore((s) => s.exerciseLevel);
  const latestReport   = useGUCStore((s) => s.latestReport);
  const profile        = useGUCStore((s) => s.profile);
  const addXP          = useGUCStore((s) => s.addXP);
  const setAvatarState = useGUCStore((s) => s.setAvatarState);

  const [data, setData]                   = useState<ExerciseResponse | null>(null);
  const [loading, setLoading]             = useState(true);
  const [selectedDay, setSelectedDay]     = useState<string | null>(null);
  const [completedDays, setCompletedDays] = useState<Set<string>>(new Set());


  const severity = latestReport?.severity_level ?? "MILD_CONCERN";

  useEffect(() => {
    const TIER_MAP: Record<string, string> = {
      Beginner: "LIGHT_WALKING_ONLY",
      Intermediate: "NORMAL_ACTIVITY",
      Advanced: "ACTIVE_ENCOURAGED",
    };

    const fetchPlan = async () => {
      try {
        setLoading(true);
        // Call Next.js API route which proxies to backend
        const res = await fetch(`/api/exercise`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({}),
        });
        if (!res.ok) throw new Error();
        const json = await res.json();

        // Transform backend ExerciseResponse β†’ frontend ExerciseResponse
        const transformed: ExerciseResponse = {
          tier: TIER_MAP[json.tier] ?? json.tier ?? "NORMAL_ACTIVITY",
          tier_description: json.tier_reason ?? json.tier_description ?? "",
          weekly_plan: json.weekly_plan ?? [],
          general_advice: json.encouragement ?? json.general_advice ?? "",
          avoid: json.restrictions ?? json.avoid ?? [],
        };
        setData(transformed);
        setSelectedDay("Monday");
      } catch {
        setData(FALLBACK_PLAN);
        setSelectedDay("Monday");
      } finally {
        setLoading(false);
      }
    };
    fetchPlan();
  }, [exerciseLevel, severity, profile.language]);

  const handleComplete = (day: string) => {
    if (completedDays.has(day)) return;
    setCompletedDays((prev) => new Set([...prev, day]));
    addXP(10);
    setAvatarState("HAPPY");
  };

  const tierCfg  = TIER_CONFIG[data?.tier ?? "NORMAL_ACTIVITY"] ?? TIER_CONFIG.NORMAL_ACTIVITY;
  const weekTotal = (data?.weekly_plan ?? []).reduce((s, d) => s + d.duration_minutes, 0);

  if (loading) {
    return (
      <PageShell>
        <div className="space-y-4">
          <div className="h-8 w-56 bg-white/5 rounded-xl animate-pulse" />
          <div className="h-28 bg-white/5 rounded-2xl animate-pulse" />
          <div className="flex gap-2">
            {[...Array(7)].map((_, i) => (
              <div key={i} className="flex-1 h-16 bg-white/5 rounded-xl animate-pulse" />
            ))}
          </div>
          <div className="h-40 bg-white/5 rounded-2xl animate-pulse" />
        </div>
      </PageShell>
    );
  }

  return (
    <PageShell>

        {/* Header */}
        <motion.div initial={{ opacity: 0, y: -12 }} animate={{ opacity: 1, y: 0 }} className="mb-6">
          <div className="flex items-center gap-3 mb-1">
            <span className="text-2xl">{tierCfg.icon}</span>
            <h1 className="text-xl font-bold tracking-tight">Exercise Plan</h1>
          </div>
          <p className="text-white/40 text-sm">Adapted to your health condition</p>
        </motion.div>

        {/* Tier banner */}
        <motion.div
          initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }}
          className="mb-5 rounded-2xl p-4 border"
          style={{ background: tierCfg.bg, borderColor: `${tierCfg.color}30` }}
        >
          <div className="flex justify-between items-start gap-3 mb-2">
            <span
              className="text-xs font-semibold px-2.5 py-1 rounded-full"
              style={{ background: `${tierCfg.color}25`, color: tierCfg.color }}
            >
              {tierCfg.badge} Tier
            </span>
            <span className="text-white/40 text-xs">{weekTotal} min/week</span>
          </div>
          <p className="text-white/70 text-sm leading-relaxed">{data?.tier_description}</p>
        </motion.div>

        {/* Stats strip */}
        <motion.div
          initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.15 }}
          className="flex gap-2 mb-5"
        >
          {[
            { label: "Days Active",   value: (data?.weekly_plan ?? []).filter((d) => d.duration_minutes > 0).length },
            { label: "Total Minutes", value: weekTotal },
            { label: "Completed",     value: completedDays.size },
          ].map((stat) => (
            <div key={stat.label} className="flex-1 bg-white/[0.04] border border-white/[0.07] rounded-xl py-2.5 px-3 text-center">
              <div className="text-white font-bold text-lg">{stat.value}</div>
              <div className="text-white/30 text-[10px]">{stat.label}</div>
            </div>
          ))}
        </motion.div>

        {/* Day selector */}
        <div className="flex gap-2 mb-4 overflow-x-auto pb-1">
          {(data?.weekly_plan ?? []).map((day, i) => {
            const isRest     = day.duration_minutes === 0;
            const isDone     = completedDays.has(day.day);
            const isSelected = selectedDay === day.day;
            return (
              <motion.button
                key={day.day}
                initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: i * 0.05 }}
                onClick={() => setSelectedDay(day.day)}
                className="flex-shrink-0 flex flex-col items-center gap-1 px-3 py-2.5 rounded-xl transition-all min-w-[52px]"
                style={
                  isSelected ? {
                    background: tierCfg.bg,
                    border: `1px solid ${tierCfg.color}60`,
                    boxShadow: `0 0 16px ${tierCfg.color}30`,
                  }
                  : isDone   ? { background: "rgba(34,197,94,0.08)", border: "1px solid rgba(34,197,94,0.2)" }
                  :            { background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.07)" }
                }
              >
                <span className="text-[10px] text-white/40">{DAY_ABBR[day.day]}</span>
                <span className="text-base">{isDone ? "βœ…" : isRest ? "πŸ’€" : tierCfg.icon}</span>
                <span className="text-[9px] font-medium" style={{ color: isSelected ? tierCfg.color : "rgba(255,255,255,0.3)" }}>
                  {isRest ? "Rest" : `${day.duration_minutes}m`}
                </span>
              </motion.button>
            );
          })}
        </div>

        {/* Day detail */}
        <AnimatePresence mode="wait">
          {selectedDay && data && (() => {
            const day = data.weekly_plan.find((d) => d.day === selectedDay);
            if (!day) return null;
            const isDone         = completedDays.has(day.day);
            const intensityColor = INTENSITY_COLORS[day.intensity] ?? "#FF9933";
            return (
              <motion.div
                key={selectedDay}
                initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}
                transition={{ duration: 0.2 }}
                className="mb-5 bg-white/[0.04] border border-white/[0.08] rounded-2xl overflow-hidden"
              >
                <div className="px-4 py-3 border-b border-white/[0.06] flex justify-between items-center">
                  <div>
                    <h3 className="text-white font-semibold text-sm">{day.day}</h3>
                    <span
                      className="text-[10px] px-1.5 py-0.5 rounded-full mt-0.5 inline-block"
                      style={{ background: `${intensityColor}20`, color: intensityColor }}
                    >
                      {day.intensity}
                    </span>
                  </div>
                  {day.duration_minutes > 0 && (
                    <div className="text-right">
                      <div className="text-xl font-bold" style={{ color: tierCfg.color }}>{day.duration_minutes}</div>
                      <div className="text-white/30 text-[10px]">minutes</div>
                    </div>
                  )}
                </div>

                <div className="p-4 space-y-3">
                  {day.duration_minutes === 0 ? (
                    <div className="text-center py-4">
                      <span className="text-4xl">πŸ’€</span>
                      <p className="text-white/40 text-sm mt-2">Full rest day. Your body repairs and grows stronger while you rest.</p>
                    </div>
                  ) : (
                    <>
                      <div className="flex items-start gap-3">
                        <span className="text-2xl">{tierCfg.icon}</span>
                        <p className="text-white font-medium text-sm">{day.activity}</p>
                      </div>
                      <div className="bg-white/[0.03] rounded-xl p-3">
                        <p className="text-white/50 text-xs leading-relaxed">πŸ’‘ {day.notes}</p>
                      </div>
                      <motion.button
                        whileTap={{ scale: 0.97 }}
                        onClick={() => handleComplete(day.day)}
                        disabled={isDone}
                        className="w-full py-2.5 rounded-xl text-sm font-medium transition-all"
                        style={
                          isDone
                            ? { background: "rgba(34,197,94,0.1)", color: "#22C55E", border: "1px solid rgba(34,197,94,0.2)" }
                            : { background: tierCfg.color, color: "#0d0d1a" }
                        }
                      >
                        {isDone ? "βœ“ Completed Β· +10 XP earned!" : "Mark Complete Β· +10 XP"}
                      </motion.button>
                    </>
                  )}
                </div>
              </motion.div>
            );
          })()}
        </AnimatePresence>

        {/* General advice */}
        {data?.general_advice && (
          <motion.div
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.3 }}
            className="mb-5 p-4 bg-white/[0.03] border border-white/[0.06] rounded-2xl"
          >
            <p className="text-white/40 text-[10px] uppercase tracking-widest mb-2">General Advice</p>
            <p className="text-white/60 text-sm leading-relaxed">{data.general_advice}</p>
          </motion.div>
        )}

        {/* Avoid list */}
        {(data?.avoid ?? []).length > 0 && (
          <motion.div
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.35 }}
            className="p-4 bg-red-500/5 border border-red-500/15 rounded-2xl"
          >
            <p className="text-red-400/80 text-[10px] uppercase tracking-widest mb-2">⚠️ Avoid</p>
            <ul className="space-y-1.5">
              {(data?.avoid ?? []).map((item, i) => (
                <li key={i} className="flex items-start gap-2 text-white/40 text-xs">
                  <span className="text-red-400/50 mt-0.5 flex-shrink-0">βœ•</span>
                  {item}
                </li>
              ))}
            </ul>
          </motion.div>
        )}
    </PageShell>
  );
}