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;