Skip to content

Commit 1c212b7

Browse files
ervwalterclaude
andcommitted
✨ feat: add demo mode with sample data for dashboard
- Create comprehensive demo data with 184 real-looking measurements - Add demo mode support to Dashboard component and API queries - Create /demo route for accessing the demo dashboard - Adjust demo data dates dynamically to end on current date - Support all dashboard features including charts, stats, and calculations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7a49224 commit 1c212b7

10 files changed

Lines changed: 1259 additions & 33 deletions

File tree

apps/web/src/components/dashboard/Dashboard.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,20 @@ import Deltas from "./Deltas";
1212
import HelpLink from "./HelpLink";
1313
import ProviderSyncErrors from "./ProviderSyncErrors";
1414

15-
const Dashboard: FC = () => {
16-
const dashboardData = useComputeDashboardData();
15+
interface DashboardProps {
16+
demoMode?: boolean;
17+
}
1718

18-
// Check if profile exists - if not, redirect to initial setup
19-
if (dashboardData.profileError instanceof ApiError && dashboardData.profileError.status === 404) {
19+
const Dashboard: FC<DashboardProps> = ({ demoMode }) => {
20+
const dashboardData = useComputeDashboardData(demoMode);
21+
22+
// Check if profile exists - if not, redirect to initial setup (skip for demo mode)
23+
if (!demoMode && dashboardData.profileError instanceof ApiError && dashboardData.profileError.status === 404) {
2024
return <Navigate to="/initial-setup" replace />;
2125
}
2226

23-
// If profile exists but no measurements, redirect to link page
24-
if (dashboardData.measurements.length === 0) {
27+
// If profile exists but no measurements, redirect to link page (skip for demo mode)
28+
if (!demoMode && dashboardData.measurements.length === 0) {
2529
return <Navigate to="/link" replace />;
2630
}
2731

apps/web/src/components/dashboard/Stats.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const Stats = () => {
2020

2121
const ppw = useMetric ? plannedPoundsPerWeek && plannedPoundsPerWeek * (2.20462262 / 2) : plannedPoundsPerWeek;
2222
const caloriesPerDay = (gainPerWeek / 7) * 3500 * (useMetric ? 2.20462262 : 1);
23-
const caloriesVsPlan = plannedPoundsPerWeek !== undefined ? caloriesPerDay - plannedPoundsPerWeek * 500 : 0;
23+
const caloriesVsPlan = plannedPoundsPerWeek !== undefined ? Math.abs(plannedPoundsPerWeek) * 500 - caloriesPerDay : 0;
2424

2525
return (
2626
<div>

apps/web/src/lib/api/queries.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useSuspenseQuery, useSuspenseQueries } from "@tanstack/react-query";
22
import { apiRequest, ApiError } from "./client";
33
import type { ProfileResponse, ProviderLink, MeasurementsResponse } from "./types";
44
import type { ProfileData, SettingsData } from "../core/interfaces";
5+
import { getDemoData, getDemoProfile } from "../demo/demoData";
56

67
// Query keys
78
export const queryKeys = {
@@ -79,11 +80,34 @@ export function useSettings() {
7980
}
8081

8182
// Combined profile and measurement data query with suspense (loads in parallel)
82-
export function useDashboardQueries() {
83+
export function useDashboardQueries(demoMode?: boolean) {
84+
// Create demo query options that match the expected types
85+
const demoQueryOptions = {
86+
profile: {
87+
queryKey: ["demo-profile"] as const,
88+
queryFn: async (): Promise<ProfileResponse> => {
89+
const demoProfile = getDemoProfile();
90+
return {
91+
user: {
92+
...demoProfile,
93+
uid: "demo",
94+
email: "demo@example.com",
95+
},
96+
timestamp: new Date().toISOString(),
97+
};
98+
},
99+
},
100+
data: {
101+
queryKey: ["demo-data"] as const,
102+
queryFn: async (): Promise<MeasurementsResponse> => getDemoData(),
103+
},
104+
};
105+
106+
// Always call the hook with consistent types
83107
const results = useSuspenseQueries({
84108
queries: [
85109
{
86-
...queryOptions.profile,
110+
...(demoMode ? demoQueryOptions.profile : queryOptions.profile),
87111
select: (data: ProfileResponse | null): ProfileData | null => {
88112
if (!data) return null;
89113
return {
@@ -98,7 +122,7 @@ export function useDashboardQueries() {
98122
};
99123
},
100124
},
101-
queryOptions.data,
125+
demoMode ? demoQueryOptions.data : queryOptions.data,
102126
],
103127
});
104128

apps/web/src/lib/dashboard/chart/create-chart-series.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export const createSinkersSeries = (data: [number, number | null, number | null,
9797
id: isInterpolated ? "estimated-sinkers" : "actual-sinkers",
9898
name: isInterpolated ? "Estimated Sinkers" : "Actual Sinkers",
9999
showInLegend: false,
100+
enableMouseTracking: false,
100101
zIndex: 2,
101102
color: isInterpolated ? "#e2e2e2" : "#999999",
102103
pointValKey: "high",

apps/web/src/lib/dashboard/chart/use-chart-options.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,31 @@ export const useChartOptions = (data: DashboardData) => {
2828

2929
const modeText = Modes[mode];
3030
const lastMeasurement = dataPoints[dataPoints.length - 1];
31-
const actualData: [number, number | null][] = dataPoints.map((m) => [toEpoch(m.date), m.isInterpolated ? null : m.actual]);
32-
const interpolatedData: [number, number | null][] = dataPoints.map((m) => [toEpoch(m.date), m.isInterpolated ? m.actual : null]);
33-
const trendData: [number, number][] = dataPoints.map((m) => [toEpoch(m.date), m.trend]);
31+
32+
// Convert to percentage for fat percent mode
33+
const multiplier = mode === "fatpercent" ? 100 : 1;
34+
35+
const actualData: [number, number | null][] = dataPoints.map((m) => [toEpoch(m.date), m.isInterpolated ? null : m.actual ? m.actual * multiplier : null]);
36+
const interpolatedData: [number, number | null][] = dataPoints.map((m) => [
37+
toEpoch(m.date),
38+
m.isInterpolated ? (m.actual ? m.actual * multiplier : null) : null,
39+
]);
40+
const trendData: [number, number][] = dataPoints.map((m) => [toEpoch(m.date), m.trend * multiplier]);
3441
const projectionsData: [number, number][] = [
35-
[toEpoch(lastMeasurement.date), lastMeasurement.trend],
36-
[toEpoch(lastMeasurement.date.plusDays(6)), lastMeasurement.trend + activeSlope * 6],
42+
[toEpoch(lastMeasurement.date), lastMeasurement.trend * multiplier],
43+
[toEpoch(lastMeasurement.date.plusDays(6)), (lastMeasurement.trend + activeSlope * 6) * multiplier],
3744
];
3845

3946
const actualSinkersData: [number, number | null, number | null, null][] = dataPoints.map((m) => [
4047
toEpoch(m.date),
41-
m.isInterpolated ? null : m.actual,
42-
m.isInterpolated ? null : m.trend,
48+
m.isInterpolated ? null : m.actual ? m.actual * multiplier : null,
49+
m.isInterpolated ? null : m.trend * multiplier,
4350
null,
4451
]);
4552
const interpolatedSinkersData: [number, number | null, number | null, null][] = dataPoints.map((m) => [
4653
toEpoch(m.date),
47-
m.isInterpolated ? m.actual : null,
48-
m.isInterpolated ? m.trend : null,
54+
m.isInterpolated ? (m.actual ? m.actual * multiplier : null) : null,
55+
m.isInterpolated ? m.trend * multiplier : null,
4956
null,
5057
]);
5158

@@ -113,7 +120,7 @@ export const useChartOptions = (data: DashboardData) => {
113120

114121
// Goal bands for weight mode
115122
if (mode === "weight" && goalWeight && options.yAxis && !Array.isArray(options.yAxis)) {
116-
const goalWidth = useMetric ? 1.134 : 5;
123+
const goalWidth = useMetric ? 1.134 : 2.5;
117124
options.yAxis.plotBands = [
118125
{
119126
from: goalWeight - goalWidth,

apps/web/src/lib/dashboard/computations/stats.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@ export const computeDeltas = (_mode: Mode, dataPoints: DataPoint[]): Delta[] =>
2323
}
2424

2525
const mostRecentTrendValue = points[0].trend;
26-
const deltaStart = today.equals(points[0].date) ? today : today.minusDays(1);
2726
let index: number;
2827

2928
// Yesterday
3029
if (daysSinceMostRecent <= 1 && points.length > 1) {
3130
const comparisonDataPoint = points[1];
32-
if (comparisonDataPoint.date.until(deltaStart, ChronoUnit.DAYS) === 1) {
31+
if (comparisonDataPoint.date.until(points[0].date, ChronoUnit.DAYS) === 1) {
3332
deltas.push({
3433
period: 1,
3534
description: "yesterday",
@@ -39,7 +38,7 @@ export const computeDeltas = (_mode: Mode, dataPoints: DataPoint[]): Delta[] =>
3938
}
4039

4140
// A week ago
42-
const targetDate7 = deltaStart.minusDays(7);
41+
const targetDate7 = points[0].date.minusDays(7);
4342
index = points.findIndex((m) => m.date.equals(targetDate7));
4443
// Needs to be at least 4 readings between now and a week ago for a valid trend comparison
4544
if (index >= 4) {
@@ -51,7 +50,7 @@ export const computeDeltas = (_mode: Mode, dataPoints: DataPoint[]): Delta[] =>
5150
}
5251

5352
// Two weeks ago
54-
const targetDate14 = deltaStart.minusDays(14);
53+
const targetDate14 = points[0].date.minusDays(14);
5554
index = points.findIndex((m) => m.date.equals(targetDate14));
5655
if (index >= 9) {
5756
deltas.push({
@@ -62,7 +61,7 @@ export const computeDeltas = (_mode: Mode, dataPoints: DataPoint[]): Delta[] =>
6261
}
6362

6463
// A month ago
65-
const targetDate28 = deltaStart.minusDays(28);
64+
const targetDate28 = points[0].date.minusDays(28);
6665
index = points.findIndex((m) => m.date.equals(targetDate28));
6766
if (index >= 19) {
6867
deltas.push({

apps/web/src/lib/dashboard/hooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ export const useDashboardData = (): DashboardData => {
1717
return data;
1818
};
1919

20-
export const useComputeDashboardData = (): DashboardData => {
20+
export const useComputeDashboardData = (demoMode?: boolean): DashboardData => {
2121
const [mode, setMode] = useState<Mode>("weight");
2222
const [timeRange, setTimeRange] = usePersistedState<TimeRange>("timeRange", "4w");
2323

2424
// Get profile and measurement data in parallel
25-
const { profile, measurementData: apiSourceData, providerStatus, profileError } = useDashboardQueries();
25+
const { profile, measurementData: apiSourceData, providerStatus, profileError } = useDashboardQueries(demoMode);
2626

2727
// Transform API data to match core interfaces
2828
const sourceData = useMemo(

0 commit comments

Comments
 (0)