Update app and ignore local artifacts
This commit is contained in:
36
lib/api.ts
Normal file
36
lib/api.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export type AttendanceStatus = 'pending' | 'attended' | 'absent' | 'cancelled';
|
||||
export type Session = { id: number; subjectId: number; date: string; time: string; endTime: string; title: string; code: string; room: string; color: string; classType: string; status: AttendanceStatus };
|
||||
|
||||
// Set EXPO_PUBLIC_API_URL to your computer's LAN address when testing on a phone,
|
||||
// e.g. EXPO_PUBLIC_API_URL=http://192.168.1.20:4000/api/v1
|
||||
const API_URL = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1';
|
||||
|
||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_URL}${path}`, { headers: { 'Content-Type': 'application/json', ...(options?.headers ?? {}) }, ...options });
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
throw new Error(body.error ?? 'Unable to reach College Kit');
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export type Profile = { id: number; name: string; initials: string; college: string; programme: string; semester: string; lectureMinutes: number; recessEnabled: boolean; recessStart: string; recessEnd: string; weekendSchedule: boolean };
|
||||
export type Subject = { id: number; name: string; code: string; shortName: string; color: string; classType: string; defaultRoom: string };
|
||||
|
||||
export const collegeApi = {
|
||||
profile() { return request<Profile>('/profile'); },
|
||||
updateProfile(data: { name: string; college: string; programme?: string; semester?: string }) { return request<Profile>('/profile', { method: 'PUT', body: JSON.stringify(data) }); },
|
||||
updateSettings(data: { lectureMinutes: number; recessEnabled: boolean; recessStart: string; recessEnd: string; weekendSchedule: boolean }) { return request<{ lectureMinutes: number; recessEnabled: boolean; recessStart: string; recessEnd: string; weekendSchedule: boolean }>('/settings', { method: 'PATCH', body: JSON.stringify(data) }); },
|
||||
subjects() { return request<Subject[]>('/subjects'); },
|
||||
createSubject(data: { name: string; code: string; shortName?: string; color?: string; classType?: string; defaultRoom?: string }) { return request<{ id: number }>('/subjects', { method: 'POST', body: JSON.stringify(data) }); },
|
||||
subject(id: number) { return request<{ id: number; name: string; code: string; shortName: string; color: string; classType: string; defaultRoom: string; summary: { total: number; attended: number; absent: number; percentage: number }; sessions: Array<{ id: number; date: string; time: string; endTime: string; room: string; status: AttendanceStatus }> }>(`/subjects/${id}`); },
|
||||
updateSubject(id: number, data: { name: string; code: string; shortName: string; color?: string; classType?: string; defaultRoom?: string }) { return request<{ id: number }>(`/subjects/${id}`, { method: 'PATCH', body: JSON.stringify(data) }); },
|
||||
schedule(date: string) { return request<{ date: string; sessions: Session[] }>(`/schedule?date=${date}`); },
|
||||
markAttendance(id: number, status: AttendanceStatus) { return request<{ id: number; status: AttendanceStatus }>(`/schedule/${id}/attendance`, { method: 'PUT', body: JSON.stringify({ status }) }); },
|
||||
removeClass(id: number) { return request<{ id: number; deleted: boolean }>(`/schedule/${id}`, { method: 'DELETE' }); },
|
||||
addOneOffClass(data: { subjectId: number; date: string; startTime: string; endTime: string; room: string }) { return request<{ id: number }>('/schedule', { method: 'POST', body: JSON.stringify(data) }); },
|
||||
attendanceSummary() { return request<{ subjects: Array<{ id: number; name: string; code: string; shortName?: string; color: string; total: number; attended: number; absent: number; cancelled: number; percentage: number }> }>('/attendance/summary'); },
|
||||
timetable(date: string) { return request<{ date: string; classes: Array<{ id: number; weekday: number; startTime: string; endTime: string; room: string; subjectId: number; subjectName: string; code: string; color: string; classType: string; effectiveFrom: string; effectiveTo: string | null }> }>(`/timetable/classes?date=${date}`); },
|
||||
updateTimetableClass(id: number, data: { subjectId?: number; weekday?: number; startTime?: string; endTime?: string; room?: string; effectiveFrom?: string }) { return request<{ id: number; replacesId: number; effectiveFrom: string }>(`/timetable/classes/${id}`, { method: 'PATCH', body: JSON.stringify(data) }); },
|
||||
addTimetableClass(data: { subjectId: number; weekday: number; startTime: string; endTime: string; room: string; effectiveFrom?: string }) { return request<{ id: number }>('/timetable/classes', { method: 'POST', body: JSON.stringify(data) }); },
|
||||
};
|
||||
34
lib/date.ts
Normal file
34
lib/date.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const dateKey = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
|
||||
export const addDays = (date: Date, amount: number) => {
|
||||
const next = new Date(date);
|
||||
next.setDate(next.getDate() + amount);
|
||||
return next;
|
||||
};
|
||||
|
||||
export const mondayOfWeek = (date: Date) => addDays(date, -((date.getDay() + 6) % 7));
|
||||
|
||||
export const dateFromKey = (value: string) => new Date(`${value}T12:00:00`);
|
||||
|
||||
export const formatDayHeading = (date: Date) => new Intl.DateTimeFormat(undefined, { weekday: 'long', month: 'long', day: 'numeric' }).format(date);
|
||||
|
||||
export const formatMonthDay = (date: Date) => new Intl.DateTimeFormat(undefined, { month: 'long', day: 'numeric' }).format(date);
|
||||
|
||||
export const formatSessionDate = (value: string) => new Intl.DateTimeFormat(undefined, { weekday: 'short', month: 'short', day: 'numeric' }).format(dateFromKey(value));
|
||||
|
||||
export const currentMinutes = () => {
|
||||
const now = new Date();
|
||||
return now.getHours() * 60 + now.getMinutes();
|
||||
};
|
||||
|
||||
export const timeToMinutes = (value: string) => {
|
||||
const [hours, minutes] = value.split(':').map(Number);
|
||||
return Number.isFinite(hours) && Number.isFinite(minutes) ? hours * 60 + minutes : -1;
|
||||
};
|
||||
|
||||
export const greetingFor = (date = new Date()) => {
|
||||
const hour = date.getHours();
|
||||
if (hour < 12) return 'Good morning';
|
||||
if (hour < 18) return 'Good afternoon';
|
||||
return 'Good evening';
|
||||
};
|
||||
31
lib/design.ts
Normal file
31
lib/design.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { colors, type SemanticTone, type SubjectTone } from '@/components/ui';
|
||||
|
||||
const subjectTones = Object.keys(colors.subject) as SubjectTone[];
|
||||
|
||||
const hash = (value: string | number) => {
|
||||
const input = String(value);
|
||||
let result = 0;
|
||||
for (let index = 0; index < input.length; index += 1) result = ((result << 5) - result + input.charCodeAt(index)) | 0;
|
||||
return Math.abs(result);
|
||||
};
|
||||
|
||||
/** Maps persisted subjects to one stable accessible surface/accent pair. */
|
||||
export function subjectToneFor(identity: string | number, savedColor?: string): SubjectTone {
|
||||
const normalized = savedColor?.toUpperCase();
|
||||
const matched = subjectTones.find((tone) => colors.subject[tone].accent.toUpperCase() === normalized || colors.subject[tone].surface.toUpperCase() === normalized);
|
||||
return matched ?? subjectTones[hash(identity) % subjectTones.length];
|
||||
}
|
||||
|
||||
export function attendanceTone(percentage: number, total: number, threshold = 75): SemanticTone {
|
||||
if (!total) return 'neutral';
|
||||
if (percentage >= threshold) return 'success';
|
||||
if (percentage >= threshold - 10) return 'warning';
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
export function attendanceMessage(percentage: number, total: number, threshold = 75) {
|
||||
if (!total) return { title: 'No attendance yet', message: 'Mark your first class to start tracking progress.' };
|
||||
if (percentage >= threshold + 5) return { title: 'You’re on track', message: `${percentage}% overall · Keep it above ${threshold}%.` };
|
||||
if (percentage >= threshold) return { title: 'You’re just above the target', message: `${percentage}% overall · Your target is ${threshold}%.` };
|
||||
return { title: 'Attendance needs attention', message: `${percentage}% overall · Your target is ${threshold}%.` };
|
||||
}
|
||||
Reference in New Issue
Block a user