diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index 5876d15..d9a261e 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -56,14 +56,15 @@ export default function TabLayout() {
}
if (!session) {
- return ;
+ return ;
}
return (
-
-
-
-
+
+
diff --git a/app/(tabs)/assignments.tsx b/app/(tabs)/assignments.tsx
deleted file mode 100644
index 8fb9f77..0000000
--- a/app/(tabs)/assignments.tsx
+++ /dev/null
@@ -1,336 +0,0 @@
-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';
-import { useCallback, useEffect, useState } from 'react';
-import {
- Alert,
- Pressable,
- SectionList,
- Text,
- View,
-} from 'react-native';
-
-export default function Assignments() {
- const [assignments, SetAssignments] = useState([]);
- const [tasksByAssignment, SetTasksByAssignment] = useState>({});
- const [session, SetSession] = useState(null);
-
- const assignmentSections = [
- {
- title: 'Upcoming Assignments',
- data: assignments.filter((assignment) => !assignment.isCompleted),
- emptyMessage: 'No upcoming assignments',
- },
- {
- title: 'Completed Assignments',
- data: assignments.filter((assignment) => assignment.isCompleted),
- emptyMessage: 'No completed assignments',
- },
- ];
-
- useEffect(() => {
- supabase.auth
- .getSession()
- .then(({ data }) => SetSession(data.session ?? null));
-
- const { data: sub } = supabase.auth.onAuthStateChange(
- (_event, newSession) => {
- SetSession(newSession);
- }
- );
-
- return () => sub.subscription.unsubscribe();
- }, []);
-
- const GetAssignments = async () => {
- const { data: assignmentsData, error: assignmentsError } = await supabase
- .from('assignments')
- .select('*')
- .order('deadline', { ascending: false });
-
- if (assignmentsError) {
- Alert.alert('Assignments could not be fetched, please try again');
- return;
- }
-
- 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 = {};
-
- for (const task of tasksData ?? []) {
- if (!groupedTasks[task.aId]) {
- groupedTasks[task.aId] = [];
- }
- groupedTasks[task.aId].push(task);
- }
-
- SetTasksByAssignment(groupedTasks);
- };
-
- useFocusEffect(
- useCallback(() => {
- if (session) {
- GetAssignments();
- }
- }, [session])
- );
-
- const DeleteAssignment = async (aId: string, sId: string) => {
- Alert.alert(
- 'Delete Assignment',
- 'Are you sure you want to delete this assignment?',
- [
- {
- text: 'Cancel',
- style: 'cancel',
- },
- {
- text: 'Delete',
- style: 'destructive',
- onPress: async () => {
- const { error } = await supabase
- .from('assignments')
- .delete()
- .eq('aId', aId);
-
- if (error) {
- Alert.alert('Assignment could not be deleted, please try again');
- return;
- }
-
- Alert.alert('Assignment deleted successfully!');
-
- try {
- await CheckSubjectCompletion(sId);
- } catch {
- Alert.alert("Failed to update subject status");
- }
-
- GetAssignments();
- },
- },
- ]
- );
- };
-
- return (
-
- (
-
-
-
-
-
- await supabase.auth.signOut()}
- >
-
- Logout
-
-
-
- ),
- }}
- />
-
-
-
-
- Assignments
-
-
- Track what is coming up and what you have already finished.
-
-
-
- router.push('/assignment/createAssignment')}
- >
-
- Create Assignment
-
-
-
- item.aId}
- showsVerticalScrollIndicator={false}
- stickySectionHeadersEnabled={false}
- contentContainerStyle={{
- paddingBottom: 32,
- }}
- renderSectionHeader={({ section: { title, data } }) => (
-
-
- {title}
-
-
-
-
- {data.length}
-
-
-
- )}
- 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 (
-
-
- router.push({
- pathname: '/assignment/viewDetailsAssignment',
- params: { aId: item.aId },
- })
- }
- >
-
-
- {item.isCompleted && (
-
- ✓
-
- )}
-
-
-
-
- {item.title}
-
-
- {item.description ? (
-
- {item.description}
-
- ) : null}
-
-
-
- Deadline: {item.deadline || 'No deadline'}
-
-
-
-
- {progress}%
-
-
-
-
-
-
-
-
-
- {isOwner && (
-
-
- router.push({
- pathname: '/assignment/editAssignment',
- params: { aId: item.aId },
- })
- }
- >
-
- Edit
-
-
-
- DeleteAssignment(item.aId, item.sId)}
- >
-
- Delete
-
-
-
- )}
-
- );
- }}
- renderSectionFooter={({ section }) =>
- section.data.length === 0 ? (
-
-
- {section.emptyMessage}
-
-
- New assignments will show up here.
-
-
- ) : (
-
- )
- }
- />
-
-
- );
-}
\ No newline at end of file
diff --git a/app/(tabs)/subjects.tsx b/app/(tabs)/subjects.tsx
index f5f5f79..86564b8 100644
--- a/app/(tabs)/subjects.tsx
+++ b/app/(tabs)/subjects.tsx
@@ -1,40 +1,21 @@
-import { defaultStyles } from '@/constants/defaultStyles';
+import { SUBJECT_COLORS } from '@/lib/subjectColors';
import { supabase } from '@/lib/supabase';
-import type { Assignment, Subject } from '@/lib/types';
-import { Ionicons } from '@expo/vector-icons';
+import { Subject } from '@/lib/types';
import { Session } from '@supabase/supabase-js';
import { router, Stack, useFocusEffect } from 'expo-router';
import { useCallback, useEffect, useState } from 'react';
-import {
- Alert,
- Pressable,
- SectionList,
- Text,
- View,
-} from 'react-native';
+import { Alert, Pressable, ScrollView, Text, View } from 'react-native';
+
+import type { SubjectColor } from '@/lib/subjectColors';
export default function Subjects() {
const [subjects, SetSubjects] = useState([]);
- const [assignmentsBySubject, SetAssignmentsBySubject] = useState>({});
const [session, SetSession] = useState(null);
- const subjectSections = [
- {
- title: 'Active Subjects',
- data: subjects.filter((subject) => subject.isActive),
- emptyMessage: 'No active subjects',
- },
- {
- title: 'Inactive Subjects',
- data: subjects.filter((subject) => !subject.isActive),
- emptyMessage: 'No inactive subjects',
- },
- ];
-
useEffect(() => {
- supabase.auth
- .getSession()
- .then(({ data }) => SetSession(data.session ?? null));
+ supabase.auth.getSession().then(({ data }) => {
+ SetSession(data.session ?? null);
+ });
const { data: sub } = supabase.auth.onAuthStateChange(
(_event, newSession) => {
@@ -46,47 +27,20 @@ export default function Subjects() {
}, []);
const GetSubjects = async () => {
- const { data: subjectsData, error: subjectsError } = await supabase
+ if (!session?.user.id) return;
+
+ const { data, error } = await supabase
.from('subjects')
.select('*')
+ .eq('uId', session.user.id)
.order('lastChanged', { ascending: false });
- if (subjectsError) {
+ if (error) {
Alert.alert('Subjects could not be fetched, please try again');
return;
}
- 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 = {};
-
- for (const assignment of assignmentsData ?? []) {
- if (!groupedAssignments[assignment.sId]) {
- groupedAssignments[assignment.sId] = [];
- }
- groupedAssignments[assignment.sId].push(assignment);
- }
-
- SetAssignmentsBySubject(groupedAssignments);
+ SetSubjects((data as Subject[]) ?? []);
};
useFocusEffect(
@@ -97,232 +51,131 @@ export default function Subjects() {
}, [session])
);
- const DeleteSubject = async (sId: string) => {
- Alert.alert(
- 'Delete Subject',
- 'Are you sure you want to delete this subject?',
- [
- {
- text: 'Cancel',
- style: 'cancel',
- },
- {
- text: 'Delete',
- style: 'destructive',
- onPress: async () => {
- const { error } = await supabase
- .from('subjects')
- .delete()
- .eq('sId', sId);
-
- if (error) {
- Alert.alert('Subject could not be deleted, please try again');
- return;
- }
-
- Alert.alert('Subject deleted successfully!');
- GetSubjects();
- },
- },
- ]
- );
- };
-
return (
(
-
-
-
-
-
- await supabase.auth.signOut()}
- >
-
- Logout
-
-
-
+ await supabase.auth.signOut()}
+ >
+
+ Logout
+
+
),
}}
/>
-
+
-
- Subjects
-
+ Subjects
- Organize your study work by subject, then break it into assignments
- and tasks.
+ Pick a subject to manage assignments and tasks.
+ {subjects.length === 0 ? (
+
+
+ No subjects yet
+
+
+ Create your first subject to get started.
+
+
+ ) : (
+
+ {subjects.map((subject) => {
+ const colorKey: SubjectColor = subject.color ?? 'slate';
+ const colorSet = SUBJECT_COLORS[colorKey];
+
+ const firstLetter =
+ subject.title?.trim().charAt(0).toUpperCase() || '?';
+
+ return (
+
+ router.push({
+ pathname: '/subject/viewDetailsSubject',
+ params: { sId: subject.sId },
+ })
+ }
+ >
+
+
+
+ {firstLetter}
+
+
+
+
+
+ {subject.title}
+
+
+
+ {subject.description || 'No description added.'}
+
+
+
+
+
+
+ {subject.isActive ? 'Active' : 'Inactive'}
+
+
+
+
+
+ );
+ })}
+
+ )}
+
router.push('/subject/createSubject')}
+ className="mt-2 h-14 items-center justify-center rounded-2xl bg-accent"
+ onPress={() => router.push('/subject/upsertSubject')}
>
Create Subject
-
- item.sId}
- showsVerticalScrollIndicator={false}
- stickySectionHeadersEnabled={false}
- contentContainerStyle={{
- paddingBottom: 32,
- }}
- renderSectionHeader={({ section: { title, data } }) => (
-
-
- {title}
-
-
-
-
- {data.length}
-
-
-
- )}
- 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 (
-
-
- router.push({
- pathname: '/subject/viewDetailsSubject',
- params: { sId: item.sId },
- })
- }
- >
-
-
- {item.isActive && (
-
- ✓
-
- )}
-
-
-
-
- {item.title}
-
-
- {item.description ? (
-
- {item.description}
-
- ) : null}
-
-
-
- {item.isActive ? 'Active' : 'Inactive'}
-
-
-
- {progress}%
-
-
-
-
-
-
-
-
-
- {isOwner && (
-
-
- router.push({
- pathname: '/subject/editSubject',
- params: { sId: item.sId },
- })
- }
- >
-
- Edit
-
-
-
- DeleteSubject(item.sId)}
- >
-
- Delete
-
-
-
- )}
-
- );
- }}
- renderSectionFooter={({ section }) =>
- section.data.length === 0 ? (
-
-
- {section.emptyMessage}
-
-
- Subjects you create will show up here.
-
-
- ) : (
-
- )
- }
- />
-
+
);
}
\ No newline at end of file
diff --git a/app/assignment/_layout.tsx b/app/assignment/_layout.tsx
index 380a294..7fb96ef 100644
--- a/app/assignment/_layout.tsx
+++ b/app/assignment/_layout.tsx
@@ -3,8 +3,7 @@ import { Stack } from "expo-router";
export default function AssignmentLayout() {
return (
-
-
+
);
diff --git a/app/assignment/editAssignment.tsx b/app/assignment/editAssignment.tsx
deleted file mode 100644
index 7115942..0000000
--- a/app/assignment/editAssignment.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-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';
-
-export default function EditAssignment() {
- const { aId } = useLocalSearchParams<{ aId: string }>();
- const [assignment, SetAssignment] = useState(null)
- const [isSaving, SetIsSaving] = useState(false);
-
- const ScheduleDeadlineReminder = async (aId: string, title: string, deadline: string) => {
- const dl = new Date(deadline);
-
- if (isNaN(dl.getTime())) return null;
-
- const deadlineReminder = new Date(dl.getTime() - 24 * 60 * 60 * 1000);
-
- if (deadlineReminder <= new Date()) return null;
-
- const nId = await Notifications.scheduleNotificationAsync({
- content: {
- title: 'Assignment deadline coming up',
- body: `${title} is due in 24 hours.`,
- data: { aId },
- },
- trigger: {
- type: Notifications.SchedulableTriggerInputTypes.DATE,
- date: deadlineReminder,
- },
- });
-
- return nId;
- }
-
- const CancelDeadlineReminder = async (aId: string) => {
- const nId = await GetAssignmentNotificationId(aId);
-
- if (!nId) return;
-
- await Notifications.cancelScheduledNotificationAsync(nId);
- await RemoveAssignmentNotificationId(aId);
- }
-
- const GetAssignment = async (aId: string) => {
- const { data, error } = await supabase.from("assignments").select("*").eq("aId", aId).single();
-
- if (error) {
- Alert.alert("Assignment could not be fetched, please try again");
- return;
- }
-
- SetAssignment(data ?? null);
- }
-
- useFocusEffect(
- useCallback(() => {
- if (aId) {
- GetAssignment(aId);
- }
- }, [aId])
- );
-
- const EditAssignment = async () => {
- if (!assignment) return;
-
- if(assignment.title.trim() === '' || assignment.deadline.trim() === '') {
- Alert.alert("Title and deadline are required!");
- return;
- }
-
- const { data: userData, error: userError } = await supabase.auth.getUser();
-
- if(userError || !userData.user) {
- router.replace("../createUser");
- return;
- }
-
- SetIsSaving(true);
-
- const { data: assignmentData, error: dbError } = await supabase.from("assignments").update({
- title: assignment.title,
- description: assignment.description,
- deadline: assignment.deadline,
- isCompleted: assignment.isCompleted,
- lastChanged: new Date().toISOString(),
- uId: userData.user.id,
- sId: assignment.sId,
- })
- .eq("aId", aId)
- .select()
- .single();
-
- SetIsSaving(false);
-
- if (dbError) {
- Alert.alert("Assignment could not be edited, please try again");
- return;
- }
-
- Alert.alert("Assignment successfully edited!");
-
- if (assignmentData) {
- await CancelDeadlineReminder(assignmentData.aId);
-
- if (!assignmentData.isCompleted) {
- const nId = await ScheduleDeadlineReminder(assignmentData.aId, assignmentData.title, assignmentData.deadline);
-
- if (nId) {
- await SaveAssignmentNotificationId(assignmentData.aId, nId);
- }
- }
- }
-
- if (assignmentData.sId) {
- try {
- await CheckSubjectCompletion(assignmentData.sId);
- } catch {
- Alert.alert("Failed to update subject status");
- }
- }
-
- router.back();
- }
-
- return (
-
-
-
- {!assignment && (
-
- Assignment not found
-
- )}
-
- {assignment && (
-
- Edit Assignment
-
-
-
- SetAssignment(prev => prev ? { ...prev, title: text } : prev)}
- />
- SetAssignment(prev => prev ? { ...prev, description: text } : prev)}
- />
- SetAssignment(prev => prev ? { ...prev, deadline: text } : prev)}
- />
- SetAssignment(prev => prev ? { ...prev, isCompleted: !prev.isCompleted } : prev)}
- style={defaultStyles.checkboxContainer}
- >
-
- {assignment.isCompleted && ✓}
-
- {assignment.isCompleted ? 'Completed' : 'Not Completed'}
-
-
-
- {isSaving && (
-
- )}
-
-
-
-
- )}
-
- )
-}
-
diff --git a/app/assignment/createAssignment.tsx b/app/assignment/upsertAssignment.tsx
similarity index 60%
rename from app/assignment/createAssignment.tsx
rename to app/assignment/upsertAssignment.tsx
index f377cb0..d70a688 100644
--- a/app/assignment/createAssignment.tsx
+++ b/app/assignment/upsertAssignment.tsx
@@ -4,7 +4,7 @@ import { CheckSubjectCompletion } from '@/lib/progress';
import { supabase } from '@/lib/supabase';
import * as Notifications from 'expo-notifications';
import { router, Stack, useLocalSearchParams } from 'expo-router';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
@@ -19,28 +19,74 @@ import {
View,
} from 'react-native';
-export default function CreateAssignment() {
- const sId = (useLocalSearchParams().sId as string) ?? null;
+export default function UpsertAssignment() {
+ const { aId, sId: routeSId } = useLocalSearchParams<{
+ aId?: string;
+ sId?: string;
+ }>();
+
+ const isEditMode = Boolean(aId);
+
const [title, SetTitle] = useState('');
const [description, SetDescription] = useState('');
const [deadline, SetDeadline] = useState('');
const [isCompleted, SetIsCompleted] = useState(false);
+ const [subjectId, SetSubjectId] = useState(routeSId ?? null);
+
+ const [isLoading, SetIsLoading] = useState(isEditMode);
const [isSaving, SetIsSaving] = useState(false);
- const ScheduleDeadlineReminder = async (aId: string, title: string, deadline: string) => {
- const dl = new Date(deadline);
+ useEffect(() => {
+ if (!isEditMode || !aId) {
+ SetIsLoading(false);
+ return;
+ }
- if (isNaN(dl.getTime())) return null;
+ const loadAssignment = async () => {
+ SetIsLoading(true);
+
+ const { data, error } = await supabase
+ .from('assignments')
+ .select('*')
+ .eq('aId', aId)
+ .single();
+
+ SetIsLoading(false);
+
+ if (error || !data) {
+ Alert.alert('Assignment could not be loaded, please try again');
+ router.back();
+ return;
+ }
+
+ SetTitle(data.title ?? '');
+ SetDescription(data.description ?? '');
+ SetDeadline(data.deadline ?? '');
+ SetIsCompleted(data.isCompleted ?? false);
+ SetSubjectId(data.sId ?? routeSId ?? null);
+ };
+
+ loadAssignment();
+ }, [aId, isEditMode, routeSId]);
+
+ const ScheduleDeadlineReminder = async (
+ assignmentId: string,
+ assignmentTitle: string,
+ assignmentDeadline: string
+ ) => {
+ const dl = new Date(assignmentDeadline);
+
+ if (Number.isNaN(dl.getTime())) return null;
const deadlineReminder = new Date(dl.getTime() - 24 * 60 * 60 * 1000);
- if (deadlineReminder <= new Date()) return null;
+ if (deadlineReminder <= new Date()) return null;
const nId = await Notifications.scheduleNotificationAsync({
content: {
title: 'Assignment deadline coming up',
- body: `${title} is due in 24 hours.`,
- data: { aId },
+ body: `${assignmentTitle} is due in 24 hours.`,
+ data: { aId: assignmentId },
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.DATE,
@@ -49,9 +95,40 @@ export default function CreateAssignment() {
});
return nId;
- }
+ };
- const CreateAssignment = async () => {
+ const updateDeadlineReminder = async (
+ assignmentId: string,
+ assignmentTitle: string,
+ assignmentDeadline: string,
+ completed: boolean
+ ) => {
+ const existingNotificationId =
+ await AsyncStorage.GetAssignmentNotificationId(assignmentId);
+
+ if (existingNotificationId) {
+ try {
+ await Notifications.cancelScheduledNotificationAsync(
+ existingNotificationId
+ );
+ } catch {}
+ await AsyncStorage.RemoveAssignmentNotificationId(assignmentId);
+ }
+
+ if (completed) return;
+
+ const nId = await ScheduleDeadlineReminder(
+ assignmentId,
+ assignmentTitle,
+ assignmentDeadline
+ );
+
+ if (nId) {
+ await AsyncStorage.SaveAssignmentNotificationId(assignmentId, nId);
+ }
+ };
+
+ const handleSubmit = async () => {
if (title.trim() === '') {
Alert.alert('Title is required!');
return;
@@ -60,54 +137,70 @@ export default function CreateAssignment() {
const { data: userData, error: userError } = await supabase.auth.getUser();
if (userError || !userData.user) {
- router.replace('../createUser');
+ router.replace('/login');
+ return;
+ }
+
+ if (!subjectId) {
+ Alert.alert('Missing subject', 'This assignment is not linked to a subject.');
return;
}
SetIsSaving(true);
- const { data: assignmentData, error: dbError } = await supabase.from('assignments').insert({
+ const payload = {
title: title.trim(),
description: description.trim(),
deadline: deadline.trim(),
isCompleted,
lastChanged: new Date().toISOString(),
uId: userData.user.id,
- sId,
- })
- .select()
- .single();
+ sId: subjectId,
+ };
- if (dbError) {
+ const result =
+ isEditMode && aId
+ ? await supabase
+ .from('assignments')
+ .update(payload)
+ .eq('aId', aId)
+ .select()
+ .single()
+ : await supabase.from('assignments').insert(payload).select().single();
+
+ if (result.error || !result.data) {
SetIsSaving(false);
- Alert.alert('Assignment could not be created, please try again');
+ Alert.alert(
+ isEditMode
+ ? 'Assignment could not be updated, please try again'
+ : 'Assignment could not be created, please try again'
+ );
return;
}
- Alert.alert('Assignment successfully created!');
+ const savedAssignment = result.data;
- if (!isCompleted && assignmentData) {
- const nId = await ScheduleDeadlineReminder(assignmentData.aId, assignmentData.title, assignmentData.deadline);
+ await updateDeadlineReminder(
+ savedAssignment.aId,
+ savedAssignment.title,
+ savedAssignment.deadline,
+ savedAssignment.isCompleted
+ );
- if (nId) {
- await AsyncStorage.SaveAssignmentNotificationId(assignmentData.aId, nId);
- }
+ try {
+ await CheckSubjectCompletion(subjectId);
+ } catch {
+ Alert.alert('Failed to update subject status');
}
- if (sId) {
- try {
- await CheckSubjectCompletion(sId);
- } catch {
- Alert.alert("Failed to update subject status");
- }
- }
-
- SetTitle('');
- SetDescription('');
- SetDeadline('');
- SetIsCompleted(false);
SetIsSaving(false);
+ Alert.alert(
+ isEditMode
+ ? 'Assignment successfully updated!'
+ : 'Assignment successfully created!'
+ );
+
router.back();
};
@@ -116,11 +209,19 @@ export default function CreateAssignment() {
const labelClassName = 'mb-2 text-sm font-semibold text-text-secondary';
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
return (
<>
@@ -142,10 +243,12 @@ export default function CreateAssignment() {
>
- Create Assignment
+ {isEditMode ? 'Edit Assignment' : 'Create Assignment'}
- Add a new assignment to keep your subject organized.
+ {isEditMode
+ ? 'Update this assignment and keep your subject organized.'
+ : 'Add a new assignment to keep your subject organized.'}
@@ -155,6 +258,7 @@ export default function CreateAssignment() {
{isSaving ? (
- Creating...
+ {isEditMode ? 'Saving...' : 'Creating...'}
) : (
- Create Assignment
+ {isEditMode ? 'Save Changes' : 'Create Assignment'}
)}
diff --git a/app/assignment/viewDetailsAssignment.tsx b/app/assignment/viewDetailsAssignment.tsx
index e7dc364..15c4ed9 100644
--- a/app/assignment/viewDetailsAssignment.tsx
+++ b/app/assignment/viewDetailsAssignment.tsx
@@ -1,17 +1,23 @@
-import { defaultStyles } from '@/constants/defaultStyles';
+import { formatDate, formatDateTime } from '@/lib/date';
import { CheckAssignmentCompletion, CheckSubjectCompletion } from '@/lib/progress';
+import { getSubjectColorSet, type SubjectColor } from '@/lib/subjectColors';
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";
+import { Alert, Pressable, SectionList, Text, View } from "react-native";
+
export default function ViewDetailsAssignment() {
const { aId } = useLocalSearchParams<{ aId: string }>();
- const [assignment, SetAssignment] = useState(null)
- const [tasks, SetTasks] = useState([])
- const [session, SetSession] = useState(null)
+ const [assignment, SetAssignment] = useState(null);
+ const [tasks, SetTasks] = useState([]);
+ const [session, SetSession] = useState(null);
+ const [subjectMeta, setSubjectMeta] = useState({
+ title: 'No Subject',
+ color: 'slate' as SubjectColor,
+ });
const taskSections = [
{ title: "Upcoming Tasks", data: tasks.filter((task) => !task.isCompleted), emptyMessage: "No upcoming tasks" },
@@ -27,16 +33,43 @@ export default function ViewDetailsAssignment() {
},
[])
- const GetAssignment = async (aId: string) => {
- const { data, error } = await supabase.from("assignments").select("*").eq("aId", aId).single();
+ const GetAssignment = async (assignmentId: string) => {
+ const { data, error } = await supabase
+ .from('assignments')
+ .select('*')
+ .eq('aId', assignmentId)
+ .single();
- if (error) {
- Alert.alert("Assignment could not be fetched, please try again");
+ if (error || !data) {
+ console.log('GetAssignment error:', error);
+ Alert.alert('Assignment could not be fetched, please try again');
return;
}
- SetAssignment(data ?? null);
- }
+ SetAssignment(data);
+
+ if (data.sId) {
+ const { data: subjectData, error: subjectError } = await supabase
+ .from('subjects')
+ .select('title, color')
+ .eq('sId', data.sId)
+ .single();
+
+ if (subjectError || !subjectData) {
+ console.log('GetSubjectMeta error:', subjectError);
+ setSubjectMeta({
+ title: 'Unknown Subject',
+ color: 'slate'
+ });
+ return;
+ }
+
+ setSubjectMeta({
+ title: subjectData.title ?? 'Unknown Subject',
+ color: (subjectData.color as SubjectColor | undefined) ?? 'slate'
+ });
+ }
+ };
const GetTasks = async (aId: string) => {
const { data, error } = await supabase.from("tasks").select("*").eq("aId", aId);
@@ -134,115 +167,311 @@ export default function ViewDetailsAssignment() {
)
}
- const progress = tasks.length === 0 ? 0 : Math.round((tasks.filter(task => task.isCompleted).length / tasks.length) * 100);
+ const colorSet = getSubjectColorSet(subjectMeta.color);
+
+ const completedTasks = tasks.filter((task) => task.isCompleted).length;
+ const totalTasks = tasks.length;
+ const remainingTasks = totalTasks - completedTasks;
+
+ const progress =
+ totalTasks === 0
+ ? 0
+ : Math.round((completedTasks / totalTasks) * 100);
+
+ if (!assignment) {
+ return (
+
+
+
+
+
+ Assignment not found
+
+
+ The assignment could not be loaded.
+
+
+ router.back()}
+ >
+ Go back
+
+
+
+ );
+ }
+
return (
-
+
{
- return (
-
-
-
- )
- },
- headerRight: () => {
- return (
-
-
- )
- },
+ title: 'Assignment Details',
+ headerRight: () => (
+ await supabase.auth.signOut()}
+ >
+
+ Logout
+
+
+ ),
}}
/>
- {!assignment && (
-
- Assignment not found
-
- )}
-
- {assignment && (
-
-
- {assignment.title}
- {assignment.description}
- {assignment.deadline}
-
- {assignment.isCompleted && ✓}
-
- {assignment.lastChanged}
-
- {progress}%
-
-
+ item.tId}
+ showsVerticalScrollIndicator={false}
+ stickySectionHeadersEnabled={false}
+ ListHeaderComponent={
+
+
+
+ >
+ {assignment.isCompleted && (
+ ✓
+ )}
+
+
+
+
+ {assignment.title}
+
+
+ {assignment.description ? (
+
+ {assignment.description}
+
+ ) : null}
+
+
+
+
+
+ {subjectMeta.title}
+
+
+
+
+
+
+ Deadline: {formatDate(assignment.deadline) || 'No deadline'}
+
+
+
+
+
+
+
+ Task Progress
+
+
+
+ {completedTasks}/{totalTasks}
+
+
+
+
+
+
+
+
+ {remainingTasks === 0
+ ? 'All tasks complete'
+ : `${remainingTasks} task${remainingTasks === 1 ? '' : 's'} remaining`}
+
+
+
+
+ Last changed: {formatDateTime(assignment.lastChanged)}
+
+
+
+
+
+
+ router.push({
+ pathname: '/assignment/upsertAssignment',
+ params: { aId: assignment.aId },
+ })
+ }
+ >
+ Edit
+
+
+ DeleteAssignment(assignment.aId)}
+ >
+
+ Delete
+
+
-
+ }
+ renderSectionHeader={({ section: { title, data } }) => (
+
+ {title}
-
- router.push({pathname: "/task/createTask", params: { aId: assignment.aId }})} />
+
+
+ {data.length}
+
+
+ )}
+ renderItem={({ item }) => {
+ const isOwner = session?.user.id === item.uId;
- item.tId}
- renderSectionHeader={({ section: { title } }) => {title}}
- renderItem={({ item }) => {
- const isOwner = session?.user.id === item.uId;
-
- return (
-
- router.push({pathname: "/task/viewDetailsTask", params: { tId: item.tId }})}>
- {item.title}
-
- {item.isCompleted && ✓}
-
+ return (
+
+
+ router.push({
+ pathname: '/task/viewDetailsTask',
+ params: { tId: item.tId },
+ })
+ }
+ >
+
+
+ {item.isCompleted && (
+ ✓
+ )}
+
+
+
+
+ {item.title}
+
+
+ {item.description ? (
+
+ {item.description}
+
+ ) : null}
+
+
+
+
+ {isOwner && (
+
+
+ router.push({
+ pathname: '/task/editTask',
+ params: { tId: item.tId },
+ })
+ }
+ >
+ Edit
+
+
+ DeleteTask(item.tId, item.aId)}
+ >
+
+ Delete
+
-
- {isOwner && (
-
- router.push({pathname: "/task/editTask", params: { tId: item.tId }})} />
- DeleteTask(item.tId, item.tId)} />
-
- )}
- );
- }}
- renderSectionFooter={({ section }) =>
- section.data.length === 0 ? (
-
- {section.emptyMessage}
-
-
- ) : (
-
- )
- }
- />
-
- )}
+ )}
+
+ );
+ }}
+ renderSectionFooter={({ section }) =>
+ section.data.length === 0 ? (
+
+
+ {section.emptyMessage}
+
+
+ Tasks for this assignment will show up here.
+
+
+ ) : (
+
+ )
+ }
+ />
);
}
\ No newline at end of file
diff --git a/app/createUser.tsx b/app/createUser.tsx
index a40082c..0fca889 100644
--- a/app/createUser.tsx
+++ b/app/createUser.tsx
@@ -1,69 +1,146 @@
-import { defaultStyles } from '@/constants/defaultStyles';
import { supabase } from '@/lib/supabase';
-import { router, Stack } from 'expo-router';
+import { router } from 'expo-router';
import { useState } from 'react';
-import { Alert, Button, Keyboard, KeyboardAvoidingView, Platform, Pressable, Text, TextInput, TouchableWithoutFeedback, View } from 'react-native';
+import {
+ Alert,
+ Keyboard,
+ KeyboardAvoidingView,
+ Platform,
+ Pressable,
+ ScrollView,
+ Text,
+ TextInput,
+ TouchableWithoutFeedback,
+ View,
+} from 'react-native';
export default function CreateUser() {
const [email, SetEmail] = useState('');
const [password, SetPassword] = useState('');
+ const [isLoading, SetIsLoading] = useState(false);
const SignUp = async () => {
- if(email.trim() === '' || password.trim() === '') {
- Alert.alert("All fields are required!");
+ if (email.trim() === '' || password.trim() === '') {
+ Alert.alert('All fields are required!');
return;
}
- const {error} = await supabase.auth.signUp({
- email: email,
- password: password,
+ SetIsLoading(true);
+
+ const { data, error } = await supabase.auth.signUp({
+ email: email.trim(),
+ password,
});
+ SetIsLoading(false);
+
if (error) {
- Alert.alert(error.message, "User could not be created, please try again");
+ Alert.alert(error.message, 'User could not be created, please try again');
return;
}
- router.replace("/");
- }
+ if (!data.session) {
+ Alert.alert(
+ 'Check your email',
+ 'Your account was created. Please confirm your email before signing in.'
+ );
+ router.replace('/login');
+ return;
+ }
+ router.replace('/');
+ };
+
+ const inputClassName =
+ 'rounded-2xl border border-app-border bg-app-subtle px-4 py-3 text-base text-text-main';
return (
-
-
-
- Create User
-
-
-
+
+
+
+
+
+ Study Sprint
+
+
+
+ Organize subjects, assignments, and tasks in one calm workflow.
+
+
+
+
+
+ Create account
+
+
+ Start your next study sprint.
+
+
+
+
+ Email
+
+
+
+
+
+ Password
+
-
-
- router.back()} />
- router.push("/login")} style={defaultStyles.buttonContainer}>
- Already have an Account? login here
-
-
-
-
-
- )
+
+
+
+ {isLoading ? 'Creating account...' : 'Create account'}
+
+
+
+ router.push('/login')}
+ >
+
+ Already have an account? Log in
+
+
+
+
+
+
+ );
}
\ No newline at end of file
diff --git a/app/login.tsx b/app/login.tsx
index b93152a..6cf89e1 100644
--- a/app/login.tsx
+++ b/app/login.tsx
@@ -1,12 +1,12 @@
-import { defaultStyles } from "@/constants/defaultStyles";
import { supabase } from "@/lib/supabase";
-import { router, Stack } from "expo-router";
+import { router } from "expo-router";
import { useState } from "react";
-import { Alert, Button, Keyboard, KeyboardAvoidingView, Platform, Text, TextInput, TouchableWithoutFeedback, View } from "react-native";
+import { Alert, Keyboard, KeyboardAvoidingView, Platform, Pressable, ScrollView, Text, TextInput, TouchableWithoutFeedback, View } from "react-native";
export default function Login() {
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
+ const [email, SetEmail] = useState('');
+ const [password, SetPassword] = useState('');
+ const [isLoading, SetIsLoading] = useState(false);
const login = async () => {
if(email.trim() === '' || password.trim() === '') {
@@ -14,11 +14,15 @@ export default function Login() {
return;
}
+ SetIsLoading(true);
+
const { error } = await supabase.auth.signInWithPassword({
email,
password
});
+ SetIsLoading(false);
+
if (error) {
Alert.alert("Login failed, please check your credentials and try again");
return;
@@ -27,40 +31,98 @@ export default function Login() {
router.replace("/");
}
- return (
-
-
+ const inputClassName =
+ 'rounded-2xl border border-app-border bg-app-subtle px-4 py-3 text-base text-text-main'
-
- Login
-
-
-
-
-
-
- router.push("/")} />
-
-
-
-
-
- )
+ return (
+
+
+
+
+
+ Study Sprint
+
+
+
+ Pick up where you left off.
+
+
+
+
+
+ Log in
+
+
+
+ Continue your study workflow.
+
+
+
+
+ Email
+
+
+
+
+
+
+ Password
+
+
+
+
+
+
+ {isLoading ? 'Logging in...' : 'Log in'}
+
+
+
+ router.push('/createUser')}
+ >
+
+ Don't have an account? Sign up
+
+
+
+
+
+
+);
}
\ No newline at end of file
diff --git a/app/subject/_layout.tsx b/app/subject/_layout.tsx
index d40e391..57d25b5 100644
--- a/app/subject/_layout.tsx
+++ b/app/subject/_layout.tsx
@@ -3,8 +3,7 @@ import { Stack } from "expo-router";
export default function SubjectLayout() {
return (
-
-
+
);
diff --git a/app/subject/createSubject.tsx b/app/subject/createSubject.tsx
deleted file mode 100644
index 5c13e01..0000000
--- a/app/subject/createSubject.tsx
+++ /dev/null
@@ -1,195 +0,0 @@
-import { defaultStyles } from '@/constants/defaultStyles';
-import { supabase } from '@/lib/supabase';
-import { router, Stack } from 'expo-router';
-import { useState } from 'react';
-import {
- ActivityIndicator,
- Alert,
- Keyboard,
- KeyboardAvoidingView,
- Platform,
- Pressable,
- ScrollView,
- Text,
- TextInput,
- TouchableWithoutFeedback,
- View,
-} from 'react-native';
-
-export default function CreateSubject() {
- const [title, SetTitle] = useState('');
- const [description, SetDescription] = useState('');
- const [isActive, SetIsActive] = useState(true);
- const [isSaving, SetIsSaving] = useState(false);
-
- const CreateSubject = async () => {
- if (title.trim() === '') {
- Alert.alert('Title is required!');
- return;
- }
-
- const { data, error: userError } = await supabase.auth.getUser();
-
- if (userError || !data.user) {
- router.replace('../createUser');
- return;
- }
-
- SetIsSaving(true);
-
- const { error: dbError } = await supabase.from('subjects').insert({
- title: title.trim(),
- description: description.trim(),
- isActive,
- lastChanged: new Date().toISOString(),
- uId: data.user.id,
- });
-
- if (dbError) {
- SetIsSaving(false);
- Alert.alert('Subject could not be created, please try again');
- return;
- }
-
- Alert.alert('Subject successfully created!');
-
- SetTitle('');
- SetDescription('');
- SetIsActive(true);
- SetIsSaving(false);
-
- router.back();
- };
-
- const inputClassName =
- 'rounded-2xl border border-app-border bg-app-subtle px-4 py-3 text-base text-text-main';
-
- const labelClassName = 'mb-2 text-sm font-semibold text-text-secondary';
-
- return (
- <>
-
-
-
-
-
-
-
- Create Subject
-
-
- Add a subject to organize your assignments and study tasks.
-
-
-
-
-
- Title
-
-
-
-
- Description
-
-
-
- SetIsActive((state) => !state)}
- disabled={isSaving}
- className={`mb-6 flex-row items-center rounded-2xl border p-4 ${
- isActive
- ? 'border-accent bg-accent-soft'
- : 'border-app-border bg-app-subtle'
- }`}
- >
-
- {isActive && (
-
- ✓
-
- )}
-
-
-
-
- Active subject
-
-
- Active subjects appear in your main study workflow.
-
-
-
-
-
- {isSaving ? (
-
-
-
- Creating...
-
-
- ) : (
-
- Create Subject
-
- )}
-
-
- router.back()}
- disabled={isSaving}
- >
-
- Cancel
-
-
-
-
-
-
- >
- );
-}
\ No newline at end of file
diff --git a/app/subject/editSubject.tsx b/app/subject/editSubject.tsx
deleted file mode 100644
index 9c21419..0000000
--- a/app/subject/editSubject.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-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';
-
-export default function EditSubject() {
- const { sId } = useLocalSearchParams<{ sId: string }>();
- const [subject, SetSubject] = useState(null)
- const [isSaving, SetIsSaving] = useState(false);
-
- const GetSubject = async (sId: string) => {
- const { data, error } = await supabase.from("subjects").select("*").eq("sId", sId).single();
-
- if (error) {
- Alert.alert("Subject could not be fetched, please try again");
- return;
- }
-
- SetSubject(data ?? null);
- }
-
- useFocusEffect(
- useCallback(() => {
- if (sId) {
- GetSubject(sId);
- }
- }, [sId])
- );
-
- const EditSubject = async () => {
- if (!subject) return;
-
- if(subject.title.trim() === '') {
- Alert.alert("Title is required!");
- return;
- }
-
- const { data, error: userError } = await supabase.auth.getUser();
-
- if(userError || !data.user) {
- router.replace("../createUser");
- return;
- }
-
- SetIsSaving(true);
-
- const { error: dbError } = await supabase.from("subjects").update({
- title: subject.title,
- description: subject.description,
- isActive: subject.isActive,
- lastChanged: new Date().toISOString(),
- uId: data.user.id,
- }).eq("sId", sId);
-
- SetIsSaving(false);
-
- if (dbError) {
- Alert.alert("Subject could not be edited, please try again");
- return;
- }
-
- Alert.alert("Subject successfully edited!");
-
- router.back();
- }
-
- return (
-
-
-
- {!subject && (
-
- Subject not found
-
- )}
-
- {subject && (
-
- Edit Subject
-
-
-
- SetSubject(prev => prev ? { ...prev, title: text } : prev)}
- />
- SetSubject(prev => prev ? { ...prev, description: text } : prev)}
- />
- SetSubject(prev => prev ? { ...prev, isActive: !prev.isActive } : prev)}
- style={defaultStyles.checkboxContainer}
- >
-
- {subject.isActive && ✓}
-
- {subject.isActive ? 'Active' : 'inactive'}
-
-
-
- {isSaving && (
-
- )}
- router.back()} />
-
-
-
-
- )}
-
- )
-}
-
diff --git a/app/subject/upsertSubject.tsx b/app/subject/upsertSubject.tsx
new file mode 100644
index 0000000..b3bef3f
--- /dev/null
+++ b/app/subject/upsertSubject.tsx
@@ -0,0 +1,351 @@
+import { defaultStyles } from '@/constants/defaultStyles';
+import { SUBJECT_COLOR_KEYS, SUBJECT_COLORS, type SubjectColor } from '@/lib/subjectColors';
+import { supabase } from '@/lib/supabase';
+import type { Subject } from '@/lib/types';
+import { router, Stack, useLocalSearchParams } from 'expo-router';
+import { useEffect, useMemo, useState } from 'react';
+import {
+ ActivityIndicator,
+ Alert,
+ Keyboard,
+ KeyboardAvoidingView,
+ Platform,
+ Pressable,
+ ScrollView,
+ Text,
+ TextInput,
+ TouchableWithoutFeedback,
+ View
+} from 'react-native';
+
+
+export default function UpsertSubject() {
+ const { sId } = useLocalSearchParams<{ sId?: string }>();
+ const isEditMode = Boolean(sId);
+
+ const [title, setTitle] = useState('');
+ const [description, setDescription] = useState('');
+ const [isActive, setIsActive] = useState(true);
+ const [color, setColor] = useState('blue');
+
+ const [isLoading, setIsLoading] = useState(isEditMode);
+ const [isSaving, setIsSaving] = useState(false);
+
+ useEffect(() => {
+ if (!isEditMode || !sId) return;
+
+ const loadSubject = async () => {
+ setIsLoading(true);
+
+ const { data, error } = await supabase
+ .from('subjects')
+ .select('*')
+ .eq('sId', sId)
+ .single();
+
+ setIsLoading(false);
+
+ if (error || !data ) {
+ Alert.alert('Subject could not be loaded, please try again');
+ router.back();
+ return;
+ }
+
+ const subject = data as Subject;
+
+ setTitle(subject.title ?? '');
+ setDescription(subject.description ?? '');
+ setIsActive(subject.isActive ?? true);
+ setColor(subject.color ?? 'blue');
+ };
+
+ loadSubject();
+ }, [isEditMode, sId]);
+
+ const handleSubmit = async () => {
+ if (title.trim() === '') {
+ Alert.alert('Title is required!');
+ return;
+ }
+
+ const { data, error: userError } = await supabase.auth.getUser();
+
+ if (userError || !data.user) {
+ router.replace('/login');
+ return;
+ }
+
+ setIsSaving(true);
+
+ const payload = {
+ title: title.trim(),
+ description : description.trim(),
+ isActive,
+ color,
+ lastChanged: new Date().toISOString(),
+ uId: data.user.id,
+ };
+
+ const result = isEditMode && sId
+ ? await supabase.from('subjects').update(payload).eq('sId', sId)
+ : await supabase.from('subjects').insert(payload);
+
+ setIsSaving(false);
+
+ if(result.error) {
+ Alert.alert(
+ isEditMode
+ ? 'Subject could not be updated, please try again'
+ : 'Subject could not be created, please try again'
+ );
+ return;
+ }
+
+ Alert.alert(
+ isEditMode ? 'Subject updated successfully!' : 'Subject created successfully!'
+ );
+
+ router.back();
+ };
+
+ const inputClassName =
+ 'rounded-2xl border border-app-border bg-app-subtle px-4 py-3 text-base text-text-main';
+
+ const labelClassName = 'mb-2 text-sm font-semibold text-text-secondary';
+
+ const selectedColor = useMemo(() => SUBJECT_COLORS[color], [color]);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {isEditMode ? 'Edit Subject' : 'Create Subject'}
+
+
+ {isEditMode? ' Update this subject and keep your study structure organized.'
+ : 'Add a subject to organize your assignments and studyt tasks.'}
+
+
+
+
+
+ Title
+
+
+
+
+ Description
+
+
+
+
+ Color
+
+
+ Preview
+
+
+
+
+
+ {title.trim().charAt(0).toUpperCase() || 'S'}
+
+
+
+
+
+ {title.trim() || 'Subject Preview'}
+
+
+
+ {description.trim() || 'This color will be used as the subject card accent.'}
+
+
+
+
+
+
+ {isActive ? 'Active' : 'Inactive'}
+
+
+
+
+
+
+
+
+ {SUBJECT_COLOR_KEYS.map((colorKey) => {
+ const colorOption = SUBJECT_COLORS[colorKey];
+ const isSelected = color === colorKey;
+
+ return (
+ setColor(colorKey)}
+ className="mr-3 mb-3 rounded-2xl border border-app-border bg-app--surface p-2"
+ style={{
+ borderColor: isSelected
+ ? colorOption.strong
+ : '#FFFFFF',
+ }}
+ >
+
+
+
+ {colorOption.label}
+
+
+
+ );
+ })}
+
+
+
+ setIsActive((state) => !state)}
+ disabled={isSaving}
+ className={`mb-6 flex-row items-center rounded-2xl border p-4 ${
+ isActive
+ ? 'border-accent bg-accent-soft'
+ : 'border-app-border bg-app-subtle'
+ }`}
+ >
+
+ {isActive && (
+ ✓
+ )}
+
+
+
+
+ Active subject
+
+
+ Active subjects appear in your main study workflow.
+
+
+
+
+
+ {isSaving ? (
+
+
+
+ {isEditMode ? 'Saving...' : 'Creating...'}
+
+
+ ) : (
+
+ {isEditMode ? 'Save Changes' : 'Create Subject'}
+
+ )}
+
+
+ router.back()}
+ disabled={isSaving}
+ >
+
+ Cancel
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/subject/viewDetailsSubject.tsx b/app/subject/viewDetailsSubject.tsx
index a8a634f..7b5480f 100644
--- a/app/subject/viewDetailsSubject.tsx
+++ b/app/subject/viewDetailsSubject.tsx
@@ -1,53 +1,81 @@
-import { defaultStyles } from '@/constants/defaultStyles';
+import { formatDate, formatDateTime } from '@/lib/date';
import { CheckSubjectCompletion } from '@/lib/progress';
+import { SUBJECT_COLORS, type SubjectColor } from '@/lib/subjectColors';
import { supabase } from '@/lib/supabase';
-import type { Assignment, Subject } from '@/lib/types';
+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, Button, Pressable, SectionList, Text, View } from "react-native";
+import { Alert, Pressable, SectionList, Text, View } from 'react-native';
+
+export type Subject = {
+ sId: string;
+ title: string;
+ description: string;
+ isActive: boolean;
+ lastChanged: string;
+ uId: string;
+ color: SubjectColor;
+};
export default function ViewDetailsSubject() {
const { sId } = useLocalSearchParams<{ sId: string }>();
- const [subject, SetSubject] = useState(null)
- const [assignments, SetAssignments] = useState([])
- const [session, SetSession] = useState(null)
+ const [subject, SetSubject] = useState(null);
+ const [assignments, SetAssignments] = useState([]);
+ const [session, SetSession] = useState(null);
const assignmentSections = [
- { title: "Upcoming Assignments", data: assignments.filter((assignment) => !assignment.isCompleted), emptyMessage: "No upcoming assignments" },
- { title: "Completed Assignments", data: assignments.filter((assignment) => assignment.isCompleted), emptyMessage: "No completed assignments" },
+ {
+ title: 'Active Assignments',
+ data: assignments.filter((assignment) => !assignment.isCompleted),
+ emptyMessage: 'No active assignments',
+ },
+ {
+ title: 'Completed Assignments',
+ data: assignments.filter((assignment) => assignment.isCompleted),
+ emptyMessage: 'No completed assignments',
+ },
];
useEffect(() => {
- supabase.auth.getSession().then(({ data }) => SetSession(data.session ?? null))
- const { data: sub } = supabase.auth.onAuthStateChange((_event, newSession) => {
- SetSession(newSession)
- })
- return () => sub.subscription.unsubscribe()
- },
- [])
+ supabase.auth.getSession().then(({ data }) => SetSession(data.session ?? null));
- const GetSubject = async (sId: string) => {
- const { data, error } = await supabase.from("subjects").select("*").eq("sId", sId).single();
+ const { data: sub } = supabase.auth.onAuthStateChange((_event, newSession) => {
+ SetSession(newSession);
+ });
+
+ return () => sub.subscription.unsubscribe();
+ }, []);
+
+ const GetSubject = async (subjectId: string) => {
+ const { data, error } = await supabase
+ .from('subjects')
+ .select('*')
+ .eq('sId', subjectId)
+ .single();
if (error) {
- Alert.alert("Subject could not be fetched, please try again");
+ Alert.alert('Subject could not be fetched, please try again');
return;
}
- SetSubject(data ?? null);
- }
+ SetSubject((data as Subject) ?? null);
+ };
- const GetAssignments = async (sId: string) => {
- const { data, error } = await supabase.from("assignments").select("*").eq("sId", sId).order("deadline", { ascending: true });
+ const GetAssignments = async (subjectId: string) => {
+ const { data, error } = await supabase
+ .from('assignments')
+ .select('*')
+ .eq('sId', subjectId)
+ .order('deadline', { ascending: true });
if (error) {
- Alert.alert("Assignments could not be fetched, please try again");
+ Alert.alert('Assignments could not be fetched, please try again');
return;
}
SetAssignments(data ?? []);
- }
+ };
useFocusEffect(
useCallback(() => {
@@ -58,180 +86,392 @@ export default function ViewDetailsSubject() {
}, [session, sId])
);
- const DeleteSubject = async (sId: string) => {
+ useEffect(() => {
+ const test = async () => {
+ try {
+ const { data, error } = await supabase.from('subjects').select('*').limit(1);
+ console.log('test data:', data);
+ console.log('test error:', error);
+ } catch (err) {
+ console.log('test crashed:', err);
+ }
+ };
+
+ test();
+ }, []);
+
+ const DeleteSubject = async (subjectId: string) => {
Alert.alert(
- "Delete Subject",
- "Are you sure you want to delete this subject?",
+ 'Delete Subject',
+ 'Are you sure you want to delete this subject?',
[
{
- text: "Cancel",
- style: "cancel"
+ text: 'Cancel',
+ style: 'cancel',
},
{
- text: "Delete",
- style: "destructive",
+ text: 'Delete',
+ style: 'destructive',
onPress: async () => {
- const { error } = await supabase.from("subjects").delete().eq("sId", sId);
+ const { error } = await supabase
+ .from('subjects')
+ .delete()
+ .eq('sId', subjectId);
if (error) {
- Alert.alert("Subject could not be deleted, please try again");
+ Alert.alert('Subject could not be deleted, please try again');
return;
}
- Alert.alert("Subject deleted successfully!");
+ Alert.alert('Subject deleted successfully!');
router.back();
- }
- }
+ },
+ },
]
- )
- }
+ );
+ };
- const DeleteAssignment = async (aId: string, sId: string) => {
+ const DeleteAssignment = async (assignmentId: string, subjectId: string) => {
Alert.alert(
- "Delete Assignment",
- "Are you sure you want to delete this assignment?",
+ 'Delete Assignment',
+ 'Are you sure you want to delete this assignment?',
[
{
- text: "Cancel",
- style: "cancel"
+ text: 'Cancel',
+ style: 'cancel',
},
{
- text: "Delete",
- style: "destructive",
+ text: 'Delete',
+ style: 'destructive',
onPress: async () => {
- const { error } = await supabase.from("assignments").delete().eq("aId", aId);
+ const { error } = await supabase
+ .from('assignments')
+ .delete()
+ .eq('aId', assignmentId);
if (error) {
- Alert.alert("Assignment could not be deleted, please try again");
+ Alert.alert('Assignment could not be deleted, please try again');
return;
}
- Alert.alert("Assignment deleted successfully!");
-
- if (sId) {
+ if (subjectId) {
try {
- await CheckSubjectCompletion(sId);
+ await CheckSubjectCompletion(subjectId);
} catch {
- Alert.alert("Failed to update subject status");
+ Alert.alert('Failed to update subject status');
}
}
- GetAssignments(sId);
- }
- }
+ await GetAssignments(subjectId);
+ await GetSubject(subjectId);
+
+ Alert.alert('Assignment deleted successfully!');
+ },
+ },
]
- )
+ );
+ };
+
+ const completedAssignments = assignments.filter((assignment) => assignment.isCompleted).length;
+ const totalAssignments = assignments.length;
+ const remainingAssignments = totalAssignments - completedAssignments;
+
+ const progress =
+ assignments.length === 0
+ ? 0
+ : Math.round((completedAssignments / totalAssignments) * 100);
+
+ if (!subject) {
+ return (
+
+
+
+
+
+ Subject not found
+
+
+ The subject could not be loaded.
+
+
+ router.back()}
+ >
+
+ Go back
+
+
+
+
+ );
}
- const progress = assignments.length === 0 ? 0 : Math.round((assignments.filter(assignment => assignment.isCompleted).length / assignments.length) * 100);
+ const colorKey: SubjectColor = subject.color ?? 'slate';
+ const colorSet = SUBJECT_COLORS[colorKey];
+
+ const firstLetter = subject.title?.trim().charAt(0).toUpperCase() || 'S';
return (
-
+
{
- return (
-
-
-
- )
- },
- headerRight: () => {
- return (
-
- await supabase.auth.signOut()} />
-
- )
- },
+ title: 'Subject Details',
+ headerRight: () => (
+ await supabase.auth.signOut()}
+ >
+
+ Logout
+
+
+ ),
}}
/>
- {!subject && (
-
- Subject not found
-
- )}
-
- {subject && (
-
-
- {subject.title}
- {subject.description}
-
- {subject.isActive && ✓}
-
- {subject.lastChanged}
-
- {progress}%
-
-
+ item.aId}
+ showsVerticalScrollIndicator={false}
+ stickySectionHeadersEnabled={false}
+ ListHeaderComponent={
+
+
+
+ className="mr-3 h-12 w-12 items-center justify-center rounded-2xl"
+ style={{ backgroundColor: colorSet.soft }}
+ >
+
+ {firstLetter}
+
+
+
+
+
+ {subject.title}
+
+
+ {subject.description ? (
+
+ {subject.description}
+
+ ) : (
+
+ No description added.
+
+ )}
+
+
+
+
+
+ {subject.isActive ? 'Active' : 'Inactive'}
+
+
+
+
+
+
+
+
+ Assignment Progress
+
+
+
+ {completedAssignments}/{totalAssignments}
+
+
+
+
+
+
+
+
+ {remainingAssignments === 0
+ ? 'All assignments complete'
+ : `${remainingAssignments} assignment${
+ remainingAssignments === 1 ? '' : 's'
+ } remaining`}
+
+
+
+
+ Last changed: {formatDateTime(subject.lastChanged)}
+
+
+
+
+ router.push({
+ pathname: '/subject/upsertSubject',
+ params: { sId: subject.sId },
+ })
+ }
+ >
+
+ Edit
+
+
+
+ DeleteSubject(subject.sId)}
+ >
+
+ Delete
+
+
- router.push({pathname: "/subject/editSubject", params: { sId: subject.sId }})} />
- DeleteSubject(subject.sId)} />
+
+ router.push({
+ pathname: '/assignment/upsertAssignment',
+ params: { sId: subject.sId },
+ })
+ }
+ >
+
+ Create Assignment
+
+
+
+ }
+ renderSectionHeader={({ section: { title, data } }) => (
+
+ {title}
-
- router.push({pathname: "/assignment/createAssignment", params: { sId: subject.sId }})} />
+
+
+ {data.length}
+
+ )}
+ renderItem={({ item }) => {
+ const isOwner = session?.user.id === item.uId;
- item.aId}
- renderSectionHeader={({ section: { title } }) => {title}}
- renderItem={({ item }) => {
- const isOwner = session?.user.id === item.uId;
-
- return (
-
- router.push({pathname: "/assignment/viewDetailsAssignment", params: { aId: item.aId }})}>
- {item.title}
- {item.deadline}
-
- {item.isCompleted && ✓}
-
+ return (
+
+
+ router.push({
+ pathname: '/assignment/viewDetailsAssignment',
+ params: { aId: item.aId },
+ })
+ }
+ >
+
+
+
+ {item.title}
+
+
+ {item.description ? (
+
+ {item.description}
+
+ ) : null}
+
+
+ Deadline: {formatDate(item.deadline)}
+
+
+
+
+
+ {isOwner && (
+
+
+ router.push({
+ pathname: '/assignment/upsertAssignment',
+ params: { aId: item.aId },
+ })
+ }
+ >
+
+ Edit
+
+
+
+ DeleteAssignment(item.aId, item.sId)}
+ >
+
+ Delete
+
-
- {isOwner && (
-
- router.push({pathname: "/assignment/editAssignment", params: { aId: item.aId }})} />
- DeleteAssignment(item.aId, item.sId)} />
-
- )}
- );
- }}
- renderSectionFooter={({ section }) =>
- section.data.length === 0 ? (
-
- {section.emptyMessage}
-
-
- ) : (
-
- )
- }
- />
-
- )}
+ )}
+
+ );
+ }}
+ renderSectionFooter={({ section }) =>
+ section.data.length === 0 ? (
+
+
+ {section.emptyMessage}
+
+
+ Assignments for this subject will show up here.
+
+
+ ) : (
+
+ )
+ }
+ />
);
}
\ No newline at end of file
diff --git a/app/task/_layout.tsx b/app/task/_layout.tsx
index 37456cb..1125408 100644
--- a/app/task/_layout.tsx
+++ b/app/task/_layout.tsx
@@ -3,6 +3,7 @@ import { Stack } from "expo-router";
export default function TaskLayout() {
return (
+
diff --git a/app/task/editTask.tsx b/app/task/editTask.tsx
index 90b4eac..45f7f11 100644
--- a/app/task/editTask.tsx
+++ b/app/task/editTask.tsx
@@ -1,16 +1,28 @@
-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';
+import {
+ ActivityIndicator,
+ Alert,
+ Keyboard,
+ KeyboardAvoidingView,
+ Platform,
+ Pressable,
+ ScrollView,
+ Text,
+ TextInput,
+ TouchableWithoutFeedback,
+ View,
+} from 'react-native';
export default function EditTask() {
const { tId } = useLocalSearchParams<{ tId: string }>();
- const [task, SetTask] = useState(null)
+ const [task, SetTask] = useState(null);
const [isSaving, SetIsSaving] = useState(false);
+
const GetTask = async (tId: string) => {
const { data, error } = await supabase.from("tasks").select("*").eq("tId", tId).single();
@@ -63,8 +75,6 @@ export default function EditTask() {
return;
}
- Alert.alert("Task successfully edited!");
-
if (task.aId) {
try {
await CheckAssignmentCompletion(task.aId);
@@ -73,63 +83,173 @@ export default function EditTask() {
}
}
+ Alert.alert("Task successfully edited!");
router.back();
- }
+ };
+
+ const inputClassName =
+ 'rounded-2xl border border-app-border bg-app-subtle px-4 py-3 text-base text-text-main';
+
+ const labelClassName = 'mb-2 text-sm font-semibold text-text-secondary';
+
return (
-
+ <>
- {!task && (
-
- Task not found
-
- )}
+ {!task ? (
+
+
+
+ Task not found
+
+
+ The task could not be loaded.
+
+
+ router.back()}
+ >
+
+ Go back
+
+
+
+
+ ) : (
+
+
+
+
+
+ Edit Task
+
+
+ Update the task details and completion state.
+
+
+
+
+
+ Title
+
+ SetTask((prev) => (prev ? { ...prev, title: text } : prev))
+ }
+ returnKeyType="next"
+ />
+
+
+
+ Description
+
+ SetTask((prev) =>
+ prev ? { ...prev, description: text } : prev
+ )
+ }
+ multiline
+ textAlignVertical="top"
+ />
+
- {task && (
-
- Edit Task
-
-
-
- SetTask(prev => prev ? { ...prev, title: text } : prev)}
- />
- SetTask(prev => prev ? { ...prev, description: text } : prev)}
- />
SetTask(prev => prev ? { ...prev, isCompleted: !prev.isCompleted } : prev)}
- style={defaultStyles.checkboxContainer}
+ onPress={() =>
+ SetTask((prev) =>
+ prev ? { ...prev, isCompleted: !prev.isCompleted } : prev
+ )
+ }
+ disabled={isSaving}
+ className={`mb-6 flex-row items-center rounded-2xl border p-4 ${
+ task.isCompleted
+ ? 'border-accent bg-accent-soft'
+ : 'border-app-border bg-app-subtle'
+ }`}
>
-
- {task.isCompleted && ✓}
+
+ {task.isCompleted && (
+
+ ✓
+
+ )}
+
+
+
+
+ Mark as completed
+
+
+ You can change this again later.
+
- {task.isCompleted ? 'Completed' : 'Not Completed'}
-
- {isSaving && (
-
- )}
- router.back()} />
-
-
-
-
- )}
-
- )
-}
+
+ {isSaving ? (
+
+
+
+ Saving...
+
+
+ ) : (
+
+ Save Changes
+
+ )}
+
+ router.back()}
+ disabled={isSaving}
+ >
+
+ Cancel
+
+
+
+
+
+
+ )}
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/(tabs)/tasks.tsx b/app/task/tasks.tsx
similarity index 100%
rename from app/(tabs)/tasks.tsx
rename to app/task/tasks.tsx
diff --git a/app/task/viewDetailsTask.tsx b/app/task/viewDetailsTask.tsx
index 23d6c02..9c44b18 100644
--- a/app/task/viewDetailsTask.tsx
+++ b/app/task/viewDetailsTask.tsx
@@ -1,36 +1,91 @@
-import { defaultStyles } from '@/constants/defaultStyles';
+import { formatDateTime } from '@/lib/date';
import { CheckAssignmentCompletion } from '@/lib/progress';
+import { getSubjectColorSet, type SubjectColor } from '@/lib/subjectColors';
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";
+import { Alert, Pressable, Text, View } from 'react-native';
export default function ViewDetailsTask() {
const { tId } = useLocalSearchParams<{ tId: string }>();
- const [task, SetTask] = useState(null)
- const [session, SetSession] = useState(null)
+
+ const [task, SetTask] = useState(null);
+ const [session, SetSession] = useState(null);
+ const [contextMeta, setContextMeta] = useState({
+ subjectTitle: 'No Subject',
+ assignmentTitle: 'No Assignment',
+ subjectColor: 'slate' as SubjectColor,
+ });
useEffect(() => {
- supabase.auth.getSession().then(({ data }) => SetSession(data.session ?? null))
+ supabase.auth.getSession().then(({ data }) => SetSession(data.session ?? null));
+
const { data: sub } = supabase.auth.onAuthStateChange((_event, newSession) => {
- SetSession(newSession)
- })
- return () => sub.subscription.unsubscribe()
- },
- [])
+ SetSession(newSession);
+ });
- const GetTask = async (tId: string) => {
- const { data, error } = await supabase.from("tasks").select("*").eq("tId", tId).single();
+ return () => sub.subscription.unsubscribe();
+ }, []);
- if (error) {
- Alert.alert("Task could not be fetched, please try again");
+ const GetTask = async (taskId: string) => {
+ const { data, error } = await supabase
+ .from('tasks')
+ .select('*')
+ .eq('tId', taskId)
+ .single();
+
+ if (error || !data) {
+ console.log('GetTask error:', error);
+ Alert.alert('Task could not be fetched, please try again');
return;
}
- SetTask(data ?? null);
- }
+ SetTask(data);
+
+ if (data.aId) {
+ const { data: assignmentData, error: assignmentError } = await supabase
+ .from('assignments')
+ .select('title, sId')
+ .eq('aId', data.aId)
+ .single();
+
+ if (assignmentError || !assignmentData) {
+ console.log('GetTaskAssignment error:', assignmentError);
+ setContextMeta({
+ subjectTitle: 'Unknown Subject',
+ assignmentTitle: 'Unknown Assignment',
+ subjectColor: 'slate',
+ });
+ return;
+ }
+
+ if (assignmentData.sId) {
+ const { data: subjectData, error: subjectError } = await supabase
+ .from('subjects')
+ .select('title, color')
+ .eq('sId', assignmentData.sId)
+ .single();
+
+ if (subjectError || !subjectData) {
+ console.log('GetTaskSubject error:', subjectError);
+ setContextMeta({
+ subjectTitle: 'Unknown Subject',
+ assignmentTitle: assignmentData.title ?? 'Unknown Assignment',
+ subjectColor: 'slate',
+ });
+ return;
+ }
+
+ setContextMeta({
+ subjectTitle: subjectData.title ?? 'Unknown Subject',
+ assignmentTitle: assignmentData.title ?? 'Unknown Assignment',
+ subjectColor: (subjectData.color as SubjectColor | undefined) ?? 'slate',
+ });
+ }
+ }
+ };
useFocusEffect(
useCallback(() => {
@@ -40,89 +95,207 @@ export default function ViewDetailsTask() {
}, [session, tId])
);
- const DeleteTask = async (tId: string) => {
+ const DeleteTask = async (taskId: string) => {
Alert.alert(
- "Delete Task",
- "Are you sure you want to delete this task?",
+ 'Delete Task',
+ 'Are you sure you want to delete this task?',
[
{
- text: "Cancel",
- style: "cancel"
+ text: 'Cancel',
+ style: 'cancel',
},
{
- text: "Delete",
- style: "destructive",
+ text: 'Delete',
+ style: 'destructive',
onPress: async () => {
- const { error } = await supabase.from("tasks").delete().eq("tId", tId);
+ const { error } = await supabase
+ .from('tasks')
+ .delete()
+ .eq('tId', taskId);
if (error) {
- Alert.alert("Task could not be deleted, please try again");
+ Alert.alert('Task could not be deleted, please try again');
return;
}
- Alert.alert("Task deleted successfully!");
-
const aId = task?.aId;
if (aId) {
try {
await CheckAssignmentCompletion(aId);
} catch {
- Alert.alert("Failed to update assignment completion state");
+ Alert.alert('Failed to update assignment completion state');
}
}
+ Alert.alert('Task deleted successfully!');
router.back();
- }
- }
+ },
+ },
]
- )
+ );
+ };
+
+ const colorSet = getSubjectColorSet(contextMeta.subjectColor);
+
+ if (!task) {
+ return (
+
+ (
+ await supabase.auth.signOut()}
+ >
+
+ Logout
+
+
+ ),
+ }}
+ />
+
+
+
+ Task not found
+
+
+ The task could not be loaded.
+
+
+ router.back()}
+ >
+
+ Go back
+
+
+
+
+ );
}
+ const isOwner = session?.user.id === task.uId;
+
return (
-
+
{
- return (
-
-
-
- )
- },
- headerRight: () => {
- return (
-
- await supabase.auth.signOut()} />
-
- )
- },
+ title: 'Task Details',
+ headerRight: () => (
+ await supabase.auth.signOut()}
+ >
+
+ Logout
+
+
+ ),
}}
/>
- {!task && (
-
- Task not found
-
- )}
-
- {task && (
-
- {task.title}
- {task.description}
-
- {task.isCompleted && ✓}
+
+
+
+ {task.isCompleted && (
+ ✓
+ )}
- {task.lastChanged}
-
- router.push({pathname: "/task/editTask", params: { tId: task.tId }})} />
- DeleteTask(task.tId)} />
+
+
+ {task.title}
+
+
+ {task.description ? (
+
+ {task.description}
+
+ ) : (
+
+ No description added.
+
+ )}
+
+
+
+
+ {contextMeta.subjectTitle}
+
+
+
+
+
+ {contextMeta.assignmentTitle}
+
+
+
+
+
+ Last changed: {formatDateTime(task.lastChanged)}
+
- )}
+
+ {isOwner && (
+
+
+ router.push({
+ pathname: '/task/editTask',
+ params: { tId: task.tId },
+ })
+ }
+ >
+
+ Edit
+
+
+
+ DeleteTask(task.tId)}
+ >
+
+ Delete
+
+
+
+ )}
+
);
}
\ No newline at end of file
diff --git a/lib/date.ts b/lib/date.ts
new file mode 100644
index 0000000..a21a5fb
--- /dev/null
+++ b/lib/date.ts
@@ -0,0 +1,29 @@
+export const formatDate = (value?: string | null) => {
+ if (!value) return 'No date';
+
+ const date = new Date(value);
+
+ if (Number.isNaN(date.getTime())) return value;
+
+ return date.toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+};
+
+export const formatDateTime = (value?: string | null) => {
+ if (!value) return 'Unknown';
+
+ const date = new Date(value);
+
+ if (Number.isNaN(date.getTime())) return value;
+
+ return date.toLocaleString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
+ });
+};
\ No newline at end of file
diff --git a/lib/subjectColors.ts b/lib/subjectColors.ts
new file mode 100644
index 0000000..1ffe774
--- /dev/null
+++ b/lib/subjectColors.ts
@@ -0,0 +1,58 @@
+export type SubjectColor =
+ | 'blue'
+ | 'emerald'
+ | 'amber'
+ | 'violet'
+ | 'cyan'
+ | 'rose'
+ | 'slate';
+
+export const SUBJECT_COLORS: Record<
+ SubjectColor,
+ { soft: string; strong: string; label: string }
+> = {
+ blue: {
+ soft: '#DCEFF5',
+ strong: '#2F6F88',
+ label: 'Blue',
+ },
+ emerald: {
+ soft: '#DDEFE5',
+ strong: '#2F7D55',
+ label: 'Emerald',
+ },
+ amber: {
+ soft: '#F6E8C6',
+ strong: '#9A6A16',
+ label: 'Amber',
+ },
+ violet: {
+ soft: '#E9E2F5',
+ strong: '#6D4BA3',
+ label: 'Violet',
+ },
+ cyan: {
+ soft: '#DDF0EF',
+ strong: '#287C7A',
+ label: 'Cyan',
+ },
+ rose: {
+ soft: '#F4E1DF',
+ strong: '#9B4A43',
+ label: 'Rose',
+ },
+ slate: {
+ soft: '#E8E4DA',
+ strong: '#52616B',
+ label: 'Slate',
+ },
+};
+
+export const SUBJECT_COLOR_KEYS = Object.keys(
+ SUBJECT_COLORS
+) as SubjectColor[];
+
+export const getSubjectColorSet = (color?: SubjectColor) => {
+ const colorKey: SubjectColor = color ?? 'slate';
+ return SUBJECT_COLORS[colorKey];
+};
\ No newline at end of file
diff --git a/lib/types.ts b/lib/types.ts
index 683707d..e1bafb6 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -1,3 +1,5 @@
+import type { SubjectColor } from '@/lib/subjectColors';
+
export type Task = {
tId: string;
title: string;
@@ -26,4 +28,5 @@ export type Subject = {
isActive: boolean;
lastChanged: string;
uId: string;
+ color?: SubjectColor;
};