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,60 +65,76 @@ 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"
> >
<div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center">
<ArrowLeft className="w-5 h-5" /> <ArrowLeft className="w-5 h-5" />
<span>Back</span> </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 ? (
<div className="animate-fade-up">
<EmergencyCard info={emergencyInfo} medications={medsList} /> <EmergencyCard info={emergencyInfo} medications={medsList} />
) : (
<div className="text-center py-12">
<div className="w-16 h-16 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4">
<Edit2 className="w-8 h-8 text-red-400" />
</div> </div>
<h2 className="text-lg font-semibold text-secondary-900 mb-2"> ) : (
<div className="section-warm text-center py-12 animate-fade-up">
<div className="w-20 h-20 rounded-full bg-alert-100 flex items-center justify-center mx-auto mb-6">
<Heart className="w-10 h-10 text-alert-400" />
</div>
<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">
<div className="w-2 h-2 rounded-full bg-primary-500 animate-pulse" />
<p className="text-sm text-primary-700 font-medium">
This information is available offline This information is available offline
</p> </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 */}
<div className="blob blob-primary w-32 h-32 -top-4 -left-4" />
<div className="relative">
<p className="text-secondary-500 text-sm font-medium tracking-wide uppercase mb-1">
{format(toZonedTime(now, TIMEZONE), 'EEEE, MMMM d')} {format(toZonedTime(now, TIMEZONE), 'EEEE, MMMM d')}
</p> </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">
<AlertTriangle className="w-6 h-6 text-white" />
</div> </div>
<div className="text-left"> <div className="text-left">
<p className="font-medium text-red-800">Emergency</p> <p className="font-semibold text-alert-800">Emergency</p>
<p className="text-sm text-red-600">Medical info</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">
<Phone className="w-6 h-6 text-white" />
</div> </div>
<div className="text-left"> <div className="text-left">
<p className="font-medium text-primary-800">Call Clinic</p> <p className="font-semibold text-primary-800">Call Clinic</p>
<p className="text-sm text-primary-600 truncate">{currentWorkspace.clinicPhone}</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> </div>
</Card>
</section> </section>
) )
} }
@@ -311,6 +339,7 @@ export default function TodayPage() {
{/* Refill Alerts */} {/* Refill Alerts */}
{medications && medications.length > 0 && ( {medications && medications.length > 0 && (
<div className={`transition-all duration-700 delay-400 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<RefillAlert <RefillAlert
medications={medications.map(m => ({ medications={medications.map(m => ({
id: m.id, id: m.id,
@@ -319,15 +348,16 @@ export default function TodayPage() {
refillThreshold: m.refillThreshold, 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,102 +62,177 @@ 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">
{/* Decorative blobs */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="blob blob-primary w-96 h-96 -top-48 -right-48" />
<div className="blob blob-accent w-80 h-80 bottom-20 -left-40" />
<div className="blob blob-cream w-64 h-64 top-1/2 right-1/4" />
</div>
<div className={`relative transition-all duration-1000 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
{/* Logo/Icon */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="w-16 h-16 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4"> <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">
<Shield className="w-8 h-8 text-amber-600" /> <Heart className="w-12 h-12 text-white" />
</div>
<h1 className="text-2xl font-bold text-secondary-900">Important Notice</h1>
</div> </div>
<Card className="mb-6"> <h1 className="font-display text-display-md text-secondary-900 mb-2">
<div className="space-y-4 text-secondary-700"> Next Step
<p> </h1>
<strong>Next Step is a tracking tool only.</strong> It helps you and your family <p className="text-secondary-500 text-lg">
stay organized with appointments and medications. Supporting you through every step
</p> </p>
</div>
{/* Disclaimer Card */}
<div className="section-warm mb-6">
<div className="flex items-center gap-3 mb-6">
<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>
<div className="space-y-4 text-secondary-700">
<div className="flex gap-3">
<Sparkles className="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" />
<p> <p>
<strong className="text-red-600">This app does not provide medical advice.</strong>{' '} <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="flex gap-3">
<div className="w-5 h-5 rounded-full bg-alert-100 flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-alert-600 text-xs font-bold">!</span>
</div>
<p>
<strong className="text-alert-600">This app does not provide medical advice.</strong>{' '}
Always consult your healthcare team for medical decisions. Always consult your healthcare team for medical decisions.
</p> </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> <p>
<strong>For emergencies:</strong> Call 000 (Australia) or your local emergency <strong>For emergencies:</strong> Call 000 (Australia) or your local emergency services immediately.
services immediately.
</p> </p>
</div>
<div className="flex gap-3">
<Users className="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" />
<p> <p>
If you have questions about your treatment, contact your clinic directly using the Have questions about your treatment? Contact your clinic directly using the button we'll help you set up.
button we'll help you set up.
</p>
<div className="pt-4 border-t border-border">
<p className="text-sm text-secondary-500">
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> <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>
</div>
</div>
<button
onClick={handleAcceptDisclaimer}
className="btn-primary w-full flex items-center justify-center gap-2 text-lg py-4"
>
I Understand I Understand
<ArrowRight className="w-5 h-5 ml-2" /> <ArrowRight className="w-5 h-5" />
</Button> </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">
<Sparkles className="w-10 h-10 text-white" />
</div>
<h1 className="font-display text-display-sm text-secondary-900 mb-2">
Set Up Your Plan
</h1>
<p className="text-secondary-500 text-lg">Create a workspace to get started</p>
</div>
{/* Form */}
<form onSubmit={handleCreateWorkspace} className="section-warm space-y-6">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Workspace Name
<span className="text-alert-500 ml-1">*</span>
</label>
<input
type="text" type="text"
value={workspaceName} value={workspaceName}
onChange={(e) => setWorkspaceName(e.target.value)} onChange={(e) => setWorkspaceName(e.target.value)}
placeholder="e.g., Grace's Plan" placeholder="e.g., Grace's Plan"
helperText="This is how family members will identify this workspace" className="input-sanctuary w-full"
required required
/> />
<Input <p className="text-xs text-secondary-400 mt-2">
label="Clinic Phone Number" 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" type="tel"
value={clinicPhone} value={clinicPhone}
onChange={(e) => setClinicPhone(e.target.value)} onChange={(e) => setClinicPhone(e.target.value)}
placeholder="e.g., 08 9400 1234" placeholder="e.g., 08 9400 1234"
helperText="We'll add a 'Call Clinic' button for quick access" 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" />
<p className="text-sm text-secondary-700 font-semibold uppercase tracking-wide">
Current Medications
</p>
</div>
<div className="bg-cream-50 rounded-card p-4 border border-cream-200">
<ul className="space-y-3">
{medications.map((med, i) => ( {medications.map((med, i) => (
<li key={i} className="text-secondary-900"> <li key={i} className="flex items-start gap-3">
<span className="font-medium">{med.name}</span> <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 && ( {med.instructions && (
<span className="text-secondary-600"> - {med.instructions}</span> <span className="text-secondary-600"> {med.instructions}</span>
)} )}
</div>
</li> </li>
))} ))}
</ul> </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,20 +174,23 @@ 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 && ( {info.clinicPhone && (
<a <a
href={`tel:${info.clinicPhone}`} href={`tel:${info.clinicPhone}`}
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-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-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-alert-500 to-alert-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">Call Clinic</p>
<p className="text-sm text-secondary-600">{info.clinicPhone}</p> <p className="text-alert-600 font-medium">{info.clinicPhone}</p>
</div> </div>
</a> </a>
)} )}
@@ -159,18 +198,19 @@ export function EmergencyCard({ info, medications, variant = 'full' }: Emergency
{info.emergencyPhone && ( {info.emergencyPhone && (
<a <a
href={`tel:${info.emergencyPhone}`} 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">Emergency Contact</p> <p className="font-semibold text-secondary-900 text-lg">Emergency Contact</p>
<p className="text-sm text-secondary-600">{info.emergencyPhone}</p> <p className="text-secondary-600 font-medium">{info.emergencyPhone}</p>
</div> </div>
</a> </a>
)} )}
</div> </div>
</div>
)} )}
</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,7 +74,10 @@ 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
} }
@@ -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">
<Pill className="w-10 h-10 text-primary-600" />
</div>
<h2 className="font-display text-display-sm text-secondary-900">
{isEditing ? 'Edit Medication' : 'Add Medication'}
</h2>
<p className="text-secondary-500 mt-2">
{isEditing ? 'Update your medication details' : 'Keep track of your medications'}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Basic Info 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-primary-100 flex items-center justify-center text-primary-600 text-sm font-semibold">1</span>
Basic Information
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Medication Name
</label>
<input
type="text" type="text"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="e.g., Paracetamol 500mg" placeholder="e.g., Paracetamol 500mg"
className="input-sanctuary w-full"
required required
/> />
</div>
<Textarea <div>
label="Instructions (optional)" <label className="block text-sm font-medium text-secondary-700 mb-2">
Instructions
<span className="text-secondary-400 font-normal ml-1">(optional)</span>
</label>
<textarea
value={instructions} value={instructions}
onChange={(e) => setInstructions(e.target.value)} onChange={(e) => setInstructions(e.target.value)}
placeholder="e.g., Take with food" placeholder="e.g., Take with food, Avoid grapefruit..."
rows={2} rows={2}
className="input-sanctuary w-full resize-none"
/> />
</div>
</div>
</div>
<Select {/* Schedule Section */}
label="Schedule Type" <div className="section-warm space-y-5">
value={scheduleType} <h3 className="font-display text-lg text-secondary-900 flex items-center gap-2">
onChange={(e) => setScheduleType(e.target.value as ScheduleType)} <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>
options={scheduleTypeOptions} Schedule
/> </h3>
{/* Schedule Type Selector */}
<div className="grid grid-cols-2 gap-3">
{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 */} {/* Schedule-specific options */}
<div className="bg-cream-50/50 rounded-card p-5 border border-cream-200/60">
{scheduleType === 'FIXED_TIMES' && ( {scheduleType === 'FIXED_TIMES' && (
<div className="space-y-3"> <div className="space-y-4">
<label className="block text-sm font-medium text-secondary-700"> <label className="block text-sm font-medium text-secondary-700">
Times to take Times to take each day
</label> </label>
<div className="space-y-3">
{times.map((time, index) => ( {times.map((time, index) => (
<div key={index} className="flex gap-2"> <div key={index} className="flex gap-2 items-center">
<Input <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" type="time"
value={time} value={time}
onChange={(e) => updateTime(index, e.target.value)} onChange={(e) => updateTime(index, e.target.value)}
className="flex-1" className="input-sanctuary w-full pl-10"
/> />
</div>
{times.length > 1 && ( {times.length > 1 && (
<Button <button
type="button" type="button"
variant="ghost"
onClick={() => removeTime(index)} 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"
> >
Remove <X className="w-5 h-5" />
</Button> </button>
)} )}
</div> </div>
))} ))}
<Button type="button" variant="secondary" onClick={addTime} size="sm"> </div>
+ Add time <button
</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> </div>
)} )}
{scheduleType === 'INTERVAL' && ( {scheduleType === 'INTERVAL' && (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-4">
<Input <div>
label="Every (hours)" <label className="block text-sm font-medium text-secondary-700 mb-2">
Every (hours)
</label>
<input
type="number" type="number"
min={1} min={1}
max={72} max={72}
value={intervalHours} value={intervalHours}
onChange={(e) => setIntervalHours(parseInt(e.target.value) || 1)} onChange={(e) => setIntervalHours(parseInt(e.target.value) || 1)}
className="input-sanctuary w-full"
/> />
<Input </div>
label="Starting at" <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" type="time"
value={startTime} value={startTime}
onChange={(e) => setStartTime(e.target.value)} onChange={(e) => setStartTime(e.target.value)}
className="input-sanctuary w-full pl-10"
/> />
</div> </div>
</div> </div>
</div>
<p className="text-sm text-secondary-500">
Example: Every {intervalHours} hours starting at {startTime}
</p>
</div>
)} )}
{scheduleType === 'WEEKDAYS' && ( {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-3">
Days Which days?
</label> </label>
<div className="flex gap-2 flex-wrap"> <div className="grid grid-cols-7 gap-2">
{weekdays.map((day) => ( {weekdays.map((day) => {
const isSelected = selectedDays.includes(day.value)
return (
<button <button
key={day.value} key={day.value}
type="button" type="button"
onClick={() => toggleDay(day.value)} onClick={() => toggleDay(day.value)}
className={`px-3 py-2 rounded-button text-sm font-medium transition-colors ${ className={`aspect-square rounded-button text-sm font-medium transition-all duration-200 ${
selectedDays.includes(day.value) isSelected
? 'bg-primary-500 text-white' ? 'bg-primary-500 text-white shadow-lg scale-105'
: 'bg-muted text-secondary-600 hover:bg-secondary-200' : 'bg-cream-100 text-secondary-600 hover:bg-cream-200'
}`} }`}
title={day.full}
> >
{day.label} {day.label}
</button> </button>
))} )
})}
</div> </div>
</div> </div>
<Input <div>
label="Time" <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" type="time"
value={weekdayTime} value={weekdayTime}
onChange={(e) => setWeekdayTime(e.target.value)} onChange={(e) => setWeekdayTime(e.target.value)}
className="input-sanctuary w-full pl-10"
/> />
</div> </div>
</div>
</div>
)} )}
{scheduleType === 'PRN' && ( {scheduleType === 'PRN' && (
<Input <div className="space-y-4">
label="Minimum hours between doses" <div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Minimum hours between doses
</label>
<input
type="number" type="number"
min={0.5} min={0.5}
max={72} max={72}
step={0.5} step={0.5}
value={minHoursBetween} value={minHoursBetween}
onChange={(e) => setMinHoursBetween(parseFloat(e.target.value) || 4)} onChange={(e) => setMinHoursBetween(parseFloat(e.target.value) || 4)}
helperText="Shows 'Available' when enough time has passed since last dose" 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 (optional) */} {/* Refill Tracking Section */}
<div className="border-t border-border pt-5"> <div className="section-warm space-y-5">
<div className="flex items-center gap-3 mb-4"> <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">
Current pill count
</label>
<input
type="number" type="number"
min={0} min={0}
value={pillCount} value={pillCount}
onChange={(e) => setPillCount(e.target.value === '' ? '' : parseInt(e.target.value))} onChange={(e) => setPillCount(e.target.value === '' ? '' : parseInt(e.target.value))}
placeholder="e.g., 30" placeholder="e.g., 30"
helperText="How many pills do you have now?" className="input-sanctuary w-full"
/> />
<div className="grid grid-cols-2 gap-3"> <p className="text-xs text-secondary-400 mt-1.5">How many pills do you have now?</p>
<Input </div>
label="Pills per dose" <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" type="number"
min={1} min={1}
value={pillsPerDose} value={pillsPerDose}
onChange={(e) => setPillsPerDose(parseInt(e.target.value) || 1)} onChange={(e) => setPillsPerDose(parseInt(e.target.value) || 1)}
className="input-sanctuary w-full"
/> />
<Input </div>
label="Alert when below" <div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Alert when below
</label>
<input
type="number" type="number"
min={0} min={0}
value={refillThreshold} value={refillThreshold}
onChange={(e) => setRefillThreshold(parseInt(e.target.value) || 7)} onChange={(e) => setRefillThreshold(parseInt(e.target.value) || 7)}
helperText="pills" 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)',
}, },
}, },
}, },