import { GetActiveSprint, RemoveActiveSprint, type ActiveSprint, } from '@/lib/asyncStorage'; import { formatDate } from '@/lib/date'; import { RegisterForLocalNotificationsAsync } from '@/lib/notifications'; import { CheckAssignmentCompletion } from '@/lib/progress'; import { supabase } from "@/lib/supabase"; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { Session } from '@supabase/supabase-js'; import { 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; }; const FLOW_STEPS = [ { label: '1', title: 'Subject', description: 'A subject is the top-level container for one course or area you are studying.', }, { label: '2', title: 'Assignment', description: 'Each subject contains assignments like projects, exercises, or exam prep blocks.', }, { label: '3', title: 'Task', description: 'Assignments are broken down into tasks so you always have one concrete thing to work on.', }, { label: '4', title: 'Sprint', description: 'A sprint is one focused work session tied to a single task and tracked by the timer.', }, ] 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')}`; } 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 [upcomingDeadlineTasks, setUpcomingDeadlineTasks] = useState([]); const [isFlowInfoVisible, setIsFlowInfoVisible] = useState(false); const [completingTaskId, setCompletingTaskId] = useState(null); const loadActiveSprint = useCallback(async () => { const storedSprint = await GetActiveSprint(); 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 RemoveActiveSprint(); setActiveSprint(null); setActiveSprintTaskTitle(null); setActiveSprintTaskDesc(null); setRemainingSeconds(0); return; } setActiveSprint(storedSprint); setRemainingSeconds(secondsLeft); 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]); 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]); useFocusEffect( useCallback(() => { void loadActiveSprint(); void loadUpcomingDeadlineTasks(); }, [loadActiveSprint, 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 RemoveActiveSprint(); setActiveSprint(null); setActiveSprintTaskTitle(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]); return ( { return ( setIsFlowInfoVisible(true)} > ) }, headerRight: () => { return ( await supabase.auth.signOut()} > Logout ) }, }} /> setIsFlowInfoVisible(false)} > setIsFlowInfoVisible(false)} /> How the app is structured Study flow setIsFlowInfoVisible(false)} > Build your work from the big container down to the focused work session. {FLOW_STEPS.map((step, index) => ( {step.label} {index < FLOW_STEPS.length - 1 ? ( ) : null} {step.title} {step.description} ))} Quick map {'Subject -> Assignment -> Task -> Sprint'} { setIsFlowInfoVisible(false); router.push('/subjects'); }} > Start with Subjects {activeSprint ? ( Active Sprint {activeSprintTaskTitle ?? 'Selected task'} {' '} {activeSprintTaskDesc ?? null} {' '} {formatTime(remainingSeconds)} remaining router.push({ pathname: '/task/timer', params: { tId: activeSprint.taskId }, }) } > Open Sprint ) : ( No active sprint right now. )} Tasks with upcoming deadlines {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(); 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. )} ); }