Final push before system formatting
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import type { SessionType } from '@/lib/types';
|
||||
|
||||
const notificationKey = (aId: string) => `assignment_notification_${aId}`;
|
||||
const setupSprintDemoKey = (userId: string) => `setup_sprint_demo_${userId}`;
|
||||
const activeSprintKey = 'active_sprint';
|
||||
const studyCycleKey = 'study_cycle';
|
||||
|
||||
export type ActiveSession = {
|
||||
sessionId: string;
|
||||
sessionType: SessionType;
|
||||
taskId: string | null;
|
||||
returnTaskId?: string | null;
|
||||
durationSeconds: number;
|
||||
endTime: number;
|
||||
};
|
||||
|
||||
export type StudyCycle = {
|
||||
taskId: string;
|
||||
completedFocusSessions: number;
|
||||
lastCompletedSessionType: SessionType;
|
||||
lastCompletedAt: number;
|
||||
};
|
||||
|
||||
export async function SaveAssignmentNotificationId(aId: string, notificationId: string) {
|
||||
await AsyncStorage.setItem(notificationKey(aId), notificationId);
|
||||
}
|
||||
|
||||
export async function GetAssignmentNotificationId(aId: string) {
|
||||
return await AsyncStorage.getItem(notificationKey(aId));
|
||||
}
|
||||
|
||||
export async function RemoveAssignmentNotificationId(aId: string) {
|
||||
await AsyncStorage.removeItem(notificationKey(aId));
|
||||
}
|
||||
|
||||
export async function SaveActiveSession(activeSession: ActiveSession) {
|
||||
await AsyncStorage.setItem(activeSprintKey, JSON.stringify(activeSession));
|
||||
}
|
||||
|
||||
export async function GetActiveSession() {
|
||||
const activeSession = await AsyncStorage.getItem(activeSprintKey);
|
||||
|
||||
if (!activeSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(activeSession) as ActiveSession;
|
||||
}
|
||||
|
||||
export async function RemoveActiveSession() {
|
||||
await AsyncStorage.removeItem(activeSprintKey);
|
||||
}
|
||||
|
||||
export async function SaveStudyCycle(studyCycle: StudyCycle) {
|
||||
await AsyncStorage.setItem(studyCycleKey, JSON.stringify(studyCycle));
|
||||
}
|
||||
|
||||
export async function GetStudyCycle() {
|
||||
const studyCycle = await AsyncStorage.getItem(studyCycleKey);
|
||||
|
||||
if (!studyCycle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(studyCycle) as StudyCycle;
|
||||
}
|
||||
|
||||
export async function RemoveStudyCycle() {
|
||||
await AsyncStorage.removeItem(studyCycleKey);
|
||||
}
|
||||
|
||||
export async function GetSetupSprintDemoUsed(userId: string) {
|
||||
const value = await AsyncStorage.getItem(setupSprintDemoKey(userId));
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
export async function SaveSetupSprintDemoUsed(userId: string) {
|
||||
await AsyncStorage.setItem(setupSprintDemoKey(userId), 'true');
|
||||
}
|
||||
29
AppDev/ikt205_2026_18_study_sprint/source/lib/date.ts
Normal file
29
AppDev/ikt205_2026_18_study_sprint/source/lib/date.ts
Normal file
@@ -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',
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: true,
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
}),
|
||||
});
|
||||
|
||||
function HandleRegistrationError(errorMessage: string) {
|
||||
alert(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
export async function RegisterForLocalNotificationsAsync() {
|
||||
if (Platform.OS === 'android') {
|
||||
await Notifications.setNotificationChannelAsync('default', {
|
||||
name: 'default',
|
||||
importance: Notifications.AndroidImportance.MAX
|
||||
});
|
||||
}
|
||||
|
||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||
|
||||
let finalStatus = existingStatus;
|
||||
|
||||
if (existingStatus !== 'granted') {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
finalStatus = status;
|
||||
}
|
||||
|
||||
if (finalStatus !== 'granted') {
|
||||
HandleRegistrationError('Permission not granted for local notifications');
|
||||
return;
|
||||
}
|
||||
}
|
||||
15
AppDev/ikt205_2026_18_study_sprint/source/lib/progress.ts
Normal file
15
AppDev/ikt205_2026_18_study_sprint/source/lib/progress.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const DEFAULT_FOCUS_DURATION_MINUTES = 25;
|
||||
export const DEFAULT_SHORT_BREAK_DURATION_MINUTES = 5;
|
||||
export const DEFAULT_LONG_BREAK_DURATION_MINUTES = 15;
|
||||
export const FOCUS_SESSIONS_PER_LONG_BREAK = 4;
|
||||
export const STUDY_CYCLE_IDLE_RESET_MINUTES = 120;
|
||||
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
GetActiveSession,
|
||||
RemoveActiveSession,
|
||||
RemoveStudyCycle,
|
||||
type ActiveSession,
|
||||
} from '@/lib/asyncStorage';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
export type FinalSessionStatus = 'completed' | 'cancelled' | 'expired';
|
||||
|
||||
export async function finalizeStoredSession(
|
||||
finalStatus: FinalSessionStatus,
|
||||
activeSessionOverride?: ActiveSession | null
|
||||
) {
|
||||
const activeSession = activeSessionOverride ?? await GetActiveSession();
|
||||
|
||||
if (!activeSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await RemoveActiveSession();
|
||||
|
||||
if (finalStatus !== 'completed') {
|
||||
await RemoveStudyCycle();
|
||||
}
|
||||
|
||||
const { error } = await supabase.rpc('finalize_sprint_session', {
|
||||
p_session_id: activeSession.sessionId,
|
||||
p_final_status: finalStatus,
|
||||
p_ended_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
activeSession,
|
||||
error,
|
||||
};
|
||||
}
|
||||
84
AppDev/ikt205_2026_18_study_sprint/source/lib/setupStatus.ts
Normal file
84
AppDev/ikt205_2026_18_study_sprint/source/lib/setupStatus.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
export type SetupStepKey = 'subject' | 'assignment' | 'task' | 'sprint';
|
||||
|
||||
export type SetupStatus = {
|
||||
subjectId: string | null;
|
||||
assignmentId: string | null;
|
||||
taskId: string | null;
|
||||
completedFocusSessions: number;
|
||||
currentStep: SetupStepKey;
|
||||
isSetupComplete: boolean;
|
||||
};
|
||||
|
||||
export async function getSetupStatus(userId: string): Promise<SetupStatus> {
|
||||
const [subjectResult, assignmentResult, taskResult, focusSessionResult] = await Promise.all([
|
||||
supabase
|
||||
.from('subjects')
|
||||
.select('sId')
|
||||
.eq('uId', userId)
|
||||
.order('lastChanged', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle(),
|
||||
supabase
|
||||
.from('assignments')
|
||||
.select('aId')
|
||||
.eq('uId', userId)
|
||||
.order('lastChanged', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle(),
|
||||
supabase
|
||||
.from('tasks')
|
||||
.select('tId')
|
||||
.eq('uId', userId)
|
||||
.order('lastChanged', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle(),
|
||||
supabase
|
||||
.from('sprint_sessions')
|
||||
.select('sessionId', { count: 'exact', head: true })
|
||||
.eq('userId', userId)
|
||||
.eq('sessionType', 'focus')
|
||||
.eq('status', 'completed'),
|
||||
]);
|
||||
|
||||
if (subjectResult.error) {
|
||||
throw subjectResult.error;
|
||||
}
|
||||
|
||||
if (assignmentResult.error) {
|
||||
throw assignmentResult.error;
|
||||
}
|
||||
|
||||
if (taskResult.error) {
|
||||
throw taskResult.error;
|
||||
}
|
||||
|
||||
if (focusSessionResult.error) {
|
||||
throw focusSessionResult.error;
|
||||
}
|
||||
|
||||
const subjectId = subjectResult.data?.sId ?? null;
|
||||
const assignmentId = assignmentResult.data?.aId ?? null;
|
||||
const taskId = taskResult.data?.tId ?? null;
|
||||
const completedFocusSessions = focusSessionResult.count ?? 0;
|
||||
|
||||
let currentStep: SetupStepKey = 'sprint';
|
||||
|
||||
if (!subjectId) {
|
||||
currentStep = 'subject';
|
||||
} else if (!assignmentId) {
|
||||
currentStep = 'assignment';
|
||||
} else if (!taskId) {
|
||||
currentStep = 'task';
|
||||
}
|
||||
|
||||
return {
|
||||
subjectId,
|
||||
assignmentId,
|
||||
taskId,
|
||||
completedFocusSessions,
|
||||
currentStep,
|
||||
isSetupComplete: taskId !== null && completedFocusSessions > 0,
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
};
|
||||
36
AppDev/ikt205_2026_18_study_sprint/source/lib/supabase.ts
Normal file
36
AppDev/ikt205_2026_18_study_sprint/source/lib/supabase.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import 'react-native-url-polyfill/auto';
|
||||
|
||||
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!
|
||||
const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
|
||||
|
||||
if (!supabaseUrl) {
|
||||
throw new Error('Missing EXPO_PUBLIC_SUPABASE_URL');
|
||||
}
|
||||
|
||||
if (!supabaseKey) {
|
||||
throw new Error('Missing EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY');
|
||||
}
|
||||
|
||||
const SecureStoreAdapter = {
|
||||
getItem: async (key: string) => {
|
||||
return await SecureStore.getItemAsync(key);
|
||||
},
|
||||
setItem: async (key: string, value: string) => {
|
||||
await SecureStore.setItemAsync(key, value);
|
||||
},
|
||||
removeItem: async (key: string) => {
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
},
|
||||
};
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseKey, {
|
||||
auth: {
|
||||
storage: SecureStoreAdapter,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
})
|
||||
|
||||
35
AppDev/ikt205_2026_18_study_sprint/source/lib/types.ts
Normal file
35
AppDev/ikt205_2026_18_study_sprint/source/lib/types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { SubjectColor } from '@/lib/subjectColors';
|
||||
|
||||
export type SessionType = 'focus' | 'short_break' | 'long_break';
|
||||
|
||||
export type Task = {
|
||||
tId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
isCompleted: boolean;
|
||||
lastChanged: string;
|
||||
uId: string;
|
||||
aId: string;
|
||||
totalTimeInSeconds: number;
|
||||
};
|
||||
|
||||
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;
|
||||
color?: SubjectColor;
|
||||
};
|
||||
Reference in New Issue
Block a user