diff --git a/apps/calendar/apps/web/src/lib/api/calendars.ts b/apps/calendar/apps/web/src/lib/api/calendars.ts index 3c46b5457..4b6197430 100644 --- a/apps/calendar/apps/web/src/lib/api/calendars.ts +++ b/apps/calendar/apps/web/src/lib/api/calendars.ts @@ -14,17 +14,25 @@ export async function getCalendar(id: string) { } export async function createCalendar(data: CreateCalendarInput) { - return fetchApi('/calendars', { + const result = await fetchApi<{ calendar: Calendar }>('/calendars', { method: 'POST', body: data, }); + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + return { data: result.data.calendar, error: null }; } export async function updateCalendar(id: string, data: UpdateCalendarInput) { - return fetchApi(`/calendars/${id}`, { + const result = await fetchApi<{ calendar: Calendar }>(`/calendars/${id}`, { method: 'PUT', body: data, }); + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + return { data: result.data.calendar, error: null }; } export async function deleteCalendar(id: string) { diff --git a/apps/calendar/apps/web/src/lib/api/events.ts b/apps/calendar/apps/web/src/lib/api/events.ts index 085c9aa2a..0a0b04d5a 100644 --- a/apps/calendar/apps/web/src/lib/api/events.ts +++ b/apps/calendar/apps/web/src/lib/api/events.ts @@ -23,7 +23,11 @@ export async function getEvents(params: QueryEventsParams) { } export async function getEvent(id: string) { - return fetchApi(`/events/${id}`); + const result = await fetchApi<{ event: CalendarEvent }>(`/events/${id}`); + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + return { data: result.data.event, error: null }; } export async function getEventsByCalendar(calendarId: string) { @@ -31,17 +35,25 @@ export async function getEventsByCalendar(calendarId: string) { } export async function createEvent(data: CreateEventInput) { - return fetchApi('/events', { + const result = await fetchApi<{ event: CalendarEvent }>('/events', { method: 'POST', body: data, }); + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + return { data: result.data.event, error: null }; } export async function updateEvent(id: string, data: UpdateEventInput) { - return fetchApi(`/events/${id}`, { + const result = await fetchApi<{ event: CalendarEvent }>(`/events/${id}`, { method: 'PUT', body: data, }); + if (result.error || !result.data) { + return { data: null, error: result.error }; + } + return { data: result.data.event, error: null }; } export async function deleteEvent(id: string) { diff --git a/apps/calendar/apps/web/src/lib/components/calendar/CalendarHeader.svelte b/apps/calendar/apps/web/src/lib/components/calendar/CalendarHeader.svelte index c9de12e9d..3cff869bd 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/CalendarHeader.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/CalendarHeader.svelte @@ -1,6 +1,7 @@ -
+
{/each} @@ -397,13 +423,16 @@ {#each timedEvents as event} {@const isBeingDragged = isDragging && draggedEvent?.id === event.id} {@const isBeingResized = isResizing && resizeEvent?.id === event.id} + {@const isDraft = eventsStore.isDraftEvent(event.id)}
startDrag(event, e)} - onclick={(e) => handleEventClick(event, e)} + onclick={(e) => !isDraft && handleEventClick(event, e)} role="button" tabindex="0" > @@ -426,7 +455,7 @@ 'HH:mm' )} - {event.title} + {event.title || (isDraft ? '(Neuer Termin)' : '')} {#if event.location} {event.location} {/if} @@ -593,6 +622,21 @@ outline-offset: -2px; } + .event-card.draft { + outline: 2px solid hsl(var(--color-primary)); + outline-offset: -1px; + animation: pulse-outline 1.5s ease-in-out infinite; + } + + @keyframes pulse-outline { + 0%, 100% { + outline-color: hsl(var(--color-primary)); + } + 50% { + outline-color: hsl(var(--color-primary) / 0.5); + } + } + /* Resize Handles */ .resize-handle { position: absolute; diff --git a/apps/calendar/apps/web/src/lib/components/calendar/MonthView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/MonthView.svelte index eb7119004..b9dfc72f7 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/MonthView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/MonthView.svelte @@ -22,9 +22,17 @@ getMinutes, differenceInMinutes, addMinutes, + setHours, + setMinutes, } from 'date-fns'; import { de } from 'date-fns/locale'; + interface Props { + onQuickCreate?: (date: Date, position: { x: number; y: number }) => void; + } + + let { onQuickCreate }: Props = $props(); + // Get all days to display in the month grid (including days from prev/next months) let allCalendarDays = $derived.by(() => { const monthStart = startOfMonth(viewStore.currentDate); @@ -143,10 +151,18 @@ const newEnd = addMinutes(newStart, duration); - eventsStore.updateEvent(draggedEvent.id, { - startTime: newStart.toISOString(), - endTime: newEnd.toISOString(), - }); + // Update event (use updateDraftEvent for draft events) + if (eventsStore.isDraftEvent(draggedEvent.id)) { + eventsStore.updateDraftEvent({ + startTime: newStart.toISOString(), + endTime: newEnd.toISOString(), + }); + } else { + eventsStore.updateEvent(draggedEvent.id, { + startTime: newStart.toISOString(), + endTime: newEnd.toISOString(), + }); + } } cleanup(); @@ -167,11 +183,30 @@ return eventsStore.getEventsForDay(day).slice(0, 3); // Max 3 events shown } - function handleDayClick(day: Date) { + function handleDayClick(day: Date, e: MouseEvent) { // Don't navigate if dragging if (isDragging) return; - viewStore.setDate(day); - viewStore.setViewType('day'); + + // If onQuickCreate callback provided, open quick event overlay + if (onQuickCreate) { + // Set start time to current hour (or 9 AM if in the past) + const now = new Date(); + let startTime = setMinutes(setHours(day, now.getHours()), 0); + + // If the selected day is today and current hour is reasonable, use next hour + if (isSameDay(day, now)) { + startTime = setMinutes(setHours(day, now.getHours() + 1), 0); + } else { + // For other days, default to 9 AM + startTime = setMinutes(setHours(day, 9), 0); + } + + onQuickCreate(startTime, { x: e.clientX, y: e.clientY }); + } else { + // Fallback: navigate to day view + viewStore.setDate(day); + viewStore.setViewType('day'); + } } function handleEventClick(event: any, e: MouseEvent) { @@ -182,7 +217,7 @@ return; } e.stopPropagation(); - goto(`/event/${event.id}`); + goto(`/?event=${event.id}`); } function handleMoreClick(day: Date, e: MouseEvent) { @@ -213,8 +248,8 @@ class:today={isToday(day)} class:drop-target={isDropTarget} use:bindDayCellRef={day} - onclick={() => handleDayClick(day)} - onkeydown={(e) => e.key === 'Enter' && handleDayClick(day)} + onclick={(e) => handleDayClick(day, e)} + onkeydown={(e) => e.key === 'Enter' && handleDayClick(day, e as unknown as MouseEvent)} role="button" tabindex="0" > @@ -225,12 +260,15 @@
{#each getEventsForDay(day) as event} {@const isBeingDragged = isDragging && draggedEvent?.id === event.id} + {@const isDraft = eventsStore.isDraftEvent(event.id)}
startDrag(event, e)} - onclick={(e) => handleEventClick(event, e)} + onclick={(e) => !isDraft && handleEventClick(event, e)} role="button" tabindex="0" > @@ -241,10 +279,15 @@ ? new Date(event.startTime) : event.startTime, 'HH:mm' + )}-{format( + typeof event.endTime === 'string' + ? new Date(event.endTime) + : event.endTime, + 'HH:mm' )} {/if} - {event.title} + {event.title || (isDraft ? '(Neuer Termin)' : '')}
{/each} @@ -379,6 +422,21 @@ transform: scale(0.95); } + .event-pill.draft { + outline: 2px solid hsl(var(--color-primary)); + outline-offset: -1px; + animation: pulse-outline 1.5s ease-in-out infinite; + } + + @keyframes pulse-outline { + 0%, 100% { + outline-color: hsl(var(--color-primary)); + } + 50% { + outline-color: hsl(var(--color-primary) / 0.5); + } + } + .event-time { font-size: 0.65rem; opacity: 0.9; diff --git a/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte index 756593ced..8736a724d 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte @@ -26,8 +26,9 @@ // Props interface Props { dayCount: 5 | 10 | 14; + onQuickCreate?: (date: Date, position: { x: number; y: number }) => void; } - let { dayCount }: Props = $props(); + let { dayCount, onQuickCreate }: Props = $props(); // Get date-fns locale based on current app locale const dateLocales = { de, en: enUS, fr, es, it }; @@ -164,16 +165,21 @@ setTimeout(() => { hasMoved = false; }, 100); return; } - goto(`/event/${event.id}`); + goto(`/?event=${event.id}`); } - function handleSlotClick(day: Date, hour: number) { + function handleSlotClick(day: Date, hour: number, e: MouseEvent) { // Don't create new event if dragging if (isDragging || isResizing) return; const startTime = new Date(day); startTime.setHours(hour, 0, 0, 0); - goto(`/event/new?start=${startTime.toISOString()}`); + + if (onQuickCreate) { + onQuickCreate(startTime, { x: e.clientX, y: e.clientY }); + } else { + goto(`/event/new?start=${startTime.toISOString()}`); + } } // ========== Drag & Drop Functions ========== @@ -278,11 +284,18 @@ const newEnd = addMinutes(newStart, duration); - // Update event via store - await eventsStore.updateEvent(draggedEvent.id, { - startTime: newStart.toISOString(), - endTime: newEnd.toISOString(), - }); + // Update event via store (use updateDraftEvent for draft events) + if (eventsStore.isDraftEvent(draggedEvent.id)) { + eventsStore.updateDraftEvent({ + startTime: newStart.toISOString(), + endTime: newEnd.toISOString(), + }); + } else { + await eventsStore.updateEvent(draggedEvent.id, { + startTime: newStart.toISOString(), + endTime: newEnd.toISOString(), + }); + } // Reset state isDragging = false; @@ -374,11 +387,18 @@ newStart = setMinutes(newStart, newMins); } - // Update event via store - await eventsStore.updateEvent(resizeEvent.id, { - startTime: newStart.toISOString(), - endTime: newEnd.toISOString(), - }); + // Update event via store (use updateDraftEvent for draft events) + if (eventsStore.isDraftEvent(resizeEvent.id)) { + eventsStore.updateDraftEvent({ + startTime: newStart.toISOString(), + endTime: newEnd.toISOString(), + }); + } else { + await eventsStore.updateEvent(resizeEvent.id, { + startTime: newStart.toISOString(), + endTime: newEnd.toISOString(), + }); + } // Reset state isResizing = false; @@ -465,7 +485,7 @@ {#each hours as hour} {/each} @@ -486,10 +506,13 @@ {#each getEventsForDay(day) as event (event.id)} {@const isBeingDragged = isDragging && draggedEvent?.id === event.id} {@const isBeingResized = isResizing && resizeEvent?.id === event.id} + {@const isDraft = eventsStore.isDraftEvent(event.id)}
startDrag(event, e)} - onclick={(e) => handleEventClick(event, e)} - onkeydown={(e) => e.key === 'Enter' && goto(`/event/${event.id}`)} - title={`${formatEventTime(event.startTime)} - ${event.title}`} + onclick={(e) => !isDraft && handleEventClick(event, e)} + onkeydown={(e) => !isDraft && e.key === 'Enter' && goto(`/?event=${event.id}`)} + title={`${formatEventTime(event.startTime)} - ${formatEventTime(event.endTime)}: ${event.title || (isDraft ? '(Neuer Termin)' : '')}`} >
- {formatEventTime(event.startTime)} + {formatEventTime(event.startTime)} - {formatEventTime(event.endTime)} {/if} - {event.title} + {event.title || (isDraft ? '(Neuer Termin)' : '')}
void; + } + + let { onQuickCreate }: Props = $props(); + // Constants const HOUR_HEIGHT = 60; // px - should match CSS --hour-height const MINUTES_PER_SLOT = 15; // Snap to 15-minute intervals @@ -101,11 +106,6 @@ // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) let hasMoved = $state(false); - // Quick Event Overlay State - let showQuickEvent = $state(false); - let quickEventStartTime = $state(null); - let quickEventPosition = $state({ x: 0, y: 0 }); - // Reference to the days container for position calculations let daysContainerEl: HTMLDivElement; @@ -165,7 +165,7 @@ setTimeout(() => { hasMoved = false; }, 100); return; } - goto(`/event/${event.id}`); + goto(`/?event=${event.id}`); } function handleSlotClick(day: Date, hour: number, e: MouseEvent) { @@ -175,15 +175,11 @@ const startTime = new Date(day); startTime.setHours(hour, 0, 0, 0); - // Show quick event overlay at click position - quickEventStartTime = startTime; - quickEventPosition = { x: e.clientX, y: e.clientY }; - showQuickEvent = true; - } - - function closeQuickEvent() { - showQuickEvent = false; - quickEventStartTime = null; + if (onQuickCreate) { + onQuickCreate(startTime, { x: e.clientX, y: e.clientY }); + } else { + goto(`/event/new?start=${startTime.toISOString()}`); + } } // ========== Drag & Drop Functions ========== @@ -288,11 +284,18 @@ const newEnd = addMinutes(newStart, duration); - // Update event via store - await eventsStore.updateEvent(draggedEvent.id, { - startTime: newStart.toISOString(), - endTime: newEnd.toISOString(), - }); + // Update event via store (use updateDraftEvent for draft events) + if (eventsStore.isDraftEvent(draggedEvent.id)) { + eventsStore.updateDraftEvent({ + startTime: newStart.toISOString(), + endTime: newEnd.toISOString(), + }); + } else { + await eventsStore.updateEvent(draggedEvent.id, { + startTime: newStart.toISOString(), + endTime: newEnd.toISOString(), + }); + } // Reset state isDragging = false; @@ -384,11 +387,18 @@ newStart = setMinutes(newStart, newMins); } - // Update event via store - await eventsStore.updateEvent(resizeEvent.id, { - startTime: newStart.toISOString(), - endTime: newEnd.toISOString(), - }); + // Update event via store (use updateDraftEvent for draft events) + if (eventsStore.isDraftEvent(resizeEvent.id)) { + eventsStore.updateDraftEvent({ + startTime: newStart.toISOString(), + endTime: newEnd.toISOString(), + }); + } else { + await eventsStore.updateEvent(resizeEvent.id, { + startTime: newStart.toISOString(), + endTime: newEnd.toISOString(), + }); + } // Reset state isResizing = false; @@ -450,7 +460,7 @@ @@ -499,7 +509,7 @@ @@ -509,10 +519,13 @@ {#each getEventsForDay(day) as event (event.id)} {@const isBeingDragged = isDragging && draggedEvent?.id === event.id} {@const isBeingResized = isResizing && resizeEvent?.id === event.id} + {@const isDraft = eventsStore.isDraftEvent(event.id)}
startDrag(event, e)} - onclick={(e) => handleEventClick(event, e)} - onkeydown={(e) => e.key === 'Enter' && goto(`/event/${event.id}`)} + onclick={(e) => !isDraft && handleEventClick(event, e)} + onkeydown={(e) => !isDraft && e.key === 'Enter' && goto(`/?event=${event.id}`)} >
- {formatEventTime(event.startTime)} + {formatEventTime(event.startTime)} - {formatEventTime(event.endTime)} - {event.title} + {event.title || (isDraft ? '(Neuer Termin)' : '')}
+ import { viewStore } from '$lib/stores/view.svelte'; + import { eventsStore } from '$lib/stores/events.svelte'; + import { settingsStore } from '$lib/stores/settings.svelte'; + import { + format, + startOfMonth, + endOfMonth, + startOfWeek, + endOfWeek, + eachDayOfInterval, + isSameMonth, + isToday, + parseISO, + setHours, + setMinutes, + } from 'date-fns'; + import { de } from 'date-fns/locale'; + import type { CalendarViewType } from '@calendar/shared'; + + interface Props { + onQuickCreate?: (date: Date, position: { x: number; y: number }) => void; + } + + let { onQuickCreate }: Props = $props(); + + // Derived values + let year = $derived(viewStore.currentDate.getFullYear()); + + let months = $derived(Array.from({ length: 12 }, (_, i) => new Date(year, i, 1))); + + // Week day headers + const weekDaysFromMonday = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; + const weekDaysFromSunday = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; + let weekDays = $derived( + settingsStore.weekStartsOn === 1 ? weekDaysFromMonday : weekDaysFromSunday + ); + + // Context menu state + let contextMenu = $state<{ visible: boolean; x: number; y: number; date: Date | null }>({ + visible: false, + x: 0, + y: 0, + date: null, + }); + + // Context menu options + const viewOptions: { type: CalendarViewType; label: string }[] = [ + { type: 'day', label: 'Tagesansicht' }, + { type: 'week', label: 'Wochenansicht' }, + { type: 'month', label: 'Monatsansicht' }, + ]; + + // Precompute event counts for performance + let eventCountsByDay = $derived.by(() => { + const counts = new Map(); + const events = eventsStore.events ?? []; + + for (const event of events) { + const start = + typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime; + const key = format(start, 'yyyy-MM-dd'); + counts.set(key, (counts.get(key) || 0) + 1); + } + + return counts; + }); + + // Helper functions + function getMonthDays(month: Date): Date[] { + const monthStart = startOfMonth(month); + const monthEnd = endOfMonth(month); + const calendarStart = startOfWeek(monthStart, { + weekStartsOn: settingsStore.weekStartsOn, + }); + const calendarEnd = endOfWeek(monthEnd, { + weekStartsOn: settingsStore.weekStartsOn, + }); + + return eachDayOfInterval({ start: calendarStart, end: calendarEnd }); + } + + function getEventCount(day: Date): number { + const key = format(day, 'yyyy-MM-dd'); + return eventCountsByDay.get(key) || 0; + } + + // Event handlers + function handleDayClick(day: Date, e: MouseEvent) { + if (onQuickCreate) { + const startTime = setMinutes(setHours(day, 9), 0); + onQuickCreate(startTime, { x: e.clientX, y: e.clientY }); + } else { + viewStore.setDate(day); + viewStore.setViewType('day'); + } + } + + function handleDayContextMenu(day: Date, e: MouseEvent) { + e.preventDefault(); + contextMenu = { + visible: true, + x: e.clientX, + y: e.clientY, + date: day, + }; + } + + function handleContextMenuSelect(viewType: CalendarViewType) { + if (contextMenu.date) { + viewStore.setDate(contextMenu.date); + viewStore.setViewType(viewType); + } + closeContextMenu(); + } + + function closeContextMenu() { + contextMenu = { visible: false, x: 0, y: 0, date: null }; + } + + function handleMonthClick(month: Date) { + viewStore.setDate(month); + viewStore.setViewType('month'); + } + + function handleKeyDown(e: KeyboardEvent, day: Date) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleDayClick(day, e as unknown as MouseEvent); + } + } + + // Close context menu on click outside + function handleWindowClick() { + if (contextMenu.visible) { + closeContextMenu(); + } + } + + // Close context menu on Escape + function handleWindowKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape' && contextMenu.visible) { + closeContextMenu(); + } + } + + + + +
+ {#each months as month} +
+ + +
+ {#each weekDays as day} + {day} + {/each} +
+ +
+ {#each getMonthDays(month) as day} + {@const eventCount = getEventCount(day)} + + {/each} +
+
+ {/each} +
+ + +{#if contextMenu.visible && contextMenu.date} + +{/if} + + diff --git a/apps/calendar/apps/web/src/lib/components/event/EventDetailModal.svelte b/apps/calendar/apps/web/src/lib/components/event/EventDetailModal.svelte new file mode 100644 index 000000000..b85cf4e44 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/event/EventDetailModal.svelte @@ -0,0 +1,603 @@ + + + + + + + + diff --git a/apps/calendar/apps/web/src/lib/components/event/EventForm.svelte b/apps/calendar/apps/web/src/lib/components/event/EventForm.svelte index 69e5568d4..6e0edb42d 100644 --- a/apps/calendar/apps/web/src/lib/components/event/EventForm.svelte +++ b/apps/calendar/apps/web/src/lib/components/event/EventForm.svelte @@ -1,7 +1,7 @@ diff --git a/apps/calendar/apps/web/src/routes/calendars/+page.svelte b/apps/calendar/apps/web/src/routes/calendars/+page.svelte deleted file mode 100644 index 52beeb4eb..000000000 --- a/apps/calendar/apps/web/src/routes/calendars/+page.svelte +++ /dev/null @@ -1,274 +0,0 @@ - - - - Meine Kalender | Kalender - - -
- - - {#if showNewForm} -
-

Neuer Kalender

-
{ - e.preventDefault(); - handleCreateCalendar(); - }} - > -
- - -
-
- - -
-
-
- {/if} - -
- {#each calendarsStore.calendars as calendar} -
- {#if editingCalendar?.id === calendar.id} -
{ - e.preventDefault(); - const form = e.target as HTMLFormElement; - const name = (form.elements.namedItem('name') as HTMLInputElement).value; - const color = (form.elements.namedItem('color') as HTMLInputElement).value; - handleUpdateCalendar(calendar, name, color); - }} - > -
- - -
-
- - -
-
- {:else} -
- - {calendar.name} - {#if calendar.isDefault} - Standard - {/if} -
-
- - {#if !calendar.isDefault} - - {/if} -
- {/if} -
- {/each} - - {#if calendarsStore.calendars.length === 0} -
-

Keine Kalender vorhanden

-
- {/if} -
-
- - diff --git a/apps/calendar/apps/web/src/routes/event/[id]/+page.svelte b/apps/calendar/apps/web/src/routes/event/[id]/+page.svelte index a69886aff..b167c8ba6 100644 --- a/apps/calendar/apps/web/src/routes/event/[id]/+page.svelte +++ b/apps/calendar/apps/web/src/routes/event/[id]/+page.svelte @@ -2,191 +2,42 @@ import { page } from '$app/stores'; import { goto } from '$app/navigation'; import { onMount } from 'svelte'; - import { authStore } from '$lib/stores/auth.svelte'; - import { eventsStore } from '$lib/stores/events.svelte'; - import { toast } from '$lib/stores/toast'; - import EventForm from '$lib/components/event/EventForm.svelte'; - import type { CalendarEvent, UpdateEventInput } from '@calendar/shared'; - import * as api from '$lib/api/events'; - - let event = $state(null); - let loading = $state(true); - let isEditing = $state(false); - - onMount(async () => { - if (!authStore.isAuthenticated) { - goto('/login'); - return; - } + // Redirect to main calendar page with event modal + onMount(() => { const eventId = $page.params.id; - const result = await api.getEvent(eventId); - - if (result.error) { - toast.error('Termin nicht gefunden'); - goto('/'); - return; - } - - event = result.data; - loading = false; + goto(`/?event=${eventId}`, { replaceState: true }); }); - - async function handleSave(data: UpdateEventInput) { - if (!event) return; - - const result = await eventsStore.updateEvent(event.id, data); - - if (result.error) { - toast.error(`Fehler beim Speichern: ${result.error.message}`); - return; - } - - toast.success('Termin aktualisiert'); - isEditing = false; - event = result.data; - } - - async function handleDelete() { - if (!event) return; - - if (!confirm('Möchten Sie diesen Termin wirklich löschen?')) { - return; - } - - const result = await eventsStore.deleteEvent(event.id); - - if (result.error) { - toast.error(`Fehler beim Löschen: ${result.error.message}`); - return; - } - - toast.success('Termin gelöscht'); - goto('/'); - } - - function handleCancel() { - if (isEditing) { - isEditing = false; - } else { - goto('/'); - } - } - - {event?.title || 'Termin'} | Kalender - - -
- {#if loading} -
Laden...
- {:else if event} -
-
-

{isEditing ? 'Termin bearbeiten' : event.title}

- {#if !isEditing} -
- - -
- {/if} -
- - {#if isEditing} - - {:else} -
-
- Zeit - - {#if event.isAllDay} - Ganztägig - {:else} - {new Date(event.startTime).toLocaleString('de-DE')} - - {new Date(event.endTime).toLocaleString('de-DE')} - {/if} - -
- - {#if event.location} -
- Ort - {event.location} -
- {/if} - - {#if event.description} -
- Beschreibung - {event.description} -
- {/if} - -
- -
-
- {/if} -
- {/if} +
+
+

Laden...

diff --git a/apps/calendar/apps/web/src/routes/settings/+page.svelte b/apps/calendar/apps/web/src/routes/settings/+page.svelte index adb8603e4..1ad82c963 100644 --- a/apps/calendar/apps/web/src/routes/settings/+page.svelte +++ b/apps/calendar/apps/web/src/routes/settings/+page.svelte @@ -6,10 +6,18 @@ import { theme } from '$lib/stores/theme'; import { userSettings } from '$lib/stores/user-settings.svelte'; import { settingsStore, type WeekStartDay, type TimeFormat, type AllDayDisplayMode } from '$lib/stores/settings.svelte'; + import { calendarsStore } from '$lib/stores/calendars.svelte'; + import { toast } from '$lib/stores/toast'; import { setLocale, supportedLocales, type SupportedLocale } from '$lib/i18n'; import { THEME_DEFINITIONS } from '@manacore/shared-theme'; import { GlobalSettingsSection } from '@manacore/shared-ui'; - import type { CalendarViewType } from '@calendar/shared'; + import type { CalendarViewType, Calendar } from '@calendar/shared'; + + // Calendar management state + let editingCalendar = $state(null); + let showNewCalendarForm = $state(false); + let newCalendarName = $state(''); + let newCalendarColor = $state('#3b82f6'); // Get current locale from svelte-i18n import { locale } from 'svelte-i18n'; @@ -23,6 +31,52 @@ await userSettings.load(); }); + // Calendar management functions + async function handleCreateCalendar() { + if (!newCalendarName.trim()) return; + + const result = await calendarsStore.createCalendar({ + name: newCalendarName.trim(), + color: newCalendarColor, + }); + + if (result.error) { + toast.error(`Fehler: ${result.error.message}`); + return; + } + + toast.success('Kalender erstellt'); + newCalendarName = ''; + showNewCalendarForm = false; + } + + async function handleDeleteCalendar(calendar: Calendar) { + if (!confirm(`Möchten Sie "${calendar.name}" wirklich löschen?`)) { + return; + } + + const result = await calendarsStore.deleteCalendar(calendar.id); + + if (result.error) { + toast.error(`Fehler: ${result.error.message}`); + return; + } + + toast.success('Kalender gelöscht'); + } + + async function handleUpdateCalendar(calendar: Calendar, name: string, color: string) { + const result = await calendarsStore.updateCalendar(calendar.id, { name, color }); + + if (result.error) { + toast.error(`Fehler: ${result.error.message}`); + return; + } + + toast.success('Kalender aktualisiert'); + editingCalendar = null; + } + function handleThemeChange(mode: 'light' | 'dark' | 'system') { theme.setMode(mode); } @@ -96,6 +150,101 @@

Einstellungen

+ +
+
+

Meine Kalender

+ +
+ + {#if showNewCalendarForm} +
+
{ + e.preventDefault(); + handleCreateCalendar(); + }} + > +
+ + +
+
+ + +
+
+
+ {/if} + +
+ {#each calendarsStore.calendars as calendar} +
+ {#if editingCalendar?.id === calendar.id} +
{ + e.preventDefault(); + const form = e.target as HTMLFormElement; + const name = (form.elements.namedItem('name') as HTMLInputElement).value; + const color = (form.elements.namedItem('color') as HTMLInputElement).value; + handleUpdateCalendar(calendar, name, color); + }} + > +
+ + +
+
+ + +
+
+ {:else} +
+ + {calendar.name} + {#if calendar.isDefault} + Standard + {/if} +
+
+ + {#if !calendar.isDefault} + + {/if} +
+ {/if} +
+ {/each} + + {#if calendarsStore.calendars.length === 0} +
+

Keine Kalender vorhanden

+
+ {/if} +
+
+

Sprache

@@ -703,4 +852,107 @@ color: hsl(var(--color-muted-foreground)); margin-top: 1.25rem; } + + /* Calendar management styles */ + .calendars-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid hsl(var(--color-border)); + } + + .calendars-header h2 { + margin: 0; + padding: 0; + border: none; + } + + .new-calendar-form { + margin-bottom: 1rem; + padding: 1rem; + background: hsl(var(--color-muted) / 0.3); + border-radius: var(--radius-md); + } + + .calendar-form-row { + display: flex; + gap: 0.75rem; + margin-bottom: 1rem; + } + + .calendar-form-row .input { + flex: 1; + } + + .color-input { + width: 48px; + height: 42px; + padding: 4px; + border: 2px solid hsl(var(--color-border)); + border-radius: var(--radius-md); + cursor: pointer; + } + + .calendar-form-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + } + + .calendar-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .calendar-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem; + background: hsl(var(--color-muted) / 0.2); + border-radius: var(--radius-md); + } + + .calendar-info { + display: flex; + align-items: center; + gap: 0.75rem; + } + + .color-dot { + width: 16px; + height: 16px; + border-radius: var(--radius-full); + } + + .calendar-name { + font-weight: 500; + } + + .badge { + font-size: 0.75rem; + padding: 0.125rem 0.5rem; + background: hsl(var(--color-muted)); + border-radius: var(--radius-sm); + color: hsl(var(--color-muted-foreground)); + } + + .calendar-actions { + display: flex; + gap: 0.5rem; + } + + .btn-sm { + padding: 0.25rem 0.75rem; + font-size: 0.875rem; + } + + .empty-state { + text-align: center; + padding: 1.5rem; + color: hsl(var(--color-muted-foreground)); + } diff --git a/apps/calendar/packages/shared/src/types/event.ts b/apps/calendar/packages/shared/src/types/event.ts index b5589294c..614eeaa1c 100644 --- a/apps/calendar/packages/shared/src/types/event.ts +++ b/apps/calendar/packages/shared/src/types/event.ts @@ -12,6 +12,20 @@ export interface EventAttendee { */ export type AllDayDisplayMode = 'header' | 'block'; +/** + * Structured location/address details + */ +export interface LocationDetails { + /** Street address */ + street?: string; + /** Postal/ZIP code */ + postalCode?: string; + /** City/Town */ + city?: string; + /** Country */ + country?: string; +} + /** * Event metadata stored in JSONB */ @@ -30,6 +44,8 @@ export interface EventMetadata { tags?: string[]; /** Override for all-day display mode (uses global setting if not set) */ allDayDisplayMode?: AllDayDisplayMode; + /** Structured location details */ + locationDetails?: LocationDetails; } /** diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index 440c07c32..1bc4d8705 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -57,6 +57,7 @@ export { SettingsToggle, SettingsDangerZone, SettingsDangerButton, + GlobalSettingsSection, } from './settings'; // Pages