mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
refactor(calendar): consolidate code patterns and reduce duplication
- Add useVisibleHours composable for shared hour filtering logic - Replace 60+ parseISO patterns with centralized toDate() utility - Remove legacy toast.ts store (keep only Svelte 5 runes version) - Standardize searchStore from class to object pattern - Remove debug console.logs from API clients Affected views: WeekView, MultiDayView, DayView, AgendaView, YearView Affected components: EventForm, EventDetailModal, QuickEventOverlay, AgendaItem Affected stores: events, search, toast 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c710f43391
commit
6bea47d4da
21 changed files with 291 additions and 352 deletions
|
|
@ -59,7 +59,6 @@ export function createApiClient(config: ApiClientConfig) {
|
|||
}
|
||||
|
||||
const url = `${baseUrl}${apiPrefix}${endpoint}`;
|
||||
console.log(`[API Client] ${method} ${url}`, { hasToken: !!authToken });
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
|
|
|
|||
|
|
@ -23,14 +23,7 @@ export async function getEvents(params: QueryEventsParams) {
|
|||
if (params.search) {
|
||||
searchParams.set('search', params.search);
|
||||
}
|
||||
console.log('[Calendar API] Fetching events:', params);
|
||||
const result = await fetchApi<{ events: CalendarEvent[] }>(`/events?${searchParams.toString()}`);
|
||||
console.log(
|
||||
'[Calendar API] Fetch events result:',
|
||||
result.data?.events?.length,
|
||||
'events',
|
||||
result.error
|
||||
);
|
||||
if (result.error || !result.data) {
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
|
|
@ -64,14 +57,11 @@ export async function getEventsByCalendar(calendarId: string) {
|
|||
}
|
||||
|
||||
export async function createEvent(data: CreateEventInput) {
|
||||
console.log('[Calendar API] Creating event:', data);
|
||||
const result = await fetchApi<{ event: CalendarEvent }>('/events', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
});
|
||||
console.log('[Calendar API] Create event result:', result);
|
||||
if (result.error || !result.data) {
|
||||
console.error('[Calendar API] Create event failed:', result.error);
|
||||
return { data: null, error: result.error };
|
||||
}
|
||||
return { data: result.data.event, error: null };
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@
|
|||
import TodoCheckbox from '$lib/components/todo/TodoCheckbox.svelte';
|
||||
import PriorityBadge from '$lib/components/todo/PriorityBadge.svelte';
|
||||
import { Calendar, MapPin, Clock } from 'lucide-svelte';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { format } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
|
||||
type ItemType = 'event' | 'todo';
|
||||
|
||||
|
|
@ -29,8 +30,8 @@
|
|||
if (!event) return '';
|
||||
if (event.isAllDay) return 'Ganztägig';
|
||||
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
return `${format(start, 'HH:mm')} - ${format(end, 'HH:mm')}`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
|
||||
import { format, parseISO, isToday, isTomorrow, startOfDay } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -25,8 +26,7 @@
|
|||
const groups: Map<string, CalendarEvent[]> = new Map();
|
||||
|
||||
for (const event of currentEvents) {
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const start = toDate(event.startTime);
|
||||
|
||||
// Skip events before the start date
|
||||
if (start < startDate) continue;
|
||||
|
|
@ -45,8 +45,8 @@
|
|||
.map(([dateKey, events]) => ({
|
||||
date: parseISO(dateKey),
|
||||
events: events.sort((a, b) => {
|
||||
const aStart = typeof a.startTime === 'string' ? parseISO(a.startTime) : a.startTime;
|
||||
const bStart = typeof b.startTime === 'string' ? parseISO(b.startTime) : b.startTime;
|
||||
const aStart = toDate(a.startTime);
|
||||
const bStart = toDate(b.startTime);
|
||||
return aStart.getTime() - bStart.getTime();
|
||||
}),
|
||||
}));
|
||||
|
|
@ -118,13 +118,8 @@
|
|||
{#if event.isAllDay}
|
||||
Ganztägig
|
||||
{:else}
|
||||
{format(
|
||||
typeof event.startTime === 'string'
|
||||
? parseISO(event.startTime)
|
||||
: event.startTime,
|
||||
'HH:mm'
|
||||
)} - {format(
|
||||
typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime,
|
||||
{format(toDate(event.startTime), 'HH:mm')} - {format(
|
||||
toDate(event.endTime),
|
||||
'HH:mm'
|
||||
)}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -6,18 +6,15 @@
|
|||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import { todosStore, type Task } from '$lib/stores/todos.svelte';
|
||||
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
|
||||
import {
|
||||
useVisibleHours,
|
||||
useCurrentTimeIndicator,
|
||||
} from '$lib/composables/useVisibleHours.svelte';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import TaskBlock from './TaskBlock.svelte';
|
||||
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
format,
|
||||
isToday,
|
||||
parseISO,
|
||||
differenceInMinutes,
|
||||
addMinutes,
|
||||
setHours,
|
||||
setMinutes,
|
||||
} from 'date-fns';
|
||||
import { format, isToday, differenceInMinutes, addMinutes, setHours, setMinutes } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
|
|
@ -34,41 +31,19 @@
|
|||
const HOUR_HEIGHT = 60; // pixels per hour
|
||||
const SNAP_MINUTES = 15; // snap to 15-minute intervals
|
||||
|
||||
// Generate hours (filtered based on settings)
|
||||
let allHours = Array.from({ length: 24 }, (_, i) => i);
|
||||
let hours = $derived(
|
||||
settingsStore.filterHoursEnabled
|
||||
? allHours.filter((h) => h >= settingsStore.dayStartHour && h < settingsStore.dayEndHour)
|
||||
: allHours
|
||||
);
|
||||
// Use composables for hour filtering and time indicator
|
||||
const visibleHours = useVisibleHours();
|
||||
const timeIndicator = useCurrentTimeIndicator();
|
||||
|
||||
// Calculate visible hours range for positioning
|
||||
let firstVisibleHour = $derived(
|
||||
settingsStore.filterHoursEnabled ? settingsStore.dayStartHour : 0
|
||||
);
|
||||
let lastVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayEndHour : 24);
|
||||
let totalVisibleHours = $derived(lastVisibleHour - firstVisibleHour);
|
||||
|
||||
// Helper to convert minutes to percentage position (accounting for hidden hours)
|
||||
function minutesToPercent(minutes: number): number {
|
||||
const adjustedMinutes = minutes - firstVisibleHour * 60;
|
||||
return (adjustedMinutes / (totalVisibleHours * 60)) * 100;
|
||||
}
|
||||
// Destructure for convenience (these are reactive getters)
|
||||
let hours = $derived(visibleHours.hours);
|
||||
let firstVisibleHour = $derived(visibleHours.firstVisibleHour);
|
||||
let lastVisibleHour = $derived(visibleHours.lastVisibleHour);
|
||||
let totalVisibleHours = $derived(visibleHours.totalVisibleHours);
|
||||
const minutesToPercent = visibleHours.minutesToPercent;
|
||||
|
||||
// Current time indicator position
|
||||
let now = $state(new Date());
|
||||
let currentTimePosition = $derived.by(() => {
|
||||
const minutes = now.getHours() * 60 + now.getMinutes();
|
||||
return minutesToPercent(minutes);
|
||||
});
|
||||
|
||||
// Update current time every minute
|
||||
$effect(() => {
|
||||
const interval = setInterval(() => {
|
||||
now = new Date();
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
let currentTimePosition = $derived(minutesToPercent(timeIndicator.currentMinutes));
|
||||
|
||||
// Get timed events, filtering out those outside visible range when hour filter is enabled
|
||||
let timedEvents = $derived.by(() => {
|
||||
|
|
@ -79,9 +54,8 @@
|
|||
const visibleEndMinutes = settingsStore.dayEndHour * 60;
|
||||
|
||||
return allEvents.filter((event) => {
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
|
||||
|
|
@ -108,9 +82,8 @@
|
|||
const visibleEndMinutes = settingsStore.dayEndHour * 60;
|
||||
|
||||
for (const event of allEvents) {
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
|
||||
|
|
@ -212,8 +185,8 @@
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
const startMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
||||
|
|
@ -257,14 +230,8 @@
|
|||
Math.min(newStartMinutes, lastVisibleHour * 60 - 30)
|
||||
);
|
||||
|
||||
const start =
|
||||
typeof draggedEvent.startTime === 'string'
|
||||
? parseISO(draggedEvent.startTime)
|
||||
: draggedEvent.startTime;
|
||||
const end =
|
||||
typeof draggedEvent.endTime === 'string'
|
||||
? parseISO(draggedEvent.endTime)
|
||||
: draggedEvent.endTime;
|
||||
const start = toDate(draggedEvent.startTime);
|
||||
const end = toDate(draggedEvent.endTime);
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
||||
// Create new start time on same day
|
||||
|
|
@ -303,8 +270,8 @@
|
|||
resizeEdge = edge;
|
||||
hasMoved = false;
|
||||
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
resizeOriginalStart = start;
|
||||
resizeOriginalEnd = end;
|
||||
|
||||
|
|
@ -647,8 +614,8 @@
|
|||
// Event Styling
|
||||
// ============================================================================
|
||||
function getEventStyle(event: CalendarEvent) {
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
const startMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
|
@ -840,14 +807,8 @@
|
|||
></div>
|
||||
|
||||
<span class="event-time">
|
||||
{format(
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime,
|
||||
'HH:mm'
|
||||
)} -
|
||||
{format(
|
||||
typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime,
|
||||
'HH:mm'
|
||||
)}
|
||||
{format(toDate(event.startTime), 'HH:mm')} -
|
||||
{format(toDate(event.endTime), 'HH:mm')}
|
||||
</span>
|
||||
<span class="event-title">{event.title || (isDraft ? '(Neuer Termin)' : '')}</span>
|
||||
{#if event.location}
|
||||
|
|
@ -893,10 +854,7 @@
|
|||
<div
|
||||
class="overflow-line"
|
||||
style="background-color: {calendarsStore.getColor(event.calendarId)}"
|
||||
title="{format(
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime,
|
||||
'HH:mm'
|
||||
)} {event.title}"
|
||||
title="{format(toDate(event.startTime), 'HH:mm')} {event.title}"
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -910,10 +868,7 @@
|
|||
<div
|
||||
class="overflow-line"
|
||||
style="background-color: {calendarsStore.getColor(event.calendarId)}"
|
||||
title="{format(
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime,
|
||||
'HH:mm'
|
||||
)} {event.title}"
|
||||
title="{format(toDate(event.startTime), 'HH:mm')} {event.title}"
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import { todosStore, type Task } from '$lib/stores/todos.svelte';
|
||||
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
|
||||
import {
|
||||
useVisibleHours,
|
||||
useCurrentTimeIndicator,
|
||||
} from '$lib/composables/useVisibleHours.svelte';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import TaskBlock from './TaskBlock.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
|
|
@ -13,7 +18,6 @@
|
|||
eachDayOfInterval,
|
||||
isToday,
|
||||
isSameDay,
|
||||
parseISO,
|
||||
differenceInMinutes,
|
||||
isWeekend,
|
||||
addMinutes,
|
||||
|
|
@ -56,41 +60,19 @@
|
|||
settingsStore.showOnlyWeekdays ? allDays.filter((day) => !isWeekend(day)) : allDays
|
||||
);
|
||||
|
||||
// Generate hours (filtered based on settings)
|
||||
let allHours = Array.from({ length: 24 }, (_, i) => i);
|
||||
let hours = $derived(
|
||||
settingsStore.filterHoursEnabled
|
||||
? allHours.filter((h) => h >= settingsStore.dayStartHour && h < settingsStore.dayEndHour)
|
||||
: allHours
|
||||
);
|
||||
// Use composables for hour filtering and time indicator
|
||||
const visibleHours = useVisibleHours();
|
||||
const timeIndicator = useCurrentTimeIndicator();
|
||||
|
||||
// Calculate visible hours range for positioning
|
||||
let firstVisibleHour = $derived(
|
||||
settingsStore.filterHoursEnabled ? settingsStore.dayStartHour : 0
|
||||
);
|
||||
let lastVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayEndHour : 24);
|
||||
let totalVisibleHours = $derived(lastVisibleHour - firstVisibleHour);
|
||||
|
||||
// Helper to convert minutes to percentage position (accounting for hidden hours)
|
||||
function minutesToPercent(minutes: number): number {
|
||||
const adjustedMinutes = minutes - firstVisibleHour * 60;
|
||||
return (adjustedMinutes / (totalVisibleHours * 60)) * 100;
|
||||
}
|
||||
// Destructure for convenience (these are reactive getters)
|
||||
let hours = $derived(visibleHours.hours);
|
||||
let firstVisibleHour = $derived(visibleHours.firstVisibleHour);
|
||||
let lastVisibleHour = $derived(visibleHours.lastVisibleHour);
|
||||
let totalVisibleHours = $derived(visibleHours.totalVisibleHours);
|
||||
const minutesToPercent = visibleHours.minutesToPercent;
|
||||
|
||||
// Current time indicator position
|
||||
let now = $state(new Date());
|
||||
let currentTimePosition = $derived.by(() => {
|
||||
const minutes = now.getHours() * 60 + now.getMinutes();
|
||||
return minutesToPercent(minutes);
|
||||
});
|
||||
|
||||
// Update current time every minute
|
||||
$effect(() => {
|
||||
const interval = setInterval(() => {
|
||||
now = new Date();
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
let currentTimePosition = $derived(minutesToPercent(timeIndicator.currentMinutes));
|
||||
|
||||
// Determine column width based on day count
|
||||
let columnClass = $derived.by(() => {
|
||||
|
|
@ -145,9 +127,8 @@
|
|||
const visibleEndMinutes = settingsStore.dayEndHour * 60;
|
||||
|
||||
return allEvents.filter((event) => {
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
|
||||
|
|
@ -174,9 +155,8 @@
|
|||
const visibleEndMinutes = settingsStore.dayEndHour * 60;
|
||||
|
||||
for (const event of allEvents) {
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
|
||||
|
|
@ -221,8 +201,8 @@
|
|||
);
|
||||
|
||||
function getEventStyle(event: CalendarEvent) {
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
const startMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
|
@ -265,8 +245,7 @@
|
|||
}
|
||||
|
||||
function formatEventTime(date: Date | string): string {
|
||||
const d = typeof date === 'string' ? parseISO(date) : date;
|
||||
return settingsStore.formatTime(d);
|
||||
return settingsStore.formatTime(toDate(date));
|
||||
}
|
||||
|
||||
function handleEventClick(event: CalendarEvent, e: MouseEvent) {
|
||||
|
|
@ -346,8 +325,8 @@
|
|||
draggedEvent = event;
|
||||
hasMoved = false;
|
||||
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
||||
// Calculate initial preview position
|
||||
|
|
@ -397,14 +376,8 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const start =
|
||||
typeof draggedEvent.startTime === 'string'
|
||||
? parseISO(draggedEvent.startTime)
|
||||
: draggedEvent.startTime;
|
||||
const end =
|
||||
typeof draggedEvent.endTime === 'string'
|
||||
? parseISO(draggedEvent.endTime)
|
||||
: draggedEvent.endTime;
|
||||
const start = toDate(draggedEvent.startTime);
|
||||
const end = toDate(draggedEvent.endTime);
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
||||
// Calculate new start time
|
||||
|
|
@ -450,8 +423,8 @@
|
|||
resizeEdge = edge;
|
||||
hasMoved = false;
|
||||
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
resizeOriginalStart = start;
|
||||
resizeOriginalEnd = end;
|
||||
|
|
|
|||
|
|
@ -6,17 +6,20 @@
|
|||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import { todosStore, type Task } from '$lib/stores/todos.svelte';
|
||||
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
|
||||
import {
|
||||
useVisibleHours,
|
||||
useCurrentTimeIndicator,
|
||||
} from '$lib/composables/useVisibleHours.svelte';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import TaskBlock from './TaskBlock.svelte';
|
||||
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
format,
|
||||
eachDayOfInterval,
|
||||
startOfDay,
|
||||
isToday,
|
||||
isWeekend,
|
||||
isSameDay,
|
||||
parseISO,
|
||||
differenceInMinutes,
|
||||
addMinutes,
|
||||
setHours,
|
||||
|
|
@ -63,41 +66,19 @@
|
|||
getWeek(viewStore.viewRange.start, { weekStartsOn: settingsStore.weekStartsOn })
|
||||
);
|
||||
|
||||
// Generate hours (filtered based on settings)
|
||||
let allHours = Array.from({ length: 24 }, (_, i) => i);
|
||||
let hours = $derived(
|
||||
settingsStore.filterHoursEnabled
|
||||
? allHours.filter((h) => h >= settingsStore.dayStartHour && h < settingsStore.dayEndHour)
|
||||
: allHours
|
||||
);
|
||||
// Use composables for hour filtering and time indicator
|
||||
const visibleHours = useVisibleHours();
|
||||
const timeIndicator = useCurrentTimeIndicator();
|
||||
|
||||
// Calculate visible hours range for positioning
|
||||
let firstVisibleHour = $derived(
|
||||
settingsStore.filterHoursEnabled ? settingsStore.dayStartHour : 0
|
||||
);
|
||||
let lastVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayEndHour : 24);
|
||||
let totalVisibleHours = $derived(lastVisibleHour - firstVisibleHour);
|
||||
|
||||
// Helper to convert minutes to percentage position (accounting for hidden hours)
|
||||
function minutesToPercent(minutes: number): number {
|
||||
const adjustedMinutes = minutes - firstVisibleHour * 60;
|
||||
return (adjustedMinutes / (totalVisibleHours * 60)) * 100;
|
||||
}
|
||||
// Destructure for convenience (these are reactive getters)
|
||||
let hours = $derived(visibleHours.hours);
|
||||
let firstVisibleHour = $derived(visibleHours.firstVisibleHour);
|
||||
let lastVisibleHour = $derived(visibleHours.lastVisibleHour);
|
||||
let totalVisibleHours = $derived(visibleHours.totalVisibleHours);
|
||||
const minutesToPercent = visibleHours.minutesToPercent;
|
||||
|
||||
// Current time indicator position
|
||||
let now = $state(new Date());
|
||||
let currentTimePosition = $derived.by(() => {
|
||||
const minutes = now.getHours() * 60 + now.getMinutes();
|
||||
return minutesToPercent(minutes);
|
||||
});
|
||||
|
||||
// Update current time every minute
|
||||
$effect(() => {
|
||||
const interval = setInterval(() => {
|
||||
now = new Date();
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
let currentTimePosition = $derived(minutesToPercent(timeIndicator.currentMinutes));
|
||||
|
||||
// Drag & Drop State
|
||||
let isDragging = $state(false);
|
||||
|
|
@ -145,9 +126,8 @@
|
|||
const visibleEndMinutes = settingsStore.dayEndHour * 60;
|
||||
|
||||
return allEvents.filter((event) => {
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
|
||||
|
|
@ -174,9 +154,8 @@
|
|||
const visibleEndMinutes = settingsStore.dayEndHour * 60;
|
||||
|
||||
for (const event of allEvents) {
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
const eventStartMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const eventEndMinutes = end.getHours() * 60 + end.getMinutes();
|
||||
|
|
@ -221,8 +200,8 @@
|
|||
);
|
||||
|
||||
function getEventStyle(event: CalendarEvent) {
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
const startMinutes = start.getHours() * 60 + start.getMinutes();
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
|
@ -267,8 +246,7 @@
|
|||
}
|
||||
|
||||
function formatEventTime(date: Date | string): string {
|
||||
const d = typeof date === 'string' ? parseISO(date) : date;
|
||||
return settingsStore.formatTime(d);
|
||||
return settingsStore.formatTime(toDate(date));
|
||||
}
|
||||
|
||||
function handleEventClick(event: CalendarEvent, e: MouseEvent) {
|
||||
|
|
@ -354,8 +332,8 @@
|
|||
draggedEvent = event;
|
||||
hasMoved = false;
|
||||
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
||||
// Calculate initial preview position
|
||||
|
|
@ -405,14 +383,8 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const start =
|
||||
typeof draggedEvent.startTime === 'string'
|
||||
? parseISO(draggedEvent.startTime)
|
||||
: draggedEvent.startTime;
|
||||
const end =
|
||||
typeof draggedEvent.endTime === 'string'
|
||||
? parseISO(draggedEvent.endTime)
|
||||
: draggedEvent.endTime;
|
||||
const start = toDate(draggedEvent.startTime);
|
||||
const end = toDate(draggedEvent.endTime);
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
||||
// Calculate new start time
|
||||
|
|
@ -458,8 +430,8 @@
|
|||
resizeEdge = edge;
|
||||
hasMoved = false;
|
||||
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
resizeOriginalStart = start;
|
||||
resizeOriginalEnd = end;
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@
|
|||
eachDayOfInterval,
|
||||
isSameMonth,
|
||||
isToday,
|
||||
parseISO,
|
||||
setHours,
|
||||
setMinutes,
|
||||
} from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import type { CalendarViewType, CalendarEvent } from '@calendar/shared';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -58,8 +58,7 @@
|
|||
const events = eventsStore.events ?? [];
|
||||
|
||||
for (const event of events) {
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const start = toDate(event.startTime);
|
||||
const key = format(start, 'yyyy-MM-dd');
|
||||
counts.set(key, (counts.get(key) || 0) + 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import EventForm from './EventForm.svelte';
|
||||
import { TagBadge } from '@manacore/shared-ui';
|
||||
import type { CalendarEvent, UpdateEventInput } from '@calendar/shared';
|
||||
import * as api from '$lib/api/events';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { format } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import { EventDetailSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -99,8 +100,8 @@
|
|||
if (event.isAllDay) {
|
||||
return 'Ganztägig';
|
||||
}
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
return `${format(start, 'PPPp', { locale: de })} - ${format(end, 'p', { locale: de })}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
EventAttendee,
|
||||
ResponsiblePerson,
|
||||
} from '@calendar/shared';
|
||||
import { format, addMinutes, parseISO } from 'date-fns';
|
||||
import { format, addMinutes } from 'date-fns';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
|
||||
interface Props {
|
||||
mode: 'create' | 'edit';
|
||||
|
|
@ -104,9 +105,8 @@
|
|||
// Initialize date/time fields using settings for default duration
|
||||
$effect(() => {
|
||||
if (event) {
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
startDate = format(start, 'yyyy-MM-dd');
|
||||
startTime = format(start, 'HH:mm');
|
||||
endDate = format(end, 'yyyy-MM-dd');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import type {
|
||||
LocationDetails,
|
||||
CalendarEvent,
|
||||
|
|
@ -13,8 +13,9 @@
|
|||
import type { ContactSummary, ContactOrManual, ManualContactEntry } from '@manacore/shared-types';
|
||||
import { ContactSelector, ContactAvatar } from '@manacore/shared-ui';
|
||||
import { Users } from 'lucide-svelte';
|
||||
import { format, addMinutes, parseISO } from 'date-fns';
|
||||
import { format, addMinutes } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import { tick, onMount, onDestroy } from 'svelte';
|
||||
|
||||
// Portal action - moves element to body to escape stacking contexts
|
||||
|
|
@ -246,9 +247,8 @@
|
|||
attendees = event.metadata?.attendees || [];
|
||||
|
||||
// Initialize time fields
|
||||
const eventStart =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const eventEnd = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const eventStart = toDate(event.startTime);
|
||||
const eventEnd = toDate(event.endTime);
|
||||
startDateStr = format(eventStart, 'yyyy-MM-dd');
|
||||
startTimeStr = format(eventStart, 'HH:mm');
|
||||
endDateStr = format(eventEnd, 'yyyy-MM-dd');
|
||||
|
|
@ -259,7 +259,7 @@
|
|||
// Date/time fields - derive from draft event (create mode) or event (edit mode)
|
||||
let draftStart = $derived(() => {
|
||||
if (isEditMode && event) {
|
||||
return typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
return toDate(event.startTime);
|
||||
}
|
||||
const draft = eventsStore.draftEvent;
|
||||
if (draft) {
|
||||
|
|
@ -270,7 +270,7 @@
|
|||
|
||||
let draftEnd = $derived(() => {
|
||||
if (isEditMode && event) {
|
||||
return typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
return toDate(event.endTime);
|
||||
}
|
||||
const draft = eventsStore.draftEvent;
|
||||
if (draft) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { todosStore } from '$lib/stores/todos.svelte';
|
||||
import type { Task, UpdateTaskInput, TaskPriority } from '$lib/api/todos';
|
||||
import { PRIORITY_LABELS, PRIORITY_COLORS } from '$lib/api/todos';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import TodoCheckbox from './TodoCheckbox.svelte';
|
||||
import PriorityBadge from './PriorityBadge.svelte';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
*/
|
||||
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
import { parseISO, differenceInMinutes, addMinutes, setHours, setMinutes } from 'date-fns';
|
||||
import { differenceInMinutes, addMinutes, setHours, setMinutes } from 'date-fns';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
|
||||
export interface DragDropConfig {
|
||||
|
|
@ -107,8 +108,8 @@ export function useDragDrop(getConfig: () => DragDropConfig) {
|
|||
draggedEvent = event;
|
||||
hasMoved = false;
|
||||
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
||||
// Calculate initial preview position
|
||||
|
|
@ -158,14 +159,8 @@ export function useDragDrop(getConfig: () => DragDropConfig) {
|
|||
}
|
||||
|
||||
const config = getConfig();
|
||||
const start =
|
||||
typeof draggedEvent.startTime === 'string'
|
||||
? parseISO(draggedEvent.startTime)
|
||||
: draggedEvent.startTime;
|
||||
const end =
|
||||
typeof draggedEvent.endTime === 'string'
|
||||
? parseISO(draggedEvent.endTime)
|
||||
: draggedEvent.endTime;
|
||||
const start = toDate(draggedEvent.startTime);
|
||||
const end = toDate(draggedEvent.endTime);
|
||||
const duration = differenceInMinutes(end, start);
|
||||
|
||||
// Calculate new start time
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
*/
|
||||
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
import { parseISO, differenceInMinutes, setHours, setMinutes } from 'date-fns';
|
||||
import { differenceInMinutes, setHours, setMinutes } from 'date-fns';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
|
||||
export interface ResizeConfig {
|
||||
|
|
@ -86,8 +87,8 @@ export function useResize(getConfig: () => ResizeConfig) {
|
|||
resizeEdge = edge;
|
||||
hasMoved = false;
|
||||
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const start = toDate(event.startTime);
|
||||
const end = toDate(event.endTime);
|
||||
|
||||
resizeOriginalStart = start;
|
||||
resizeOriginalEnd = end;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* useVisibleHours Composable
|
||||
*
|
||||
* Provides hour filtering and time-to-position calculations for calendar views.
|
||||
* Extracts common logic from WeekView, MultiDayView, and DayView.
|
||||
*/
|
||||
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
|
||||
const ALL_HOURS = Array.from({ length: 24 }, (_, i) => i);
|
||||
|
||||
/**
|
||||
* Creates reactive hour visibility state and helper functions
|
||||
*/
|
||||
export function useVisibleHours() {
|
||||
// Filtered hours based on settings
|
||||
let hours = $derived(
|
||||
settingsStore.filterHoursEnabled
|
||||
? ALL_HOURS.filter((h) => h >= settingsStore.dayStartHour && h < settingsStore.dayEndHour)
|
||||
: ALL_HOURS
|
||||
);
|
||||
|
||||
// Calculate visible hours range for positioning
|
||||
let firstVisibleHour = $derived(
|
||||
settingsStore.filterHoursEnabled ? settingsStore.dayStartHour : 0
|
||||
);
|
||||
|
||||
let lastVisibleHour = $derived(settingsStore.filterHoursEnabled ? settingsStore.dayEndHour : 24);
|
||||
|
||||
let totalVisibleHours = $derived(lastVisibleHour - firstVisibleHour);
|
||||
|
||||
/**
|
||||
* Convert minutes (from midnight) to percentage position
|
||||
* accounting for hidden hours when filtering is enabled
|
||||
*/
|
||||
function minutesToPercent(minutes: number): number {
|
||||
const adjustedMinutes = minutes - firstVisibleHour * 60;
|
||||
return (adjustedMinutes / (totalVisibleHours * 60)) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert percentage position back to minutes (from midnight)
|
||||
*/
|
||||
function percentToMinutes(percent: number): number {
|
||||
return (percent / 100) * (totalVisibleHours * 60) + firstVisibleHour * 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a time range overlaps with the visible hours range
|
||||
*/
|
||||
function isTimeRangeVisible(startMinutes: number, endMinutes: number): boolean {
|
||||
const visibleStartMinutes = firstVisibleHour * 60;
|
||||
const visibleEndMinutes = lastVisibleHour * 60;
|
||||
return startMinutes < visibleEndMinutes && endMinutes > visibleStartMinutes;
|
||||
}
|
||||
|
||||
return {
|
||||
get hours() {
|
||||
return hours;
|
||||
},
|
||||
get firstVisibleHour() {
|
||||
return firstVisibleHour;
|
||||
},
|
||||
get lastVisibleHour() {
|
||||
return lastVisibleHour;
|
||||
},
|
||||
get totalVisibleHours() {
|
||||
return totalVisibleHours;
|
||||
},
|
||||
minutesToPercent,
|
||||
percentToMinutes,
|
||||
isTimeRangeVisible,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reactive current time indicator
|
||||
* Updates every minute and provides position calculation
|
||||
*/
|
||||
export function useCurrentTimeIndicator() {
|
||||
let now = $state(new Date());
|
||||
|
||||
// Update current time every minute
|
||||
$effect(() => {
|
||||
const interval = setInterval(() => {
|
||||
now = new Date();
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
return {
|
||||
get now() {
|
||||
return now;
|
||||
},
|
||||
/**
|
||||
* Get current time as minutes from midnight
|
||||
*/
|
||||
get currentMinutes() {
|
||||
return now.getHours() * 60 + now.getMinutes();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -4,7 +4,8 @@
|
|||
|
||||
import type { CalendarEvent, CreateEventInput, UpdateEventInput } from '@calendar/shared';
|
||||
import * as api from '$lib/api/events';
|
||||
import { format, isWithinInterval, parseISO, isSameDay } from 'date-fns';
|
||||
import { format, isWithinInterval, isSameDay } from 'date-fns';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import { toastStore } from './toast.svelte';
|
||||
|
||||
// State
|
||||
|
|
@ -68,9 +69,8 @@ export const eventsStore = {
|
|||
if (!Array.isArray(currentEvents)) return [];
|
||||
|
||||
const result = currentEvents.filter((event) => {
|
||||
const eventStart =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const eventEnd = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const eventStart = toDate(event.startTime);
|
||||
const eventEnd = toDate(event.endTime);
|
||||
|
||||
// For all-day events, check if day falls within event range
|
||||
if (event.isAllDay) {
|
||||
|
|
@ -86,10 +86,7 @@ export const eventsStore = {
|
|||
|
||||
// Include draft event if it exists and is on this day
|
||||
if (includeDraft && draftEvent) {
|
||||
const draftStart =
|
||||
typeof draftEvent.startTime === 'string'
|
||||
? parseISO(draftEvent.startTime)
|
||||
: draftEvent.startTime;
|
||||
const draftStart = toDate(draftEvent.startTime);
|
||||
if (isSameDay(date, draftStart)) {
|
||||
result.push(draftEvent);
|
||||
}
|
||||
|
|
@ -107,9 +104,8 @@ export const eventsStore = {
|
|||
if (!Array.isArray(currentEvents)) return [];
|
||||
|
||||
return currentEvents.filter((event) => {
|
||||
const eventStart =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const eventEnd = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
const eventStart = toDate(event.startTime);
|
||||
const eventEnd = toDate(event.endTime);
|
||||
|
||||
// Check if event overlaps with the range
|
||||
return eventStart <= end && eventEnd >= start;
|
||||
|
|
|
|||
|
|
@ -7,39 +7,55 @@ interface SearchItem {
|
|||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
class SearchStore {
|
||||
// Current search query
|
||||
query = $state('');
|
||||
// State
|
||||
let query = $state('');
|
||||
let matchingEventIds = $state<Set<string>>(new Set());
|
||||
let isSearching = $state(false);
|
||||
|
||||
// Event IDs that match the search
|
||||
matchingEventIds = $state<Set<string>>(new Set());
|
||||
|
||||
// Whether search is active (user is typing in InputBar)
|
||||
isSearching = $state(false);
|
||||
|
||||
// Set search query and matching items (events or any items with an id)
|
||||
setSearch(query: string, matchingItems: SearchItem[]) {
|
||||
this.query = query;
|
||||
this.matchingEventIds = new Set(matchingItems.map((item) => item.id));
|
||||
this.isSearching = query.trim().length > 0;
|
||||
}
|
||||
|
||||
// Clear search
|
||||
clear() {
|
||||
this.query = '';
|
||||
this.matchingEventIds = new Set();
|
||||
this.isSearching = false;
|
||||
}
|
||||
|
||||
// Check if an event matches the search
|
||||
isEventHighlighted(eventId: string): boolean {
|
||||
return this.isSearching && this.matchingEventIds.has(eventId);
|
||||
}
|
||||
|
||||
// Check if an event should be dimmed (search active but event doesn't match)
|
||||
isEventDimmed(eventId: string): boolean {
|
||||
return this.isSearching && !this.matchingEventIds.has(eventId);
|
||||
}
|
||||
/**
|
||||
* Set search query and matching items (events or any items with an id)
|
||||
*/
|
||||
function setSearch(newQuery: string, matchingItems: SearchItem[]) {
|
||||
query = newQuery;
|
||||
matchingEventIds = new Set(matchingItems.map((item) => item.id));
|
||||
isSearching = newQuery.trim().length > 0;
|
||||
}
|
||||
|
||||
export const searchStore = new SearchStore();
|
||||
/**
|
||||
* Clear search
|
||||
*/
|
||||
function clear() {
|
||||
query = '';
|
||||
matchingEventIds = new Set();
|
||||
isSearching = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event matches the search
|
||||
*/
|
||||
function isEventHighlighted(eventId: string): boolean {
|
||||
return isSearching && matchingEventIds.has(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event should be dimmed (search active but event doesn't match)
|
||||
*/
|
||||
function isEventDimmed(eventId: string): boolean {
|
||||
return isSearching && !matchingEventIds.has(eventId);
|
||||
}
|
||||
|
||||
export const searchStore = {
|
||||
get query() {
|
||||
return query;
|
||||
},
|
||||
get matchingEventIds() {
|
||||
return matchingEventIds;
|
||||
},
|
||||
get isSearching() {
|
||||
return isSearching;
|
||||
},
|
||||
setSearch,
|
||||
clear,
|
||||
isEventHighlighted,
|
||||
isEventDimmed,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
function createToastStore() {
|
||||
const { subscribe, update } = writable<Toast[]>([]);
|
||||
|
||||
function add(message: string, type: ToastType = 'info', duration: number = 4000) {
|
||||
const id = crypto.randomUUID();
|
||||
const toast: Toast = { id, type, message, duration };
|
||||
|
||||
update((toasts) => [...toasts, toast]);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
remove(id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
function remove(id: string) {
|
||||
update((toasts) => toasts.filter((t) => t.id !== id));
|
||||
}
|
||||
|
||||
function clear() {
|
||||
update(() => []);
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
add,
|
||||
remove,
|
||||
clear,
|
||||
success: (message: string, duration?: number) => add(message, 'success', duration),
|
||||
error: (message: string, duration?: number) => add(message, 'error', duration),
|
||||
warning: (message: string, duration?: number) => add(message, 'warning', duration),
|
||||
info: (message: string, duration?: number) => add(message, 'info', duration),
|
||||
};
|
||||
}
|
||||
|
||||
export const toast = createToastStore();
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
|
||||
function handleSubscribe(planId: string) {
|
||||
console.log('Subscribe to plan:', planId);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import type { TimeFormat, AllDayDisplayMode } from '$lib/stores/settings.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import { GlobalSettingsSection } from '@manacore/shared-ui';
|
||||
import type { CalendarViewType, Calendar } from '@calendar/shared';
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
isBefore,
|
||||
} from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import { CheckSquare, AlertTriangle, Plus } from 'lucide-svelte';
|
||||
|
||||
// State
|
||||
|
|
@ -47,8 +48,7 @@
|
|||
const currentEvents = eventsStore.events ?? [];
|
||||
if (Array.isArray(currentEvents)) {
|
||||
for (const event of currentEvents) {
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const start = toDate(event.startTime);
|
||||
const dateKey = format(start, 'yyyy-MM-dd');
|
||||
|
||||
if (!groups.has(dateKey)) {
|
||||
|
|
@ -100,14 +100,8 @@
|
|||
|
||||
// Sort events by time
|
||||
if (a.type === 'event' && b.type === 'event' && a.event && b.event) {
|
||||
const aStart =
|
||||
typeof a.event.startTime === 'string'
|
||||
? parseISO(a.event.startTime)
|
||||
: a.event.startTime;
|
||||
const bStart =
|
||||
typeof b.event.startTime === 'string'
|
||||
? parseISO(b.event.startTime)
|
||||
: b.event.startTime;
|
||||
const aStart = toDate(a.event.startTime);
|
||||
const bStart = toDate(b.event.startTime);
|
||||
return aStart.getTime() - bStart.getTime();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue