175 lines
4.8 KiB
TypeScript
175 lines
4.8 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
IonButton,
|
|
IonButtons,
|
|
IonContent,
|
|
IonHeader,
|
|
IonIcon,
|
|
IonPage,
|
|
IonSegment,
|
|
IonSegmentButton,
|
|
IonTitle,
|
|
IonToolbar,
|
|
} from '@ionic/react';
|
|
import { IonBackButton } from '@ionic/react';
|
|
import { calendarOutline } from 'ionicons/icons';
|
|
import { Redirect } from 'react-router-dom';
|
|
import type { Tables } from '../database.types';
|
|
import StatsHabitCard from '../components/StatsHabitCard';
|
|
import { supabase } from '../supabase';
|
|
import {
|
|
Style,
|
|
setStatusBarBackground,
|
|
setStatusBarStyle,
|
|
} from '../utils/statusBar';
|
|
import { useIonViewWillEnter } from '@ionic/react';
|
|
|
|
interface HabitWithMeta extends Tables<'habits'> {
|
|
completions: Tables<'habit_completions'>[];
|
|
}
|
|
|
|
const formatDate = (date: Date) => date.toISOString().slice(0, 10);
|
|
|
|
const calculateCurrentStreak = (dates: string[]) => {
|
|
const unique = Array.from(new Set(dates)).sort((a, b) => b.localeCompare(a));
|
|
let streak = 0;
|
|
let cursor = new Date();
|
|
|
|
if (!unique.includes(formatDate(cursor))) {
|
|
cursor.setDate(cursor.getDate() - 1);
|
|
}
|
|
|
|
while (unique.includes(formatDate(cursor))) {
|
|
streak += 1;
|
|
cursor.setDate(cursor.getDate() - 1);
|
|
}
|
|
|
|
return streak;
|
|
};
|
|
|
|
const Stats: React.FC = () => {
|
|
const [period, setPeriod] = useState<'7' | '30'>('7');
|
|
const [sessionChecked, setSessionChecked] = useState(false);
|
|
const [userId, setUserId] = useState<string | null>(null);
|
|
const [habits, setHabits] = useState<HabitWithMeta[]>([]);
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
const {
|
|
data: { session },
|
|
} = await supabase.auth.getSession();
|
|
|
|
if (!session?.user) {
|
|
setSessionChecked(true);
|
|
setUserId(null);
|
|
return;
|
|
}
|
|
|
|
setUserId(session.user.id);
|
|
setSessionChecked(true);
|
|
|
|
const { data } = await supabase
|
|
.from('habits')
|
|
.select('*, completions:habit_completions(*)')
|
|
.order('created_at', { ascending: true });
|
|
|
|
setHabits((data ?? []) as HabitWithMeta[]);
|
|
};
|
|
|
|
load();
|
|
}, []);
|
|
|
|
const days = Number(period);
|
|
const visibleDates = useMemo(() => {
|
|
return Array.from({ length: days }, (_, index) => {
|
|
const date = new Date();
|
|
date.setDate(date.getDate() - index);
|
|
return formatDate(date);
|
|
});
|
|
}, [days]);
|
|
|
|
const summary = useMemo(() => {
|
|
return habits.map((habit) => {
|
|
const completionDates = habit.completions.map(
|
|
(entry) => entry.completed_on,
|
|
);
|
|
const completedCount = completionDates.filter((date) =>
|
|
visibleDates.includes(date),
|
|
).length;
|
|
return {
|
|
id: habit.id,
|
|
name: habit.name,
|
|
color: habit.color,
|
|
completedCount,
|
|
streak: calculateCurrentStreak(completionDates),
|
|
};
|
|
});
|
|
}, [habits, visibleDates]);
|
|
|
|
if (sessionChecked && !userId) {
|
|
return <Redirect to="/auth" />;
|
|
}
|
|
|
|
return (
|
|
<IonPage>
|
|
<IonHeader className="ion-no-border">
|
|
<IonToolbar>
|
|
<IonButtons slot="start">
|
|
<IonBackButton defaultHref="/home" />
|
|
</IonButtons>
|
|
<IonTitle>Stats</IonTitle>
|
|
</IonToolbar>
|
|
</IonHeader>
|
|
<IonContent fullscreen>
|
|
<div className="dashboard-shell ion-padding">
|
|
<section className="hero-card compact">
|
|
<span className="eyebrow">Consistency view</span>
|
|
<h1>See which habits you're actually keeping.</h1>
|
|
<p>
|
|
Flip between the last week and month to spot patterns before a
|
|
streak disappears.
|
|
</p>
|
|
</section>
|
|
|
|
<div className="stats-filter-row">
|
|
<IonIcon icon={calendarOutline} />
|
|
<IonSegment
|
|
value={period}
|
|
onIonChange={(e) => setPeriod(e.detail.value as '7' | '30')}
|
|
>
|
|
<IonSegmentButton value="7">Last 7 days</IonSegmentButton>
|
|
<IonSegmentButton value="30">Last 30 days</IonSegmentButton>
|
|
</IonSegment>
|
|
</div>
|
|
|
|
{summary.length === 0 ? (
|
|
<div className="inline-message">
|
|
<h3>No stats yet</h3>
|
|
<p>
|
|
Create a habit and start checking it off daily to unlock
|
|
consistency insights.
|
|
</p>
|
|
<IonButton routerLink="/home">Go to dashboard</IonButton>
|
|
</div>
|
|
) : (
|
|
<div className="stack-list">
|
|
{summary.map((habit) => (
|
|
<StatsHabitCard
|
|
key={habit.id}
|
|
name={habit.name}
|
|
color={habit.color}
|
|
completedCount={habit.completedCount}
|
|
totalDays={days}
|
|
streak={habit.streak}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</IonContent>
|
|
</IonPage>
|
|
);
|
|
};
|
|
|
|
export default Stats;
|