203 lines
6.7 KiB
TypeScript
203 lines
6.7 KiB
TypeScript
import React from 'react';
|
|
import { Medication, Dose, DoseStatus } from '../../types';
|
|
import {
|
|
ClockIcon,
|
|
CheckCircleIcon,
|
|
XCircleIcon,
|
|
InfoIcon,
|
|
getMedicationIcon,
|
|
ZzzIcon,
|
|
} from '../icons/Icons';
|
|
|
|
interface DoseCardProps {
|
|
dose: Dose & { takenAt?: string };
|
|
medication: Medication;
|
|
status: DoseStatus;
|
|
onToggleDose: (doseId: string) => void;
|
|
onSnooze: (doseId: string) => void;
|
|
snoozedUntil?: Date;
|
|
}
|
|
|
|
const statusStyles = {
|
|
[DoseStatus.UPCOMING]: {
|
|
bg: 'bg-white dark:bg-slate-800',
|
|
icon: <ClockIcon className='w-6 h-6 text-slate-400 dark:text-slate-500' />,
|
|
text: 'text-slate-500 dark:text-slate-400',
|
|
button:
|
|
'border-indigo-600 text-indigo-600 hover:bg-indigo-600 hover:text-white dark:text-indigo-400 dark:border-indigo-400 dark:hover:bg-indigo-400 dark:hover:text-white',
|
|
buttonText: 'Take',
|
|
ring: 'hover:ring-indigo-300 dark:hover:ring-indigo-500',
|
|
},
|
|
[DoseStatus.TAKEN]: {
|
|
bg: 'bg-green-50 dark:bg-green-900/20',
|
|
icon: (
|
|
<CheckCircleIcon className='w-6 h-6 text-green-500 dark:text-green-400' />
|
|
),
|
|
text: 'text-green-700 dark:text-green-400',
|
|
button:
|
|
'border-green-500 text-green-500 hover:bg-green-500 hover:text-white dark:text-green-400 dark:border-green-400 dark:hover:bg-green-400 dark:hover:text-slate-900',
|
|
buttonText: 'Untake',
|
|
ring: '',
|
|
},
|
|
[DoseStatus.MISSED]: {
|
|
bg: 'bg-red-50 dark:bg-red-900/20',
|
|
icon: <XCircleIcon className='w-6 h-6 text-red-500 dark:text-red-400' />,
|
|
text: 'text-red-700 dark:text-red-400',
|
|
button:
|
|
'border-red-500 text-red-500 hover:bg-red-500 hover:text-white dark:text-red-400 dark:border-red-400 dark:hover:bg-red-400 dark:hover:text-slate-900',
|
|
buttonText: 'Take Now',
|
|
ring: '',
|
|
},
|
|
[DoseStatus.SNOOZED]: {
|
|
bg: 'bg-amber-50 dark:bg-amber-900/20',
|
|
icon: <ZzzIcon className='w-6 h-6 text-amber-500 dark:text-amber-400' />,
|
|
text: 'text-amber-700 dark:text-amber-400',
|
|
button:
|
|
'border-indigo-600 text-indigo-600 hover:bg-indigo-600 hover:text-white dark:text-indigo-400 dark:border-indigo-400 dark:hover:bg-indigo-400 dark:hover:text-white',
|
|
buttonText: 'Take',
|
|
ring: '',
|
|
},
|
|
};
|
|
|
|
const statusLabels: Record<DoseStatus, string> = {
|
|
[DoseStatus.UPCOMING]: 'Upcoming dose',
|
|
[DoseStatus.TAKEN]: 'Dose taken',
|
|
[DoseStatus.MISSED]: 'Dose missed',
|
|
[DoseStatus.SNOOZED]: 'Dose snoozed',
|
|
};
|
|
|
|
const DoseCard: React.FC<DoseCardProps> = ({
|
|
dose,
|
|
medication,
|
|
status,
|
|
onToggleDose,
|
|
onSnooze,
|
|
snoozedUntil,
|
|
}) => {
|
|
const styles = statusStyles[status];
|
|
const statusLabel = statusLabels[status];
|
|
const timeString = dose.scheduledTime.toLocaleTimeString([], {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
const takenTimeString = dose.takenAt
|
|
? new Date(dose.takenAt).toLocaleTimeString([], {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
: '';
|
|
const snoozedTimeString = snoozedUntil
|
|
? snoozedUntil.toLocaleTimeString([], {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
: '';
|
|
const MedicationIcon = getMedicationIcon(medication.icon);
|
|
const cardTitleId = `dose-${dose.id}-title`;
|
|
const statusId = `dose-${dose.id}-status`;
|
|
|
|
const handleCardKeyDown = (
|
|
event: React.KeyboardEvent<HTMLLIElement>
|
|
): void => {
|
|
if (event.target !== event.currentTarget) {
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
onToggleDose(dose.id);
|
|
}
|
|
|
|
if (
|
|
(event.key.toLowerCase() === 's' || event.key === 'S') &&
|
|
status === DoseStatus.UPCOMING
|
|
) {
|
|
event.preventDefault();
|
|
onSnooze(dose.id);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<li
|
|
role='group'
|
|
tabIndex={0}
|
|
onKeyDown={handleCardKeyDown}
|
|
aria-labelledby={cardTitleId}
|
|
aria-describedby={statusId}
|
|
className={`shadow-md rounded-lg p-4 flex flex-col justify-between transition-all duration-300 ${styles.bg} ${styles.ring} ring-4 ring-transparent border border-slate-200 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-400 dark:focus:ring-offset-slate-900`}
|
|
>
|
|
<div>
|
|
<div className='flex justify-between items-start'>
|
|
<div className='flex items-center space-x-3'>
|
|
<MedicationIcon className='w-7 h-7 text-indigo-500 dark:text-indigo-400 flex-shrink-0' />
|
|
<div>
|
|
<h4
|
|
id={cardTitleId}
|
|
className='font-bold text-lg text-slate-800 dark:text-slate-100'
|
|
>
|
|
{medication.name}
|
|
</h4>
|
|
<p className='text-slate-600 dark:text-slate-300'>
|
|
{medication.dosage}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{styles.icon}
|
|
</div>
|
|
<div
|
|
id={statusId}
|
|
role='status'
|
|
aria-live='polite'
|
|
aria-atomic='true'
|
|
className={`flex items-center space-x-2 mt-4 font-semibold text-lg ${styles.text}`}
|
|
>
|
|
<ClockIcon className='w-5 h-5' />
|
|
<span className='sr-only'>{statusLabel}</span>
|
|
<span>{timeString}</span>
|
|
</div>
|
|
|
|
{status === DoseStatus.SNOOZED && (
|
|
<p className='text-sm text-amber-600 dark:text-amber-500 mt-1'>
|
|
Snoozed until {snoozedTimeString}
|
|
</p>
|
|
)}
|
|
|
|
{status === DoseStatus.TAKEN && (
|
|
<p className='text-sm text-green-600 dark:text-green-500 mt-1'>
|
|
Taken at {takenTimeString}
|
|
</p>
|
|
)}
|
|
|
|
{medication.notes && (
|
|
<div className='mt-3 p-2 bg-indigo-50 dark:bg-indigo-900/30 rounded-lg flex items-start space-x-2'>
|
|
<InfoIcon className='w-4 h-4 text-indigo-500 dark:text-indigo-400 mt-0.5 flex-shrink-0' />
|
|
<p className='text-sm text-indigo-800 dark:text-indigo-200'>
|
|
{medication.notes}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className='mt-4 flex items-center space-x-2'>
|
|
{status === DoseStatus.UPCOMING && (
|
|
<button
|
|
onClick={() => onSnooze(dose.id)}
|
|
className='w-1/3 py-2 px-2 rounded-lg font-semibold border-2 transition-colors duration-200 border-slate-300 text-slate-500 hover:bg-slate-100 dark:border-slate-600 dark:text-slate-400 dark:hover:bg-slate-700'
|
|
aria-label={`Snooze ${medication.name} for 5 minutes`}
|
|
>
|
|
<ZzzIcon className='w-5 h-5 mx-auto' />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => onToggleDose(dose.id)}
|
|
className={`w-full py-2 px-4 rounded-lg font-semibold border-2 transition-colors duration-200 ${styles.button}`}
|
|
aria-label={`${styles.buttonText} ${medication.name} at ${timeString}`}
|
|
>
|
|
{styles.buttonText}
|
|
</button>
|
|
</div>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
export default DoseCard;
|