From b966c91c32ec22ec20d4cfcc0661a13a0f968009 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:26:32 +0100 Subject: [PATCH] refactor(calendar): extract EventCard component from views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create EventCard component encapsulating event block with resize handles - Create TimeColumn component for hour labels (not yet integrated) - Create OverflowIndicator component for overflow events (not yet integrated) - Integrate EventCard into DayView (154 lines reduced) - Integrate EventCard into WeekView (166 lines reduced) - Remove duplicate event-card styles from both views đŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../lib/components/calendar/DayView.svelte | 196 ++------------ .../lib/components/calendar/EventCard.svelte | 251 ++++++++++++++++++ .../calendar/OverflowIndicator.svelte | 57 ++++ .../lib/components/calendar/TimeColumn.svelte | 42 +++ .../lib/components/calendar/WeekView.svelte | 224 ++-------------- 5 files changed, 400 insertions(+), 370 deletions(-) create mode 100644 apps/calendar/apps/web/src/lib/components/calendar/EventCard.svelte create mode 100644 apps/calendar/apps/web/src/lib/components/calendar/OverflowIndicator.svelte create mode 100644 apps/calendar/apps/web/src/lib/components/calendar/TimeColumn.svelte diff --git a/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte index 5a501906a..83aef58d6 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte @@ -17,6 +17,7 @@ getVisibleOverflowEvents, type OverflowEvents, } from '$lib/utils/eventFiltering'; + import EventCard from './EventCard.svelte'; import TaskBlock from './TaskBlock.svelte'; import EventContextMenu from '$lib/components/event/EventContextMenu.svelte'; import { goto } from '$app/navigation'; @@ -625,9 +626,11 @@ const top = minutesToPercent(startMinutes); const height = Math.max((duration / (totalVisibleHours * 60)) * 100, 1.5); // minimum ~20px at 60px/hour - const color = calendarsStore.getColor(event.calendarId); + return `top: ${top}%; height: ${height}%;`; + } - return `top: ${top}%; height: ${height}%; background-color: ${color};`; + function formatEventTime(event: CalendarEvent): string { + return `${format(toDate(event.startTime), 'HH:mm')} - ${format(toDate(event.endTime), 'HH:mm')}`; } /** @@ -782,63 +785,27 @@ {/each} - {#each timedEvents as event} + {#each timedEvents as event (event.id)} {@const isBeingDragged = isDragging && draggedEvent?.id === event.id} {@const isBeingResized = isResizing && resizeEvent?.id === event.id} - {@const isDraft = eventsStore.isDraftEvent(event.id)} - {@const isSearchHighlighted = searchStore.isEventHighlighted(event.id)} - {@const isSearchDimmed = searchStore.isEventDimmed(event.id)} - -
startDrag(event, e)} - onclick={(e) => !isDraft && handleEventClick(event, e)} - oncontextmenu={(e) => handleEventContextMenu(event, e)} - role="button" - tabindex="0" - > - -
startResize(event, 'top', e)} - role="separator" - aria-orientation="horizontal" - aria-label="Startzeit ändern" - aria-valuenow={0} - tabindex="-1" - >
- - - {format(toDate(event.startTime), 'HH:mm')} - - {format(toDate(event.endTime), 'HH:mm')} - - {event.title || (isDraft ? '(Neuer Termin)' : '')} - {#if event.location} - {event.location} - {/if} - - -
startResize(event, 'bottom', e)} - role="separator" - aria-orientation="horizontal" - aria-label="Endzeit ändern" - aria-valuenow={0} - tabindex="-1" - >
-
+ color={calendarsStore.getColor(event.calendarId)} + isDragging={isBeingDragged} + isResizing={isBeingResized} + isSearchHighlighted={searchStore.isEventHighlighted(event.id)} + isSearchDimmed={searchStore.isEventDimmed(event.id)} + formattedTime={formatEventTime(event)} + onClick={handleEventClick} + onPointerDown={startDrag} + onContextMenu={handleEventContextMenu} + onResizeStart={startResize} + /> {/each} @@ -1046,127 +1013,6 @@ outline-offset: -2px; } - .event-card { - position: absolute; - left: 8px; - width: calc(100% - 16px); - max-width: 400px; - color: white; - border: none; - text-align: left; - cursor: grab; - z-index: 1; - display: flex; - flex-direction: column; - gap: 2px; - padding: 6px 10px; - border-radius: var(--radius-md); - overflow: hidden; - touch-action: none; - user-select: none; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - transition: - box-shadow 150ms ease, - opacity 150ms ease; - } - - .event-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - } - - .event-card.dragging { - cursor: grabbing; - opacity: 0.9; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25); - z-index: 100; - } - - .event-card.resizing { - cursor: ns-resize; - opacity: 0.85; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); - z-index: 100; - outline: 2px dashed rgba(255, 255, 255, 0.6); - 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; - } - - /* Search highlighting */ - .event-card.search-highlighted { - outline: 2px solid hsl(var(--color-primary)); - outline-offset: 1px; - box-shadow: - 0 0 0 4px hsl(var(--color-primary) / 0.3), - 0 4px 12px rgba(0, 0, 0, 0.25); - z-index: 10; - } - - .event-card.search-dimmed { - opacity: 0.35; - filter: grayscale(0.3); - } - - @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; - left: 0; - right: 0; - height: 8px; - cursor: ns-resize; - opacity: 0; - transition: opacity 150ms ease; - z-index: 10; - } - - .resize-handle.top { - top: 0; - border-radius: var(--radius-sm) var(--radius-sm) 0 0; - } - - .resize-handle.bottom { - bottom: 0; - border-radius: 0 0 var(--radius-sm) var(--radius-sm); - } - - .event-card:hover .resize-handle { - opacity: 1; - background: rgba(255, 255, 255, 0.3); - } - - .resize-handle:hover { - background: rgba(255, 255, 255, 0.5) !important; - } - - .event-time { - font-size: 0.75rem; - opacity: 0.9; - } - - .event-title { - font-size: 0.875rem; - font-weight: 500; - } - - .event-location { - font-size: 0.75rem; - opacity: 0.8; - } - /* Time indicator */ .time-indicator { position: absolute; diff --git a/apps/calendar/apps/web/src/lib/components/calendar/EventCard.svelte b/apps/calendar/apps/web/src/lib/components/calendar/EventCard.svelte new file mode 100644 index 000000000..a106e4cea --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/calendar/EventCard.svelte @@ -0,0 +1,251 @@ + + +
+ + {#if onResizeStart} +
+ {/if} + + {formattedTime} + {event.title || (isDraft ? $_('calendar.draftEvent') : '')} + {#if event.location} + {event.location} + {/if} + + + {#if onResizeStart} +
+ {/if} +
+ + diff --git a/apps/calendar/apps/web/src/lib/components/calendar/OverflowIndicator.svelte b/apps/calendar/apps/web/src/lib/components/calendar/OverflowIndicator.svelte new file mode 100644 index 000000000..bbb58a343 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/calendar/OverflowIndicator.svelte @@ -0,0 +1,57 @@ + + +{#if events.length > 0} +
+ {#each events as { color, tooltip }} +
+ {/each} +
+{/if} + + diff --git a/apps/calendar/apps/web/src/lib/components/calendar/TimeColumn.svelte b/apps/calendar/apps/web/src/lib/components/calendar/TimeColumn.svelte new file mode 100644 index 000000000..04e68b959 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/calendar/TimeColumn.svelte @@ -0,0 +1,42 @@ + + +
+ {#each hours as hour} +
+ {formatHour(hour)} +
+ {/each} +
+ + diff --git a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte index ff30f1e55..421291570 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte @@ -17,6 +17,7 @@ getVisibleOverflowEvents, type OverflowEvents, } from '$lib/utils/eventFiltering'; + import EventCard from './EventCard.svelte'; import TaskBlock from './TaskBlock.svelte'; import EventContextMenu from '$lib/components/event/EventContextMenu.svelte'; import { goto } from '$app/navigation'; @@ -201,9 +202,11 @@ const top = minutesToPercent(startMinutes); const height = Math.max((duration / (totalVisibleHours * 60)) * 100, 2); // Min 2% height - const color = calendarsStore.getColor(event.calendarId); + return `top: ${top}%; height: ${height}%;`; + } - return `top: ${top}%; height: ${height}%; background-color: ${color};`; + function formatEventTimeRange(event: CalendarEvent): string { + return `${formatEventTime(event.startTime)} - ${formatEventTime(event.endTime)}`; } /** @@ -955,60 +958,27 @@ {#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)} {@const isCrossDayDrag = - isBeingDragged && dragTargetDay && !isSameDay(day, dragTargetDay)} - {@const isSearchHighlighted = searchStore.isEventHighlighted(event.id)} - {@const isSearchDimmed = searchStore.isEventDimmed(event.id)} -
startDrag(event, e)} - onclick={(e) => !isDraft && handleEventClick(event, e)} - onkeydown={(e) => !isDraft && e.key === 'Enter' && goto(`/?event=${event.id}`)} - oncontextmenu={(e) => handleEventContextMenu(event, e)} - > - -
startResize(event, 'top', e)} - role="slider" - aria-label={$_('event.changeStartTime')} - aria-valuenow={0} - tabindex="-1" - >
- - - {formatEventTime(event.startTime)} - {formatEventTime(event.endTime)} - - {event.title || (isDraft ? $_('calendar.draftEvent') : '')} - - -
startResize(event, 'bottom', e)} - role="slider" - aria-label={$_('event.changeEndTime')} - aria-valuenow={0} - tabindex="-1" - >
-
+ color={calendarsStore.getColor(event.calendarId)} + isDragging={isBeingDragged && !isCrossDayDrag} + isDraggingSource={isCrossDayDrag} + isResizing={isBeingResized} + isSearchHighlighted={searchStore.isEventHighlighted(event.id)} + isSearchDimmed={searchStore.isEventDimmed(event.id)} + formattedTime={formatEventTimeRange(event)} + onClick={handleEventClick} + onPointerDown={startDrag} + onContextMenu={handleEventContextMenu} + onResizeStart={startResize} + /> {/each} @@ -1046,15 +1016,13 @@ {#if isDragging && draggedEvent && dragTargetDay && isSameDay(day, dragTargetDay) && !getEventsForDay(day).some((e) => e.id === draggedEvent!.id)} -
- {formatEventTime(draggedEvent.startTime)} - {draggedEvent.title} -
+ {/if} @@ -1329,140 +1297,6 @@ background: hsl(var(--color-muted) / 0.3); } - .event-card { - position: absolute; - left: 2px; - right: 2px; - padding: 2px 4px; - color: white; - border: none; - border-radius: var(--radius-sm); - text-align: left; - cursor: grab; - z-index: 1; - overflow: hidden; - transition: - box-shadow 0.15s ease, - opacity 0.15s ease; - touch-action: none; - user-select: none; - } - - .event-card:hover { - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - } - - .event-card.dragging { - cursor: grabbing; - opacity: 0.9; - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); - z-index: 100; - } - - .event-card.resizing { - opacity: 0.85; - z-index: 100; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); - outline: 2px dashed rgba(255, 255, 255, 0.6); - outline-offset: -2px; - } - - /* Ghost style for source position during cross-day drag */ - .event-card.dragging-source { - opacity: 0.4; - background: transparent !important; - border: 2px dashed hsl(var(--color-border)); - pointer-events: none; - } - - .event-card.dragging-source .event-title, - .event-card.dragging-source .event-time { - opacity: 0.5; - } - - /* Solid preview at target position during cross-day drag */ - .event-card.drag-preview { - pointer-events: none; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - } - - /* Search highlighting */ - .event-card.search-highlighted { - outline: 2px solid hsl(var(--color-primary)); - outline-offset: 1px; - box-shadow: - 0 0 0 4px hsl(var(--color-primary) / 0.3), - 0 4px 12px rgba(0, 0, 0, 0.25); - z-index: 10; - } - - .event-card.search-dimmed { - opacity: 0.35; - filter: grayscale(0.3); - } - - .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); - } - } - - .event-time { - font-size: 0.65rem; - opacity: 0.9; - display: block; - } - - .event-title { - display: block; - font-size: 0.75rem; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - /* Resize handles */ - .resize-handle { - position: absolute; - left: 0; - right: 0; - height: 8px; - cursor: ns-resize; - opacity: 0; - transition: opacity 0.15s ease; - z-index: 2; - } - - .resize-handle.top { - top: 0; - border-radius: var(--radius-sm) var(--radius-sm) 0 0; - } - - .resize-handle.bottom { - bottom: 0; - border-radius: 0 0 var(--radius-sm) var(--radius-sm); - } - - .event-card:hover .resize-handle { - opacity: 1; - background: rgba(255, 255, 255, 0.3); - } - - .resize-handle:hover { - background: rgba(255, 255, 255, 0.5) !important; - } - .time-indicator { position: absolute; left: 0;