import { GetActiveSession, type ActiveSession, } from '@/lib/asyncStorage'; import type { SessionType } from '@/lib/types'; import { formatDate, formatDateTime } from '@/lib/date'; import { RegisterForLocalNotificationsAsync } from '@/lib/notifications'; import { CheckAssignmentCompletion } from '@/lib/progress'; import { DEFAULT_FOCUS_DURATION_MINUTES } from '@/lib/sessionDefaults'; import { getSetupStatus } from '@/lib/setupStatus'; import { finalizeStoredSession } from '@/lib/sessionLifecycle'; import { supabase } from "@/lib/supabase"; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { Session } from '@supabase/supabase-js'; import { Redirect, router, Stack, useFocusEffect } from "expo-router"; import { useCallback, useEffect, useState } from 'react'; import { Alert, Modal, Pressable, ScrollView, Text, View, } from "react-native"; type UpcomingDeadlineTask = { tId: string; title: string; description: string; aId: string; subjectTitle: string; assignmentTitle: string; deadline: string; }; type DashboardProgressSummary = { completedFocusSessionsToday: number; minutesStudiedToday: number; minutesStudiedThisWeek: number; }; type RecentSession = { sessionId: string; taskTitle: string | null; sessionType: SessionType; elapsedSeconds: number; status: string; startedAt: string | null; endedAt: string | null; }; type RecentlyCompletedTask = { tId: string; title: string; assignmentTitle: string; lastChanged: string; }; const FLOW_STEPS = [ { label: '1', title: 'Subject', description: 'Start with the broad area you are studying, like one course or one exam you are preparing for.', }, { label: '2', title: 'Assignment', description: 'Inside that, add the bigger piece of work you want to move forward, like a project, a problem set, or revision block.', }, { label: '3', title: 'Task', description: 'Then break it down into one task that feels concrete enough to begin without overthinking it.', }, { label: '4', title: 'Sprint', description: 'That task is what you bring into a focus session. After a sprint, you take a short pause, then come back to the same kind of focused work. After a few rounds, the app gives you a longer pause so the rhythm still feels sustainable.', }, ] as const; function formatTime(totalSeconds: number) { const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } function getSessionLabel(sessionType: SessionType) { switch (sessionType) { case 'short_break': return 'Short Break'; case 'long_break': return 'Long Break'; default: return 'Active Sprint'; } } function formatTrackedMinutes(totalSeconds: number) { return Math.floor(totalSeconds / 60); } function formatTrackedDuration(totalSeconds: number) { if (totalSeconds <= 0) { return '0m'; } const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); if (hours === 0) { return `${minutes}m`; } if (minutes === 0) { return `${hours}h`; } return `${hours}h ${minutes}m`; } function getSessionStatusLabel(status: string) { switch (status) { case 'completed': return 'Completed'; case 'cancelled': return 'Cancelled'; case 'expired': return 'Timed out'; default: return status; } } function getStartOfToday() { const now = new Date(); return new Date(now.getFullYear(), now.getMonth(), now.getDate()); } function getStartOfWeek() { const today = getStartOfToday(); const currentDay = today.getDay(); const daysSinceMonday = (currentDay + 6) % 7; const startOfWeek = new Date(today); startOfWeek.setDate(today.getDate() - daysSinceMonday); return startOfWeek; } export default function HomeScreen() { const [session, SetSession] = useState(null); const [activeSprint, setActiveSprint] = useState(null); const [activeSprintTaskTitle, setActiveSprintTaskTitle] = useState(null); const [activeSprintTaskDesc, setActiveSprintTaskDesc] = useState(null); const [remainingSeconds, setRemainingSeconds] = useState(0); const [dashboardSummary, setDashboardSummary] = useState({ completedFocusSessionsToday: 0, minutesStudiedToday: 0, minutesStudiedThisWeek: 0, }); const [recentSessions, setRecentSessions] = useState([]); const [recentlyCompletedTasks, setRecentlyCompletedTasks] = useState([]); const [upcomingDeadlineTasks, setUpcomingDeadlineTasks] = useState([]); const [isFlowInfoVisible, setIsFlowInfoVisible] = useState(false); const [completingTaskId, setCompletingTaskId] = useState(null); const [subjectCount, setSubjectCount] = useState(0); const [needsSetup, setNeedsSetup] = useState(null); const loadActiveSprint = useCallback(async () => { const storedSprint = await GetActiveSession(); if (!storedSprint) { setActiveSprint(null); setActiveSprintTaskTitle(null); setActiveSprintTaskDesc(null); setRemainingSeconds(0); return; } const secondsLeft = Math.max( 0, Math.ceil((storedSprint.endTime - Date.now()) / 1000) ); if (secondsLeft <= 0) { await finalizeStoredSession('expired', storedSprint); setActiveSprint(null); setActiveSprintTaskTitle(null); setActiveSprintTaskDesc(null); setRemainingSeconds(0); return; } setActiveSprint(storedSprint); setRemainingSeconds(secondsLeft); if (!storedSprint.taskId) { setActiveSprintTaskTitle(getSessionLabel(storedSprint.sessionType)); setActiveSprintTaskDesc('Take the break before you jump into the next focus session.'); return; } const { data: dbTitle } = await supabase .from('tasks') .select('title') .eq('tId', storedSprint.taskId) .single(); const { data: dbDesc } = await supabase .from('tasks') .select('description') .eq('tId', storedSprint.taskId) .single(); setActiveSprintTaskTitle(dbTitle?.title ?? null); setActiveSprintTaskDesc(dbDesc?.description); }, []); const loadUpcomingDeadlineTasks = useCallback(async () => { if (!session?.user.id) { setUpcomingDeadlineTasks([]); return; } const { data: tasksData, error: tasksError } = await supabase .from('tasks') .select('tId, title, description, aId') .eq('uId', session.user.id) .eq('isCompleted', false); if (tasksError || !tasksData || tasksData.length === 0) { setUpcomingDeadlineTasks([]); return; } const assignmentIds = [...new Set(tasksData.map((task) => task.aId).filter(Boolean))]; if (assignmentIds.length === 0) { setUpcomingDeadlineTasks([]); return; } const { data: assignmentsData, error: assignmentsError } = await supabase .from('assignments') .select('aId, sId, title, deadline') .in('aId', assignmentIds) .eq('isCompleted', false); if (assignmentsError || !assignmentsData) { setUpcomingDeadlineTasks([]); return; } const subjectIds = [...new Set(assignmentsData.map((assignment) => assignment.sId).filter(Boolean))]; const { data: subjectsData, error: subjectsError } = await supabase .from('subjects') .select('sId, title') .in('sId', subjectIds); if (subjectsError || !subjectsData) { setUpcomingDeadlineTasks([]); return; } const now = Date.now(); const assignmentsById = new Map( assignmentsData.map((assignment) => [assignment.aId, assignment]) ); const subjectsById = new Map( subjectsData.map((subject) => [subject.sId, subject]) ); const enrichedTasks = tasksData .map((task) => { const assignment = assignmentsById.get(task.aId); if (!assignment?.deadline) { return null; } const deadlineTime = new Date(assignment.deadline).getTime(); if (Number.isNaN(deadlineTime) || deadlineTime < now) { return null; } const subject = subjectsById.get(assignment.sId); return { tId: task.tId, title: task.title, description: task.description, aId: task.aId, subjectTitle: subject?.title ?? 'Unknown Subject', assignmentTitle: assignment.title, deadline: assignment.deadline, } satisfies UpcomingDeadlineTask; }) .filter((task): task is UpcomingDeadlineTask => task !== null) .sort( (left, right) => new Date(left.deadline).getTime() - new Date(right.deadline).getTime() ); setUpcomingDeadlineTasks(enrichedTasks); }, [session?.user.id]); const loadDashboardProgress = useCallback(async () => { if (!session?.user.id) { setDashboardSummary({ completedFocusSessionsToday: 0, minutesStudiedToday: 0, minutesStudiedThisWeek: 0, }); setRecentSessions([]); setRecentlyCompletedTasks([]); return; } const startOfToday = getStartOfToday().toISOString(); const startOfWeek = getStartOfWeek().toISOString(); const [ { data: weeklySessions, error: weeklySessionsError }, { data: rawRecentSessions, error: recentSessionsError }, { data: completedTasks, error: completedTasksError }, { count: fetchedSubjectCount, error: subjectCountError }, ] = await Promise.all([ supabase .from('sprint_sessions') .select('sessionId, sessionType, elapsedSeconds, status, startedAt, endedAt') .eq('userId', session.user.id) .eq('sessionType', 'focus') .not('endedAt', 'is', null) .gte('endedAt', startOfWeek), supabase .from('sprint_sessions') .select('sessionId, taskId, sessionType, elapsedSeconds, status, startedAt, endedAt') .eq('userId', session.user.id) .not('endedAt', 'is', null) .order('endedAt', { ascending: false }) .limit(6), supabase .from('tasks') .select('tId, title, aId, lastChanged') .eq('uId', session.user.id) .eq('isCompleted', true) .order('lastChanged', { ascending: false }) .limit(3), supabase .from('subjects') .select('sId', { count: 'exact', head: true }) .eq('uId', session.user.id), ]); if (weeklySessionsError || recentSessionsError || completedTasksError || subjectCountError) { setDashboardSummary({ completedFocusSessionsToday: 0, minutesStudiedToday: 0, minutesStudiedThisWeek: 0, }); setRecentSessions([]); setRecentlyCompletedTasks([]); setSubjectCount(0); return; } setSubjectCount(fetchedSubjectCount ?? 0); const weeklySessionRows = weeklySessions ?? []; const todaySummary = weeklySessionRows.reduce( (summary, currentSession) => { const endedAt = currentSession.endedAt ? new Date(currentSession.endedAt) : null; if (!endedAt || Number.isNaN(endedAt.getTime())) { return summary; } const elapsedSeconds = currentSession.elapsedSeconds ?? 0; summary.minutesStudiedThisWeek += formatTrackedMinutes(elapsedSeconds); if (endedAt >= new Date(startOfToday)) { summary.minutesStudiedToday += formatTrackedMinutes(elapsedSeconds); if (currentSession.status === 'completed') { summary.completedFocusSessionsToday += 1; } } return summary; }, { completedFocusSessionsToday: 0, minutesStudiedToday: 0, minutesStudiedThisWeek: 0, } satisfies DashboardProgressSummary ); setDashboardSummary(todaySummary); const recentSessionRows = rawRecentSessions ?? []; const recentTaskIds = [ ...new Set( recentSessionRows .map((recentSession) => recentSession.taskId) .filter((taskId): taskId is string => Boolean(taskId)) ), ]; const completedTaskRows = completedTasks ?? []; const completedAssignmentIds = [ ...new Set( completedTaskRows .map((task) => task.aId) .filter((assignmentId): assignmentId is string => Boolean(assignmentId)) ), ]; const [{ data: recentTasks }, { data: completedAssignments }] = await Promise.all([ recentTaskIds.length > 0 ? supabase .from('tasks') .select('tId, title') .in('tId', recentTaskIds) : Promise.resolve({ data: [], error: null }), completedAssignmentIds.length > 0 ? supabase .from('assignments') .select('aId, title') .in('aId', completedAssignmentIds) : Promise.resolve({ data: [], error: null }), ]); const tasksById = new Map((recentTasks ?? []).map((task) => [task.tId, task.title])); const assignmentsById = new Map( (completedAssignments ?? []).map((assignment) => [assignment.aId, assignment.title]) ); setRecentSessions( recentSessionRows.map((recentSession) => ({ sessionId: recentSession.sessionId, taskTitle: recentSession.taskId ? (tasksById.get(recentSession.taskId) ?? null) : null, sessionType: recentSession.sessionType, elapsedSeconds: recentSession.elapsedSeconds ?? 0, status: recentSession.status, startedAt: recentSession.startedAt, endedAt: recentSession.endedAt, })) ); setRecentlyCompletedTasks( completedTaskRows.map((task) => ({ tId: task.tId, title: task.title, assignmentTitle: assignmentsById.get(task.aId) ?? 'Unknown Assignment', lastChanged: task.lastChanged, })) ); }, [session?.user.id]); useEffect(() => { supabase.auth .getSession() .then(({ data }) => SetSession(data.session ?? null)); const { data: sub } = supabase.auth.onAuthStateChange( (_event, newSession) => { SetSession(newSession); } ); return () => sub.subscription.unsubscribe(); }, []); useEffect(() => { if (session) { RegisterForLocalNotificationsAsync(); } }, [session]); useEffect(() => { const loadSetupGate = async () => { if (!session?.user.id) { setNeedsSetup(false); return; } try { const setupStatus = await getSetupStatus(session.user.id); setNeedsSetup(!setupStatus.isSetupComplete); } catch { setNeedsSetup(true); } }; setNeedsSetup(null); void loadSetupGate(); }, [session?.user.id]); useFocusEffect( useCallback(() => { void loadActiveSprint(); void loadDashboardProgress(); void loadUpcomingDeadlineTasks(); }, [loadActiveSprint, loadDashboardProgress, loadUpcomingDeadlineTasks]) ); useEffect(() => { if (!activeSprint) { return; } const intervalId = setInterval(() => { const secondsLeft = Math.max( 0, Math.ceil((activeSprint.endTime - Date.now()) / 1000) ); setRemainingSeconds(secondsLeft); if (secondsLeft <= 0) { void finalizeStoredSession('expired', activeSprint); setActiveSprint(null); setActiveSprintTaskTitle(null); setActiveSprintTaskDesc(null); } }, 1000); return () => clearInterval(intervalId); }, [activeSprint]); const handleTaskCompletion = useCallback(async (task: UpcomingDeadlineTask) => { if (completingTaskId) { return; } setCompletingTaskId(task.tId); const { error } = await supabase .from('tasks') .update({ isCompleted: true, lastChanged: new Date().toISOString(), }) .eq('tId', task.tId); if (error) { setCompletingTaskId(null); Alert.alert('Task could not be completed, please try again'); return; } try { await CheckAssignmentCompletion(task.aId); } catch { setCompletingTaskId(null); Alert.alert('Task was updated, but assignment progress could not be refreshed'); return; } setUpcomingDeadlineTasks((currentTasks) => currentTasks.filter((currentTask) => currentTask.tId !== task.tId) ); setCompletingTaskId(null); }, [completingTaskId]); const handleStartSprint = useCallback(async (task: UpcomingDeadlineTask) => { const storedSession = await GetActiveSession(); if (!storedSession) { router.push({ pathname: '/task/timer', params: { tId: task.tId, durationMinutes: String(DEFAULT_FOCUS_DURATION_MINUTES), }, }); return; } const secondsLeft = Math.ceil((storedSession.endTime - Date.now()) / 1000); if (secondsLeft <= 0) { await finalizeStoredSession('expired', storedSession); router.push({ pathname: '/task/timer', params: { tId: task.tId, durationMinutes: String(DEFAULT_FOCUS_DURATION_MINUTES), }, }); return; } if (storedSession.taskId === task.tId) { router.push({ pathname: '/task/timer', params: { tId: task.tId, durationMinutes: String(DEFAULT_FOCUS_DURATION_MINUTES), }, }); return; } Alert.alert( 'Active session in progress', `End the current session and start a new ${DEFAULT_FOCUS_DURATION_MINUTES} minute sprint on "${task.title}"?`, [ { text: 'Cancel', style: 'cancel', }, { text: 'Start sprint', style: 'destructive', onPress: async () => { await finalizeStoredSession('cancelled', storedSession); router.push({ pathname: '/task/timer', params: { tId: task.tId, durationMinutes: String(DEFAULT_FOCUS_DURATION_MINUTES), }, }); }, }, ] ); }, []); if (session && needsSetup === null) { return null; } if (needsSetup) { return ; } return ( { return ( setIsFlowInfoVisible(true)} > ) }, headerRight: () => { return ( await supabase.auth.signOut()} > Logout ) }, }} /> setIsFlowInfoVisible(false)} > setIsFlowInfoVisible(false)} /> How work is organized Study flow setIsFlowInfoVisible(false)} > The idea is to make getting started feel lighter. You decide what you are studying, narrow it down to one clear task, and then let the app carry you through a simple rhythm of focus and recovery. {FLOW_STEPS.map((step, index) => ( {step.label} {index < FLOW_STEPS.length - 1 ? ( ) : null} {step.title} {step.description} ))} Quick map {'Subject -> Assignment -> Task -> Sprint'} In practice, that usually becomes focus session, short pause, focus session again, and eventually a longer pause when you have done a few solid rounds. { setIsFlowInfoVisible(false); router.push('/subjects'); }} > Open Subjects {subjectCount === 0 ? ( First step Build your first study path Start with one subject, then add one assignment and one task so you can reach your first sprint without guessing what to do next. router.push('/setup')} > Start Guided Setup ) : null} {activeSprint ? ( {getSessionLabel(activeSprint.sessionType)} {activeSprintTaskTitle ?? 'Selected task'} {' '} {activeSprintTaskDesc ?? null} {' '} {formatTime(remainingSeconds)} remaining router.push({ pathname: '/task/timer', params: activeSprint.taskId ? { tId: activeSprint.taskId } : { sessionType: activeSprint.sessionType, durationMinutes: String(Math.max(1, Math.round(activeSprint.durationSeconds / 60))), }, }) } > {activeSprint.sessionType === 'focus' ? 'Resume Sprint' : 'Resume Break'} ) : ( No active sprint right now. )} Study progress A quick view of today's and this week's focused study effort. Focus sessions today {dashboardSummary.completedFocusSessionsToday} Minutes today {dashboardSummary.minutesStudiedToday} Minutes this week {dashboardSummary.minutesStudiedThisWeek} Tasks with upcoming deadlines The next concrete work items that are most likely to matter soon. {upcomingDeadlineTasks.length > 0 ? ( upcomingDeadlineTasks.map((task) => ( router.push({ pathname: '/task/viewDetailsTask', params: { tId: task.tId }, }) } > {task.title} {task.description ? ( {task.description} ) : null} {task.subjectTitle} • {task.assignmentTitle} • {formatDate(task.deadline)} { event.stopPropagation(); void handleStartSprint(task); }} > Start Sprint { event.stopPropagation(); Alert.alert( 'Complete task', 'Mark this task as completed?', [ { text: 'Cancel', style: 'cancel', }, { text: 'Complete', onPress: () => { void handleTaskCompletion(task); }, }, ] ); }} > {completingTaskId === task.tId ? 'Completing...' : 'Mark as completed'} )) ) : ( No upcoming task deadlines. )} Recent sessions The latest recorded sprints and breaks. {recentSessions.length > 0 ? ( recentSessions.map((recentSession) => ( {recentSession.taskTitle ?? getSessionLabel(recentSession.sessionType)} {getSessionLabel(recentSession.sessionType)} • {formatTrackedDuration(recentSession.elapsedSeconds)} {getSessionStatusLabel(recentSession.status)} {formatDateTime(recentSession.endedAt ?? recentSession.startedAt)} )) ) : ( No recent sessions yet. )} Recently completed tasks Tasks you have recently finished and moved out of the queue. {recentlyCompletedTasks.length > 0 ? ( recentlyCompletedTasks.map((task) => ( router.push({ pathname: '/task/viewDetailsTask', params: { tId: task.tId }, }) } > {task.title} {task.assignmentTitle} Completed {formatDateTime(task.lastChanged)} )) ) : ( No completed tasks yet. )} ); }