'some' changes
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import {
|
||||
GetActiveSprint,
|
||||
RemoveActiveSprint,
|
||||
type ActiveSprint,
|
||||
GetActiveSession,
|
||||
RemoveActiveSession,
|
||||
type ActiveSession,
|
||||
} from '@/lib/asyncStorage';
|
||||
import { formatDate } from '@/lib/date';
|
||||
import type { SessionType } from '@/lib/types';
|
||||
import { formatDate, formatDateTime } from '@/lib/date';
|
||||
import { RegisterForLocalNotificationsAsync } from '@/lib/notifications';
|
||||
import { CheckAssignmentCompletion } from '@/lib/progress';
|
||||
import { supabase } from "@/lib/supabase";
|
||||
@@ -30,6 +31,29 @@ type UpcomingDeadlineTask = {
|
||||
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',
|
||||
@@ -60,18 +84,88 @@ function formatTime(totalSeconds: number) {
|
||||
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<Session | null>(null);
|
||||
const [activeSprint, setActiveSprint] = useState<ActiveSprint | null>(null);
|
||||
const [activeSprint, setActiveSprint] = useState<ActiveSession | null>(null);
|
||||
const [activeSprintTaskTitle, setActiveSprintTaskTitle] = useState<string | null>(null);
|
||||
const [activeSprintTaskDesc, setActiveSprintTaskDesc] = useState<string | null>(null);
|
||||
const [remainingSeconds, setRemainingSeconds] = useState(0);
|
||||
const [dashboardSummary, setDashboardSummary] = useState<DashboardProgressSummary>({
|
||||
completedFocusSessionsToday: 0,
|
||||
minutesStudiedToday: 0,
|
||||
minutesStudiedThisWeek: 0,
|
||||
});
|
||||
const [recentSessions, setRecentSessions] = useState<RecentSession[]>([]);
|
||||
const [recentlyCompletedTasks, setRecentlyCompletedTasks] = useState<RecentlyCompletedTask[]>([]);
|
||||
const [upcomingDeadlineTasks, setUpcomingDeadlineTasks] = useState<UpcomingDeadlineTask[]>([]);
|
||||
const [isFlowInfoVisible, setIsFlowInfoVisible] = useState(false);
|
||||
const [completingTaskId, setCompletingTaskId] = useState<string | null>(null);
|
||||
const [subjectCount, setSubjectCount] = useState(0);
|
||||
|
||||
const loadActiveSprint = useCallback(async () => {
|
||||
const storedSprint = await GetActiveSprint();
|
||||
const storedSprint = await GetActiveSession();
|
||||
|
||||
if (!storedSprint) {
|
||||
setActiveSprint(null);
|
||||
@@ -87,7 +181,7 @@ export default function HomeScreen() {
|
||||
);
|
||||
|
||||
if (secondsLeft <= 0) {
|
||||
await RemoveActiveSprint();
|
||||
await RemoveActiveSession();
|
||||
setActiveSprint(null);
|
||||
setActiveSprintTaskTitle(null);
|
||||
setActiveSprintTaskDesc(null);
|
||||
@@ -98,6 +192,12 @@ export default function HomeScreen() {
|
||||
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')
|
||||
@@ -204,6 +304,160 @@ export default function HomeScreen() {
|
||||
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()
|
||||
@@ -227,8 +481,9 @@ export default function HomeScreen() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
void loadActiveSprint();
|
||||
void loadDashboardProgress();
|
||||
void loadUpcomingDeadlineTasks();
|
||||
}, [loadActiveSprint, loadUpcomingDeadlineTasks])
|
||||
}, [loadActiveSprint, loadDashboardProgress, loadUpcomingDeadlineTasks])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -245,9 +500,10 @@ export default function HomeScreen() {
|
||||
setRemainingSeconds(secondsLeft);
|
||||
|
||||
if (secondsLeft <= 0) {
|
||||
void RemoveActiveSprint();
|
||||
void RemoveActiveSession();
|
||||
setActiveSprint(null);
|
||||
setActiveSprintTaskTitle(null);
|
||||
setActiveSprintTaskDesc(null);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
@@ -324,7 +580,11 @@ export default function HomeScreen() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<View className="m-1 flex-1 p-6">
|
||||
<ScrollView
|
||||
className="m-1 flex-1"
|
||||
contentContainerStyle={{ padding: 24, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent
|
||||
@@ -408,9 +668,35 @@ export default function HomeScreen() {
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{subjectCount === 0 ? (
|
||||
<View className="mb-6 rounded-3xl border border-app-border bg-app-surface p-5">
|
||||
<Text className="text-xs font-bold uppercase tracking-[0.8px] text-text-muted">
|
||||
First step
|
||||
</Text>
|
||||
<Text className="mt-2 text-2xl font-bold text-text-main">
|
||||
Build your first study path
|
||||
</Text>
|
||||
<Text className="mt-2 text-sm leading-5 text-text-secondary">
|
||||
Start with one subject, then add one assignment and one task so you
|
||||
can reach your first sprint without guessing what to do next.
|
||||
</Text>
|
||||
|
||||
<Pressable
|
||||
className="mt-5 h-14 items-center justify-center rounded-2xl bg-accent"
|
||||
onPress={() => router.push('/setup')}
|
||||
>
|
||||
<Text className="text-base font-bold text-text-inverse">
|
||||
Start Guided Setup
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{activeSprint ? (
|
||||
<View className="gap-2 rounded-2xl border border-[#D5D9DF] bg-[#F7F9FC] p-4">
|
||||
<Text className="text-[13px] font-semibold text-[#5D6B7A]">Active Sprint</Text>
|
||||
<Text className="text-[13px] font-semibold text-[#5D6B7A]">
|
||||
{getSessionLabel(activeSprint.sessionType)}
|
||||
</Text>
|
||||
<Text className="text-[20px] font-bold text-[#1F2933]">
|
||||
{activeSprintTaskTitle ?? 'Selected task'}
|
||||
</Text>
|
||||
@@ -428,11 +714,16 @@ export default function HomeScreen() {
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: '/task/timer',
|
||||
params: { tId: activeSprint.taskId },
|
||||
params: activeSprint.taskId
|
||||
? { tId: activeSprint.taskId }
|
||||
: {
|
||||
sessionType: activeSprint.sessionType,
|
||||
durationMinutes: String(Math.max(1, Math.round(activeSprint.durationSeconds / 60))),
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-white">Open Sprint</Text>
|
||||
<Text className="text-[15px] font-bold text-white">Open Session</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : (
|
||||
@@ -441,10 +732,51 @@ export default function HomeScreen() {
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<View className="mt-6 gap-3">
|
||||
<Text className="text-lg font-bold text-[#1F2933]">
|
||||
Study progress
|
||||
</Text>
|
||||
<Text className="text-sm leading-[20px] text-[#6B7580]">
|
||||
A quick view of today's and this week's focused study effort.
|
||||
</Text>
|
||||
|
||||
<View className="flex-row gap-3">
|
||||
<View className="flex-1 rounded-2xl border border-[#D5D9DF] bg-white p-4">
|
||||
<Text className="text-[12px] font-semibold uppercase tracking-[0.6px] text-[#7B8794]">
|
||||
Focus sessions today
|
||||
</Text>
|
||||
<Text className="mt-2 text-[24px] font-extrabold text-[#1F2933]">
|
||||
{dashboardSummary.completedFocusSessionsToday}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-1 rounded-2xl border border-[#D5D9DF] bg-white p-4">
|
||||
<Text className="text-[12px] font-semibold uppercase tracking-[0.6px] text-[#7B8794]">
|
||||
Minutes today
|
||||
</Text>
|
||||
<Text className="mt-2 text-[24px] font-extrabold text-[#1F2933]">
|
||||
{dashboardSummary.minutesStudiedToday}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="rounded-2xl border border-[#D5D9DF] bg-[#F7F9FC] p-4">
|
||||
<Text className="text-[12px] font-semibold uppercase tracking-[0.6px] text-[#7B8794]">
|
||||
Minutes this week
|
||||
</Text>
|
||||
<Text className="mt-2 text-[24px] font-extrabold text-[#1F2933]">
|
||||
{dashboardSummary.minutesStudiedThisWeek}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mt-6 gap-3">
|
||||
<Text className="text-lg font-bold text-[#1F2933]">
|
||||
Tasks with upcoming deadlines
|
||||
</Text>
|
||||
<Text className="text-sm leading-[20px] text-[#6B7580]">
|
||||
The next concrete work items that are most likely to matter soon.
|
||||
</Text>
|
||||
|
||||
{upcomingDeadlineTasks.length > 0 ? (
|
||||
upcomingDeadlineTasks.map((task) => (
|
||||
@@ -503,7 +835,82 @@ export default function HomeScreen() {
|
||||
<Text className="text-sm text-[#7B8794]">No upcoming task deadlines.</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mt-6 gap-6 md:flex-row">
|
||||
<View className="gap-3 md:flex-1">
|
||||
<Text className="text-lg font-bold text-[#1F2933]">
|
||||
Recent sessions
|
||||
</Text>
|
||||
<Text className="text-sm leading-[20px] text-[#6B7580]">
|
||||
The latest recorded sprints and breaks.
|
||||
</Text>
|
||||
|
||||
{recentSessions.length > 0 ? (
|
||||
recentSessions.map((recentSession) => (
|
||||
<View
|
||||
key={recentSession.sessionId}
|
||||
className="gap-[6px] rounded-2xl border border-[#D5D9DF] bg-white p-4"
|
||||
>
|
||||
<View className="flex-row items-start justify-between gap-3">
|
||||
<View className="flex-1">
|
||||
<Text className="text-base font-bold text-[#1F2933]">
|
||||
{recentSession.taskTitle ?? getSessionLabel(recentSession.sessionType)}
|
||||
</Text>
|
||||
<Text className="mt-1 text-sm text-[#52606D]">
|
||||
{getSessionLabel(recentSession.sessionType)} • {formatTrackedDuration(recentSession.elapsedSeconds)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="rounded-full bg-[#EFF3F8] px-3 py-[6px]">
|
||||
<Text className="text-[12px] font-bold text-[#52606D]">
|
||||
{getSessionStatusLabel(recentSession.status)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text className="text-[13px] font-semibold text-[#7B8794]">
|
||||
{formatDateTime(recentSession.endedAt ?? recentSession.startedAt)}
|
||||
</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Text className="text-sm text-[#7B8794]">No recent sessions yet.</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="gap-3 md:flex-1">
|
||||
<Text className="text-lg font-bold text-[#1F2933]">
|
||||
Recently completed tasks
|
||||
</Text>
|
||||
<Text className="text-sm leading-[20px] text-[#6B7580]">
|
||||
Tasks you have recently finished and moved out of the queue.
|
||||
</Text>
|
||||
|
||||
{recentlyCompletedTasks.length > 0 ? (
|
||||
recentlyCompletedTasks.map((task) => (
|
||||
<Pressable
|
||||
key={task.tId}
|
||||
className="gap-[6px] rounded-2xl border border-[#D5D9DF] bg-white p-4"
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: '/task/viewDetailsTask',
|
||||
params: { tId: task.tId },
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text className="text-base font-bold text-[#1F2933]">{task.title}</Text>
|
||||
<Text className="text-sm text-[#52606D]">{task.assignmentTitle}</Text>
|
||||
<Text className="text-[13px] font-semibold text-[#7B8794]">
|
||||
Completed {formatDateTime(task.lastChanged)}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))
|
||||
) : (
|
||||
<Text className="text-sm text-[#7B8794]">No completed tasks yet.</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user