diff --git a/frontend/src/components/EventDetailModal.jsx b/frontend/src/components/EventDetailModal.jsx new file mode 100644 index 0000000..6be39ad --- /dev/null +++ b/frontend/src/components/EventDetailModal.jsx @@ -0,0 +1,84 @@ +import { useDeleteEvent } from '../api/queries.js'; +import { useConfirm } from './ConfirmDialogProvider.jsx'; +import { useTranslation } from '../i18n/I18nContext.jsx'; +import Icon from './Icon.jsx'; + +function dateOnly(value) { + return value ? value.slice(0, 10) : ''; +} + +function timeOnly(value) { + return value && value.length > 10 ? value.slice(11, 16) : null; +} + +export default function EventDetailModal({ event, onClose, onEdit }) { + const deleteEvent = useDeleteEvent(); + const confirmDialog = useConfirm(); + const { t } = useTranslation(); + + const startTime = timeOnly(event.start_at); + const endTime = timeOnly(event.end_at); + const multiDay = dateOnly(event.start_at) !== dateOnly(event.end_at); + const dayLabel = multiDay ? `${dateOnly(event.start_at)} – ${dateOnly(event.end_at)}` : dateOnly(event.start_at); + const timeLabel = event.all_day + ? `${t('calendar.allDay')} · ${dayLabel}` + : `${dayLabel} · ${startTime} – ${endTime}`; + + async function handleDelete() { + const ok = await confirmDialog({ + title: t('eventFormModal.deleteConfirmTitle'), + message: t('eventFormModal.deleteConfirmMessage'), + confirmLabel: t('common.delete'), + }); + if (!ok) return; + await deleteEvent.mutateAsync(event.id); + onClose(); + } + + return ( +
+
e.stopPropagation()}> +
+

{event.title}

+ +
+ +
+
+ + {timeLabel} +
+ {event.location && ( +
+ + {event.location} +
+ )} + {event.attendees.length > 0 && ( +
+ + {event.attendees.map((a) => a.name).join(', ')} +
+ )} + {event.notes && ( +
+ + {event.notes} +
+ )} +
+ +
+ + +
+
+
+ ); +} diff --git a/frontend/src/pages/Calendar.jsx b/frontend/src/pages/Calendar.jsx index 550e20d..0e4d0ec 100644 --- a/frontend/src/pages/Calendar.jsx +++ b/frontend/src/pages/Calendar.jsx @@ -5,6 +5,7 @@ import { useAuth } from '../auth/AuthContext.jsx'; import { useTranslation } from '../i18n/I18nContext.jsx'; import EventListItem from '../components/EventListItem.jsx'; import EventFormModal from '../components/EventFormModal.jsx'; +import EventDetailModal from '../components/EventDetailModal.jsx'; import Icon from '../components/Icon.jsx'; const WEEKDAY_LABELS = { @@ -48,6 +49,7 @@ export default function Calendar() { return new Date(now.getFullYear(), now.getMonth(), 1); }); const [selectedDate, setSelectedDate] = useState(todayStr()); + const [viewingEvent, setViewingEvent] = useState(null); const [editingEvent, setEditingEvent] = useState(null); const [showCreate, setShowCreate] = useState(false); @@ -139,7 +141,7 @@ export default function Calendar() { {isLoading &&

{t('common.loading')}

} {!isLoading && selectedEvents.length === 0 &&

{t('calendar.emptyState')}

} {selectedEvents.map((event) => ( - setEditingEvent(event)} /> + setViewingEvent(event)} /> ))} @@ -152,13 +154,24 @@ export default function Calendar() { /> )} + {viewingEvent && !editingEvent && ( + setViewingEvent(null)} + onEdit={() => setEditingEvent(viewingEvent)} + /> + )} + {editingEvent && ( setEditingEvent(null)} + onClose={() => { + setEditingEvent(null); + setViewingEvent(null); + }} /> )} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 14c2278..cac59a7 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -415,6 +415,11 @@ input, select, button, textarea { font-family: inherit; font-size: 1rem; color: } .split-slider-amounts { display: flex; gap: 10px; } .split-slider-amounts .field { flex: 1; } +.split-slider-amounts .field label { + height: 1.2em; + line-height: 1.2; + white-space: nowrap; +} /* History */ .filters { display: flex; flex-direction: column; gap: 10px; margin-bottom: 16px; } @@ -615,6 +620,10 @@ input, select, button, textarea { font-family: inherit; font-size: 1rem; color: .modal-actions { display: flex; gap: 10px; } .modal-actions button { flex: 1; } +.event-detail-body { display: flex; flex-direction: column; gap: 14px; margin-bottom: 20px; } +.event-detail-row { display: flex; align-items: flex-start; gap: 10px; color: var(--text); } +.event-detail-row .material-symbols-outlined { color: var(--muted); font-size: 1.2rem; flex-shrink: 0; } + .modal-overlay { position: fixed; inset: 0;