import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonPage, IonRefresher, IonRefresherContent, IonSkeletonText, IonToolbar, useIonViewWillEnter, } from '@ionic/react'; import { addOutline, analyticsOutline, checkmarkCircle, personCircleOutline, sparklesOutline, } from 'ionicons/icons'; import { Redirect, useHistory } from 'react-router-dom'; import type { Tables } from '../database.types'; import { supabase } from '../supabase'; import EmptyState from '../components/EmptyState'; import HabitCard from '../components/HabitCard'; import HabitFormModal from '../components/HabitFormModal'; import { Style, setStatusBarBackground, setStatusBarStyle, } from '../utils/statusBar'; 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 Home: React.FC = () => { const history = useHistory(); const [sessionChecked, setSessionChecked] = useState(false); const [userId, setUserId] = useState(null); const [habits, setHabits] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [showCreateModal, setShowCreateModal] = useState(false); const [busyHabitId, setBusyHabitId] = useState(null); const loadDashboard = useCallback(async () => { setError(''); const { data: { session }, error: sessionError, } = await supabase.auth.getSession(); if (sessionError) { setError(sessionError.message); setSessionChecked(true); setLoading(false); return; } if (!session?.user) { setUserId(null); setSessionChecked(true); setLoading(false); return; } setUserId(session.user.id); setSessionChecked(true); const { data, error: habitsError } = await supabase .from('habits') .select('*, completions:habit_completions(*)') .order('created_at', { ascending: true }); if (habitsError) { setError(habitsError.message); setLoading(false); return; } setHabits((data ?? []) as HabitWithMeta[]); setLoading(false); }, []); useEffect(() => { loadDashboard(); }, [loadDashboard]); const today = formatDate(new Date()); const habitStats = useMemo(() => { const last7Days = Array.from({ length: 7 }, (_, index) => { const date = new Date(); date.setDate(date.getDate() - index); return formatDate(date); }); return habits.reduce< Record >((acc, habit) => { const completionDates = habit.completions.map( (entry) => entry.completed_on, ); const completedInWeek = completionDates.filter((date) => last7Days.includes(date), ).length; acc[habit.id] = { streak: calculateCurrentStreak(completionDates), weekRate: Math.round((completedInWeek / last7Days.length) * 100), doneToday: completionDates.includes(today), }; return acc; }, {}); }, [habits, today]); const totalCompletedToday = habits.filter( (habit) => habitStats[habit.id]?.doneToday, ).length; const longestStreak = habits.reduce( (max, habit) => Math.max(max, habitStats[habit.id]?.streak ?? 0), 0, ); const completionRatio = habits.length ? totalCompletedToday / habits.length : 0; const pendingCount = Math.max(habits.length - totalCompletedToday, 0); const createHabit = async (values: { name: string; targetDescription: string; color: string; }) => { if (!userId) throw new Error('You need to be logged in.'); const { error: insertError } = await supabase.from('habits').insert({ user_id: userId, name: values.name, target_description: values.targetDescription || null, color: values.color, }); if (insertError) throw insertError; await loadDashboard(); }; const toggleHabit = async (habit: HabitWithMeta) => { if (!userId) return; setBusyHabitId(habit.id); const existing = habit.completions.find( (entry) => entry.completed_on === today, ); if (existing) { const { error: deleteError } = await supabase .from('habit_completions') .delete() .eq('id', existing.id); if (deleteError) setError(deleteError.message); } else { const { error: insertError } = await supabase .from('habit_completions') .insert({ habit_id: habit.id, user_id: userId, completed_on: today, }); if (insertError) setError(insertError.message); } await loadDashboard(); setBusyHabitId(null); }; const deleteHabit = async (habitId: string) => { setBusyHabitId(habitId); const { error: deleteError } = await supabase .from('habits') .delete() .eq('id', habitId); if (deleteError) { setError(deleteError.message); } await loadDashboard(); setBusyHabitId(null); }; if (sessionChecked && !userId) { return ; } return ( history.push('/stats')} > history.push('/account')} > { await loadDashboard(); e.detail.complete(); }} >

Habits ({habits.length})

Daily

Weekly
setShowCreateModal(true)} >
{totalCompletedToday} completed today {Math.round(completionRatio * 100)}% completed
{loading ? (
) : error ? (

Couldn't load your habits

{error}

Try Again
) : habits.length === 0 ? (
setShowCreateModal(true)} />
) : ( <>
{habits.map((habit) => ( toggleHabit(habit)} onDelete={() => deleteHabit(habit.id)} busy={busyHabitId === habit.id} /> ))}

Momentum

{Math.round(completionRatio * 100)}%

Keep your rhythm

{pendingCount === 0 ? 'Everything is complete today. Enjoy the streak.' : `${pendingCount} habit${pendingCount === 1 ? '' : 's'} left to protect today's routine.`}

{totalCompletedToday}{' '} done {longestStreak} day best streak
)}
setShowCreateModal(false)} onSubmit={createHabit} />
); }; export default Home;