Merge branch 'tailwind'

This commit is contained in:
Fhj0607
2026-04-25 15:57:26 +02:00
19 changed files with 589 additions and 124 deletions

View File

@@ -2,6 +2,7 @@
"expo": {
"name": "Study Sprint",
"slug": "Study-Sprint",
"owner": "ikt205g26v-g18",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
@@ -19,7 +20,8 @@
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
"predictiveBackGestureEnabled": false,
"package": "com.teodorsa.StudySprint"
},
"web": {
"output": "static",
@@ -44,6 +46,12 @@
"experiments": {
"typedRoutes": true,
"reactCompiler": true
},
"extra": {
"router": {},
"eas": {
"projectId": "2b2ec99b-a2ea-4991-8694-93f9e3d042a3"
}
}
}
}

View File

@@ -1,5 +1,7 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { CheckSubjectCompletion } from '@/lib/progress';
import { supabase } from '@/lib/supabase';
import type { Assignment, Task } from '@/lib/types';
import { Ionicons } from '@expo/vector-icons';
import { Session } from '@supabase/supabase-js';
import { router, Stack, useFocusEffect } from 'expo-router';
@@ -12,19 +14,9 @@ import {
View,
} from 'react-native';
type Assignment = {
aId: string;
title: string;
description: string;
deadline: string;
isCompleted: boolean;
lastChanged: string;
uId: string;
sId: string;
};
export default function Assignments() {
const [assignments, SetAssignments] = useState<Assignment[]>([]);
const [tasksByAssignment, SetTasksByAssignment] = useState<Record<string, Task[]>>({});
const [session, SetSession] = useState<Session | null>(null);
const assignmentSections = [
@@ -55,17 +47,47 @@ export default function Assignments() {
}, []);
const GetAssignments = async () => {
const { data, error } = await supabase
const { data: assignmentsData, error: assignmentsError } = await supabase
.from('assignments')
.select('*')
.order('deadline', { ascending: false });
if (error) {
if (assignmentsError) {
Alert.alert('Assignments could not be fetched, please try again');
return;
}
SetAssignments(data ?? []);
const assignmentRows = assignmentsData ?? [];
SetAssignments(assignmentRows);
if (assignmentRows.length === 0) {
SetTasksByAssignment({});
return;
}
const aIds = assignmentRows.map((assignment) => assignment.aId);
const { data: tasksData, error: tasksError } = await supabase
.from('tasks')
.select('*')
.in('aId', aIds);
if (tasksError) {
Alert.alert('Assignment tasks could not be fetched, please try again');
SetTasksByAssignment({});
return;
}
const groupedTasks: Record<string, Task[]> = {};
for (const task of tasksData ?? []) {
if (!groupedTasks[task.aId]) {
groupedTasks[task.aId] = [];
}
groupedTasks[task.aId].push(task);
}
SetTasksByAssignment(groupedTasks);
};
useFocusEffect(
@@ -76,7 +98,7 @@ export default function Assignments() {
}, [session])
);
const DeleteAssignment = async (aId: string) => {
const DeleteAssignment = async (aId: string, sId: string) => {
Alert.alert(
'Delete Assignment',
'Are you sure you want to delete this assignment?',
@@ -100,6 +122,13 @@ export default function Assignments() {
}
Alert.alert('Assignment deleted successfully!');
try {
await CheckSubjectCompletion(sId);
} catch {
Alert.alert("Failed to update subject status");
}
GetAssignments();
},
},
@@ -178,6 +207,9 @@ export default function Assignments() {
renderItem={({ item }) => {
const isOwner = session?.user.id === item.uId;
const assignmentTasks = tasksByAssignment[item.aId] ?? [];
const progress = assignmentTasks.length === 0 ? 0 : Math.round((assignmentTasks.filter(task => task.isCompleted).length / assignmentTasks.length) * 100);
return (
<View className="mb-4 rounded-3xl border border-app-border bg-app-surface p-4 shadow-sm">
<Pressable
@@ -228,6 +260,28 @@ export default function Assignments() {
Deadline: {item.deadline || 'No deadline'}
</Text>
</View>
<View style={{ marginTop: 10 }}>
<Text style={{ marginBottom: 4 }}>{progress}%</Text>
<View
style={{
width: "100%",
height: 12,
backgroundColor: "#D9D9D9",
borderRadius: 999,
overflow: "hidden",
}}
>
<View
style={{
width: `${progress}%`,
height: "100%",
backgroundColor: "#4CAF50",
}}
/>
</View>
</View>
</View>
</View>
</Pressable>
@@ -250,7 +304,7 @@ export default function Assignments() {
<Pressable
className="flex-1 items-center justify-center rounded-2xl border border-app-border bg-app-surface py-3"
onPress={() => DeleteAssignment(item.aId)}
onPress={() => DeleteAssignment(item.aId, item.sId)}
>
<Text className="text-sm font-bold text-status-danger">
Delete

View File

@@ -1,5 +1,6 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { supabase } from '@/lib/supabase';
import type { Assignment, Subject } from '@/lib/types';
import { Ionicons } from '@expo/vector-icons';
import { Session } from '@supabase/supabase-js';
import { router, Stack, useFocusEffect } from 'expo-router';
@@ -12,17 +13,9 @@ import {
View,
} from 'react-native';
type Subject = {
sId: string;
title: string;
description: string;
isActive: boolean;
lastChanged: string;
uId: string;
};
export default function Subjects() {
const [subjects, SetSubjects] = useState<Subject[]>([]);
const [assignmentsBySubject, SetAssignmentsBySubject] = useState<Record<string, Assignment[]>>({});
const [session, SetSession] = useState<Session | null>(null);
const subjectSections = [
@@ -53,17 +46,47 @@ export default function Subjects() {
}, []);
const GetSubjects = async () => {
const { data, error } = await supabase
const { data: subjectsData, error: subjectsError } = await supabase
.from('subjects')
.select('*')
.order('lastChanged', { ascending: false });
if (error) {
if (subjectsError) {
Alert.alert('Subjects could not be fetched, please try again');
return;
}
SetSubjects(data ?? []);
const subjectRows = subjectsData ?? [];
SetSubjects(subjectsData ?? []);
if (subjectRows.length === 0) {
SetAssignmentsBySubject({});
return;
}
const sIds = subjectRows.map((subject) => subject.sId);
const { data: assignmentsData, error: assignmentsError } = await supabase
.from('assignments')
.select('*')
.in('sId', sIds);
if (assignmentsError) {
Alert.alert('Subject assignments could not be fetched, please try again');
SetAssignmentsBySubject({});
return;
}
const groupedAssignments: Record<string, Assignment[]> = {};
for (const assignment of assignmentsData ?? []) {
if (!groupedAssignments[assignment.sId]) {
groupedAssignments[assignment.sId] = [];
}
groupedAssignments[assignment.sId].push(assignment);
}
SetAssignmentsBySubject(groupedAssignments);
};
useFocusEffect(
@@ -177,6 +200,9 @@ export default function Subjects() {
renderItem={({ item }) => {
const isOwner = session?.user.id === item.uId;
const subjectAssignments = assignmentsBySubject[item.sId] ?? [];
const progress = subjectAssignments.length === 0 ? 0 : Math.round((subjectAssignments.filter(assignment => assignment.isCompleted).length / subjectAssignments.length) * 100);
return (
<View className="mb-4 rounded-3xl border border-app-border bg-app-surface p-4 shadow-sm">
<Pressable
@@ -227,6 +253,27 @@ export default function Subjects() {
{item.isActive ? 'Active' : 'Inactive'}
</Text>
</View>
<View style={{ marginTop: 10 }}>
<Text style={{ marginBottom: 4 }}>{progress}%</Text>
<View
style={{
width: "100%",
height: 12,
backgroundColor: "#D9D9D9",
borderRadius: 999,
overflow: "hidden",
}}
>
<View
style={{
width: `${progress}%`,
height: "100%",
backgroundColor: "#4CAF50",
}}
/>
</View>
</View>
</View>
</View>
</Pressable>

View File

@@ -1,5 +1,7 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { CheckAssignmentCompletion } from '@/lib/progress';
import { supabase } from '@/lib/supabase';
import type { Task } from '@/lib/types';
import { Ionicons } from '@expo/vector-icons';
import { Session } from '@supabase/supabase-js';
import { router, Stack, useFocusEffect } from 'expo-router';
@@ -12,16 +14,6 @@ import {
View,
} from 'react-native';
type Task = {
tId: string;
title: string;
description: string;
isCompleted: boolean;
lastChanged: string;
uId: string;
aId: string;
};
export default function Tasks() {
const [tasks, SetTasks] = useState<Task[]>([]);
const [session, SetSession] = useState<Session | null>(null);
@@ -72,7 +64,7 @@ export default function Tasks() {
}, [session])
);
const DeleteTask = async (tId: string) => {
const DeleteTask = async (tId: string, aId: string) => {
Alert.alert(
'Delete Task',
'Are you sure you want to delete this task?',
@@ -96,6 +88,13 @@ export default function Tasks() {
}
Alert.alert('Task deleted successfully!');
try {
await CheckAssignmentCompletion(aId);
} catch {
Alert.alert("Failed to update assignment completion state");
}
GetTasks();
},
},
@@ -246,7 +245,7 @@ export default function Tasks() {
<Pressable
className="flex-1 items-center justify-center rounded-2xl border border-app-border bg-app-surface py-3"
onPress={() => DeleteTask(item.tId)}
onPress={() => DeleteTask(item.tId, item.aId)}
>
<Text className="text-sm font-bold text-status-danger">
Delete

View File

@@ -1,5 +1,6 @@
import { defaultStyles } from '@/constants/defaultStyles';
import * as AsyncStorage from '@/lib/asyncStorage';
import { CheckSubjectCompletion } from '@/lib/progress';
import { supabase } from '@/lib/supabase';
import * as Notifications from 'expo-notifications';
import { router, Stack, useLocalSearchParams } from 'expo-router';
@@ -93,6 +94,14 @@ export default function CreateAssignment() {
}
}
if (sId) {
try {
await CheckSubjectCompletion(sId);
} catch {
Alert.alert("Failed to update subject status");
}
}
SetTitle('');
SetDescription('');
SetDeadline('');

View File

@@ -1,22 +1,13 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { GetAssignmentNotificationId, RemoveAssignmentNotificationId, SaveAssignmentNotificationId } from '@/lib/asyncStorage';
import { CheckSubjectCompletion } from '@/lib/progress';
import { supabase } from '@/lib/supabase';
import type { Assignment } from '@/lib/types';
import * as Notifications from 'expo-notifications';
import { router, Stack, useFocusEffect, useLocalSearchParams } from 'expo-router';
import { useCallback, useState } from 'react';
import { ActivityIndicator, Alert, Button, Keyboard, KeyboardAvoidingView, Platform, Pressable, Text, TextInput, TouchableWithoutFeedback, View } from 'react-native';
type Assignment = {
aId: string;
title: string;
description: string;
deadline: string;
isCompleted: boolean;
lastChanged: string;
uId: string;
sId: string;
}
export default function EditAssignment() {
const { aId } = useLocalSearchParams<{ aId: string }>();
const [assignment, SetAssignment] = useState<Assignment | null>(null)
@@ -125,6 +116,14 @@ export default function EditAssignment() {
}
}
if (assignmentData.sId) {
try {
await CheckSubjectCompletion(assignmentData.sId);
} catch {
Alert.alert("Failed to update subject status");
}
}
router.back();
}

View File

@@ -1,31 +1,12 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { CheckAssignmentCompletion, CheckSubjectCompletion } from '@/lib/progress';
import { supabase } from '@/lib/supabase';
import type { Assignment, Task } 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, Button, Pressable, SectionList, Text, View } from "react-native";
type Assignment = {
aId: string;
title: string;
description: string;
deadline: string;
isCompleted: boolean;
lastChanged: string;
uId: string;
sId: string;
}
type Task = {
tId: string;
title: string;
description: string;
isCompleted: boolean;
lastChanged: string;
uId: string;
aId: string;
}
export default function ViewDetailsAssignment() {
const { aId } = useLocalSearchParams<{ aId: string }>();
const [assignment, SetAssignment] = useState<Assignment | null>(null)
@@ -98,6 +79,17 @@ export default function ViewDetailsAssignment() {
}
Alert.alert("Assignment deleted successfully!");
const sId = assignment?.sId;
if (sId) {
try {
await CheckSubjectCompletion(sId);
} catch {
Alert.alert("Failed to update subject status");
}
}
router.back();
}
}
@@ -126,6 +118,15 @@ export default function ViewDetailsAssignment() {
}
Alert.alert("Task deleted successfully!");
if (aId) {
try {
await CheckAssignmentCompletion(aId);
} catch {
Alert.alert("Failed to update assignment completion state");
}
}
GetTasks(aId);
}
}
@@ -133,6 +134,8 @@ export default function ViewDetailsAssignment() {
)
}
const progress = tasks.length === 0 ? 0 : Math.round((tasks.filter(task => task.isCompleted).length / tasks.length) * 100);
return (
<View style={defaultStyles.container}>
<Stack.Screen
@@ -172,6 +175,27 @@ export default function ViewDetailsAssignment() {
{assignment.isCompleted && <Text style={defaultStyles.checkboxMark}></Text>}
</View>
<Text style={defaultStyles.body}>{assignment.lastChanged}</Text>
<View style={{ marginTop: 10 }}>
<Text style={{ marginBottom: 4 }}>{progress}%</Text>
<View
style={{
width: "100%",
height: 12,
backgroundColor: "#D9D9D9",
borderRadius: 999,
overflow: "hidden",
}}
>
<View
style={{
width: `${progress}%`,
height: "100%",
backgroundColor: "#4CAF50",
}}
/>
</View>
</View>
<Button title="Edit" onPress={() => router.push({pathname: "/assignment/editAssignment", params: { aId: assignment.aId }})} />
<Button title="Delete" onPress={() => DeleteAssignment(assignment.aId)} />

View File

@@ -1,18 +1,10 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { supabase } from '@/lib/supabase';
import type { Subject } from '@/lib/types';
import { router, Stack, useFocusEffect, useLocalSearchParams } from 'expo-router';
import { useCallback, useState } from 'react';
import { ActivityIndicator, Alert, Button, Keyboard, KeyboardAvoidingView, Platform, Pressable, Text, TextInput, TouchableWithoutFeedback, View } from 'react-native';
type Subject = {
sId: string;
title: string;
description: string;
isActive: boolean;
lastChanged: string;
uId: string;
}
export default function EditSubject() {
const { sId } = useLocalSearchParams<{ sId: string }>();
const [subject, SetSubject] = useState<Subject | null>(null)

View File

@@ -1,30 +1,12 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { CheckSubjectCompletion } from '@/lib/progress';
import { supabase } from '@/lib/supabase';
import type { Assignment, Subject } 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, Button, Pressable, SectionList, Text, View } from "react-native";
type Subject = {
sId: string;
title: string;
description: string;
isActive: boolean;
lastChanged: string;
uId: string;
}
type Assignment = {
aId: string;
title: string;
description: string;
deadline: string;
isCompleted: boolean;
lastChanged: string;
uId: string;
sId: string;
}
export default function ViewDetailsSubject() {
const { sId } = useLocalSearchParams<{ sId: string }>();
const [subject, SetSubject] = useState<Subject | null>(null)
@@ -125,6 +107,15 @@ export default function ViewDetailsSubject() {
}
Alert.alert("Assignment deleted successfully!");
if (sId) {
try {
await CheckSubjectCompletion(sId);
} catch {
Alert.alert("Failed to update subject status");
}
}
GetAssignments(sId);
}
}
@@ -132,6 +123,8 @@ export default function ViewDetailsSubject() {
)
}
const progress = assignments.length === 0 ? 0 : Math.round((assignments.filter(assignment => assignment.isCompleted).length / assignments.length) * 100);
return (
<View style={defaultStyles.container}>
<Stack.Screen
@@ -170,6 +163,27 @@ export default function ViewDetailsSubject() {
{subject.isActive && <Text style={defaultStyles.checkboxMark}></Text>}
</View>
<Text style={defaultStyles.body}>{subject.lastChanged}</Text>
<View style={{ marginTop: 10 }}>
<Text style={{ marginBottom: 4 }}>{progress}%</Text>
<View
style={{
width: "100%",
height: 12,
backgroundColor: "#D9D9D9",
borderRadius: 999,
overflow: "hidden",
}}
>
<View
style={{
width: `${progress}%`,
height: "100%",
backgroundColor: "#4CAF50",
}}
/>
</View>
</View>
<Button title="Edit" onPress={() => router.push({pathname: "/subject/editSubject", params: { sId: subject.sId }})} />
<Button title="Delete" onPress={() => DeleteSubject(subject.sId)} />

View File

@@ -1,4 +1,5 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { CheckAssignmentCompletion } from '@/lib/progress';
import { supabase } from '@/lib/supabase';
import { router, Stack, useLocalSearchParams } from 'expo-router';
import { useState } from 'react';
@@ -56,6 +57,14 @@ export default function CreateTask() {
Alert.alert('Task successfully created!');
if (aId) {
try {
await CheckAssignmentCompletion(aId);
} catch {
Alert.alert("Failed to update assignment completion state");
}
}
SetTitle('');
SetDescription('');
SetIsCompleted(false);

View File

@@ -1,19 +1,11 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { CheckAssignmentCompletion } from '@/lib/progress';
import { supabase } from '@/lib/supabase';
import type { Task } from '@/lib/types';
import { router, Stack, useFocusEffect, useLocalSearchParams } from 'expo-router';
import { useCallback, useState } from 'react';
import { ActivityIndicator, Alert, Button, Keyboard, KeyboardAvoidingView, Platform, Pressable, Text, TextInput, TouchableWithoutFeedback, View } from 'react-native';
type Task = {
tId: string;
title: string;
description: string;
isCompleted: boolean;
lastChanged: string;
uId: string;
aId: string;
}
export default function EditTask() {
const { tId } = useLocalSearchParams<{ tId: string }>();
const [task, SetTask] = useState<Task | null>(null)
@@ -73,6 +65,14 @@ export default function EditTask() {
Alert.alert("Task successfully edited!");
if (task.aId) {
try {
await CheckAssignmentCompletion(task.aId);
} catch {
Alert.alert("Failed to update assignment completion state");
}
}
router.back();
}

View File

@@ -1,20 +1,12 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { CheckAssignmentCompletion } from '@/lib/progress';
import { supabase } from '@/lib/supabase';
import type { Task } 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, Button, Text, View } from "react-native";
type Task = {
tId: string;
title: string;
description: string;
isCompleted: boolean;
lastChanged: string;
uId: string;
aId: string;
}
export default function ViewDetailsTask() {
const { tId } = useLocalSearchParams<{ tId: string }>();
const [task, SetTask] = useState<Task | null>(null)
@@ -69,6 +61,17 @@ export default function ViewDetailsTask() {
}
Alert.alert("Task deleted successfully!");
const aId = task?.aId;
if (aId) {
try {
await CheckAssignmentCompletion(aId);
} catch {
Alert.alert("Failed to update assignment completion state");
}
}
router.back();
}
}

21
eas.json Normal file
View File

@@ -0,0 +1,21 @@
{
"cli": {
"version": ">= 18.8.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}

29
lib/progress.ts Normal file
View File

@@ -0,0 +1,29 @@
import { supabase } from '@/lib/supabase';
export async function CheckAssignmentCompletion(aId: string) {
const { data, error } = await supabase.from("tasks").select("tId, isCompleted").eq("aId", aId);
if (error) throw error;
const tasks = data ?? [];
const allCompleted = tasks.length > 0 && tasks.every((task) => task.isCompleted === true);
const { error: updateError } = await supabase.from("assignments").update({ isCompleted: allCompleted, lastChanged: new Date().toISOString()}).eq("aId", aId);
if (updateError) throw updateError;
}
export async function CheckSubjectCompletion(sId: string) {
const { data, error } = await supabase.from("assignments").select("aId, isCompleted").eq("sId", sId);
if (error) throw error;
const assignments = data ?? [];
const allCompleted = assignments.length > 0 && assignments.every((assignment) => assignment.isCompleted === true);
const { error: updateError } = await supabase.from("subjects").update({ isActive: allCompleted, lastChanged: new Date().toISOString()}).eq("sId", sId);
if (updateError) throw updateError;
}

29
lib/types.ts Normal file
View File

@@ -0,0 +1,29 @@
export type Task = {
tId: string;
title: string;
description: string;
isCompleted: boolean;
lastChanged: string;
uId: string;
aId: string;
};
export type Assignment = {
aId: string;
title: string;
description: string;
deadline: string;
isCompleted: boolean;
lastChanged: string;
uId: string;
sId: string;
};
export type Subject = {
sId: string;
title: string;
description: string;
isActive: boolean;
lastChanged: string;
uId: string;
};

71
notes/new-build-guide.md Normal file
View File

@@ -0,0 +1,71 @@
# DevelopmentBuildGuide
This project uses an **Expo development build** for features that are not fully supported in Expo Go, such as our notification setup.
---
## WhyWeUseThis
We do **not** use Expo Go for this project when testing notifications or other native features.
Reason:
- Expo Go has limitations for some native modules
- `expo-notifications` on Android requires a development build for reliable testing
- a development build works like a custom Expo Go app made specifically for this project
---
## #ImportantRule
You do **not** need to rebuild the APK for every code change.
### Rebuild is **not needed** for:
- changing React components
- changing screen layouts
- changing styles
- changing Supabase queries
- changing JS/TS functions
- changing form logic
- changing routing logic
- changing notification scheduling logic in JavaScript only
### Rebuild **is needed** for:
- adding a new native dependency
- removing a native dependency
- changing `app.json`
- changing Expo plugins
- changing Android/iOS permissions
- changing native notification config
- anything that affects the native app shell
---
## OneTimeSetup
Install EAS CLI globally if needed:
npm install -g eas-cli
Log in to Expo:
eas login
Install the Expo development client in the project:
npx expo install expo-dev-client
# BuildTheDevelopmentAPK
Run this command from the project root:
eas build --platform android --profile development
This sends the build to Expo's cloud build service.
When the build is finished:
open the build link
click Install
install the APK on your Android phone or emulator
Do not place the APK inside the project folder.
The APK is something you install on the device, not a source file.
# DailyWorkflow
After the development build APK is installed:
Start the project:
npx expo start

View File

@@ -0,0 +1,38 @@
# Progress Tracking (Assignments & Subjects)
## What was done
- Implemented progress tracking for both assignments and subjects
- Used `Task.isCompleted` as the source of truth
- Synced `Assignment.isCompleted` based on task completion
## Logic implemented
- Created `CheckAssignmentCompletion(aId)`
- Assignment is marked completed only if all its tasks are completed
- Assignment remains incomplete if:
- Any task is incomplete
- No tasks exist
## Data handling
- Fetched assignments from Supabase
- Fetched all related tasks using assignment IDs
- Grouped tasks by `aId` into `tasksByAssignment`
- Used grouped data to calculate progress efficiently
## Progress calculation
- Assignment progress:
- completed tasks / total tasks
- Subject progress:
- completed tasks across all assignments / total tasks
## UI work
- Added progress bars to:
- Assignment cards
- Subject views
- Used basic inline styling for progress bars
- Fixed layout issues caused by incorrect placement inside `flex-row`
- Moved progress bar into content column to prevent UI breaking
## Result
- Progress updates dynamically based on task completion
- Assignment completion stays in sync with tasks
- UI correctly displays both assignment and subject progress

118
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"@supabase/supabase-js": "^2.103.1",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",
"expo-dev-client": "~6.0.20",
"expo-font": "~14.0.11",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
@@ -6577,6 +6578,79 @@
"react-native": "*"
}
},
"node_modules/expo-dev-client": {
"version": "6.0.20",
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.20.tgz",
"integrity": "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==",
"license": "MIT",
"dependencies": {
"expo-dev-launcher": "6.0.20",
"expo-dev-menu": "7.0.18",
"expo-dev-menu-interface": "2.0.0",
"expo-manifests": "~1.0.10",
"expo-updates-interface": "~2.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-launcher": {
"version": "6.0.20",
"resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.20.tgz",
"integrity": "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==",
"license": "MIT",
"dependencies": {
"ajv": "^8.11.0",
"expo-dev-menu": "7.0.18",
"expo-manifests": "~1.0.10"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-launcher/node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/expo-dev-launcher/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/expo-dev-menu": {
"version": "7.0.18",
"resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.18.tgz",
"integrity": "sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==",
"license": "MIT",
"dependencies": {
"expo-dev-menu-interface": "2.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-menu-interface": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz",
"integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-file-system": {
"version": "19.0.21",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
@@ -6627,6 +6701,12 @@
}
}
},
"node_modules/expo-json-utils": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz",
"integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==",
"license": "MIT"
},
"node_modules/expo-keep-awake": {
"version": "15.0.8",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz",
@@ -6651,6 +6731,19 @@
"react-native": "*"
}
},
"node_modules/expo-manifests": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.10.tgz",
"integrity": "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==",
"license": "MIT",
"dependencies": {
"@expo/config": "~12.0.11",
"expo-json-utils": "~0.15.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-modules-autolinking": {
"version": "3.0.24",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz",
@@ -7029,6 +7122,15 @@
}
}
},
"node_modules/expo-updates-interface": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz",
"integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-web-browser": {
"version": "15.0.10",
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.10.tgz",
@@ -7092,6 +7194,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",

View File

@@ -7,8 +7,8 @@
"reset-project": "node ./scripts/reset-project.js",
"postinstall": "patch-package",
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"lint": "expo lint"
},
@@ -21,6 +21,7 @@
"@supabase/supabase-js": "^2.103.1",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",
"expo-dev-client": "~6.0.20",
"expo-font": "~14.0.11",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",