Skip to content

Commit 030e96f

Browse files
authored
Merge pull request #749 from CodeWithCJ/pr-740-fix-format
Pr 740 fix format
2 parents 68cc1a8 + 9adf7d3 commit 030e96f

File tree

23 files changed

+3374
-42
lines changed

23 files changed

+3374
-42
lines changed

SparkyFitnessFrontend/public/locales/en/translation.json

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,5 +2127,62 @@
21272127
"liveAiContextTitle": "Live AI Context",
21282128
"liveAiContextDescription": "This is the current context that would be provided to the AI assistant",
21292129
"loadingAiContext": "Loading AI context..."
2130+
},
2131+
"sleepScience": {
2132+
"title": "Sleep Science",
2133+
"sleepDebt": "Sleep Debt",
2134+
"debtLow": "Low",
2135+
"debtLowDesc": "Your sleep debt is minimal. Great recovery!",
2136+
"debtModerate": "Moderate",
2137+
"debtModerateDesc": "Some accumulated debt. Try to catch up on sleep.",
2138+
"debtHigh": "High",
2139+
"debtHighDesc": "Significant sleep debt. Prioritize rest.",
2140+
"debtCritical": "Critical",
2141+
"debtCriticalDesc": "Critical sleep debt. Immediate rest needed.",
2142+
"paybackTime": "~{{nights}} nights to recover",
2143+
"debtHistory": "14-Day Sleep Debt",
2144+
"debt": "Debt",
2145+
"surplus": "Surplus",
2146+
"dailyBreakdown": "Daily Breakdown",
2147+
"date": "Date",
2148+
"slept": "Slept",
2149+
"deviation": "Deviation",
2150+
"weight": "Weight",
2151+
"weightExplanation": "Weight indicates recency: more recent days have higher impact.",
2152+
"energyCurve": "Energy Curve",
2153+
"currentEnergy": "Now: {{energy}}%",
2154+
"energy": "Energy",
2155+
"zone_peak": "Peak",
2156+
"zone_rising": "Rising",
2157+
"zone_dip": "Dip",
2158+
"zone_wind-down": "Wind Down",
2159+
"zone_sleep": "Sleep",
2160+
"chronotype": "Chronotype",
2161+
"chronotypeEarly": "Early Bird",
2162+
"chronotypeEarlyDesc": "You naturally wake early and perform best in the morning.",
2163+
"chronotypeIntermediate": "Intermediate",
2164+
"chronotypeIntermediateDesc": "Your rhythm is balanced between morning and evening.",
2165+
"chronotypeLate": "Night Owl",
2166+
"chronotypeLateDesc": "You naturally stay up late and peak in the evening.",
2167+
"avgWake": "Avg Wake",
2168+
"avgSleep": "Avg Sleep",
2169+
"nadir": "Nadir",
2170+
"acrophase": "Acrophase",
2171+
"melatoninWindow": "Melatonin Window",
2172+
"basedOn": "Based on {{days}} days",
2173+
"sleepNeedTonight": "Sleep Need Tonight",
2174+
"baseline": "Baseline",
2175+
"strain": "Strain",
2176+
"debtRecovery": "Debt Recovery",
2177+
"naps": "Naps",
2178+
"method": "Method",
2179+
"recalculate": "Recalculate",
2180+
"insufficientData": "More data needed for full analysis",
2181+
"workdays": "Workdays",
2182+
"freedays": "Free days",
2183+
"noData": "No sleep data available",
2184+
"noDataDesc": "Start tracking your sleep to unlock advanced insights.",
2185+
"baselineCalculated": "Sleep need baseline calculated successfully",
2186+
"baselineError": "Failed to calculate sleep need baseline"
21302187
}
21312188
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { api } from '@/services/api';
2+
3+
// ====== Types ======
4+
5+
export interface SleepDebtDailyEntry {
6+
date: string;
7+
tst: number;
8+
deviation: number;
9+
weight: number;
10+
weightedDebt: number;
11+
}
12+
13+
export interface SleepDebtData {
14+
currentDebt: number;
15+
debtCategory: 'low' | 'moderate' | 'high' | 'critical';
16+
sleepNeed: number;
17+
last14Days: SleepDebtDailyEntry[];
18+
trend: {
19+
direction: 'improving' | 'stable' | 'worsening';
20+
change7d: number;
21+
};
22+
paybackTime: number;
23+
}
24+
25+
export interface MCTQStatsData {
26+
profile: {
27+
baselineSleepNeed: number;
28+
method: string;
29+
confidence: string;
30+
basedOnDays: number;
31+
lastCalculated: string | null;
32+
sdWorkday: number | null;
33+
sdFreeday: number | null;
34+
socialJetlag: number | null;
35+
} | null;
36+
latestCalculation: Record<string, unknown> | null;
37+
dayClassifications: {
38+
dayOfWeek: number;
39+
classifiedAs: string;
40+
meanWakeHour: number | null;
41+
varianceMinutes: number | null;
42+
sampleCount: number;
43+
}[];
44+
}
45+
46+
export interface BaselineResult {
47+
success: boolean;
48+
sleepNeedIdeal?: number;
49+
sdWorkday?: number;
50+
sdFreeday?: number;
51+
sdWeek?: number;
52+
socialJetlag?: number | null;
53+
confidence?: string;
54+
basedOnDays?: number;
55+
workdaysCount?: number;
56+
freedaysCount?: number;
57+
method?: string;
58+
error?: string;
59+
message?: string;
60+
}
61+
62+
export interface DailyNeedData {
63+
date: string;
64+
baseline: number;
65+
strainAddition: number;
66+
debtAddition: number;
67+
napSubtraction: number;
68+
totalNeed: number;
69+
method: string;
70+
confidence: string;
71+
trainingLoadScore: number | null;
72+
currentDebtHours: number;
73+
napMinutes: number;
74+
recoveryScoreYesterday: number | null;
75+
}
76+
77+
export interface EnergyCurvePoint {
78+
hour: number;
79+
time: string;
80+
energy: number;
81+
zone: 'peak' | 'rising' | 'dip' | 'wind-down' | 'sleep';
82+
processS: number;
83+
processC: number;
84+
}
85+
86+
export interface EnergyCurveData {
87+
success: boolean;
88+
points?: EnergyCurvePoint[];
89+
currentEnergy?: number;
90+
currentZone?: string;
91+
nextPeak?: { hour: number; energy: number } | null;
92+
nextDip?: { hour: number; energy: number } | null;
93+
melatoninWindow?: { start: number; end: number };
94+
wakeTime?: number;
95+
sleepDebtPenalty?: number;
96+
error?: string;
97+
message?: string;
98+
}
99+
100+
export interface ChronotypeData {
101+
success: boolean;
102+
chronotype?: 'early' | 'intermediate' | 'late';
103+
averageWakeTime?: string;
104+
averageSleepTime?: string | null;
105+
circadianNadir?: string;
106+
circadianAcrophase?: string;
107+
melatoninWindowStart?: string | null;
108+
melatoninWindowEnd?: string | null;
109+
basedOnDays?: number;
110+
confidence?: string;
111+
error?: string;
112+
message?: string;
113+
}
114+
115+
export interface DataSufficiencyData {
116+
sufficient: boolean;
117+
totalDays: number;
118+
daysWithTimestamps: number;
119+
workdaysAvailable: number;
120+
freedaysAvailable: number;
121+
workdaysNeeded: number;
122+
freedaysNeeded: number;
123+
projectedConfidence: string;
124+
recommendation: string;
125+
}
126+
127+
// ====== API Calls ======
128+
129+
export const getSleepDebt = async (
130+
targetUserId?: string
131+
): Promise<SleepDebtData> => {
132+
return api.get('/sleep-science/sleep-debt', {
133+
params: { targetUserId },
134+
});
135+
};
136+
137+
export const calculateBaseline = async (
138+
windowDays: number = 90
139+
): Promise<BaselineResult> => {
140+
return api.post('/sleep-science/calculate-baseline', {
141+
body: { windowDays },
142+
});
143+
};
144+
145+
export const getMCTQStats = async (
146+
targetUserId?: string
147+
): Promise<MCTQStatsData> => {
148+
return api.get('/sleep-science/mctq-stats', {
149+
params: { targetUserId },
150+
});
151+
};
152+
153+
export const getDailyNeed = async (
154+
date?: string,
155+
targetUserId?: string
156+
): Promise<DailyNeedData> => {
157+
return api.get('/sleep-science/daily-need', {
158+
params: { date, targetUserId },
159+
});
160+
};
161+
162+
export const getEnergyCurve = async (
163+
targetUserId?: string
164+
): Promise<EnergyCurveData> => {
165+
return api.get('/sleep-science/energy-curve', {
166+
params: { targetUserId },
167+
});
168+
};
169+
170+
export const getChronotype = async (
171+
targetUserId?: string
172+
): Promise<ChronotypeData> => {
173+
return api.get('/sleep-science/chronotype', {
174+
params: { targetUserId },
175+
});
176+
};
177+
178+
export const getDataSufficiency = async (
179+
targetUserId?: string
180+
): Promise<DataSufficiencyData> => {
181+
return api.get('/sleep-science/data-sufficiency', {
182+
params: { targetUserId },
183+
});
184+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const sleepScienceKeys = {
2+
all: ['sleepScience'] as const,
3+
sleepDebt: (targetUserId?: string) =>
4+
[...sleepScienceKeys.all, 'sleepDebt', targetUserId] as const,
5+
mctqStats: (targetUserId?: string) =>
6+
[...sleepScienceKeys.all, 'mctqStats', targetUserId] as const,
7+
dailyNeed: (date: string, targetUserId?: string) =>
8+
[...sleepScienceKeys.all, 'dailyNeed', date, targetUserId] as const,
9+
energyCurve: (targetUserId?: string) =>
10+
[...sleepScienceKeys.all, 'energyCurve', targetUserId] as const,
11+
chronotype: (targetUserId?: string) =>
12+
[...sleepScienceKeys.all, 'chronotype', targetUserId] as const,
13+
dataSufficiency: (targetUserId?: string) =>
14+
[...sleepScienceKeys.all, 'dataSufficiency', targetUserId] as const,
15+
};

SparkyFitnessFrontend/src/contexts/ThemeContext.tsx

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type React from 'react';
2-
import { createContext, useContext, useEffect, useState } from 'react';
1+
import { whoopCSSVariables } from '@/lib/sleep/whoop-colors';
32
import { info } from '@/utils/logging';
3+
import type React from 'react';
4+
import { createContext, useContext, useEffect, useRef, useState } from 'react';
45

5-
type ThemeSetting = 'light' | 'dark' | 'system';
6+
type ThemeSetting = 'light' | 'dark' | 'whoop' | 'system';
67
type ResolvedTheme = 'light' | 'dark';
78

89
interface ThemeContextType {
@@ -34,10 +35,27 @@ const getSystemTheme = (): ResolvedTheme => {
3435
return 'light';
3536
};
3637

38+
/** Apply WHOOP CSS custom properties to the root element */
39+
const applyWhoopCSS = () => {
40+
const root = document.documentElement;
41+
for (const [key, value] of Object.entries(whoopCSSVariables)) {
42+
root.style.setProperty(key, value);
43+
}
44+
};
45+
46+
/** Remove WHOOP CSS custom properties from the root element */
47+
const removeWhoopCSS = () => {
48+
const root = document.documentElement;
49+
for (const key of Object.keys(whoopCSSVariables)) {
50+
root.style.removeProperty(key);
51+
}
52+
};
53+
3754
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
3855
children,
3956
}) => {
4057
const { loggingLevel } = usePreferences();
58+
const previousTheme = useRef<ThemeSetting | null>(null);
4159
const [theme, setThemeState] = useState<ThemeSetting>(() => {
4260
const saved = localStorage.getItem('theme');
4361
const initialTheme = (saved as ThemeSetting) || 'system';
@@ -54,6 +72,10 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
5472
if (saved === 'light' || saved === 'dark') {
5573
return saved;
5674
}
75+
// WHOOP resolves as dark
76+
if (saved === 'whoop') {
77+
return 'dark';
78+
}
5779
return getSystemTheme();
5880
});
5981

@@ -81,6 +103,8 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
81103
if (theme === 'system') {
82104
// eslint-disable-next-line react-hooks/set-state-in-effect
83105
setResolvedTheme(getSystemTheme());
106+
} else if (theme === 'whoop') {
107+
setResolvedTheme('dark');
84108
} else {
85109
setResolvedTheme(theme);
86110
}
@@ -96,11 +120,22 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
96120
resolvedTheme
97121
);
98122
localStorage.setItem('theme', theme);
123+
124+
// Apply dark class for dark-based themes
99125
if (resolvedTheme === 'dark') {
100126
document.documentElement.classList.add('dark');
101127
} else {
102128
document.documentElement.classList.remove('dark');
103129
}
130+
131+
// WHOOP theme: inject/remove CSS variables
132+
if (theme === 'whoop') {
133+
applyWhoopCSS();
134+
} else if (previousTheme.current === 'whoop') {
135+
removeWhoopCSS();
136+
}
137+
138+
previousTheme.current = theme;
104139
}, [theme, resolvedTheme, loggingLevel]);
105140

106141
const setTheme = (newTheme: ThemeSetting) => {
@@ -111,7 +146,13 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
111146
const toggleTheme = () => {
112147
setThemeState((prev) => {
113148
const newTheme =
114-
prev === 'light' ? 'dark' : prev === 'dark' ? 'system' : 'light';
149+
prev === 'light'
150+
? 'dark'
151+
: prev === 'dark'
152+
? 'whoop'
153+
: prev === 'whoop'
154+
? 'system'
155+
: 'light';
115156
info(loggingLevel, 'ThemeProvider: Toggling theme to:', newTheme);
116157
return newTheme;
117158
});

0 commit comments

Comments
 (0)