mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-24 21:31:43 +08:00
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:
@@ -2,26 +2,27 @@
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
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'
|
||||
|
||||
type ScheduleType = 'FIXED_TIMES' | 'INTERVAL' | 'WEEKDAYS' | 'PRN'
|
||||
|
||||
const scheduleTypeOptions = [
|
||||
{ value: 'FIXED_TIMES', label: 'Fixed times daily' },
|
||||
{ value: 'INTERVAL', label: 'Every X hours' },
|
||||
{ value: 'WEEKDAYS', label: 'Specific days of week' },
|
||||
{ value: 'PRN', label: 'As needed (PRN)' },
|
||||
{ value: 'FIXED_TIMES', label: 'Fixed times daily', icon: Clock, desc: 'Same times every day' },
|
||||
{ value: 'INTERVAL', label: 'Every X hours', icon: Repeat, desc: 'Regular intervals' },
|
||||
{ value: 'WEEKDAYS', label: 'Specific days', icon: Calendar, desc: 'Certain days of the week' },
|
||||
{ value: 'PRN', label: 'As needed (PRN)', icon: Pill, desc: 'When you need it' },
|
||||
]
|
||||
|
||||
const weekdays = [
|
||||
{ value: 0, label: 'Sun' },
|
||||
{ value: 1, label: 'Mon' },
|
||||
{ value: 2, label: 'Tue' },
|
||||
{ value: 3, label: 'Wed' },
|
||||
{ value: 4, label: 'Thu' },
|
||||
{ value: 5, label: 'Fri' },
|
||||
{ value: 6, label: 'Sat' },
|
||||
{ value: 0, label: 'Sun', full: 'Sunday' },
|
||||
{ value: 1, label: 'Mon', full: 'Monday' },
|
||||
{ value: 2, label: 'Tue', full: 'Tuesday' },
|
||||
{ value: 3, label: 'Wed', full: 'Wednesday' },
|
||||
{ value: 4, label: 'Thu', full: 'Thursday' },
|
||||
{ value: 5, label: 'Fri', full: 'Friday' },
|
||||
{ value: 6, label: 'Sat', full: 'Saturday' },
|
||||
]
|
||||
|
||||
interface MedicationFormProps {
|
||||
@@ -42,6 +43,7 @@ interface MedicationFormProps {
|
||||
export function MedicationForm({ initialData, isEditing = false }: MedicationFormProps) {
|
||||
const router = useRouter()
|
||||
const { currentWorkspace, refreshData } = useApp()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
const [name, setName] = useState(initialData?.name || '')
|
||||
const [instructions, setInstructions] = useState(initialData?.instructions || '')
|
||||
@@ -72,9 +74,12 @@ export function MedicationForm({ initialData, isEditing = false }: MedicationFor
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
// Reset defaults if switching types and no initial data for that type
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
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])
|
||||
|
||||
@@ -134,13 +139,11 @@ export function MedicationForm({ initialData, isEditing = false }: MedicationFor
|
||||
scheduleType,
|
||||
scheduleData: buildScheduleData(),
|
||||
active: true,
|
||||
// Refill tracking
|
||||
...(trackRefills && pillCount !== '' && {
|
||||
pillCount: Number(pillCount),
|
||||
pillsPerDose,
|
||||
refillThreshold,
|
||||
}),
|
||||
// Explicitly nullify if disabled during edit
|
||||
...(isEditing && !trackRefills && {
|
||||
pillCount: 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 (
|
||||
<Card>
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<Input
|
||||
label="Medication Name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g., Paracetamol 500mg"
|
||||
required
|
||||
/>
|
||||
<div className={`space-y-6 transition-all duration-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
|
||||
{/* Form Header */}
|
||||
<div className="text-center mb-8">
|
||||
<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>
|
||||
|
||||
<Textarea
|
||||
label="Instructions (optional)"
|
||||
value={instructions}
|
||||
onChange={(e) => setInstructions(e.target.value)}
|
||||
placeholder="e.g., Take with food"
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<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' && (
|
||||
<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">
|
||||
Days
|
||||
Medication Name
|
||||
</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{weekdays.map((day) => (
|
||||
<button
|
||||
key={day.value}
|
||||
type="button"
|
||||
onClick={() => toggleDay(day.value)}
|
||||
className={`px-3 py-2 rounded-button text-sm font-medium transition-colors ${
|
||||
selectedDays.includes(day.value)
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-muted text-secondary-600 hover:bg-secondary-200'
|
||||
}`}
|
||||
>
|
||||
{day.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g., Paracetamol 500mg"
|
||||
className="input-sanctuary w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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}
|
||||
onChange={(e) => setInstructions(e.target.value)}
|
||||
placeholder="e.g., Take with food, Avoid grapefruit..."
|
||||
rows={2}
|
||||
className="input-sanctuary w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Time"
|
||||
type="time"
|
||||
value={weekdayTime}
|
||||
onChange={(e) => setWeekdayTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{scheduleType === 'PRN' && (
|
||||
<Input
|
||||
label="Minimum hours between doses"
|
||||
type="number"
|
||||
min={0.5}
|
||||
max={72}
|
||||
step={0.5}
|
||||
value={minHoursBetween}
|
||||
onChange={(e) => setMinHoursBetween(parseFloat(e.target.value) || 4)}
|
||||
helperText="Shows 'Available' when enough time has passed since last dose"
|
||||
/>
|
||||
)}
|
||||
{/* Schedule 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">2</span>
|
||||
Schedule
|
||||
</h3>
|
||||
|
||||
{/* Refill Tracking (optional) */}
|
||||
<div className="border-t border-border pt-5">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
{/* 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 */}
|
||||
<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
|
||||
type="checkbox"
|
||||
id="trackRefills"
|
||||
checked={trackRefills}
|
||||
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">
|
||||
Track pill count for refill reminders (optional)
|
||||
<label htmlFor="trackRefills" className="text-sm text-secondary-700">
|
||||
Track pill count and get refill reminders
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{trackRefills && (
|
||||
<div className="space-y-4 pl-8">
|
||||
<Input
|
||||
label="Current pill count"
|
||||
type="number"
|
||||
min={0}
|
||||
value={pillCount}
|
||||
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"
|
||||
<div className="space-y-4 pt-2 pl-8 border-l-2 border-cream-200">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-secondary-700 mb-2">
|
||||
Current pill count
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={refillThreshold}
|
||||
onChange={(e) => setRefillThreshold(parseInt(e.target.value) || 7)}
|
||||
helperText="pills"
|
||||
value={pillCount}
|
||||
onChange={(e) => setPillCount(e.target.value === '' ? '' : parseInt(e.target.value))}
|
||||
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>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-red-600 bg-red-50 px-3 py-2 rounded-button">
|
||||
{error}
|
||||
</p>
|
||||
<div className="bg-alert-50 border border-alert-200 rounded-card p-4">
|
||||
<p className="text-sm text-alert-700">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={() => router.back()}
|
||||
className="btn-secondary flex-1"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" fullWidth loading={loading}>
|
||||
{isEditing ? 'Update Medication' : 'Save Medication'}
|
||||
</Button>
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn-primary flex-1 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Saving...' : isEditing ? 'Update Medication' : 'Save Medication'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user