build: eb8f08f0-e41a-4086-9130-c5af05b58ec7

This commit is contained in:
AppCakes
2026-06-15 09:03:51 +00:00
commit fc0dbd735d
110 changed files with 10424 additions and 0 deletions
+339
View File
@@ -0,0 +1,339 @@
import React, { useState, useEffect } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonButtons,
IonButton,
IonIcon,
IonRange,
IonInput,
IonTextarea,
IonAlert,
useIonViewWillEnter,
} from '@ionic/react';
import { useHistory } from 'react-router-dom';
import {
chevronBackOutline,
trashOutline,
medkitOutline,
} from 'ionicons/icons';
import {
getCheckups,
addCheckup,
deleteCheckup,
clearCheckups,
} from '../services/localHealthStore';
import { CheckupEntry, CheckupMood } from '../types/health';
import HealthLogList from '../components/health/HealthLogList';
const Checkups: React.FC = () => {
const history = useHistory();
const [entries, setEntries] = useState<CheckupEntry[]>([]);
const [mood, setMood] = useState<CheckupMood | ''>('');
const [energy, setEnergy] = useState<number>(70);
const [symptoms, setSymptoms] = useState<string>('');
const [note, setNote] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const [showClearAlert, setShowClearAlert] = useState(false);
const refreshData = () => {
setEntries(getCheckups());
};
useIonViewWillEnter(() => {
refreshData();
});
const handleSave = () => {
if (!mood) {
setError('Please select a mood');
setTimeout(() => setError(null), 4000);
return;
}
addCheckup({
mood: mood as CheckupMood,
energy,
symptoms,
note,
});
setMood('');
setEnergy(70);
setSymptoms('');
setNote('');
refreshData();
};
const handleDelete = (id: string) => {
deleteCheckup(id);
refreshData();
};
const handleClear = () => {
clearCheckups();
refreshData();
};
const moods: CheckupMood[] = ['Great', 'Okay', 'Low', 'Unwell'];
return (
<IonPage style={{ background: '#eef7fb' }}>
<IonHeader className="ion-no-border">
<IonToolbar style={{ '--background': '#eef7fb', '--color': '#1e3a5f' }}>
<IonButtons slot="start">
<IonButton onClick={() => history.goBack()}>
<IonIcon icon={chevronBackOutline} slot="icon-only" />
</IonButton>
</IonButtons>
<IonTitle style={{ fontWeight: '700' }}>Self Checkup</IonTitle>
<IonButtons slot="end">
{entries.length > 0 && (
<IonButton onClick={() => setShowClearAlert(true)}>
<IonIcon
icon={trashOutline}
slot="icon-only"
style={{ opacity: 0.5 }}
/>
</IonButton>
)}
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent
style={{
'--background': '#eef7fb',
'--padding-start': '20px',
'--padding-end': '20px',
'--padding-top': '16px',
}}
>
{/* Form Card */}
<div
style={{
background: '#ffffff',
borderRadius: '24px',
padding: '24px',
marginBottom: '20px',
display: 'flex',
flexDirection: 'column',
gap: '16px',
}}
>
<div
style={{ fontSize: '18px', fontWeight: '700', color: '#1e3a5f' }}
>
How do you feel right now?
</div>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '12px',
}}
>
Mood
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{moods.map((m) => (
<div
key={m}
onClick={() => setMood(m)}
style={{
padding: '10px 16px',
borderRadius: '999px',
fontSize: '13px',
fontWeight: '700',
cursor: 'pointer',
background: mood === m ? '#0369a1' : '#e2f2f8',
color: mood === m ? '#ffffff' : '#1e3a5f',
transition: 'all 0.2s ease',
}}
>
{m}
</div>
))}
</div>
</div>
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: '8px',
}}
>
<span
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
}}
>
Energy
</span>
<span
style={{
fontSize: '13px',
fontWeight: '700',
color: '#0369a1',
}}
>
{energy}%
</span>
</div>
<IonRange
value={energy}
onIonChange={(e) => setEnergy(e.detail.value as number)}
style={{
'--bar-background': '#e2f2f8',
'--bar-background-active': '#38bdf8',
'--knob-background': '#0369a1',
padding: '0',
}}
/>
</div>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '8px',
}}
>
Symptoms
</div>
<IonInput
value={symptoms}
onIonInput={(e) => setSymptoms(e.detail.value!)}
placeholder="e.g. Headache, fatigue..."
style={{
'--background': '#f3f8fb',
'--padding-start': '16px',
'--padding-end': '16px',
'--border-radius': '12px',
fontSize: '14px',
color: '#1e3a5f',
}}
/>
</div>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '8px',
}}
>
Notes
</div>
<IonTextarea
value={note}
onIonInput={(e) => setNote(e.detail.value!)}
placeholder="Add more details..."
rows={3}
style={{
'--background': '#f3f8fb',
'--padding-start': '16px',
'--padding-end': '16px',
'--padding-top': '12px',
'--padding-bottom': '12px',
'--border-radius': '12px',
fontSize: '14px',
color: '#1e3a5f',
}}
/>
</div>
{error && (
<div
style={{
fontSize: '12px',
color: '#dc2626',
textAlign: 'center',
}}
>
{error}
</div>
)}
<IonButton
expand="block"
onClick={handleSave}
style={{
'--background': '#0369a1',
'--border-radius': '999px',
'--color': '#ffffff',
fontWeight: '700',
height: '48px',
marginTop: '8px',
}}
>
Save checkup
</IonButton>
</div>
{/* History Section */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'baseline',
margin: '0 0 12px',
}}
>
<span
style={{ fontSize: '17px', fontWeight: '700', color: '#1e3a5f' }}
>
History
</span>
<span
style={{
fontSize: '12px',
fontWeight: '600',
color: 'rgba(30,58,95,0.40)',
}}
>
{entries.length} logs
</span>
</div>
<HealthLogList
type="checkup"
entries={entries}
onDelete={handleDelete}
emptyTitle="No checkups yet"
emptyActionLabel="Save your first checkup"
onEmptyAction={() => {}} // Form is already on page
/>
<div style={{ height: '40px' }} />
<IonAlert
isOpen={showClearAlert}
onDidDismiss={() => setShowClearAlert(false)}
header="Clear history?"
message="This will delete all checkup logs permanently."
buttons={[
{ text: 'Cancel', role: 'cancel' },
{ text: 'Clear All', role: 'destructive', handler: handleClear },
]}
/>
</IonContent>
</IonPage>
);
};
export default Checkups;
+376
View File
@@ -0,0 +1,376 @@
import React, { useState } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonButtons,
IonButton,
IonIcon,
IonInput,
IonTextarea,
IonSelect,
IonSelectOption,
IonAlert,
useIonViewWillEnter,
} from '@ionic/react';
import { useHistory } from 'react-router-dom';
import {
chevronBackOutline,
trashOutline,
waterOutline,
leafOutline,
} from 'ionicons/icons';
import {
getDietEntries,
addDietEntry,
deleteDietEntry,
clearDietEntries,
getWellbeingSummary,
} from '../services/localHealthStore';
import { DietEntry, MealType, WellbeingSummary } from '../types/health';
import HealthLogList from '../components/health/HealthLogList';
import MetricMiniCard from '../components/health/MetricMiniCard';
const Diet: React.FC = () => {
const history = useHistory();
const [entries, setEntries] = useState<DietEntry[]>([]);
const [summary, setSummary] = useState<WellbeingSummary | null>(null);
const [mealType, setMealType] = useState<MealType | ''>('');
const [waterCups, setWaterCups] = useState<string>('');
const [produceServings, setProduceServings] = useState<string>('');
const [notes, setNotes] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const [showClearAlert, setShowClearAlert] = useState(false);
const refreshData = () => {
setEntries(getDietEntries());
setSummary(getWellbeingSummary());
};
useIonViewWillEnter(() => {
refreshData();
});
const handleSave = () => {
if (!mealType) {
setError('Please select a meal type');
setTimeout(() => setError(null), 4000);
return;
}
addDietEntry({
mealType: mealType as MealType,
waterCups: parseInt(waterCups) || 0,
produceServings: parseInt(produceServings) || 0,
notes,
});
setMealType('');
setWaterCups('');
setProduceServings('');
setNotes('');
refreshData();
};
const handleDelete = (id: string) => {
deleteDietEntry(id);
refreshData();
};
const handleClear = () => {
clearDietEntries();
refreshData();
};
const mealTypes: MealType[] = ['Breakfast', 'Lunch', 'Dinner', 'Snack'];
return (
<IonPage style={{ background: '#eef7fb' }}>
<IonHeader className="ion-no-border">
<IonToolbar style={{ '--background': '#eef7fb', '--color': '#1e3a5f' }}>
<IonButtons slot="start">
<IonButton onClick={() => history.goBack()}>
<IonIcon icon={chevronBackOutline} slot="icon-only" />
</IonButton>
</IonButtons>
<IonTitle style={{ fontWeight: '700' }}>Diet</IonTitle>
<IonButtons slot="end">
{entries.length > 0 && (
<IonButton onClick={() => setShowClearAlert(true)}>
<IonIcon
icon={trashOutline}
slot="icon-only"
style={{ opacity: 0.5 }}
/>
</IonButton>
)}
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent
style={{
'--background': '#eef7fb',
'--padding-start': '20px',
'--padding-end': '20px',
'--padding-top': '16px',
}}
>
{/* Form Card */}
<div
style={{
background: '#ffffff',
borderRadius: '24px',
padding: '24px',
marginBottom: '20px',
display: 'flex',
flexDirection: 'column',
gap: '16px',
}}
>
<div
style={{ fontSize: '18px', fontWeight: '700', color: '#1e3a5f' }}
>
Log todays nutrition
</div>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '8px',
}}
>
Meal Type
</div>
<IonSelect
value={mealType}
onIonChange={(e) => setMealType(e.detail.value)}
interface="action-sheet"
placeholder="Select a meal"
style={{
'--background': '#f3f8fb',
'--padding-start': '16px',
'--padding-end': '16px',
'--border-radius': '12px',
fontSize: '14px',
color: '#1e3a5f',
width: '100%',
'--placeholder-color': 'rgba(30,58,95,0.38)',
}}
>
{mealTypes.map((m) => (
<IonSelectOption key={m} value={m}>
{m}
</IonSelectOption>
))}
</IonSelect>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '12px',
}}
>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '8px',
}}
>
Water (cups)
</div>
<IonInput
type="text"
inputMode="numeric"
value={waterCups}
onIonInput={(e) =>
setWaterCups(e.detail.value!.replace(/[^0-9]/g, ''))
}
placeholder="0"
style={{
'--background': '#f3f8fb',
'--padding-start': '16px',
'--border-radius': '12px',
fontSize: '14px',
color: '#1e3a5f',
}}
/>
</div>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '8px',
}}
>
Produce (servings)
</div>
<IonInput
type="text"
inputMode="numeric"
value={produceServings}
onIonInput={(e) =>
setProduceServings(e.detail.value!.replace(/[^0-9]/g, ''))
}
placeholder="0"
style={{
'--background': '#f3f8fb',
'--padding-start': '16px',
'--border-radius': '12px',
fontSize: '14px',
color: '#1e3a5f',
}}
/>
</div>
</div>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '8px',
}}
>
Notes
</div>
<IonTextarea
value={notes}
onIonInput={(e) => setNotes(e.detail.value!)}
placeholder="Add more details..."
rows={3}
style={{
'--background': '#f3f8fb',
'--padding-start': '16px',
'--padding-end': '16px',
'--padding-top': '12px',
'--padding-bottom': '12px',
'--border-radius': '12px',
fontSize: '14px',
color: '#1e3a5f',
}}
/>
</div>
{error && (
<div
style={{
fontSize: '12px',
color: '#dc2626',
textAlign: 'center',
}}
>
{error}
</div>
)}
<IonButton
expand="block"
onClick={handleSave}
style={{
'--background': '#0369a1',
'--border-radius': '999px',
'--color': '#ffffff',
fontWeight: '700',
height: '48px',
marginTop: '8px',
}}
>
Save diet log
</IonButton>
</div>
{/* Today Summary */}
{summary && (
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '12px',
marginBottom: '20px',
}}
>
<MetricMiniCard
label="Today's Water"
value={summary.hydrationCups.toString()}
helper="cups"
progress={summary.hydrationCups / 8}
icon={waterOutline}
/>
<MetricMiniCard
label="Today's Produce"
value={summary.produceServings.toString()}
helper="servings"
progress={summary.produceServings / 5}
icon={leafOutline}
/>
</div>
)}
{/* History Section */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'baseline',
margin: '0 0 12px',
}}
>
<span
style={{ fontSize: '17px', fontWeight: '700', color: '#1e3a5f' }}
>
History
</span>
<span
style={{
fontSize: '12px',
fontWeight: '600',
color: 'rgba(30,58,95,0.40)',
}}
>
{entries.length} logs
</span>
</div>
<HealthLogList
type="diet"
entries={entries}
onDelete={handleDelete}
emptyTitle="No diet logs yet"
emptyActionLabel="Log a meal"
onEmptyAction={() => {}} // Form is already on page
/>
<div style={{ height: '40px' }} />
<IonAlert
isOpen={showClearAlert}
onDidDismiss={() => setShowClearAlert(false)}
header="Clear history?"
message="This will delete all diet logs permanently."
buttons={[
{ text: 'Cancel', role: 'cancel' },
{ text: 'Clear All', role: 'destructive', handler: handleClear },
]}
/>
</IonContent>
</IonPage>
);
};
export default Diet;
+242
View File
@@ -0,0 +1,242 @@
import React, { useState } from 'react';
import {
IonContent,
IonPage,
IonIcon,
IonText,
useIonViewWillEnter,
} from '@ionic/react';
import { useHistory } from 'react-router-dom';
import {
heartCircleOutline,
medkitOutline,
restaurantOutline,
moonOutline,
chevronForwardOutline,
} from 'ionicons/icons';
import WellbeingScoreCard from '../components/health/WellbeingScoreCard';
import MetricMiniCard from '../components/health/MetricMiniCard';
import RecentActivityList from '../components/health/RecentActivityList';
import {
getWellbeingSummary,
getRecentActivity,
} from '../services/localHealthStore';
import { WellbeingSummary, ActivityEntry } from '../types/health';
const Home: React.FC = () => {
const history = useHistory();
const [summary, setSummary] = useState<WellbeingSummary>(
getWellbeingSummary()
);
const [recentEntries, setRecentEntries] =
useState<ActivityEntry[]>(getRecentActivity());
const refreshData = () => {
setSummary(getWellbeingSummary());
setRecentEntries(getRecentActivity());
};
useIonViewWillEnter(() => {
refreshData();
});
return (
<IonPage style={{ background: '#eef7fb' }}>
<IonContent
fullscreen
style={{
'--background':
'linear-gradient(160deg, rgba(56,189,248,0.16) 0%, #eef7fb 58%, #f8fbfd 100%)',
}}
>
{/* Top Content Row */}
<div
style={{
padding: 'calc(16px + var(--ion-safe-area-top)) 20px 12px',
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<div
style={{
width: '40px',
height: '40px',
background: 'rgba(3,105,161,0.12)',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<IonIcon
icon={heartCircleOutline}
style={{ fontSize: '22px', color: '#0369a1' }}
/>
</div>
<div style={{ flex: 1 }}>
<div
style={{
fontSize: '12px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
lineHeight: '16px',
}}
>
Wellbeing
</div>
<div
style={{
fontSize: '20px',
fontWeight: '700',
color: '#1e3a5f',
lineHeight: '24px',
}}
>
Good morning
</div>
</div>
<div
style={{
background: '#ffffff',
borderRadius: '999px',
padding: '8px 12px',
fontSize: '12px',
fontWeight: '700',
color: '#0369a1',
}}
>
Local
</div>
</div>
{/* Score Widget */}
<WellbeingScoreCard
score={summary.score}
message={summary.scoreMessage}
progress={summary.scoreProgress}
/>
{/* Metric Row */}
<div
style={{
margin: '0 20px 12px',
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '12px',
}}
>
<MetricMiniCard
label="Sleep"
value={summary.sleepHours.toString()}
helper="hrs"
progress={summary.sleepHours / 8}
icon={moonOutline}
/>
<MetricMiniCard
label="Water"
value={summary.hydrationCups.toString()}
helper="cups"
progress={summary.hydrationCups / 8}
icon={restaurantOutline}
/>
</div>
{/* Action Strip */}
<div
style={{
display: 'flex',
gap: '8px',
overflowX: 'auto',
padding: '0 20px 20px',
msOverflowStyle: 'none',
scrollbarWidth: 'none',
}}
>
{[
{ label: 'Checkup', icon: medkitOutline, path: '/checkups' },
{ label: 'Diet', icon: restaurantOutline, path: '/diet' },
{ label: 'Sleep', icon: moonOutline, path: '/sleep' },
].map((action) => (
<div
key={action.label}
onClick={() => history.push(action.path)}
style={{
flexShrink: 0,
background: '#ffffff',
borderRadius: '999px',
padding: '10px 18px',
display: 'flex',
alignItems: 'center',
gap: '8px',
minHeight: '44px',
cursor: 'pointer',
}}
>
<IonIcon
icon={action.icon}
style={{ fontSize: '18px', color: '#0369a1' }}
/>
<span
style={{
fontSize: '13px',
fontWeight: '700',
color: '#1e3a5f',
}}
>
{action.label}
</span>
</div>
))}
</div>
{/* Section Header */}
<div
style={{
margin: '4px 20px 12px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'baseline',
}}
>
<span
style={{ fontSize: '17px', fontWeight: '700', color: '#1e3a5f' }}
>
Recent Activity
</span>
<div
onClick={() => history.push('/insights')}
style={{
display: 'flex',
alignItems: 'center',
gap: '2px',
cursor: 'pointer',
}}
>
<span
style={{ fontSize: '13px', fontWeight: '700', color: '#0369a1' }}
>
See insights
</span>
<IonIcon
icon={chevronForwardOutline}
style={{ fontSize: '14px', color: '#0369a1' }}
/>
</div>
</div>
{/* Recent Activity List */}
<RecentActivityList
entries={recentEntries}
onCreate={() => history.push('/checkups')}
onSeeInsights={() => history.push('/insights')}
/>
<div style={{ height: '20px' }} />
</IonContent>
</IonPage>
);
};
export default Home;
+204
View File
@@ -0,0 +1,204 @@
import React, { useState } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonButtons,
IonButton,
IonIcon,
IonGrid,
IonRow,
IonCol,
useIonViewWillEnter,
} from '@ionic/react';
import { useHistory } from 'react-router-dom';
import {
chevronBackOutline,
analyticsOutline,
medkitOutline,
restaurantOutline,
moonOutline,
} from 'ionicons/icons';
import {
getWellbeingSummary,
calculateInsights,
} from '../services/localHealthStore';
import { WellbeingSummary, LocalInsight } from '../types/health';
import {
InsightSummaryCard,
RecommendationList,
} from '../components/health/InsightCards';
const Insights: React.FC = () => {
const history = useHistory();
const [summary, setSummary] = useState<WellbeingSummary>(
getWellbeingSummary()
);
const [insights, setInsights] = useState<LocalInsight[]>(calculateInsights());
const refreshData = () => {
setSummary(getWellbeingSummary());
setInsights(calculateInsights());
};
useIonViewWillEnter(() => {
refreshData();
});
return (
<IonPage style={{ background: '#eef7fb' }}>
<IonHeader className="ion-no-border">
<IonToolbar style={{ '--background': '#eef7fb', '--color': '#1e3a5f' }}>
<IonButtons slot="start">
<IonButton onClick={() => history.goBack()}>
<IonIcon icon={chevronBackOutline} slot="icon-only" />
</IonButton>
</IonButtons>
<IonTitle style={{ fontWeight: '700' }}>Insights</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent
style={{
'--background': '#eef7fb',
'--padding-start': '20px',
'--padding-end': '20px',
'--padding-top': '16px',
}}
>
{insights.length > 0 ? (
<>
<InsightSummaryCard
summary={summary}
primaryInsight={insights[0]}
/>
<div
style={{
margin: '24px 0 12px',
fontSize: '17px',
fontWeight: '700',
color: '#1e3a5f',
}}
>
Habit Guidance
</div>
<RecommendationList insights={insights} />
</>
) : (
<div
style={{
background: '#ffffff',
borderRadius: '24px',
padding: '40px 24px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
marginTop: '20px',
}}
>
<div
style={{
width: '64px',
height: '64px',
borderRadius: '20px',
background: 'rgba(3,105,161,0.06)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '20px',
}}
>
<IonIcon
icon={analyticsOutline}
style={{ fontSize: '32px', color: 'rgba(3,105,161,0.30)' }}
/>
</div>
<div
style={{
fontSize: '18px',
fontWeight: '700',
color: '#1e3a5f',
marginBottom: '8px',
}}
>
Insights need a few logs first
</div>
<div
style={{
fontSize: '14px',
color: 'rgba(30,58,95,0.58)',
lineHeight: '20px',
marginBottom: '32px',
}}
>
Add a checkup, meal, or sleep log to see personalized guidance
based on your patterns.
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
width: '100%',
}}
>
<IonButton
onClick={() => history.push('/checkups')}
style={{
'--background': '#0369a1',
'--border-radius': '999px',
'--color': '#ffffff',
fontWeight: '700',
height: '48px',
}}
>
<IonIcon icon={medkitOutline} slot="start" />
Add Checkup
</IonButton>
<div style={{ display: 'flex', gap: '12px' }}>
<IonButton
onClick={() => history.push('/diet')}
style={{
flex: 1,
'--background': '#e2f2f8',
'--border-radius': '999px',
'--color': '#1e3a5f',
fontWeight: '700',
height: '48px',
}}
>
<IonIcon icon={restaurantOutline} slot="start" />
Diet
</IonButton>
<IonButton
onClick={() => history.push('/sleep')}
style={{
flex: 1,
'--background': '#e2f2f8',
'--border-radius': '999px',
'--color': '#1e3a5f',
fontWeight: '700',
height: '48px',
}}
>
<IonIcon icon={moonOutline} slot="start" />
Sleep
</IonButton>
</div>
</div>
</div>
)}
<div style={{ height: '40px' }} />
</IonContent>
</IonPage>
);
};
export default Insights;
+351
View File
@@ -0,0 +1,351 @@
import React, { useState } from 'react';
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonButtons,
IonButton,
IonIcon,
IonInput,
IonTextarea,
IonAlert,
useIonViewWillEnter,
} from '@ionic/react';
import { useHistory } from 'react-router-dom';
import { chevronBackOutline, trashOutline, moonOutline } from 'ionicons/icons';
import {
getSleepEntries,
addSleepEntry,
deleteSleepEntry,
clearSleepEntries,
} from '../services/localHealthStore';
import { SleepEntry, SleepQuality } from '../types/health';
import HealthLogList from '../components/health/HealthLogList';
import SleepTrendCard from '../components/health/SleepTrendCard';
const Sleep: React.FC = () => {
const history = useHistory();
const [entries, setEntries] = useState<SleepEntry[]>([]);
const [hours, setHours] = useState<string>('');
const [quality, setQuality] = useState<SleepQuality | ''>('');
const [interruptions, setInterruptions] = useState<string>('');
const [notes, setNotes] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const [showClearAlert, setShowClearAlert] = useState(false);
const refreshData = () => {
setEntries(getSleepEntries());
};
useIonViewWillEnter(() => {
refreshData();
});
const handleSave = () => {
const hoursNum = parseFloat(hours);
if (!hours || isNaN(hoursNum) || hoursNum <= 0) {
setError('Please enter a valid sleep duration');
setTimeout(() => setError(null), 4000);
return;
}
if (!quality) {
setError('Please select sleep quality');
setTimeout(() => setError(null), 4000);
return;
}
addSleepEntry({
hours: hoursNum,
quality: quality as SleepQuality,
interruptions: parseInt(interruptions) || 0,
notes,
});
setHours('');
setQuality('');
setInterruptions('');
setNotes('');
refreshData();
};
const handleDelete = (id: string) => {
deleteSleepEntry(id);
refreshData();
};
const handleClear = () => {
clearSleepEntries();
refreshData();
};
const qualities: SleepQuality[] = ['Restful', 'Okay', 'Restless'];
return (
<IonPage style={{ background: '#eef7fb' }}>
<IonHeader className="ion-no-border">
<IonToolbar style={{ '--background': '#eef7fb', '--color': '#1e3a5f' }}>
<IonButtons slot="start">
<IonButton onClick={() => history.goBack()}>
<IonIcon icon={chevronBackOutline} slot="icon-only" />
</IonButton>
</IonButtons>
<IonTitle style={{ fontWeight: '700' }}>Sleep</IonTitle>
<IonButtons slot="end">
{entries.length > 0 && (
<IonButton onClick={() => setShowClearAlert(true)}>
<IonIcon
icon={trashOutline}
slot="icon-only"
style={{ opacity: 0.5 }}
/>
</IonButton>
)}
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent
style={{
'--background': '#eef7fb',
'--padding-start': '20px',
'--padding-end': '20px',
'--padding-top': '16px',
}}
>
{/* Form Card */}
<div
style={{
background: '#ffffff',
borderRadius: '24px',
padding: '24px',
marginBottom: '20px',
display: 'flex',
flexDirection: 'column',
gap: '16px',
}}
>
<div
style={{ fontSize: '18px', fontWeight: '700', color: '#1e3a5f' }}
>
Record your sleep
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '12px',
}}
>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '8px',
}}
>
Duration (hours)
</div>
<IonInput
type="text"
inputMode="decimal"
value={hours}
onIonInput={(e) => {
const val = e.detail.value!.replace(/[^0-9.]/g, '');
if ((val.match(/\./g) || []).length <= 1) setHours(val);
}}
placeholder="0.0"
style={{
'--background': '#f3f8fb',
'--padding-start': '16px',
'--border-radius': '12px',
fontSize: '14px',
color: '#1e3a5f',
}}
/>
</div>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '8px',
}}
>
Interruptions
</div>
<IonInput
type="text"
inputMode="numeric"
value={interruptions}
onIonInput={(e) =>
setInterruptions(e.detail.value!.replace(/[^0-9]/g, ''))
}
placeholder="0"
style={{
'--background': '#f3f8fb',
'--padding-start': '16px',
'--border-radius': '12px',
fontSize: '14px',
color: '#1e3a5f',
}}
/>
</div>
</div>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '12px',
}}
>
Quality
</div>
<div style={{ display: 'flex', gap: '8px' }}>
{qualities.map((q) => (
<div
key={q}
onClick={() => setQuality(q)}
style={{
padding: '10px 16px',
borderRadius: '999px',
fontSize: '13px',
fontWeight: '700',
cursor: 'pointer',
background: quality === q ? '#0369a1' : '#e2f2f8',
color: quality === q ? '#ffffff' : '#1e3a5f',
transition: 'all 0.2s ease',
flex: 1,
textAlign: 'center',
}}
>
{q}
</div>
))}
</div>
</div>
<div>
<div
style={{
fontSize: '13px',
fontWeight: '700',
color: 'rgba(30,58,95,0.60)',
marginBottom: '8px',
}}
>
Notes
</div>
<IonTextarea
value={notes}
onIonInput={(e) => setNotes(e.detail.value!)}
placeholder="Add more details..."
rows={3}
style={{
'--background': '#f3f8fb',
'--padding-start': '16px',
'--padding-end': '16px',
'--padding-top': '12px',
'--padding-bottom': '12px',
'--border-radius': '12px',
fontSize: '14px',
color: '#1e3a5f',
}}
/>
</div>
{error && (
<div
style={{
fontSize: '12px',
color: '#dc2626',
textAlign: 'center',
}}
>
{error}
</div>
)}
<IonButton
expand="block"
onClick={handleSave}
style={{
'--background': '#0369a1',
'--border-radius': '999px',
'--color': '#ffffff',
fontWeight: '700',
height: '48px',
marginTop: '8px',
}}
>
Save sleep log
</IonButton>
</div>
{/* Trend Widget */}
<div style={{ marginBottom: '20px' }}>
<SleepTrendCard entries={entries} />
</div>
{/* History Section */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'baseline',
margin: '0 0 12px',
}}
>
<span
style={{ fontSize: '17px', fontWeight: '700', color: '#1e3a5f' }}
>
History
</span>
<span
style={{
fontSize: '12px',
fontWeight: '600',
color: 'rgba(30,58,95,0.40)',
}}
>
{entries.length} logs
</span>
</div>
<HealthLogList
type="sleep"
entries={entries}
onDelete={handleDelete}
emptyTitle="No sleep logs yet"
emptyActionLabel="Log sleep"
onEmptyAction={() => {}} // Form is already on page
/>
<div style={{ height: '40px' }} />
<IonAlert
isOpen={showClearAlert}
onDidDismiss={() => setShowClearAlert(false)}
header="Clear history?"
message="This will delete all sleep logs permanently."
buttons={[
{ text: 'Cancel', role: 'cancel' },
{ text: 'Clear All', role: 'destructive', handler: handleClear },
]}
/>
</IonContent>
</IonPage>
);
};
export default Sleep;