Notifications + AsyncStorage

This commit is contained in:
Teodor
2026-04-22 20:58:00 +02:00
parent 6b298633bc
commit 4947d012c0
20 changed files with 580 additions and 61 deletions

View File

@@ -1,12 +1,40 @@
import { supabase } from "@/lib/supabase";
import { Session } from "@supabase/supabase-js";
import { Redirect, Tabs } from "expo-router";
import * as Notifications from 'expo-notifications';
import { Redirect, router, Tabs } from "expo-router";
import { useEffect, useState } from "react";
function UseNotificationObserver() {
useEffect(() => {
function redirect(notification: Notifications.Notification) {
const aId = notification.request.content.data?.aId;
if (typeof aId === 'string') {
router.push({pathname: "/assignment/viewDetailsAssignment", params: { aId }});
}
}
const response = Notifications.getLastNotificationResponse();
if (response?.notification) {
redirect(response.notification);
}
const subscription = Notifications.addNotificationResponseReceivedListener(response => {
redirect(response.notification);
});
return () => {
subscription.remove();
};
}, []);
}
export default function TabLayout() {
const [session, SetSession] = useState<Session | null>(null)
const [loading, SetLoading] = useState(true);
UseNotificationObserver();
useEffect(() => {
const loadSession = async () => {
const { data } = await supabase.auth.getSession();
@@ -38,18 +66,6 @@ export default function TabLayout() {
<Tabs.Screen name="assignments" options={{title: "Assignments"}} />
<Tabs.Screen name="subjects" options={{title: "Subjects"}} />
<Tabs.Screen name="timer" options={{title: "Timer"}} />
<Tabs.Screen name="subject/createSubject" options={{ href: null }} />
<Tabs.Screen name="subject/editSubject" options={{ href: null }} />
<Tabs.Screen name="subject/viewDetailsSubject" options={{ href: null }} />
<Tabs.Screen name="assignment/createAssignment" options={{ href: null }} />
<Tabs.Screen name="assignment/editAssignment" options={{ href: null }} />
<Tabs.Screen name="assignment/viewDetailsAssignment" options={{ href: null }} />
<Tabs.Screen name="task/createTask" options={{ href: null }} />
<Tabs.Screen name="task/editTask" options={{ href: null }} />
<Tabs.Screen name="task/viewDetailsTask" options={{ href: null }} />
</Tabs>
);
}

View File

@@ -1,9 +1,34 @@
import { defaultStyles } from "@/constants/defaultStyles";
import { RegisterForLocalNotificationsAsync } from '@/lib/notifications';
import { supabase } from "@/lib/supabase";
import { Session } from '@supabase/supabase-js';
import { Stack } from "expo-router";
import { useEffect, useState } from 'react';
import { Button, Text, View } from "react-native";
export default function HomeScreen() {
const [session, SetSession] = useState<Session | null>(null);
useEffect(() => {
supabase.auth
.getSession()
.then(({ data }) => SetSession(data.session ?? null));
const { data: sub } = supabase.auth.onAuthStateChange(
(_event, newSession) => {
SetSession(newSession);
}
);
return () => sub.subscription.unsubscribe();
}, []);
useEffect(() => {
if (session) {
RegisterForLocalNotificationsAsync();
}
}, [session])
return (
<View style={defaultStyles.container}>
<Stack.Screen

View File

@@ -1,6 +1,15 @@
import { Stack } from "expo-router";
import '../global.css';
import "../global.css";
export default function RootLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="subject" options={{ headerShown: false }} />
<Stack.Screen name="assignment" options={{ headerShown: false }} />
<Stack.Screen name="task" options={{ headerShown: false }} />
<Stack.Screen name="createUser" options={{ headerShown: false }} />
<Stack.Screen name="login" options={{ headerShown: false }} />
</Stack>
);
}

View File

@@ -0,0 +1,11 @@
import { Stack } from "expo-router";
export default function AssignmentLayout() {
return (
<Stack>
<Stack.Screen name="createAssignment" options={{ title: "Create Assignment" }} />
<Stack.Screen name="editAssignment" options={{ title: "Edit Assignment" }} />
<Stack.Screen name="viewDetailsAssignment" options={{ title: "Assignment Details" }} />
</Stack>
);
}

View File

@@ -1,5 +1,7 @@
import { defaultStyles } from '@/constants/defaultStyles';
import * as AsyncStorage from '@/lib/asyncStorage';
import { supabase } from '@/lib/supabase';
import * as Notifications from 'expo-notifications';
import { router, Stack, useLocalSearchParams } from 'expo-router';
import { useState } from 'react';
import {
@@ -18,37 +20,62 @@ import {
export default function CreateAssignment() {
const sId = (useLocalSearchParams().sId as string) ?? null;
const [title, SetTitle] = useState('');
const [description, SetDescription] = useState('');
const [deadline, SetDeadline] = useState('');
const [isCompleted, SetIsCompleted] = useState(false);
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 CreateAssignment = async () => {
if (title.trim() === '') {
Alert.alert('Title is required!');
return;
}
const { data, error: userError } = await supabase.auth.getUser();
const { data: userData, error: userError } = await supabase.auth.getUser();
if (userError || !data.user) {
if (userError || !userData.user) {
router.replace('../createUser');
return;
}
SetIsSaving(true);
const { error: dbError } = await supabase.from('assignments').insert({
const { data: assignmentData, error: dbError } = await supabase.from('assignments').insert({
title: title.trim(),
description: description.trim(),
deadline: deadline.trim(),
isCompleted,
lastChanged: new Date().toISOString(),
uId: data.user.id,
uId: userData.user.id,
sId,
});
})
.select()
.single();
if (dbError) {
SetIsSaving(false);
@@ -58,6 +85,14 @@ export default function CreateAssignment() {
Alert.alert('Assignment successfully created!');
if (!isCompleted && assignmentData) {
const nId = await ScheduleDeadlineReminder(assignmentData.aId, assignmentData.title, assignmentData.deadline);
if (nId) {
await AsyncStorage.SaveAssignmentNotificationId(assignmentData.aId, nId);
}
}
SetTitle('');
SetDescription('');
SetDeadline('');

View File

@@ -1,5 +1,7 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { GetAssignmentNotificationId, RemoveAssignmentNotificationId, SaveAssignmentNotificationId } from '@/lib/asyncStorage';
import { supabase } from '@/lib/supabase';
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';
@@ -20,6 +22,39 @@ export default function EditAssignment() {
const [assignment, SetAssignment] = useState<Assignment | null>(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();
@@ -47,24 +82,27 @@ export default function EditAssignment() {
return;
}
const { data, error: userError } = await supabase.auth.getUser();
const { data: userData, error: userError } = await supabase.auth.getUser();
if(userError || !data.user) {
if(userError || !userData.user) {
router.replace("../createUser");
return;
}
SetIsSaving(true);
const { error: dbError } = await supabase.from("assignments").update({
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: data.user.id,
uId: userData.user.id,
sId: assignment.sId,
}).eq("aId", aId);
})
.eq("aId", aId)
.select()
.single();
SetIsSaving(false);
@@ -75,6 +113,18 @@ export default function EditAssignment() {
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);
}
}
}
router.back();
}

11
app/subject/_layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Stack } from "expo-router";
export default function SubjectLayout() {
return (
<Stack>
<Stack.Screen name="createSubject" options={{ title: "Create Subject" }} />
<Stack.Screen name="editSubject" options={{ title: "Edit Subject" }} />
<Stack.Screen name="viewDetailsSubject" options={{ title: "Subject Details" }} />
</Stack>
);
}

11
app/task/_layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { Stack } from "expo-router";
export default function TaskLayout() {
return (
<Stack>
<Stack.Screen name="createTask" options={{ title: "Create Task" }} />
<Stack.Screen name="editTask" options={{ title: "Edit Task" }} />
<Stack.Screen name="viewDetailsTask" options={{ title: "Task Details" }} />
</Stack>
);
}