Merge branch 'main' into timerTask

This commit is contained in:
Chris Sanden
2026-05-05 20:34:58 +02:00
committed by GitHub
22 changed files with 4954 additions and 160 deletions

View File

@@ -175,6 +175,7 @@ export default function UpsertSubject() {
<Text className={labelClassName}>Title</Text>
<TextInput className={inputClassName}
placeholder={isSetupFlow ? 'e.g. Algorithms' : 'Enter subject title'}
testID = "subject-title-input"
placeholderTextColor="#9CA3AF"
value={title}
onChangeText={setTitle}
@@ -324,6 +325,7 @@ export default function UpsertSubject() {
</Pressable>
<Pressable
testID = "upsert-subject-button"
className={`h-14 items-center justify-center rounded-2xl ${
isSaving
? 'bg-accent-disabled'

View File

@@ -6,7 +6,7 @@ import type { Assignment } from '@/lib/types';
import { Session } from '@supabase/supabase-js';
import { router, Stack, useFocusEffect, useLocalSearchParams } from 'expo-router';
import { useCallback, useEffect, useState } from 'react';
import { Alert, Pressable, SectionList, Text, View } from 'react-native';
import { ActivityIndicator, Alert, Pressable, SectionList, Text, View } from 'react-native';
export type Subject = {
sId: string;
@@ -23,6 +23,7 @@ export default function ViewDetailsSubject() {
const [subject, SetSubject] = useState<Subject | null>(null);
const [assignments, SetAssignments] = useState<Assignment[]>([]);
const [session, SetSession] = useState<Session | null>(null);
const [isLoading, SetIsLoading] = useState(false);
const assignmentSections = [
{
@@ -48,12 +49,16 @@ export default function ViewDetailsSubject() {
}, []);
const GetSubject = async (subjectId: string) => {
SetIsLoading(true);
const { data, error } = await supabase
.from('subjects')
.select('*')
.eq('sId', subjectId)
.single();
SetIsLoading(false);
if (error) {
Alert.alert('Subject could not be fetched, please try again');
return;
@@ -63,12 +68,16 @@ export default function ViewDetailsSubject() {
};
const GetAssignments = async (subjectId: string) => {
SetIsLoading(true);
const { data, error } = await supabase
.from('assignments')
.select('*')
.eq('sId', subjectId)
.order('deadline', { ascending: true });
SetIsLoading(false);
if (error) {
Alert.alert('Assignments could not be fetched, please try again');
return;
@@ -77,12 +86,44 @@ export default function ViewDetailsSubject() {
SetAssignments(data ?? []);
};
const ToggleAssignmentCompletion = async (assignment: Assignment) => {
const nextIsCompleted = !assignment.isCompleted;
const { error } = await supabase
.from('assignments')
.update({
isCompleted: nextIsCompleted,
lastChanged: new Date().toISOString(),
})
.eq('aId', assignment.aId);
if (error) {
Alert.alert('Assignment could not be updated, please try again');
return;
}
try {
await CheckSubjectCompletion(assignment.sId);
} catch {
Alert.alert('Failed to update subject status');
}
await GetAssignments(assignment.sId);
await GetSubject(assignment.sId);
};
useFocusEffect(
useCallback(() => {
if (session && sId) {
GetSubject(sId);
GetAssignments(sId);
if (!session || !sId) {
return;
}
SetIsLoading(true);
SetSubject(null);
Promise.all([GetSubject(sId), GetAssignments(sId)]).finally(() => {
SetIsLoading(false);
});
}, [session, sId])
);
@@ -167,6 +208,25 @@ export default function ViewDetailsSubject() {
? 0
: Math.round((completedAssignments / totalAssignments) * 100);
if (isLoading) {
return (
<View className="flex-1 bg-app-bg px-5 pt-6">
<Stack.Screen
options={{
title: 'Subject Details',
}}
/>
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" color="#2563eb" />
<Text className="mt-4 text-base font-semibold text-text-secondary">
Loading subject...
</Text>
</View>
</View>
);
}
if (!subject) {
return (
<View className="flex-1 bg-app-bg px-5 pt-6">
@@ -228,7 +288,7 @@ export default function ViewDetailsSubject() {
paddingTop: 20,
paddingBottom: 32,
}}
sections={assignmentSections}
sections={totalAssignments === 0 ? [] : assignmentSections}
keyExtractor={(item) => item.aId}
showsVerticalScrollIndicator={false}
stickySectionHeadersEnabled={false}
@@ -290,34 +350,37 @@ export default function ViewDetailsSubject() {
<Text className="text-sm font-semibold text-text-secondary">
Assignments completed
</Text>
{totalAssignments > 0 ? (
<View className="mt-5">
<View className="mb-2 flex-row items-center justify-between">
<Text className="text-sm font-semibold text-text-secondary">
Assignment Progress
</Text>
<Text className="text-sm font-bold text-text-main">
{completedAssignments}/{totalAssignments}
<Text className="text-sm font-bold text-text-main">
{completedAssignments}/{totalAssignments}
</Text>
</View>
<View className="h-3 overflow-hidden rounded-full bg-app-subtle">
<View
className="h-full rounded-full"
style={{
width: `${progress}%`,
backgroundColor: colorSet.strong,
}}
/>
</View>
<Text className="mt-2 text-xs font-medium text-text-secondary">
{remainingAssignments === 0
? 'All assignments complete'
: `${remainingAssignments} assignment${
remainingAssignments === 1 ? '' : 's'
} remaining`}
</Text>
</View>
<View className="h-3 overflow-hidden rounded-full bg-app-subtle">
<View
className="h-full rounded-full"
style={{
width: `${progress}%`,
backgroundColor: colorSet.strong,
}}
/>
</View>
<Text className="mt-2 text-xs font-medium text-text-secondary">
{remainingAssignments === 0
? 'All assignments complete'
: `${remainingAssignments} assignment${
remainingAssignments === 1 ? '' : 's'
} remaining`}
</Text>
<Text className="mt-1 text-xs text-text-muted">
Based only on completed assignments in this subject.
</Text>
</View>
) : null}
<Text className="mt-4 text-sm text-text-muted">
Last changed: {formatDateTime(subject.lastChanged)}
@@ -328,7 +391,7 @@ export default function ViewDetailsSubject() {
className="mr-3 flex-1 items-center justify-center rounded-2xl border border-app-border bg-app-subtle py-3"
onPress={() =>
router.push({
pathname: '/subject/upsertSubject',
pathname: '../subject/upsertSubject',
params: { sId: subject.sId },
})
}
@@ -339,6 +402,7 @@ export default function ViewDetailsSubject() {
</Pressable>
<Pressable
testID="delete-subject-button"
className="flex-1 items-center justify-center rounded-2xl border border-app-border bg-app-surface py-3"
onPress={() => DeleteSubject(subject.sId)}
>
@@ -353,13 +417,13 @@ export default function ViewDetailsSubject() {
className="mb-6 mt-5 h-14 items-center justify-center rounded-2xl bg-accent"
onPress={() =>
router.push({
pathname: '/assignment/upsertAssignment',
pathname: '../assignment/upsertAssignment',
params: { sId: subject.sId },
})
}
>
<Text className="text-base font-bold text-text-inverse">
Create Assignment
Add Assignment
</Text>
</Pressable>
</View>
@@ -385,15 +449,16 @@ export default function ViewDetailsSubject() {
borderColor: colorSet.strong,
}}
>
<Pressable
onPress={() =>
router.push({
pathname: '/assignment/viewDetailsAssignment',
params: { aId: item.aId },
})
}
>
<View className="flex-row items-center">
<View className="flex-row items-center">
<Pressable
className="flex-1"
onPress={() =>
router.push({
pathname: '/assignment/viewDetailsAssignment',
params: { aId: item.aId },
})
}
>
<View className="flex-1">
<Text
className={`text-base font-bold ${
@@ -416,16 +481,29 @@ export default function ViewDetailsSubject() {
Deadline: {formatDate(item.deadline)}
</Text>
</View>
</View>
</Pressable>
</Pressable>
</View>
{isOwner && (
<View className="mt-4 flex-row border-t border-app-border pt-4">
<Pressable
className="mr-3 flex-1 items-center justify-center rounded-2xl py-3"
style={{ backgroundColor: item.isCompleted ? '#EFEBE3' : colorSet.soft }}
onPress={() => ToggleAssignmentCompletion(item)}
>
<Text
className="text-sm font-bold"
style={{ color: colorSet.strong }}
>
{item.isCompleted ? 'Reopen' : 'Complete'}
</Text>
</Pressable>
<Pressable
className="mr-3 flex-1 items-center justify-center rounded-2xl border border-app-border bg-app-subtle py-3"
onPress={() =>
router.push({
pathname: '/assignment/upsertAssignment',
pathname: '../assignment/upsertAssignment',
params: { aId: item.aId },
})
}
@@ -448,6 +526,16 @@ export default function ViewDetailsSubject() {
</View>
);
}}
ListEmptyComponent={
<View className="mb-6 rounded-3xl border border-app-border bg-app-surface p-5">
<Text className="text-center text-base font-semibold text-text-secondary">
No assignments yet
</Text>
<Text className="mt-1 text-center text-sm text-text-muted">
Add one when this subject has work to track.
</Text>
</View>
}
renderSectionFooter={({ section }) =>
section.data.length === 0 ? (
<View className="mb-6 rounded-3xl border border-app-border bg-app-surface p-5">