8.7 KiB
8.7 KiB
MedMinder - Agent Guide
Project Overview
MedMinder is a medication reminder PWA (Progressive Web App) built with React 19, TypeScript, and Vite. It uses Appwrite for backend (auth, database, messaging) and Firebase Cloud Messaging for push notifications.
Key Characteristics:
- Passwordless magic-link authentication via Appwrite
- Brazilian Portuguese UI (all user-facing text is in pt-BR)
- PWA with offline support via Workbox
- Push notifications for medication reminders
- Mobile-first design with bottom navigation
Essential Commands
# Development
npm run dev # Start Vite dev server (includes PWA in dev mode)
# Build & Deploy
npm run build # TypeScript compile + Vite build (outputs to dist/)
npm run preview # Preview production build locally
# Linting
npm run lint # ESLint with TypeScript, React Hooks, React Refresh rules
# Task (go-task)
task run-dev # Same as npm run dev
task build # Same as npm run build
task zip-functions # Package Appwrite functions for deployment
Project Structure
src/
├── components/
│ ├── ui/ # shadcn/ui components (button, card, dialog, etc.)
│ └── layout/ # AppLayout, BottomNav
├── hooks/
│ └── useAuth.ts # Authentication state hook
├── lib/
│ ├── appwrite.ts # Appwrite client + collection constants
│ ├── auth.ts # Magic link auth helpers
│ ├── medications.ts # Medication CRUD + types + scheduling logic
│ ├── notifications.ts # FCM push token registration
│ ├── profiles.ts # User profile management
│ └── utils.ts # cn() helper for Tailwind classes
├── pages/
│ ├── Login.tsx # Magic link login
│ ├── AuthCallback.tsx # OTP verification
│ ├── Dashboard.tsx # Today's medication schedule
│ ├── Medications.tsx # Medication management
│ └── Settings.tsx # Theme, notifications, copy supplements
├── sw.ts # Service worker (Workbox + FCM)
├── App.tsx # Routes + protected route wrapper
└── main.tsx # React root + PWA registration
functions/
├── send-reminders/ # Appwrite Function: cron job for push notifications
└── copy-medications/ # Appwrite Function: copy meds between users
Architecture & Data Flow
Authentication Flow
- User enters email on Login page
sendEmailOTP()creates Appwrite email token- User receives magic link with OTP
verifyEmailOTP()creates sessionuseAuthhook maintains session stateProtectedRoutecomponent guards authenticated routes
Medication Scheduling
- Medications stored in Appwrite with: name, dosage, frequency, times[], startDate, durationDays, timezone
isScheduledToday()determines if medication is due based on frequency and date rangeupdateNotificationCache()writes denormalized medication data to cache collection for the reminder function
Push Notification Flow
- User enables notifications →
requestAndRegisterPushToken() - FCM token registered with Appwrite via
account.createPushTarget() send-remindersfunction runs every minute (cron:* * * * *)- Function reads cache collection, checks local time per user timezone
- If medication time matches and not already taken →
messaging.createPush() - FCM delivers to browser, service worker handles background/foreground
Copy Medications Feature
- User enters target email in Settings
copy-medicationsfunction resolves email → userId via profiles collection- Copies all medications from current user to target user
- Resets startDate to today
Code Patterns & Conventions
shadcn/ui Components
- Uses
@base-ui/reactprimitives (not Radix) - Style: "base-nova" (defined in components.json)
- All UI components in
src/components/ui/ - Import pattern:
import { Button } from '@/components/ui/button'
Styling
- Tailwind CSS v4 with
@theme inlinefor CSS variables - Font: Geist Variable
- Color scheme: Neutral base with OKLCH colors
- Dark mode:
next-themeswithattribute="class" - Container width:
w-[85%] mx-autofor mobile-first content
TypeScript Patterns
- Types defined alongside functions in lib files (e.g.,
Medication,MedicationLogin medications.ts) - Appwrite documents cast with
as unknown as Typepattern - React 19 types (no explicit
React.FCneeded)
Error Handling
- Toast notifications via
sonnerfor user feedback - Portuguese error messages
- Try/catch with graceful degradation (e.g., Firebase not configured)
Date/Time Handling
- All dates stored as ISO strings
todayString()returnsYYYY-MM-DDin local time- Timezone stored per medication for accurate reminder scheduling
- Server function converts UTC to user's local timezone for comparisons
Appwrite Collections Schema
medications
name: stringdosage: stringfrequency: enum ('once_daily' | 'twice_daily' | 'three_daily' | 'weekly' | 'as_needed')times: string[] (HH:MM format)startDate: string (YYYY-MM-DD)durationDays: number | nulltimezone: string (IANA timezone, e.g., 'America/Sao_Paulo')userId: string
logs (dose confirmations)
medicationId: stringuserId: stringtakenAt: string (ISO timestamp)scheduledDate: string (YYYY-MM-DD)scheduledTime: string | undefined (HH:MM)
profiles
userId: string (document ID)email: string (lowercase, for lookup)
cache
userId: string (document ID)data: string (JSON array of medications)
Permissions: All collections use row-level security. Create/Read for Users, Update/Delete for document owner only.
Environment Variables
All required env vars are in .env.example:
Appwrite:
VITE_APPWRITE_ENDPOINTVITE_APPWRITE_PROJECT_IDVITE_APPWRITE_DATABASE_IDVITE_APPWRITE_MEDICATIONS_COLLECTION_IDVITE_APPWRITE_LOGS_COLLECTION_IDVITE_APPWRITE_PROFILES_COLLECTION_IDVITE_APPWRITE_CACHE_COLLECTION_IDVITE_APPWRITE_COPY_FUNCTION_IDVITE_APPWRITE_FCM_PROVIDER_ID
Firebase (for push notifications):
VITE_FIREBASE_API_KEYVITE_FIREBASE_AUTH_DOMAINVITE_FIREBASE_PROJECT_IDVITE_FIREBASE_STORAGE_BUCKETVITE_FIREBASE_MESSAGING_SENDER_IDVITE_FIREBASE_APP_IDVITE_FIREBASE_VAPID_KEY
Build:
BASE_URL: Set to/medminder/for GitHub Pages deployment
Important Gotchas
Service Worker & Firebase
sw.tsis the custom service worker using Workbox injectManifest strategy- Firebase is conditionally initialized (checks for API key presence)
- Background messages: Only shows notification for data-only payloads (no
notificationfield) to avoid duplicates - Foreground messages: Handled in App.tsx via
listenForegroundMessages(), displays toast
Notification Cache
- Every medication mutation (create/update/delete) calls
updateNotificationCache() - Cache is a JSON string in the cache collection for fast reads by the reminder function
- Cache updates are fire-and-forget (errors logged but not blocking)
Copy Medications Permissions
- When copying, permissions must include both
fromUserId(for the write to succeed under row security) andtoUserId(for access) - See
copyMedications()in medications.ts for the permission pattern
Timezone Handling
- Reminder function uses
Intl.DateTimeFormatwith user's timezone to get local time - Sleeping hours filter hardcoded for America/Sao_Paulo (11 PM - 6 AM local = 2 AM - 9 AM UTC)
- If expanding to other timezones, this needs to be made configurable
Push Target Storage
- Push target ID stored in localStorage (
medminder_push_target_id) - On token refresh, attempts to update existing target before creating new one
PWA in Development
vite-plugin-pwahasdevOptions.enabled: trueso service worker runs in dev- TypeScript types for virtual modules come from
vite-plugin-pwa/client
Testing Considerations
- No test framework currently configured
- For manual testing: Use browser DevTools → Application → Service Workers to inspect SW
- Push notifications require HTTPS or localhost
- Firebase config can be omitted for development (notifications gracefully disabled)
Deployment
GitHub Pages (configured in .github/workflows/deploy.yml):
- Triggered on push to main
- Requires all env vars as GitHub Secrets
- Outputs to
dist/with.nojekyllfor proper SPA routing - Base URL set to
/medminder/
Appwrite Functions:
- Must be manually packaged with
task zip-functionsand uploaded to Appwrite Console send-remindersrequires cron schedule:* * * * *- Both functions need env vars from
.env.example(withoutVITE_prefix)