refactor(frontend): modularize App.tsx into components, pages, hooks, and context (#60)

- Extract App.tsx from 764 lines to ~404 lines
- Create reusable components: MedDetailModal, MobileEditModal, ShareDialog, etc.
- Add AppContext for global state management
- Split pages: DashboardPage, MedicationsPage, SchedulePage, SettingsPage, PlannerPage
- Create custom hooks: useAuth, useMedications, useSettings, useDoses, useSchedule
- Add utility functions in separate modules
- Fix stock status logic (>30 days = green/normal)
- Fix reminder threshold calculation (use reminderDaysBefore not lowStockDays)
- Fix takenBy validation (send [] instead of null)
- Fix datetime format for blister start times (add Z suffix)
- Style 'All OK' status as green/bold

BREAKING: None - all existing functionality preserved
This commit is contained in:
Daniel Volz
2026-01-22 05:38:34 +01:00
committed by GitHub
parent 89edd74de3
commit 8718311876
44 changed files with 7448 additions and 5139 deletions
+72
View File
@@ -0,0 +1,72 @@
// =============================================================================
// ICS Calendar Generation
// =============================================================================
import type { Medication } from "../types";
/**
* Format a Date for ICS format (YYYYMMDDTHHMMSSZ)
*/
function formatICSDate(date: Date): string {
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
}
/**
* Generate and download an ICS calendar file for a medication's schedule
*/
export function generateICS(med: Medication): void {
const events = med.blisters
.map((blister, idx) => {
const start = new Date(blister.start);
const end = new Date(start.getTime() + 15 * 60 * 1000); // 15 min duration
const interval = blister.every;
const pillInfo = `${blister.usage} pill${blister.usage !== 1 ? "s" : ""}${med.pillWeightMg ? ` (${blister.usage * med.pillWeightMg} mg)` : ""}`;
const summary = `💊 ${med.name} - ${pillInfo}`;
const description = [
`Medication: ${med.name}`,
med.genericName ? `Generic: ${med.genericName}` : "",
med.takenBy && med.takenBy.length > 0 ? `For: ${med.takenBy.join(", ")}` : "",
`Dosage: ${pillInfo}`,
`Frequency: every ${interval} day${interval !== 1 ? "s" : ""}`,
med.notes ? `Notes: ${med.notes}` : ""
]
.filter(Boolean)
.join("\\n");
return `BEGIN:VEVENT
UID:medassist-ng-${med.id}-${idx}@medassist-ng
DTSTAMP:${formatICSDate(new Date())}
DTSTART:${formatICSDate(start)}
DTEND:${formatICSDate(end)}
RRULE:FREQ=DAILY;INTERVAL=${interval}
SUMMARY:${summary}
DESCRIPTION:${description}
BEGIN:VALARM
TRIGGER:-PT5M
ACTION:DISPLAY
DESCRIPTION:Time to take ${med.name}
END:VALARM
END:VEVENT`;
})
.join("\n");
const ics = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//MedAssist-ng//Medication Schedule//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:${med.name} Schedule
${events}
END:VCALENDAR`;
const blob = new Blob([ics], { type: "text/calendar;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${med.name.replace(/[^a-zA-Z0-9]/g, "_")}_schedule.ics`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}