Redesign: Warm Sanctuary aesthetic for core pages

- Implement cohesive 'Warm Sanctuary' design system
- Add Playfair Display + Source Sans 3 typography
- Create paper texture background and warm color palette
- Redesign Today Dashboard with elegant cards and animations
- Redesign Medication Form with step-by-step visual flow
- Redesign Emergency Card with clear visual hierarchy
- Redesign Onboarding with floating blobs and welcoming feel
- Update Tailwind config with new colors, shadows, and animations
This commit is contained in:
Gemini Agent
2026-03-01 07:06:58 +00:00
parent a5181cf6fe
commit 065250c1cf
8 changed files with 1078 additions and 552 deletions

View File

@@ -1,18 +1,23 @@
'use client' 'use client'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ArrowLeft, Edit2 } from 'lucide-react' import { ArrowLeft, Edit2, Heart } from 'lucide-react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { db } from '@/lib/sync' import { db } from '@/lib/sync'
import { EmergencyCard } from '@/components/emergency/EmergencyCard' import { EmergencyCard } from '@/components/emergency/EmergencyCard'
import { Button, LoadingState } from '@/components/ui' import { LoadingState } from '@/components/ui'
import { useApp } from '../provider' import { useApp } from '../provider'
export default function EmergencyPage() { export default function EmergencyPage() {
const router = useRouter() const router = useRouter()
const { currentWorkspace } = useApp() const { currentWorkspace } = useApp()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// Fetch workspace from IndexedDB for offline access // Fetch workspace from IndexedDB for offline access
const workspace = useLiveQuery( const workspace = useLiveQuery(
@@ -32,7 +37,11 @@ export default function EmergencyPage() {
) )
if (!workspace) { if (!workspace) {
return <LoadingState message="Loading emergency info..." /> return (
<div className="min-h-screen paper-texture flex items-center justify-center">
<LoadingState message="Loading emergency info..." />
</div>
)
} }
const emergencyInfo = { const emergencyInfo = {
@@ -56,58 +65,74 @@ export default function EmergencyPage() {
})) || [] })) || []
return ( return (
<div className="min-h-screen bg-red-50"> <div className={`min-h-screen paper-texture transition-opacity duration-500 ${mounted ? 'opacity-100' : 'opacity-0'}`}>
{/* Header */} {/* Header */}
<div className="bg-red-600 text-white safe-top"> <div className="bg-gradient-to-r from-alert-500 to-alert-600 text-white safe-area-top sticky top-0 z-10">
<div className="flex items-center justify-between px-4 py-3"> <div className="flex items-center justify-between px-6 py-4">
<button <button
onClick={() => router.back()} onClick={() => router.back()}
className="flex items-center gap-2 text-white/90 hover:text-white" className="flex items-center gap-2 text-white/90 hover:text-white transition-colors"
> >
<ArrowLeft className="w-5 h-5" /> <div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center">
<span>Back</span> <ArrowLeft className="w-5 h-5" />
</div>
<span className="font-medium">Back</span>
</button> </button>
{currentWorkspace.role !== 'VIEWER' && ( {currentWorkspace.role !== 'VIEWER' && (
<button <button
onClick={() => router.push('/settings/emergency')} onClick={() => router.push('/settings/emergency')}
className="flex items-center gap-2 text-white/90 hover:text-white" className="flex items-center gap-2 text-white/90 hover:text-white transition-colors"
> >
<Edit2 className="w-4 h-4" /> <span className="font-medium">Edit</span>
<span>Edit</span> <div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center">
<Edit2 className="w-5 h-5" />
</div>
</button> </button>
)} )}
</div> </div>
</div> </div>
<div className="p-4"> <div className="p-6 pb-24">
{hasInfo ? ( {hasInfo ? (
<EmergencyCard info={emergencyInfo} medications={medsList} /> <div className="animate-fade-up">
<EmergencyCard info={emergencyInfo} medications={medsList} />
</div>
) : ( ) : (
<div className="text-center py-12"> <div className="section-warm text-center py-12 animate-fade-up">
<div className="w-16 h-16 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4"> <div className="w-20 h-20 rounded-full bg-alert-100 flex items-center justify-center mx-auto mb-6">
<Edit2 className="w-8 h-8 text-red-400" /> <Heart className="w-10 h-10 text-alert-400" />
</div> </div>
<h2 className="text-lg font-semibold text-secondary-900 mb-2">
<h2 className="font-display text-2xl text-secondary-900 mb-3">
No Emergency Info Set No Emergency Info Set
</h2> </h2>
<p className="text-secondary-600 mb-4">
Add important medical information for emergencies. <p className="text-secondary-600 mb-8 max-w-sm mx-auto">
Add important medical information that could be crucial in an emergency situation.
</p> </p>
{currentWorkspace.role !== 'VIEWER' && ( {currentWorkspace.role !== 'VIEWER' && (
<Button onClick={() => router.push('/settings/emergency')}> <button
onClick={() => router.push('/settings/emergency')}
className="btn-primary"
>
Add Emergency Info Add Emergency Info
</Button> </button>
)} )}
</div> </div>
)} )}
</div> </div>
{/* Offline indicator */} {/* Offline indicator */}
<div className="fixed bottom-4 left-4 right-4"> <div className="fixed bottom-6 left-6 right-6">
<div className="bg-green-100 border border-green-300 rounded-lg p-3 text-center"> <div className="bg-primary-50 border border-primary-200 rounded-card p-4 text-center shadow-elevated">
<p className="text-sm text-green-800 font-medium"> <div className="flex items-center justify-center gap-2">
This information is available offline <div className="w-2 h-2 rounded-full bg-primary-500 animate-pulse" />
</p> <p className="text-sm text-primary-700 font-medium">
This information is available offline
</p>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@ import { useEffect, useState, useCallback } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { format, isToday, isTomorrow } from 'date-fns' import { format, isToday, isTomorrow } from 'date-fns'
import { toZonedTime } from 'date-fns-tz' import { toZonedTime } from 'date-fns-tz'
import { Phone, MapPin, Clock, ChevronRight, Pill, Calendar, Plus, AlertTriangle, ClipboardCheck } from 'lucide-react' import { Phone, MapPin, Clock, ChevronRight, Pill, Calendar, Plus, AlertTriangle, ClipboardCheck, Heart } from 'lucide-react'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { db, logDose, undoDose } from '@/lib/sync' import { db, logDose, undoDose } from '@/lib/sync'
@@ -23,6 +23,11 @@ export default function TodayPage() {
const [now, setNow] = useState(() => new Date()) const [now, setNow] = useState(() => new Date())
const [quickNote, setQuickNote] = useState('') const [quickNote, setQuickNote] = useState('')
const [isAddingNote, setIsAddingNote] = useState(false) const [isAddingNote, setIsAddingNote] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// Update time every minute // Update time every minute
useEffect(() => { useEffect(() => {
@@ -149,7 +154,14 @@ export default function TodayPage() {
const date = toZonedTime(new Date(datetime), TIMEZONE) const date = toZonedTime(new Date(datetime), TIMEZONE)
if (isToday(date)) return `Today at ${format(date, 'h:mm a')}` if (isToday(date)) return `Today at ${format(date, 'h:mm a')}`
if (isTomorrow(date)) return `Tomorrow at ${format(date, 'h:mm a')}` if (isTomorrow(date)) return `Tomorrow at ${format(date, 'h:mm a')}`
return format(date, 'EEE, MMM d \'at\' h:mm a') return format(date, "EEE, MMM d 'at' h:mm a")
}
const getGreeting = () => {
const hour = now.getHours()
if (hour < 12) return 'Good morning'
if (hour < 17) return 'Good afternoon'
return 'Good evening'
} }
if (!appointments || !medications) { if (!appointments || !medications) {
@@ -166,27 +178,41 @@ export default function TodayPage() {
return ( return (
<> <>
<Header title="Today" /> <Header title="Today" />
<PageContainer className="pt-4 space-y-6"> <PageContainer className="pt-6 pb-24 space-y-8">
{/* Greeting */} {/* Greeting Section with decorative elements */}
<div className="mb-2"> <div className={`relative transition-all duration-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<p className="text-secondary-500 text-sm"> {/* Decorative blob */}
{format(toZonedTime(now, TIMEZONE), 'EEEE, MMMM d')} <div className="blob blob-primary w-32 h-32 -top-4 -left-4" />
</p>
<div className="relative">
<p className="text-secondary-500 text-sm font-medium tracking-wide uppercase mb-1">
{format(toZonedTime(now, TIMEZONE), 'EEEE, MMMM d')}
</p>
<h1 className="font-display text-display-sm text-secondary-900">
{getGreeting()}
</h1>
<p className="text-secondary-600 mt-2 flex items-center gap-2">
<Heart className="w-4 h-4 text-accent-500" />
<span>Take it one step at a time</span>
</p>
</div>
</div> </div>
{/* Emergency & Call Clinic Buttons */} {/* Emergency & Call Clinic Buttons - Floating cards */}
<div className="flex gap-3"> <div className={`flex gap-3 transition-all duration-700 delay-100 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
{/* Emergency Info Button */} {/* Emergency Info Button */}
<button <button
onClick={() => router.push('/emergency')} onClick={() => router.push('/emergency')}
className="flex items-center gap-3 p-4 bg-red-50 rounded-card border border-red-200 hover:bg-red-100 transition-colors flex-1" className="flex-1 group relative overflow-hidden"
> >
<div className="w-10 h-10 rounded-full bg-red-600 flex items-center justify-center"> <div className="relative flex items-center gap-3 p-4 bg-alert-50/80 backdrop-blur-sm rounded-card border border-alert-200/60 hover:border-alert-300 hover:shadow-elevated transition-all duration-300">
<AlertTriangle className="w-5 h-5 text-white" /> <div className="w-12 h-12 rounded-full bg-gradient-to-br from-alert-500 to-alert-600 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform duration-300">
</div> <AlertTriangle className="w-6 h-6 text-white" />
<div className="text-left"> </div>
<p className="font-medium text-red-800">Emergency</p> <div className="text-left">
<p className="text-sm text-red-600">Medical info</p> <p className="font-semibold text-alert-800">Emergency</p>
<p className="text-sm text-alert-600">Medical info</p>
</div>
</div> </div>
</button> </button>
@@ -194,26 +220,28 @@ export default function TodayPage() {
{currentWorkspace.clinicPhone && ( {currentWorkspace.clinicPhone && (
<a <a
href={`tel:${currentWorkspace.clinicPhone}`} href={`tel:${currentWorkspace.clinicPhone}`}
className="flex items-center gap-3 p-4 bg-primary-50 rounded-card border border-primary-100 hover:bg-primary-100 transition-colors flex-1" className="flex-1 group"
> >
<div className="w-10 h-10 rounded-full bg-primary-500 flex items-center justify-center"> <div className="flex items-center gap-3 p-4 bg-primary-50/80 backdrop-blur-sm rounded-card border border-primary-200/60 hover:border-primary-300 hover:shadow-elevated transition-all duration-300">
<Phone className="w-5 h-5 text-white" /> <div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform duration-300">
</div> <Phone className="w-6 h-6 text-white" />
<div className="text-left"> </div>
<p className="font-medium text-primary-800">Call Clinic</p> <div className="text-left">
<p className="text-sm text-primary-600 truncate">{currentWorkspace.clinicPhone}</p> <p className="font-semibold text-primary-800">Call Clinic</p>
<p className="text-sm text-primary-600 truncate max-w-[100px]">{currentWorkspace.clinicPhone}</p>
</div>
</div> </div>
</a> </a>
)} )}
</div> </div>
{/* Next Appointment */} {/* Next Appointment - Hero Card */}
<section> <section className={`transition-all duration-700 delay-200 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-secondary-900">Next Appointment</h2> <h2 className="font-display text-xl text-secondary-900">Next Appointment</h2>
<button <button
onClick={() => router.push('/appointments')} onClick={() => router.push('/appointments')}
className="text-sm text-primary-600 font-medium flex items-center" className="text-sm text-primary-600 font-medium flex items-center gap-0.5 hover:text-primary-700 transition-colors"
> >
View all View all
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
@@ -221,30 +249,30 @@ export default function TodayPage() {
</div> </div>
{nextAppointment ? ( {nextAppointment ? (
<Card <div
className="card-appointment" className="card-appointment cursor-pointer group"
onClick={() => router.push(`/appointments/${nextAppointment.id}`)} onClick={() => router.push(`/appointments/${nextAppointment.id}`)}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0"> <div className="w-14 h-14 rounded-full bg-gradient-to-br from-primary-100 to-primary-200 flex items-center justify-center flex-shrink-0 shadow-inner">
<Calendar className="w-5 h-5 text-primary-600" /> <Calendar className="w-7 h-7 text-primary-600" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-semibold text-secondary-900 truncate"> <h3 className="font-display text-lg text-secondary-900 truncate group-hover:text-primary-700 transition-colors">
{nextAppointment.title} {nextAppointment.title}
</h3> </h3>
<p className="text-sm text-secondary-600 flex items-center gap-1 mt-1"> <p className="text-sm text-secondary-600 flex items-center gap-1.5 mt-1.5">
<Clock className="w-4 h-4" /> <Clock className="w-4 h-4 text-primary-500" />
{formatAppointmentDate(nextAppointment.datetime)} {formatAppointmentDate(nextAppointment.datetime)}
</p> </p>
{nextAppointment.location && ( {nextAppointment.location && (
<p className="text-sm text-secondary-500 flex items-center gap-1 mt-0.5"> <p className="text-sm text-secondary-500 flex items-center gap-1.5 mt-1">
<MapPin className="w-4 h-4" /> <MapPin className="w-4 h-4 text-cream-600" />
<span className="truncate">{nextAppointment.location}</span> <span className="truncate">{nextAppointment.location}</span>
</p> </p>
)} )}
</div> </div>
<ChevronRight className="w-5 h-5 text-secondary-400" /> <ChevronRight className="w-5 h-5 text-secondary-300 group-hover:text-primary-500 group-hover:translate-x-1 transition-all" />
</div> </div>
{nextAppointment.mapUrl && ( {nextAppointment.mapUrl && (
<a <a
@@ -252,27 +280,27 @@ export default function TodayPage() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-1.5 mt-3 text-sm text-primary-600 font-medium hover:text-primary-700" className="inline-flex items-center gap-1.5 mt-4 text-sm text-primary-600 font-medium hover:text-primary-700 hover:underline"
> >
<MapPin className="w-4 h-4" /> <MapPin className="w-4 h-4" />
Open in Maps Open in Maps
</a> </a>
)} )}
</Card> </div>
) : ( ) : (
<Card variant="outline" className="text-center py-6"> <div className="section-warm text-center py-8">
<Calendar className="w-8 h-8 text-secondary-300 mx-auto mb-2" /> <div className="w-16 h-16 rounded-full bg-cream-100 flex items-center justify-center mx-auto mb-4">
<p className="text-secondary-500">No upcoming appointments</p> <Calendar className="w-8 h-8 text-cream-600" />
<Button </div>
variant="ghost" <p className="text-secondary-600 font-medium">No upcoming appointments</p>
size="sm" <button
className="mt-2"
onClick={() => router.push('/appointments/new')} onClick={() => router.push('/appointments/new')}
className="mt-4 inline-flex items-center gap-2 text-primary-600 font-medium hover:text-primary-700"
> >
<Plus className="w-4 h-4 mr-1" /> <Plus className="w-4 h-4" />
Add one Add one
</Button> </button>
</Card> </div>
)} )}
</section> </section>
@@ -283,26 +311,26 @@ export default function TodayPage() {
) )
if (tomorrowAppt) { if (tomorrowAppt) {
return ( return (
<section> <section className={`transition-all duration-700 delay-300 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<Card <div
className="bg-green-50 border border-green-200 cursor-pointer hover:bg-green-100 transition-colors" className="bg-gradient-to-r from-cream-100 to-cream-50 border border-cream-200 rounded-card p-5 cursor-pointer hover:shadow-elevated transition-all duration-300 group"
onClick={() => router.push(`/appointments/${tomorrowAppt.id}/prep`)} onClick={() => router.push(`/appointments/${tomorrowAppt.id}/prep`)}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0"> <div className="w-14 h-14 rounded-full bg-gradient-to-br from-accent-400 to-accent-500 flex items-center justify-center flex-shrink-0 shadow-lg group-hover:scale-105 transition-transform">
<ClipboardCheck className="w-5 h-5 text-white" /> <ClipboardCheck className="w-7 h-7 text-white" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="font-medium text-green-800"> <p className="font-display text-lg text-secondary-900">
Prepare for tomorrow Prepare for tomorrow
</p> </p>
<p className="text-sm text-green-600"> <p className="text-sm text-secondary-600">
{tomorrowAppt.title} at {format(toZonedTime(new Date(tomorrowAppt.datetime), TIMEZONE), 'h:mm a')} {tomorrowAppt.title} at {format(toZonedTime(new Date(tomorrowAppt.datetime), TIMEZONE), 'h:mm a')}
</p> </p>
</div> </div>
<ChevronRight className="w-5 h-5 text-green-500" /> <ChevronRight className="w-5 h-5 text-accent-500 group-hover:translate-x-1 transition-transform" />
</div> </div>
</Card> </div>
</section> </section>
) )
} }
@@ -311,23 +339,25 @@ export default function TodayPage() {
{/* Refill Alerts */} {/* Refill Alerts */}
{medications && medications.length > 0 && ( {medications && medications.length > 0 && (
<RefillAlert <div className={`transition-all duration-700 delay-400 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
medications={medications.map(m => ({ <RefillAlert
id: m.id, medications={medications.map(m => ({
name: m.name, id: m.id,
pillCount: m.pillCount, name: m.name,
refillThreshold: m.refillThreshold, pillCount: m.pillCount,
}))} refillThreshold: m.refillThreshold,
/> }))}
/>
</div>
)} )}
{/* Meds Due */} {/* Meds Due */}
<section> <section className={`transition-all duration-700 delay-500 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-secondary-900">Medications</h2> <h2 className="font-display text-xl text-secondary-900">Medications</h2>
<button <button
onClick={() => router.push('/meds')} onClick={() => router.push('/meds')}
className="text-sm text-primary-600 font-medium flex items-center" className="text-sm text-primary-600 font-medium flex items-center gap-0.5 hover:text-primary-700 transition-colors"
> >
View all View all
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
@@ -335,21 +365,25 @@ export default function TodayPage() {
</div> </div>
{medsDueSoon.length > 0 ? ( {medsDueSoon.length > 0 ? (
<div className="space-y-3"> <div className="space-y-4">
{medsDueSoon.map((status) => ( {medsDueSoon.map((status, index) => (
<MedicationCard <MedicationCard
key={status.medication.id} key={status.medication.id}
status={status} status={status}
now={now} now={now}
onTake={() => handleTakeMed(status)} onTake={() => handleTakeMed(status)}
index={index}
/> />
))} ))}
</div> </div>
) : medications.length > 0 ? ( ) : medications.length > 0 ? (
<Card variant="outline" className="text-center py-6"> <div className="section-warm text-center py-8">
<Pill className="w-8 h-8 text-secondary-300 mx-auto mb-2" /> <div className="w-16 h-16 rounded-full bg-primary-50 flex items-center justify-center mx-auto mb-4">
<p className="text-secondary-500">All caught up! No meds due soon.</p> <Pill className="w-8 h-8 text-primary-400" />
</Card> </div>
<p className="text-secondary-600 font-medium">All caught up!</p>
<p className="text-sm text-secondary-400 mt-1">No medications due soon</p>
</div>
) : ( ) : (
<EmptyState <EmptyState
type="medications" type="medications"
@@ -364,16 +398,16 @@ export default function TodayPage() {
</section> </section>
{/* Quick Note */} {/* Quick Note */}
<section> <section className={`transition-all duration-700 delay-600 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<h2 className="text-lg font-semibold text-secondary-900 mb-3">Quick Note</h2> <h2 className="font-display text-xl text-secondary-900 mb-4">Quick Note</h2>
<Card padding="sm"> <div className="section-warm">
<div className="flex gap-2"> <div className="flex gap-3">
<input <input
type="text" type="text"
value={quickNote} value={quickNote}
onChange={(e) => setQuickNote(e.target.value)} onChange={(e) => setQuickNote(e.target.value)}
placeholder="Jot down a thought..." placeholder="Jot down a thought..."
className="flex-1 px-3 py-2.5 border border-border rounded-button text-base focus:outline-none focus:ring-2 focus:ring-primary-200 focus:border-primary-500" className="input-sanctuary flex-1"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && quickNote.trim()) { if (e.key === 'Enter' && quickNote.trim()) {
handleAddQuickNote() handleAddQuickNote()
@@ -384,11 +418,12 @@ export default function TodayPage() {
onClick={handleAddQuickNote} onClick={handleAddQuickNote}
disabled={!quickNote.trim() || isAddingNote} disabled={!quickNote.trim() || isAddingNote}
loading={isAddingNote} loading={isAddingNote}
className="btn-primary whitespace-nowrap"
> >
Add Add
</Button> </Button>
</div> </div>
</Card> </div>
</section> </section>
</PageContainer> </PageContainer>
</> </>
@@ -399,9 +434,10 @@ interface MedicationCardProps {
status: MedicationDueStatus status: MedicationDueStatus
now: Date now: Date
onTake: () => void onTake: () => void
index: number
} }
function MedicationCard({ status, now, onTake }: MedicationCardProps) { function MedicationCard({ status, now, onTake, index }: MedicationCardProps) {
const { medication, isOverdue, isPRN, prnAvailable, prnAvailableAt, nextDueAt } = status const { medication, isOverdue, isPRN, prnAvailable, prnAvailableAt, nextDueAt } = status
const getTimeLabel = () => { const getTimeLabel = () => {
@@ -426,35 +462,38 @@ function MedicationCard({ status, now, onTake }: MedicationCardProps) {
const canTake = !isPRN || prnAvailable const canTake = !isPRN || prnAvailable
return ( return (
<Card className={isOverdue ? 'overdue' : ''}> <div className={`card-medication ${isOverdue ? 'overdue' : ''} animate-fade-up`} style={{ animationDelay: `${index * 0.1}s` }}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0"> <div className={`w-14 h-14 rounded-full flex items-center justify-center flex-shrink-0 shadow-inner transition-all duration-300 ${
<Pill className="w-5 h-5 text-primary-600" /> isOverdue
? 'bg-gradient-to-br from-accent-100 to-accent-200'
: 'bg-gradient-to-br from-primary-100 to-primary-200'
}`}>
<Pill className={`w-7 h-7 ${isOverdue ? 'text-accent-600' : 'text-primary-600'}`} />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-semibold text-secondary-900">{medication.name}</h3> <h3 className="font-display text-lg text-secondary-900">{medication.name}</h3>
<p className={`text-sm ${isOverdue ? 'text-red-600 font-medium' : 'text-secondary-500'}`}> <p className={`text-sm ${isOverdue ? 'text-accent-600 font-medium' : 'text-secondary-500'}`}>
{getTimeLabel()} {getTimeLabel()}
{isPRN && ' • As needed'} {isPRN && ' • As needed'}
</p> </p>
</div> </div>
<Button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onTake() onTake()
}} }}
variant="success"
size="md"
disabled={!canTake} disabled={!canTake}
className={`btn-primary text-sm px-5 py-2.5 ${!canTake ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
Taken Taken
</Button> </button>
</div> </div>
{medication.instructions && ( {medication.instructions && (
<p className="text-sm text-secondary-500 mt-2 ml-13"> <p className="text-sm text-secondary-500 mt-3 ml-[72px]">
{medication.instructions} {medication.instructions}
</p> </p>
)} )}
</Card> </div>
) )
} }

View File

@@ -4,10 +4,14 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Source+Sans+3:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap');
@layer base { @layer base {
:root { :root {
--background: 250 251 252; --background: 250 247 242;
--foreground: 31 38 49; --foreground: 38 35 32;
--surface: 255 255 255;
} }
html { html {
@@ -17,6 +21,16 @@
body { body {
@apply bg-background text-secondary-900 antialiased; @apply bg-background text-secondary-900 antialiased;
font-feature-settings: 'rlig' 1, 'calt' 1; font-feature-settings: 'rlig' 1, 'calt' 1;
font-family: 'Source Sans 3', system-ui, sans-serif;
}
/* Paper texture background */
.paper-texture {
background-color: #faf7f2;
background-image:
url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
background-blend-mode: soft-light;
background-size: 200px 200px;
} }
/* Large text mode */ /* Large text mode */
@@ -38,12 +52,12 @@
} }
.large-text .text-sm { .large-text .text-sm {
font-size: 1rem; /* text-base equivalent */ font-size: 1rem;
line-height: 1.5rem; line-height: 1.5rem;
} }
.large-text .text-xs { .large-text .text-xs {
font-size: 0.875rem; /* text-sm equivalent */ font-size: 0.875rem;
line-height: 1.25rem; line-height: 1.25rem;
} }
@@ -56,9 +70,9 @@
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
} }
/* Focus styles for accessibility */ /* Focus styles for accessibility - warm glow */
*:focus-visible { *:focus-visible {
@apply outline-none ring-2 ring-primary-500 ring-offset-2; @apply outline-none ring-2 ring-primary-300 ring-offset-2 ring-offset-background;
} }
/* Better touch targets */ /* Better touch targets */
@@ -79,64 +93,151 @@
} }
@layer components { @layer components {
/* Primary taken button */ /* Warm sanctuary card styles */
.btn-taken { .card-sanctuary {
@apply bg-surface rounded-card shadow-card;
@apply border border-cream-200/50;
@apply transition-all duration-300 ease-sanctuary;
}
.card-sanctuary:hover {
@apply shadow-card-hover;
@apply border-cream-300;
}
/* Primary action button - warm green */
.btn-primary {
@apply bg-primary-500 hover:bg-primary-600 text-white font-semibold; @apply bg-primary-500 hover:bg-primary-600 text-white font-semibold;
@apply py-3 px-6 rounded-button min-h-touch; @apply py-3.5 px-6 rounded-button min-h-touch;
@apply shadow-button transition-all duration-200; @apply shadow-button transition-all duration-300 ease-sanctuary;
@apply active:scale-95; @apply active:scale-[0.98] hover:shadow-button-hover;
} }
/* Card styles */ /* Secondary button - cream */
.btn-secondary {
@apply bg-cream-100 hover:bg-cream-200 text-secondary-800 font-medium;
@apply py-3.5 px-6 rounded-button min-h-touch;
@apply border border-cream-300;
@apply transition-all duration-300 ease-sanctuary;
@apply active:scale-[0.98];
}
/* Accent button - terracotta */
.btn-accent {
@apply bg-accent-500 hover:bg-accent-600 text-white font-semibold;
@apply py-3.5 px-6 rounded-button min-h-touch;
@apply shadow-button transition-all duration-300 ease-sanctuary;
@apply active:scale-[0.98] hover:shadow-button-hover;
}
/* Ghost button */
.btn-ghost {
@apply bg-transparent hover:bg-cream-100 text-secondary-700 font-medium;
@apply py-3.5 px-6 rounded-button min-h-touch;
@apply transition-all duration-300 ease-sanctuary;
@apply active:scale-[0.98];
}
/* Taken button */
.btn-taken {
@apply btn-primary;
}
/* Appointment card */
.card-appointment { .card-appointment {
@apply bg-surface rounded-card shadow-card p-4; @apply card-sanctuary p-5;
@apply border-l-4 border-primary-500; @apply border-l-[6px] border-l-primary-400;
} }
/* Medication card */
.card-medication { .card-medication {
@apply bg-surface rounded-card shadow-card p-4; @apply card-sanctuary p-5;
@apply flex items-center justify-between;
} }
/* Overdue styles */ /* Overdue styles - warm terracotta */
.overdue { .overdue {
@apply border-l-4 border-red-500 bg-red-50; @apply border-l-[6px] border-l-accent-500;
@apply bg-accent-50/50;
}
/* Emergency card - alert red but softened */
.card-emergency {
@apply bg-alert-50 border-2 border-alert-200 rounded-card-lg;
@apply overflow-hidden;
}
/* Section styling */
.section-warm {
@apply bg-surface rounded-card-lg shadow-soft p-6;
@apply border border-cream-200/60;
}
/* Input styling */
.input-sanctuary {
@apply bg-surface border border-cream-300 rounded-button;
@apply px-4 py-3.5 text-secondary-900 placeholder:text-secondary-400;
@apply focus:outline-none focus:border-primary-400 focus:ring-2 focus:ring-primary-200;
@apply transition-all duration-200;
} }
/* Timeline styles */ /* Timeline styles */
.timeline-item { .timeline-item {
@apply relative pl-6 pb-4; @apply relative pl-8 pb-6;
} }
.timeline-item::before { .timeline-item::before {
content: ''; content: '';
@apply absolute left-0 top-2 w-2 h-2 rounded-full bg-primary-400; @apply absolute left-0 top-2 w-3 h-3 rounded-full bg-primary-300;
@apply ring-4 ring-primary-100;
} }
.timeline-item::after { .timeline-item::after {
content: ''; content: '';
@apply absolute left-[3px] top-4 w-0.5 h-full bg-border; @apply absolute left-[5px] top-5 w-0.5 h-full bg-cream-300;
} }
.timeline-item:last-child::after { .timeline-item:last-child::after {
@apply hidden; @apply hidden;
} }
/* Glass effect for overlays */
.glass {
@apply bg-surface/80 backdrop-blur-md;
@apply border border-cream-200/50;
}
/* Decorative blob shapes */
.blob {
@apply absolute rounded-full blur-3xl opacity-30 pointer-events-none;
}
.blob-primary {
@apply bg-primary-200;
}
.blob-accent {
@apply bg-accent-200;
}
.blob-cream {
@apply bg-cream-300;
}
} }
@layer utilities { @layer utilities {
/* Animation utilities */ /* Animation utilities */
.animate-in { .animate-in {
animation: animateIn 0.2s ease-out; animation: animateIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
} }
.animate-out { .animate-out {
animation: animateOut 0.15s ease-in forwards; animation: animateOut 0.2s ease-in forwards;
} }
@keyframes animateIn { @keyframes animateIn {
from { from {
opacity: 0; opacity: 0;
transform: translateY(8px); transform: translateY(12px);
} }
to { to {
opacity: 1; opacity: 1;
@@ -151,55 +252,46 @@
} }
to { to {
opacity: 0; opacity: 0;
transform: translateY(8px); transform: translateY(12px);
} }
} }
.slide-in-from-bottom-4 { /* Stagger animation delays */
animation: slideInFromBottom 0.2s ease-out; .stagger-1 { animation-delay: 0.05s; }
} .stagger-2 { animation-delay: 0.1s; }
.stagger-3 { animation-delay: 0.15s; }
.slide-out-to-bottom-4 { .stagger-4 { animation-delay: 0.2s; }
animation: slideOutToBottom 0.15s ease-in forwards; .stagger-5 { animation-delay: 0.25s; }
} .stagger-6 { animation-delay: 0.3s; }
@keyframes slideInFromBottom {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideOutToBottom {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(16px);
}
}
/* Fade utilities */
.fade-in { .fade-in {
animation: fadeIn 0.2s ease-out; animation: fadeIn 0.3s ease-out forwards;
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-up {
animation: fadeUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes fadeUp {
from { from {
opacity: 0; opacity: 0;
transform: translateY(20px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0);
} }
} }
/* Scale utilities */
.zoom-in-95 { .zoom-in-95 {
animation: zoomIn 0.2s ease-out; animation: zoomIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
} }
@keyframes zoomIn { @keyframes zoomIn {
@@ -212,4 +304,29 @@
transform: scale(1); transform: scale(1);
} }
} }
/* Soft pulse for live elements */
.pulse-soft {
animation: pulseSoft 3s ease-in-out infinite;
}
@keyframes pulseSoft {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* Gradient text */
.gradient-text {
@apply bg-clip-text text-transparent;
background-image: linear-gradient(135deg, #528252 0%, #3f663f 100%);
}
/* Hide scrollbar but keep functionality */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
} }

View File

@@ -1,10 +1,7 @@
import type { Metadata, Viewport } from 'next' import type { Metadata, Viewport } from 'next'
import { Inter } from 'next/font/google'
import './globals.css' import './globals.css'
import { Toaster } from '@/components/ui' import { Toaster } from '@/components/ui'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Next Step - Health Management', title: 'Next Step - Health Management',
description: 'A calm, reliable app to help manage appointments, medications, and notes for health care.', description: 'A calm, reliable app to help manage appointments, medications, and notes for health care.',
@@ -21,7 +18,7 @@ export const viewport: Viewport = {
initialScale: 1, initialScale: 1,
maximumScale: 1, maximumScale: 1,
userScalable: false, userScalable: false,
themeColor: '#3a9563', themeColor: '#528252',
} }
export default function RootLayout({ export default function RootLayout({
@@ -33,8 +30,11 @@ export default function RootLayout({
<html lang="en"> <html lang="en">
<head> <head>
<link rel="apple-touch-icon" href="/icon-192.png" /> <link rel="apple-touch-icon" href="/icon-192.png" />
{/* Preconnect to Google Fonts for performance */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
</head> </head>
<body className={inter.className}> <body className="paper-texture">
{children} {children}
<Toaster /> <Toaster />
</body> </body>

View File

@@ -1,9 +1,9 @@
'use client' 'use client'
import { useState } from 'react' import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { Heart, Shield, ArrowRight } from 'lucide-react' import { Heart, Shield, ArrowRight, Sparkles, Users, Bell } from 'lucide-react'
import { Button, Input, Card, showToast } from '@/components/ui' import { Button, Input, showToast } from '@/components/ui'
export default function OnboardingPage() { export default function OnboardingPage() {
const router = useRouter() const router = useRouter()
@@ -12,6 +12,11 @@ export default function OnboardingPage() {
const [clinicPhone, setClinicPhone] = useState('') const [clinicPhone, setClinicPhone] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const handleAcceptDisclaimer = () => { const handleAcceptDisclaimer = () => {
setStep('workspace') setStep('workspace')
@@ -45,7 +50,7 @@ export default function OnboardingPage() {
}) })
} }
showToast('All set! Welcome to Next Step.', 'success') showToast('Welcome to Next Step', 'success')
router.push('/today') router.push('/today')
router.refresh() router.refresh()
} catch (err) { } catch (err) {
@@ -57,101 +62,176 @@ export default function OnboardingPage() {
if (step === 'disclaimer') { if (step === 'disclaimer') {
return ( return (
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4"> <div className="min-h-screen paper-texture flex flex-col items-center justify-center p-6">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="text-center mb-8"> {/* Decorative blobs */}
<div className="w-16 h-16 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4"> <div className="fixed inset-0 overflow-hidden pointer-events-none">
<Shield className="w-8 h-8 text-amber-600" /> <div className="blob blob-primary w-96 h-96 -top-48 -right-48" />
</div> <div className="blob blob-accent w-80 h-80 bottom-20 -left-40" />
<h1 className="text-2xl font-bold text-secondary-900">Important Notice</h1> <div className="blob blob-cream w-64 h-64 top-1/2 right-1/4" />
</div> </div>
<Card className="mb-6"> <div className={`relative transition-all duration-1000 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
<div className="space-y-4 text-secondary-700"> {/* Logo/Icon */}
<p> <div className="text-center mb-8">
<strong>Next Step is a tracking tool only.</strong> It helps you and your family <div className="w-24 h-24 rounded-card-lg bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center mx-auto mb-6 shadow-elevated">
stay organized with appointments and medications. <Heart className="w-12 h-12 text-white" />
</p> </div>
<p> <h1 className="font-display text-display-md text-secondary-900 mb-2">
<strong className="text-red-600">This app does not provide medical advice.</strong>{' '} Next Step
Always consult your healthcare team for medical decisions. </h1>
<p className="text-secondary-500 text-lg">
Supporting you through every step
</p> </p>
</div>
<p> {/* Disclaimer Card */}
<strong>For emergencies:</strong> Call 000 (Australia) or your local emergency <div className="section-warm mb-6">
services immediately. <div className="flex items-center gap-3 mb-6">
</p> <div className="w-12 h-12 rounded-full bg-accent-100 flex items-center justify-center">
<Shield className="w-6 h-6 text-accent-600" />
</div>
<h2 className="font-display text-xl text-secondary-900">Important Notice</h2>
</div>
<p> <div className="space-y-4 text-secondary-700">
If you have questions about your treatment, contact your clinic directly using the <div className="flex gap-3">
button we'll help you set up. <Sparkles className="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" />
</p> <p>
<strong className="text-secondary-900">Next Step is a tracking tool only.</strong>{' '}
It helps you and your family stay organized with appointments and medications.
</p>
</div>
<div className="pt-4 border-t border-border"> <div className="flex gap-3">
<p className="text-sm text-secondary-500"> <div className="w-5 h-5 rounded-full bg-alert-100 flex items-center justify-center flex-shrink-0 mt-0.5">
By continuing, you acknowledge that Next Step is for tracking purposes only and <span className="text-alert-600 text-xs font-bold">!</span>
does not replace professional medical advice. </div>
<p>
<strong className="text-alert-600">This app does not provide medical advice.</strong>{' '}
Always consult your healthcare team for medical decisions.
</p>
</div>
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-alert-500 flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-white text-xs font-bold">000</span>
</div>
<p>
<strong>For emergencies:</strong> Call 000 (Australia) or your local emergency services immediately.
</p>
</div>
<div className="flex gap-3">
<Users className="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" />
<p>
Have questions about your treatment? Contact your clinic directly using the button we'll help you set up.
</p>
</div>
</div>
<div className="mt-6 pt-6 border-t border-cream-200">
<p className="text-sm text-secondary-500 text-center">
By continuing, you acknowledge that Next Step is for tracking purposes only and does not replace professional medical advice.
</p> </p>
</div> </div>
</div> </div>
</Card>
<Button onClick={handleAcceptDisclaimer} fullWidth> <button
I Understand onClick={handleAcceptDisclaimer}
<ArrowRight className="w-5 h-5 ml-2" /> className="btn-primary w-full flex items-center justify-center gap-2 text-lg py-4"
</Button> >
I Understand
<ArrowRight className="w-5 h-5" />
</button>
</div>
</div> </div>
</div> </div>
) )
} }
return ( return (
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4"> <div className="min-h-screen paper-texture flex flex-col items-center justify-center p-6">
<div className="w-full max-w-sm"> <div className="w-full max-w-md">
<div className="text-center mb-8"> {/* Decorative blobs */}
<div className="w-16 h-16 bg-primary-500 rounded-2xl flex items-center justify-center mx-auto mb-4"> <div className="fixed inset-0 overflow-hidden pointer-events-none">
<Heart className="w-8 h-8 text-white" /> <div className="blob blob-primary w-80 h-80 -top-32 right-0" />
</div> <div className="blob blob-cream w-64 h-64 bottom-0 left-0" />
<h1 className="text-2xl font-bold text-secondary-900">Set Up Your Plan</h1>
<p className="text-secondary-500 mt-1">Create a workspace to get started</p>
</div> </div>
<Card className="mb-6"> <div className={`relative transition-all duration-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<form onSubmit={handleCreateWorkspace} className="space-y-4"> {/* Header */}
<Input <div className="text-center mb-8">
label="Workspace Name" <div className="w-20 h-20 rounded-card-lg bg-gradient-to-br from-accent-400 to-accent-500 flex items-center justify-center mx-auto mb-6 shadow-elevated">
type="text" <Sparkles className="w-10 h-10 text-white" />
value={workspaceName} </div>
onChange={(e) => setWorkspaceName(e.target.value)}
placeholder="e.g., Grace's Plan" <h1 className="font-display text-display-sm text-secondary-900 mb-2">
helperText="This is how family members will identify this workspace" Set Up Your Plan
required </h1>
/> <p className="text-secondary-500 text-lg">Create a workspace to get started</p>
<Input </div>
label="Clinic Phone Number"
type="tel" {/* Form */}
value={clinicPhone} <form onSubmit={handleCreateWorkspace} className="section-warm space-y-6">
onChange={(e) => setClinicPhone(e.target.value)} <div>
placeholder="e.g., 08 9400 1234" <label className="block text-sm font-medium text-secondary-700 mb-2">
helperText="We'll add a 'Call Clinic' button for quick access" Workspace Name
/> <span className="text-alert-500 ml-1">*</span>
</label>
<input
type="text"
value={workspaceName}
onChange={(e) => setWorkspaceName(e.target.value)}
placeholder="e.g., Grace's Plan"
className="input-sanctuary w-full"
required
/>
<p className="text-xs text-secondary-400 mt-2">
This is how family members will identify this workspace
</p>
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Clinic Phone Number
<span className="text-secondary-400 font-normal ml-1">(optional)</span>
</label>
<div className="relative">
<Bell className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-secondary-400" />
<input
type="tel"
value={clinicPhone}
onChange={(e) => setClinicPhone(e.target.value)}
placeholder="e.g., 08 9400 1234"
className="input-sanctuary w-full pl-10"
/>
</div>
<p className="text-xs text-secondary-400 mt-2">
We'll add a quick "Call Clinic" button for easy access
</p>
</div>
{error && ( {error && (
<p className="text-sm text-red-600 bg-red-50 px-3 py-2 rounded-button"> <div className="bg-alert-50 border border-alert-200 rounded-card p-4">
{error} <p className="text-sm text-alert-700">{error}</p>
</p> </div>
)} )}
<Button type="submit" fullWidth loading={loading}> <button
Create Workspace type="submit"
</Button> disabled={loading}
className="btn-primary w-full text-lg py-4 disabled:opacity-50"
>
{loading ? 'Creating...' : 'Create Workspace'}
</button>
</form> </form>
</Card>
<p className="text-center text-sm text-secondary-500"> <p className="text-center text-sm text-secondary-400 mt-6">
You can add family members later from Settings You can add family members later from Settings
</p> </p>
</div>
</div> </div>
</div> </div>
) )

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { Phone, User, Droplets, AlertTriangle, Activity, Stethoscope } from 'lucide-react' import { Phone, User, Droplets, AlertTriangle, Activity, Stethoscope, HeartPulse, FileText } from 'lucide-react'
import { format } from 'date-fns' import { format } from 'date-fns'
interface EmergencyInfo { interface EmergencyInfo {
@@ -39,49 +39,66 @@ export function EmergencyCard({ info, medications, variant = 'full' }: Emergency
} }
return ( return (
<div className="bg-red-50 border-2 border-red-200 rounded-xl overflow-hidden"> <div className="bg-surface border-2 border-alert-200 rounded-card-lg overflow-hidden shadow-elevated">
{/* Header */} {/* Header */}
<div className="bg-red-600 text-white px-4 py-3"> <div className="bg-gradient-to-r from-alert-500 to-alert-600 text-white px-6 py-5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<AlertTriangle className="w-6 h-6" /> <div className="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center">
<h2 className="text-xl font-bold">Emergency Information</h2> <AlertTriangle className="w-7 h-7" />
</div>
<div>
<h2 className="font-display text-2xl">Emergency Information</h2>
<p className="text-alert-100 text-sm">Critical medical details for emergencies</p>
</div>
</div> </div>
</div> </div>
<div className="p-4 space-y-4"> <div className="p-6 space-y-6">
{/* Patient Info */} {/* Patient Info */}
{info.patientName && ( {info.patientName && (
<div className="flex items-start gap-3"> <div className="flex items-start gap-4 bg-cream-50 rounded-card p-4 border border-cream-200">
<User className="w-5 h-5 text-red-600 mt-0.5" /> <div className="w-12 h-12 rounded-full bg-cream-200 flex items-center justify-center flex-shrink-0">
<div> <User className="w-6 h-6 text-cream-700" />
<p className="text-sm text-red-700 font-medium">Patient Name</p> </div>
<p className="text-lg font-bold text-secondary-900">{info.patientName}</p> <div className="flex-1">
<p className="text-xs text-secondary-500 uppercase tracking-wide font-semibold mb-1">
Patient
</p>
<p className="font-display text-xl text-secondary-900">{info.patientName}</p>
{info.patientDOB && ( {info.patientDOB && (
<p className="text-sm text-secondary-600">DOB: {formatDate(info.patientDOB)}</p> <p className="text-sm text-secondary-600 mt-1">
Born: {formatDate(info.patientDOB)}
</p>
)} )}
</div> </div>
</div> </div>
)} )}
{/* Blood Type */} {/* Blood Type - Large and prominent */}
{info.bloodType && ( {info.bloodType && (
<div className="flex items-start gap-3"> <div className="flex items-center gap-4">
<Droplets className="w-5 h-5 text-red-600 mt-0.5" /> <div className="w-12 h-12 rounded-full bg-gradient-to-br from-alert-100 to-alert-200 flex items-center justify-center flex-shrink-0">
<Droplets className="w-6 h-6 text-alert-600" />
</div>
<div> <div>
<p className="text-sm text-red-700 font-medium">Blood Type</p> <p className="text-xs text-secondary-500 uppercase tracking-wide font-semibold">
<p className="text-2xl font-bold text-red-600">{info.bloodType}</p> Blood Type
</p>
<p className="text-3xl font-display text-alert-600">{info.bloodType}</p>
</div> </div>
</div> </div>
)} )}
{/* Allergies - High visibility */} {/* Allergies - High visibility */}
{info.allergies && ( {info.allergies && (
<div className="bg-red-100 border border-red-300 rounded-lg p-3"> <div className="bg-alert-50 border-l-4 border-alert-500 rounded-r-card p-5">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" /> <AlertTriangle className="w-6 h-6 text-alert-600 flex-shrink-0 mt-0.5" />
<div> <div>
<p className="text-sm text-red-700 font-bold uppercase">Allergies</p> <p className="text-sm text-alert-700 font-bold uppercase tracking-wide mb-2">
<p className="text-secondary-900 font-medium mt-1">{info.allergies}</p> Allergies
</p>
<p className="text-secondary-900 font-medium text-lg">{info.allergies}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -89,10 +106,14 @@ export function EmergencyCard({ info, medications, variant = 'full' }: Emergency
{/* Medical Conditions */} {/* Medical Conditions */}
{info.medicalConditions && ( {info.medicalConditions && (
<div className="flex items-start gap-3"> <div className="flex items-start gap-4">
<Activity className="w-5 h-5 text-red-600 mt-0.5" /> <div className="w-12 h-12 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0">
<div> <HeartPulse className="w-6 h-6 text-primary-600" />
<p className="text-sm text-red-700 font-medium">Medical Conditions</p> </div>
<div className="flex-1">
<p className="text-xs text-secondary-500 uppercase tracking-wide font-semibold mb-1">
Medical Conditions
</p>
<p className="text-secondary-900">{info.medicalConditions}</p> <p className="text-secondary-900">{info.medicalConditions}</p>
</div> </div>
</div> </div>
@@ -100,34 +121,49 @@ export function EmergencyCard({ info, medications, variant = 'full' }: Emergency
{/* Current Medications */} {/* Current Medications */}
{variant === 'full' && medications && medications.length > 0 && ( {variant === 'full' && medications && medications.length > 0 && (
<div className="border-t border-red-200 pt-4"> <div className="border-t-2 border-cream-200 pt-6">
<p className="text-sm text-red-700 font-bold mb-2">Current Medications</p> <div className="flex items-center gap-2 mb-4">
<ul className="space-y-1"> <FileText className="w-5 h-5 text-primary-500" />
{medications.map((med, i) => ( <p className="text-sm text-secondary-700 font-semibold uppercase tracking-wide">
<li key={i} className="text-secondary-900"> Current Medications
<span className="font-medium">{med.name}</span> </p>
{med.instructions && ( </div>
<span className="text-secondary-600"> - {med.instructions}</span> <div className="bg-cream-50 rounded-card p-4 border border-cream-200">
)} <ul className="space-y-3">
</li> {medications.map((med, i) => (
))} <li key={i} className="flex items-start gap-3">
</ul> <span className="w-2 h-2 rounded-full bg-primary-400 mt-2 flex-shrink-0" />
<div>
<span className="font-semibold text-secondary-900">{med.name}</span>
{med.instructions && (
<span className="text-secondary-600"> {med.instructions}</span>
)}
</div>
</li>
))}
</ul>
</div>
</div> </div>
)} )}
{/* Doctor Info */} {/* Doctor Info */}
{info.primaryPhysician && ( {info.primaryPhysician && (
<div className="border-t border-red-200 pt-4"> <div className="border-t-2 border-cream-200 pt-6">
<div className="flex items-start gap-3"> <div className="flex items-start gap-4">
<Stethoscope className="w-5 h-5 text-red-600 mt-0.5" /> <div className="w-12 h-12 rounded-full bg-secondary-100 flex items-center justify-center flex-shrink-0">
<div> <Stethoscope className="w-6 h-6 text-secondary-600" />
<p className="text-sm text-red-700 font-medium">Primary Physician</p> </div>
<p className="text-secondary-900 font-medium">{info.primaryPhysician}</p> <div className="flex-1">
<p className="text-xs text-secondary-500 uppercase tracking-wide font-semibold mb-1">
Primary Physician
</p>
<p className="font-display text-lg text-secondary-900">{info.primaryPhysician}</p>
{info.physicianPhone && ( {info.physicianPhone && (
<a <a
href={`tel:${info.physicianPhone}`} href={`tel:${info.physicianPhone}`}
className="text-primary-600 hover:underline" className="inline-flex items-center gap-2 mt-2 text-primary-600 font-medium hover:text-primary-700 hover:underline"
> >
<Phone className="w-4 h-4" />
{info.physicianPhone} {info.physicianPhone}
</a> </a>
)} )}
@@ -138,38 +174,42 @@ export function EmergencyCard({ info, medications, variant = 'full' }: Emergency
{/* Emergency Contacts */} {/* Emergency Contacts */}
{(info.clinicPhone || info.emergencyPhone) && ( {(info.clinicPhone || info.emergencyPhone) && (
<div className="border-t border-red-200 pt-4 space-y-3"> <div className="border-t-2 border-cream-200 pt-6">
<p className="text-sm text-red-700 font-bold">Emergency Contacts</p> <p className="text-sm text-secondary-700 font-semibold uppercase tracking-wide mb-4 flex items-center gap-2">
<Phone className="w-5 h-5 text-alert-500" />
Emergency Contacts
</p>
<div className="grid gap-3">
{info.clinicPhone && (
<a
href={`tel:${info.clinicPhone}`}
className="flex items-center gap-4 p-4 bg-alert-50 rounded-card border border-alert-200 hover:bg-alert-100 hover:border-alert-300 hover:shadow-soft transition-all duration-300 group"
>
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-alert-500 to-alert-600 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
<Phone className="w-7 h-7 text-white" />
</div>
<div>
<p className="font-semibold text-secondary-900 text-lg">Call Clinic</p>
<p className="text-alert-600 font-medium">{info.clinicPhone}</p>
</div>
</a>
)}
{info.clinicPhone && ( {info.emergencyPhone && (
<a <a
href={`tel:${info.clinicPhone}`} href={`tel:${info.emergencyPhone}`}
className="flex items-center gap-3 p-3 bg-white rounded-lg border border-red-200 hover:bg-red-50 transition-colors" className="flex items-center gap-4 p-4 bg-cream-50 rounded-card border border-cream-200 hover:bg-cream-100 hover:border-cream-300 hover:shadow-soft transition-all duration-300 group"
> >
<div className="w-10 h-10 rounded-full bg-red-600 flex items-center justify-center"> <div className="w-14 h-14 rounded-full bg-gradient-to-br from-secondary-500 to-secondary-600 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
<Phone className="w-5 h-5 text-white" /> <Phone className="w-7 h-7 text-white" />
</div> </div>
<div> <div>
<p className="font-medium text-secondary-900">Call Clinic</p> <p className="font-semibold text-secondary-900 text-lg">Emergency Contact</p>
<p className="text-sm text-secondary-600">{info.clinicPhone}</p> <p className="text-secondary-600 font-medium">{info.emergencyPhone}</p>
</div> </div>
</a> </a>
)} )}
</div>
{info.emergencyPhone && (
<a
href={`tel:${info.emergencyPhone}`}
className="flex items-center gap-3 p-3 bg-white rounded-lg border border-red-200 hover:bg-red-50 transition-colors"
>
<div className="w-10 h-10 rounded-full bg-red-600 flex items-center justify-center">
<Phone className="w-5 h-5 text-white" />
</div>
<div>
<p className="font-medium text-secondary-900">Emergency Contact</p>
<p className="text-sm text-secondary-600">{info.emergencyPhone}</p>
</div>
</a>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -2,26 +2,27 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { Button, Input, Textarea, Select, Card, showToast } from '@/components/ui' import { Clock, Calendar, Repeat, Pill, Package, ChevronDown, Plus, X } from 'lucide-react'
import { Button, Input, Textarea, Select, showToast } from '@/components/ui'
import { useApp } from '@/app/(app)/provider' import { useApp } from '@/app/(app)/provider'
type ScheduleType = 'FIXED_TIMES' | 'INTERVAL' | 'WEEKDAYS' | 'PRN' type ScheduleType = 'FIXED_TIMES' | 'INTERVAL' | 'WEEKDAYS' | 'PRN'
const scheduleTypeOptions = [ const scheduleTypeOptions = [
{ value: 'FIXED_TIMES', label: 'Fixed times daily' }, { value: 'FIXED_TIMES', label: 'Fixed times daily', icon: Clock, desc: 'Same times every day' },
{ value: 'INTERVAL', label: 'Every X hours' }, { value: 'INTERVAL', label: 'Every X hours', icon: Repeat, desc: 'Regular intervals' },
{ value: 'WEEKDAYS', label: 'Specific days of week' }, { value: 'WEEKDAYS', label: 'Specific days', icon: Calendar, desc: 'Certain days of the week' },
{ value: 'PRN', label: 'As needed (PRN)' }, { value: 'PRN', label: 'As needed (PRN)', icon: Pill, desc: 'When you need it' },
] ]
const weekdays = [ const weekdays = [
{ value: 0, label: 'Sun' }, { value: 0, label: 'Sun', full: 'Sunday' },
{ value: 1, label: 'Mon' }, { value: 1, label: 'Mon', full: 'Monday' },
{ value: 2, label: 'Tue' }, { value: 2, label: 'Tue', full: 'Tuesday' },
{ value: 3, label: 'Wed' }, { value: 3, label: 'Wed', full: 'Wednesday' },
{ value: 4, label: 'Thu' }, { value: 4, label: 'Thu', full: 'Thursday' },
{ value: 5, label: 'Fri' }, { value: 5, label: 'Fri', full: 'Friday' },
{ value: 6, label: 'Sat' }, { value: 6, label: 'Sat', full: 'Saturday' },
] ]
interface MedicationFormProps { interface MedicationFormProps {
@@ -42,6 +43,7 @@ interface MedicationFormProps {
export function MedicationForm({ initialData, isEditing = false }: MedicationFormProps) { export function MedicationForm({ initialData, isEditing = false }: MedicationFormProps) {
const router = useRouter() const router = useRouter()
const { currentWorkspace, refreshData } = useApp() const { currentWorkspace, refreshData } = useApp()
const [mounted, setMounted] = useState(false)
const [name, setName] = useState(initialData?.name || '') const [name, setName] = useState(initialData?.name || '')
const [instructions, setInstructions] = useState(initialData?.instructions || '') const [instructions, setInstructions] = useState(initialData?.instructions || '')
@@ -72,9 +74,12 @@ export function MedicationForm({ initialData, isEditing = false }: MedicationFor
const [error, setError] = useState('') const [error, setError] = useState('')
useEffect(() => { useEffect(() => {
// Reset defaults if switching types and no initial data for that type setMounted(true)
}, [])
useEffect(() => {
if (initialData?.scheduleType !== scheduleType) { if (initialData?.scheduleType !== scheduleType) {
// Keep current state if user is just switching around in new mode // Keep current state if user is just switching around in new mode
} }
}, [scheduleType, initialData]) }, [scheduleType, initialData])
@@ -134,13 +139,11 @@ export function MedicationForm({ initialData, isEditing = false }: MedicationFor
scheduleType, scheduleType,
scheduleData: buildScheduleData(), scheduleData: buildScheduleData(),
active: true, active: true,
// Refill tracking
...(trackRefills && pillCount !== '' && { ...(trackRefills && pillCount !== '' && {
pillCount: Number(pillCount), pillCount: Number(pillCount),
pillsPerDose, pillsPerDose,
refillThreshold, refillThreshold,
}), }),
// Explicitly nullify if disabled during edit
...(isEditing && !trackRefills && { ...(isEditing && !trackRefills && {
pillCount: null, pillCount: null,
pillsPerDose: null, pillsPerDose: null,
@@ -164,197 +167,337 @@ export function MedicationForm({ initialData, isEditing = false }: MedicationFor
} }
} }
const currentScheduleOption = scheduleTypeOptions.find(opt => opt.value === scheduleType)
const ScheduleIcon = currentScheduleOption?.icon || Clock
return ( return (
<Card> <div className={`space-y-6 transition-all duration-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<form onSubmit={handleSubmit} className="space-y-5"> {/* Form Header */}
<Input <div className="text-center mb-8">
label="Medication Name" <div className="w-20 h-20 rounded-full bg-gradient-to-br from-primary-100 to-primary-200 flex items-center justify-center mx-auto mb-4 shadow-lg">
type="text" <Pill className="w-10 h-10 text-primary-600" />
value={name} </div>
onChange={(e) => setName(e.target.value)} <h2 className="font-display text-display-sm text-secondary-900">
placeholder="e.g., Paracetamol 500mg" {isEditing ? 'Edit Medication' : 'Add Medication'}
required </h2>
/> <p className="text-secondary-500 mt-2">
{isEditing ? 'Update your medication details' : 'Keep track of your medications'}
</p>
</div>
<Textarea <form onSubmit={handleSubmit} className="space-y-6">
label="Instructions (optional)" {/* Basic Info Section */}
value={instructions} <div className="section-warm space-y-5">
onChange={(e) => setInstructions(e.target.value)} <h3 className="font-display text-lg text-secondary-900 flex items-center gap-2">
placeholder="e.g., Take with food" <span className="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center text-primary-600 text-sm font-semibold">1</span>
rows={2} Basic Information
/> </h3>
<Select
label="Schedule Type"
value={scheduleType}
onChange={(e) => setScheduleType(e.target.value as ScheduleType)}
options={scheduleTypeOptions}
/>
{/* Schedule-specific options */}
{scheduleType === 'FIXED_TIMES' && (
<div className="space-y-3">
<label className="block text-sm font-medium text-secondary-700">
Times to take
</label>
{times.map((time, index) => (
<div key={index} className="flex gap-2">
<Input
type="time"
value={time}
onChange={(e) => updateTime(index, e.target.value)}
className="flex-1"
/>
{times.length > 1 && (
<Button
type="button"
variant="ghost"
onClick={() => removeTime(index)}
>
Remove
</Button>
)}
</div>
))}
<Button type="button" variant="secondary" onClick={addTime} size="sm">
+ Add time
</Button>
</div>
)}
{scheduleType === 'INTERVAL' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<Input
label="Every (hours)"
type="number"
min={1}
max={72}
value={intervalHours}
onChange={(e) => setIntervalHours(parseInt(e.target.value) || 1)}
/>
<Input
label="Starting at"
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
/>
</div>
</div>
)}
{scheduleType === 'WEEKDAYS' && (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-secondary-700 mb-2"> <label className="block text-sm font-medium text-secondary-700 mb-2">
Days Medication Name
</label> </label>
<div className="flex gap-2 flex-wrap"> <input
{weekdays.map((day) => ( type="text"
<button value={name}
key={day.value} onChange={(e) => setName(e.target.value)}
type="button" placeholder="e.g., Paracetamol 500mg"
onClick={() => toggleDay(day.value)} className="input-sanctuary w-full"
className={`px-3 py-2 rounded-button text-sm font-medium transition-colors ${ required
selectedDays.includes(day.value) />
? 'bg-primary-500 text-white' </div>
: 'bg-muted text-secondary-600 hover:bg-secondary-200'
}`} <div>
> <label className="block text-sm font-medium text-secondary-700 mb-2">
{day.label} Instructions
</button> <span className="text-secondary-400 font-normal ml-1">(optional)</span>
))} </label>
</div> <textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="e.g., Take with food, Avoid grapefruit..."
rows={2}
className="input-sanctuary w-full resize-none"
/>
</div> </div>
<Input
label="Time"
type="time"
value={weekdayTime}
onChange={(e) => setWeekdayTime(e.target.value)}
/>
</div> </div>
)} </div>
{scheduleType === 'PRN' && ( {/* Schedule Section */}
<Input <div className="section-warm space-y-5">
label="Minimum hours between doses" <h3 className="font-display text-lg text-secondary-900 flex items-center gap-2">
type="number" <span className="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center text-primary-600 text-sm font-semibold">2</span>
min={0.5} Schedule
max={72} </h3>
step={0.5}
value={minHoursBetween}
onChange={(e) => setMinHoursBetween(parseFloat(e.target.value) || 4)}
helperText="Shows 'Available' when enough time has passed since last dose"
/>
)}
{/* Refill Tracking (optional) */} {/* Schedule Type Selector */}
<div className="border-t border-border pt-5"> <div className="grid grid-cols-2 gap-3">
<div className="flex items-center gap-3 mb-4"> {scheduleTypeOptions.map((option) => {
const Icon = option.icon
const isSelected = scheduleType === option.value
return (
<button
key={option.value}
type="button"
onClick={() => setScheduleType(option.value as ScheduleType)}
className={`p-4 rounded-card border-2 text-left transition-all duration-300 ${
isSelected
? 'border-primary-400 bg-primary-50/50 shadow-soft'
: 'border-cream-200 bg-surface hover:border-cream-300 hover:shadow-soft'
}`}
>
<Icon className={`w-6 h-6 mb-2 ${isSelected ? 'text-primary-600' : 'text-secondary-400'}`} />
<p className={`font-semibold text-sm ${isSelected ? 'text-primary-800' : 'text-secondary-700'}`}>
{option.label}
</p>
<p className="text-xs text-secondary-400 mt-0.5">{option.desc}</p>
</button>
)
})}
</div>
{/* Schedule-specific options */}
<div className="bg-cream-50/50 rounded-card p-5 border border-cream-200/60">
{scheduleType === 'FIXED_TIMES' && (
<div className="space-y-4">
<label className="block text-sm font-medium text-secondary-700">
Times to take each day
</label>
<div className="space-y-3">
{times.map((time, index) => (
<div key={index} className="flex gap-2 items-center">
<div className="flex-1 relative">
<Clock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-secondary-400" />
<input
type="time"
value={time}
onChange={(e) => updateTime(index, e.target.value)}
className="input-sanctuary w-full pl-10"
/>
</div>
{times.length > 1 && (
<button
type="button"
onClick={() => removeTime(index)}
className="w-10 h-10 rounded-button bg-cream-100 hover:bg-cream-200 flex items-center justify-center text-secondary-500 transition-colors"
>
<X className="w-5 h-5" />
</button>
)}
</div>
))}
</div>
<button
type="button"
onClick={addTime}
className="btn-secondary w-full flex items-center justify-center gap-2 text-sm"
>
<Plus className="w-4 h-4" />
Add another time
</button>
</div>
)}
{scheduleType === 'INTERVAL' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Every (hours)
</label>
<input
type="number"
min={1}
max={72}
value={intervalHours}
onChange={(e) => setIntervalHours(parseInt(e.target.value) || 1)}
className="input-sanctuary w-full"
/>
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Starting at
</label>
<div className="relative">
<Clock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-secondary-400" />
<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="input-sanctuary w-full pl-10"
/>
</div>
</div>
</div>
<p className="text-sm text-secondary-500">
Example: Every {intervalHours} hours starting at {startTime}
</p>
</div>
)}
{scheduleType === 'WEEKDAYS' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-3">
Which days?
</label>
<div className="grid grid-cols-7 gap-2">
{weekdays.map((day) => {
const isSelected = selectedDays.includes(day.value)
return (
<button
key={day.value}
type="button"
onClick={() => toggleDay(day.value)}
className={`aspect-square rounded-button text-sm font-medium transition-all duration-200 ${
isSelected
? 'bg-primary-500 text-white shadow-lg scale-105'
: 'bg-cream-100 text-secondary-600 hover:bg-cream-200'
}`}
title={day.full}
>
{day.label}
</button>
)
})}
</div>
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
At what time?
</label>
<div className="relative">
<Clock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-secondary-400" />
<input
type="time"
value={weekdayTime}
onChange={(e) => setWeekdayTime(e.target.value)}
className="input-sanctuary w-full pl-10"
/>
</div>
</div>
</div>
)}
{scheduleType === 'PRN' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Minimum hours between doses
</label>
<input
type="number"
min={0.5}
max={72}
step={0.5}
value={minHoursBetween}
onChange={(e) => setMinHoursBetween(parseFloat(e.target.value) || 4)}
className="input-sanctuary w-full"
/>
</div>
<p className="text-sm text-secondary-500">
Shows "Available" when enough time has passed since your last dose
</p>
</div>
)}
</div>
</div>
{/* Refill Tracking Section */}
<div className="section-warm space-y-5">
<h3 className="font-display text-lg text-secondary-900 flex items-center gap-2">
<span className="w-8 h-8 rounded-full bg-cream-200 flex items-center justify-center text-secondary-600 text-sm font-semibold">
<Package className="w-4 h-4" />
</span>
Refill Tracking
<span className="text-sm font-normal text-secondary-400 ml-auto">Optional</span>
</h3>
<div className="flex items-center gap-3">
<input <input
type="checkbox" type="checkbox"
id="trackRefills" id="trackRefills"
checked={trackRefills} checked={trackRefills}
onChange={(e) => setTrackRefills(e.target.checked)} onChange={(e) => setTrackRefills(e.target.checked)}
className="w-5 h-5 rounded border-border text-primary-600 focus:ring-primary-500" className="w-5 h-5 rounded border-cream-300 text-primary-500 focus:ring-primary-400"
/> />
<label htmlFor="trackRefills" className="text-sm font-medium text-secondary-700"> <label htmlFor="trackRefills" className="text-sm text-secondary-700">
Track pill count for refill reminders (optional) Track pill count and get refill reminders
</label> </label>
</div> </div>
{trackRefills && ( {trackRefills && (
<div className="space-y-4 pl-8"> <div className="space-y-4 pt-2 pl-8 border-l-2 border-cream-200">
<Input <div>
label="Current pill count" <label className="block text-sm font-medium text-secondary-700 mb-2">
type="number" Current pill count
min={0} </label>
value={pillCount} <input
onChange={(e) => setPillCount(e.target.value === '' ? '' : parseInt(e.target.value))}
placeholder="e.g., 30"
helperText="How many pills do you have now?"
/>
<div className="grid grid-cols-2 gap-3">
<Input
label="Pills per dose"
type="number"
min={1}
value={pillsPerDose}
onChange={(e) => setPillsPerDose(parseInt(e.target.value) || 1)}
/>
<Input
label="Alert when below"
type="number" type="number"
min={0} min={0}
value={refillThreshold} value={pillCount}
onChange={(e) => setRefillThreshold(parseInt(e.target.value) || 7)} onChange={(e) => setPillCount(e.target.value === '' ? '' : parseInt(e.target.value))}
helperText="pills" placeholder="e.g., 30"
className="input-sanctuary w-full"
/> />
<p className="text-xs text-secondary-400 mt-1.5">How many pills do you have now?</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Pills per dose
</label>
<input
type="number"
min={1}
value={pillsPerDose}
onChange={(e) => setPillsPerDose(parseInt(e.target.value) || 1)}
className="input-sanctuary w-full"
/>
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Alert when below
</label>
<input
type="number"
min={0}
value={refillThreshold}
onChange={(e) => setRefillThreshold(parseInt(e.target.value) || 7)}
className="input-sanctuary w-full"
/>
<p className="text-xs text-secondary-400 mt-1.5">pills remaining</p>
</div>
</div> </div>
</div> </div>
)} )}
</div> </div>
{error && ( {error && (
<p className="text-sm text-red-600 bg-red-50 px-3 py-2 rounded-button"> <div className="bg-alert-50 border border-alert-200 rounded-card p-4">
{error} <p className="text-sm text-alert-700">{error}</p>
</p> </div>
)} )}
<div className="flex gap-3 pt-2"> {/* Action Buttons */}
<Button <div className="flex gap-3 pt-4">
<button
type="button" type="button"
variant="secondary"
fullWidth
onClick={() => router.back()} onClick={() => router.back()}
className="btn-secondary flex-1"
> >
Cancel Cancel
</Button> </button>
<Button type="submit" fullWidth loading={loading}> <button
{isEditing ? 'Update Medication' : 'Save Medication'} type="submit"
</Button> disabled={loading}
className="btn-primary flex-1 disabled:opacity-50"
>
{loading ? 'Saving...' : isEditing ? 'Update Medication' : 'Save Medication'}
</button>
</div> </div>
</form> </form>
</Card> </div>
) )
} }

View File

@@ -9,55 +9,93 @@ const config: Config = {
theme: { theme: {
extend: { extend: {
colors: { colors: {
// Calm, healing palette // Warm Sanctuary - Primary: Soft sage green (healing, calm)
primary: { primary: {
50: '#f0f9f4', 50: '#f4f7f4',
100: '#dcf1e4', 100: '#e3ebe3',
200: '#bbe3cc', 200: '#c5d9c5',
300: '#8dcda8', 300: '#9bbf9b',
400: '#5bb17f', 400: '#729f72',
500: '#3a9563', 500: '#528252',
600: '#2a784e', 600: '#3f663f',
700: '#235f40', 700: '#345234',
800: '#1f4c35', 800: '#2b412b',
900: '#1b3f2d', 900: '#243624',
950: '#0d2319', 950: '#121f12',
}, },
// Warm neutrals - cream, stone, warm gray
cream: {
50: '#fdfcfa',
100: '#faf7f2',
200: '#f5efe6',
300: '#ede3d5',
400: '#e0d0bc',
500: '#d4bfa3',
600: '#c4a882',
700: '#a88b65',
800: '#8a7255',
900: '#705d47',
950: '#3d3226',
},
// Secondary: Warm stone gray (sophisticated, grounded)
secondary: { secondary: {
50: '#f5f7fa', 50: '#f8f7f6',
100: '#ebeef3', 100: '#f0eeeb',
200: '#d2dae5', 200: '#e0dcd5',
300: '#aab9ce', 300: '#ccc6bb',
400: '#7c93b3', 400: '#b5ad9f',
500: '#5c769a', 500: '#a09484',
600: '#485e80', 600: '#857a6d',
700: '#3b4d68', 700: '#6d6359',
800: '#344257', 800: '#5a524a',
900: '#2f3a4a', 900: '#4a443f',
950: '#1f2631', 950: '#262320',
}, },
// Accent: Terracotta (warmth, energy, gentle urgency)
accent: { accent: {
50: '#fef6ee', 50: '#fdf8f6',
100: '#fdebd7', 100: '#faeee9',
200: '#fad3ae', 200: '#f5dcd2',
300: '#f6b37b', 300: '#ecc0b0',
400: '#f18946', 400: '#e09b82',
500: '#ed6b22', 500: '#d67b58',
600: '#de5118', 600: '#c6603e',
700: '#b83c16', 700: '#a54c30',
800: '#93311a', 800: '#88402b',
900: '#772b18', 900: '#703728',
950: '#40130b', 950: '#3d1a11',
}, },
background: '#fafbfc', // Alert red (emergency - softened)
alert: {
50: '#fdf5f4',
100: '#fce8e6',
200: '#f9d5d2',
300: '#f4b7b1',
400: '#ec8c85',
500: '#e0635a',
600: '#c9453d',
700: '#a83832',
800: '#8b322e',
900: '#742f2c',
950: '#3e1514',
},
// Semantic aliases
background: '#faf7f2',
surface: '#ffffff', surface: '#ffffff',
muted: '#f1f5f9', muted: '#f0eeeb',
border: '#e2e8f0', border: '#e0dcd5',
}, },
fontFamily: { fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'], // Playfair Display for elegant headings
display: ['Playfair Display', 'Georgia', 'serif'],
// Source Sans 3 for warm, readable body text
sans: ['Source Sans 3', 'system-ui', 'sans-serif'],
}, },
fontSize: { fontSize: {
'display-xl': ['3.5rem', { lineHeight: '1.1', letterSpacing: '-0.02em' }],
'display-lg': ['2.75rem', { lineHeight: '1.15', letterSpacing: '-0.02em' }],
'display-md': ['2.25rem', { lineHeight: '1.2', letterSpacing: '-0.01em' }],
'display-sm': ['1.875rem', { lineHeight: '1.25', letterSpacing: '-0.01em' }],
// Large text mode sizes // Large text mode sizes
'lg-base': '1.125rem', 'lg-base': '1.125rem',
'lg-lg': '1.25rem', 'lg-lg': '1.25rem',
@@ -67,17 +105,61 @@ const config: Config = {
}, },
spacing: { spacing: {
// Touch-friendly spacing // Touch-friendly spacing
'touch': '44px', 'touch': '48px',
'touch-lg': '56px', 'touch-lg': '60px',
}, },
borderRadius: { borderRadius: {
'card': '16px', 'card': '20px',
'button': '12px', 'card-lg': '28px',
'button': '14px',
'pill': '9999px',
}, },
boxShadow: { boxShadow: {
'card': '0 1px 3px 0 rgb(0 0 0 / 0.04), 0 1px 2px -1px rgb(0 0 0 / 0.04)', // Soft, warm shadows
'card-hover': '0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05)', 'card': '0 2px 8px -2px rgba(93, 82, 70, 0.06), 0 4px 16px -4px rgba(93, 82, 70, 0.04)',
'button': '0 1px 2px 0 rgb(0 0 0 / 0.05)', 'card-hover': '0 8px 24px -4px rgba(93, 82, 70, 0.08), 0 4px 12px -2px rgba(93, 82, 70, 0.05)',
'button': '0 1px 3px rgba(93, 82, 70, 0.08)',
'button-hover': '0 4px 12px -2px rgba(93, 82, 70, 0.15)',
'soft': '0 2px 16px rgba(93, 82, 70, 0.06)',
'elevated': '0 8px 32px -4px rgba(93, 82, 70, 0.1)',
},
animation: {
// Gentle, breathing animations
'breathe': 'breathe 4s ease-in-out infinite',
'fade-up': 'fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards',
'fade-in': 'fadeIn 0.4s ease-out forwards',
'scale-in': 'scaleIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards',
'slide-up': 'slideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards',
'pulse-soft': 'pulseSoft 2s ease-in-out infinite',
},
keyframes: {
breathe: {
'0%, 100%': { opacity: '1', transform: 'scale(1)' },
'50%': { opacity: '0.9', transform: 'scale(1.02)' },
},
fadeUp: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
scaleIn: {
'0%': { opacity: '0', transform: 'scale(0.95)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(30px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
pulseSoft: {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0.7' },
},
},
transitionTimingFunction: {
'sanctuary': 'cubic-bezier(0.16, 1, 0.3, 1)',
}, },
}, },
}, },