From 868e1ba68d510f54443181ed5184e3199a471cb6 Mon Sep 17 00:00:00 2001 From: Linus Waldowsky Date: Mon, 9 Feb 2026 18:59:03 +0100 Subject: [PATCH] perf: preload events and CalDAV config to avoid empty screens Add CaldavConfigStore and preloadAppData() to load events (current month) and CalDAV config into stores before dismissing the auth loading spinner. This prevents the brief empty flash when first navigating to Calendar or Settings tabs. Also applies Prettier formatting across codebase. --- CLAUDE.md | 13 ++-- apps/client/src/app/(tabs)/calendar.tsx | 7 +-- apps/client/src/app/(tabs)/chat.tsx | 6 +- apps/client/src/app/(tabs)/settings.tsx | 61 +++++++++++-------- apps/client/src/app/editEvent.tsx | 7 ++- apps/client/src/app/login.tsx | 2 + apps/client/src/components/AuthGuard.tsx | 42 ++++++++++++- apps/client/src/components/BaseButton.tsx | 7 ++- apps/client/src/components/DateTimePicker.tsx | 4 +- .../src/components/ProposedEventCard.tsx | 13 +++- apps/client/src/stores/CaldavConfigStore.ts | 14 +++++ apps/client/src/stores/index.ts | 1 + apps/server/src/app.ts | 2 +- .../mongo/MongoCaldavRepository.ts | 2 +- .../mongo/MongoEventRepository.ts | 5 +- .../repositories/mongo/models/EventModel.ts | 4 +- apps/server/src/routes/index.ts | 2 +- apps/server/src/services/ChatService.ts | 35 ++++++----- apps/server/src/services/EventService.ts | 5 +- .../src/services/interfaces/AIProvider.ts | 5 +- .../services/interfaces/EventRepository.ts | 5 +- packages/shared/src/utils/rruleHelpers.ts | 5 +- 22 files changed, 178 insertions(+), 69 deletions(-) create mode 100644 apps/client/src/stores/CaldavConfigStore.ts diff --git a/CLAUDE.md b/CLAUDE.md index d172f1f..a2ad7e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,7 +84,7 @@ src/ │ └── note/ │ └── [id].tsx # Note editor for event (dynamic route) ├── components/ -│ ├── AuthGuard.tsx # Auth wrapper: loads user, CalDAV sync on auto-login, shows loading, redirects if unauthenticated +│ ├── AuthGuard.tsx # Auth wrapper: loads user, preloads app data (events + CalDAV config), CalDAV sync, shows loading, redirects if unauthenticated. Exports preloadAppData() │ ├── BaseBackground.tsx # Common screen wrapper (themed) │ ├── BaseButton.tsx # Reusable button component (themed, supports children) │ ├── Header.tsx # Header component (themed) @@ -118,6 +118,7 @@ src/ │ │ # Uses expo-secure-store (native) / localStorage (web) │ ├── ChatStore.ts # messages[], isWaitingForResponse, addMessage(), addMessages(), updateMessage(), clearMessages(), setWaitingForResponse(), chatMessageToMessageData() │ ├── EventsStore.ts # events[], setEvents(), addEvent(), updateEvent(), deleteEvent() +│ ├── CaldavConfigStore.ts # config (CaldavConfig | null), setConfig() - cached CalDAV config │ └── ThemeStore.ts # theme, setTheme() - reactive theme switching with Zustand └── hooks/ └── useDropdownPosition.ts # Hook for positioning dropdowns relative to trigger element @@ -128,9 +129,12 @@ src/ **Authentication Flow:** - `AuthGuard` component wraps the tab layout in `(tabs)/_layout.tsx` - On app start, `AuthGuard` calls `loadStoredUser()` and shows loading indicator +- After auth, `preloadAppData()` loads events (current month) + CalDAV config into stores before dismissing spinner - If not authenticated, redirects to `/login` +- `login.tsx` also calls `preloadAppData()` after successful login (spinner stays visible during preload) - `index.tsx` simply redirects to `/(tabs)/chat` - AuthGuard handles the rest - This pattern handles Expo Router's navigation state caching (avoids race conditions) +- Preloading prevents empty screens when navigating to Calendar or Settings tabs for the first time ### Theme System @@ -597,8 +601,8 @@ NODE_ENV=development # development = pretty logs, production = JSON - `AuthStore`: Manages user state with expo-secure-store (native) / localStorage (web) - `AuthService`: login(), register(), logout() - calls backend API - `ApiClient`: Automatically injects X-User-Id header for authenticated requests, handles empty responses (204) - - `AuthGuard`: Reusable component that wraps protected routes - loads user, triggers CalDAV sync on auto-login, shows loading, redirects if unauthenticated - - Login screen: Supports email OR userName login, triggers CalDAV sync after successful login + - `AuthGuard`: Reusable component that wraps protected routes - loads user, preloads app data (events + CalDAV config) into stores before dismissing spinner, triggers CalDAV sync, shows loading, redirects if unauthenticated. Exports `preloadAppData()` (also called by `login.tsx`) + - Login screen: Supports email OR userName login, preloads app data + triggers CalDAV sync after successful login - Register screen: Email validation, checks for existing email/userName - `AuthButton`: Reusable button component with themed shadow - `Header`: Themed header component (logout moved to Settings) @@ -644,7 +648,8 @@ NODE_ENV=development # development = pretty logs, production = JSON - `DeleteEventModal`: Delete confirmation modal using ModalBase - shows three options for recurring events (single/future/all), simple confirm for non-recurring - `EventOverlay` (in calendar.tsx): Day events overlay using ModalBase - shows EventCards for selected day - `Themes.tsx`: Theme definitions with THEMES object (defaultLight, defaultDark) including all color tokens (textPrimary, borderPrimary, eventIndicator, secondaryBg, shadowColor, etc.) -- `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[] +- `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[], preloaded by AuthGuard +- `CaldavConfigStore`: Zustand store with config (CaldavConfig | null), setConfig() - cached CalDAV config, preloaded by AuthGuard, used by Settings to avoid API call on mount - `ChatStore`: Zustand store with addMessage(), addMessages(), updateMessage(), clearMessages(), isWaitingForResponse/setWaitingForResponse() for typing indicator - loads from server on mount and persists across tab switches - `ThemeStore`: Zustand store with theme/setTheme() for reactive theme switching across all components - `ChatBubble`: Reusable chat bubble component with Tailwind styling, used by ChatMessage and TypingIndicator diff --git a/apps/client/src/app/(tabs)/calendar.tsx b/apps/client/src/app/(tabs)/calendar.tsx index 07811bb..7c87b30 100644 --- a/apps/client/src/app/(tabs)/calendar.tsx +++ b/apps/client/src/app/(tabs)/calendar.tsx @@ -11,12 +11,7 @@ import { EventCard } from "../../components/EventCard"; import { DeleteEventModal } from "../../components/DeleteEventModal"; import { ModalBase } from "../../components/ModalBase"; import { ScrollableDropdown } from "../../components/ScrollableDropdown"; -import React, { - useCallback, - useEffect, - useMemo, - useState, -} from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { router, useFocusEffect } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; import { useThemeStore } from "../../stores/ThemeStore"; diff --git a/apps/client/src/app/(tabs)/chat.tsx b/apps/client/src/app/(tabs)/chat.tsx index 1333a3b..ea3b077 100644 --- a/apps/client/src/app/(tabs)/chat.tsx +++ b/apps/client/src/app/(tabs)/chat.tsx @@ -175,7 +175,11 @@ const Chat = () => { params: { mode: "chat", eventData: JSON.stringify(proposal.event), - proposalContext: JSON.stringify({ messageId, proposalId, conversationId }), + proposalContext: JSON.stringify({ + messageId, + proposalId, + conversationId, + }), }, }); }; diff --git a/apps/client/src/app/(tabs)/settings.tsx b/apps/client/src/app/(tabs)/settings.tsx index 19dbe6a..b9c0fdf 100644 --- a/apps/client/src/app/(tabs)/settings.tsx +++ b/apps/client/src/app/(tabs)/settings.tsx @@ -8,8 +8,9 @@ import { Ionicons } from "@expo/vector-icons"; import { SimpleHeader } from "../../components/Header"; import { THEMES } from "../../Themes"; import CustomTextInput from "../../components/CustomTextInput"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { CaldavConfigService } from "../../services/CaldavConfigService"; +import { useCaldavConfigStore } from "../../stores"; const handleLogout = async () => { await AuthService.logout(); @@ -34,38 +35,38 @@ type CaldavTextInputProps = { onValueChange: (text: string) => void; }; -const CaldavTextInput = ({ title, value, onValueChange }: CaldavTextInputProps) => { +const CaldavTextInput = ({ + title, + value, + onValueChange, +}: CaldavTextInputProps) => { return ( {title}: - + ); }; const CaldavSettings = () => { const { theme } = useThemeStore(); + const { config, setConfig } = useCaldavConfigStore(); - const [serverUrl, setServerUrl] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - - useEffect(() => { - const loadConfig = async () => { - try { - const config = await CaldavConfigService.getConfig(); - setServerUrl(config.serverUrl); - setUsername(config.username); - setPassword(config.password); - } catch { - // No config saved yet - } - }; - loadConfig(); - }, []); + const [serverUrl, setServerUrl] = useState(config?.serverUrl ?? ""); + const [username, setUsername] = useState(config?.username ?? ""); + const [password, setPassword] = useState(config?.password ?? ""); const saveConfig = async () => { - await CaldavConfigService.saveConfig(serverUrl, username, password); + const saved = await CaldavConfigService.saveConfig( + serverUrl, + username, + password, + ); + setConfig(saved); }; const sync = async () => { @@ -84,9 +85,21 @@ const CaldavSettings = () => { - - - + + + diff --git a/apps/client/src/app/editEvent.tsx b/apps/client/src/app/editEvent.tsx index 8505ee4..d1968d4 100644 --- a/apps/client/src/app/editEvent.tsx +++ b/apps/client/src/app/editEvent.tsx @@ -19,7 +19,12 @@ import { Ionicons } from "@expo/vector-icons"; import { ScrollableDropdown } from "../components/ScrollableDropdown"; import { useDropdownPosition } from "../hooks/useDropdownPosition"; import { EventService, ChatService } from "../services"; -import { buildRRule, CreateEventDTO, REPEAT_TYPE_LABELS, RepeatType } from "@calchat/shared"; +import { + buildRRule, + CreateEventDTO, + REPEAT_TYPE_LABELS, + RepeatType, +} from "@calchat/shared"; import { useChatStore } from "../stores"; import CustomTextInput, { CustomTextInputProps, diff --git a/apps/client/src/app/login.tsx b/apps/client/src/app/login.tsx index 002b09d..c33911d 100644 --- a/apps/client/src/app/login.tsx +++ b/apps/client/src/app/login.tsx @@ -5,6 +5,7 @@ import BaseBackground from "../components/BaseBackground"; import AuthButton from "../components/AuthButton"; import { AuthService } from "../services"; import { CaldavConfigService } from "../services/CaldavConfigService"; +import { preloadAppData } from "../components/AuthGuard"; import { useThemeStore } from "../stores/ThemeStore"; const LoginScreen = () => { @@ -25,6 +26,7 @@ const LoginScreen = () => { setIsLoading(true); try { await AuthService.login({ identifier, password }); + await preloadAppData(); try { await CaldavConfigService.sync(); } catch { diff --git a/apps/client/src/components/AuthGuard.tsx b/apps/client/src/components/AuthGuard.tsx index f25e6ad..ddde39b 100644 --- a/apps/client/src/components/AuthGuard.tsx +++ b/apps/client/src/components/AuthGuard.tsx @@ -1,17 +1,52 @@ -import { useEffect, ReactNode } from "react"; +import { useEffect, useState, ReactNode } from "react"; import { View, ActivityIndicator } from "react-native"; import { Redirect } from "expo-router"; import { useAuthStore } from "../stores"; import { useThemeStore } from "../stores/ThemeStore"; +import { useEventsStore } from "../stores/EventsStore"; +import { useCaldavConfigStore } from "../stores/CaldavConfigStore"; +import { EventService } from "../services"; import { CaldavConfigService } from "../services/CaldavConfigService"; type AuthGuardProps = { children: ReactNode; }; +/** + * Preloads app data (events + CalDAV config) into stores. + * Called before the loading spinner is dismissed so screens have data immediately. + */ +export const preloadAppData = async () => { + const now = new Date(); + const firstOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const dayOfWeek = firstOfMonth.getDay(); + const daysFromPrevMonth = dayOfWeek === 0 ? 6 : dayOfWeek - 1; + const startDate = new Date( + now.getFullYear(), + now.getMonth(), + 1 - daysFromPrevMonth, + ); + const endDate = new Date(startDate); + endDate.setDate(startDate.getDate() + 41); + endDate.setHours(23, 59, 59); + + const [eventsResult, configResult] = await Promise.allSettled([ + EventService.getByDateRange(startDate, endDate), + CaldavConfigService.getConfig(), + ]); + + if (eventsResult.status === "fulfilled") { + useEventsStore.getState().setEvents(eventsResult.value); + } + if (configResult.status === "fulfilled") { + useCaldavConfigStore.getState().setConfig(configResult.value); + } +}; + /** * Wraps content that requires authentication. * - Loads stored user on mount + * - Preloads app data (events, CalDAV config) before dismissing spinner * - Shows loading indicator while checking auth state * - Redirects to login if not authenticated * - Renders children if authenticated @@ -19,11 +54,14 @@ type AuthGuardProps = { export const AuthGuard = ({ children }: AuthGuardProps) => { const { theme } = useThemeStore(); const { isAuthenticated, isLoading, loadStoredUser } = useAuthStore(); + const [dataReady, setDataReady] = useState(false); useEffect(() => { const init = async () => { await loadStoredUser(); if (!useAuthStore.getState().isAuthenticated) return; + await preloadAppData(); + setDataReady(true); try { await CaldavConfigService.sync(); } catch { @@ -33,7 +71,7 @@ export const AuthGuard = ({ children }: AuthGuardProps) => { init(); }, [loadStoredUser]); - if (isLoading) { + if (isLoading || (isAuthenticated && !dataReady)) { return ( { +const BaseButton = ({ + className, + children, + onPress, + solid = false, +}: BaseButtonProps) => { const { theme } = useThemeStore(); return ( + props: Omit, ) => ; export const TimePickerButton = ( - props: Omit + props: Omit, ) => ; export default DateTimePickerButton; diff --git a/apps/client/src/components/ProposedEventCard.tsx b/apps/client/src/components/ProposedEventCard.tsx index d2f9694..6e98c9f 100644 --- a/apps/client/src/components/ProposedEventCard.tsx +++ b/apps/client/src/components/ProposedEventCard.tsx @@ -124,8 +124,12 @@ export const ProposedEventCard = ({ color={theme.confirmButton} style={{ marginRight: 8 }} /> - - Neue Ausnahme: {formatDate(new Date(proposedChange.occurrenceDate!))} + + Neue Ausnahme:{" "} + {formatDate(new Date(proposedChange.occurrenceDate!))} )} @@ -138,7 +142,10 @@ export const ProposedEventCard = ({ color={theme.confirmButton} style={{ marginRight: 8 }} /> - + Neues Ende: {formatDate(newUntilDate)} diff --git a/apps/client/src/stores/CaldavConfigStore.ts b/apps/client/src/stores/CaldavConfigStore.ts new file mode 100644 index 0000000..a591de6 --- /dev/null +++ b/apps/client/src/stores/CaldavConfigStore.ts @@ -0,0 +1,14 @@ +import { create } from "zustand"; +import { CaldavConfig } from "@calchat/shared"; + +interface CaldavConfigState { + config: CaldavConfig | null; + setConfig: (config: CaldavConfig | null) => void; +} + +export const useCaldavConfigStore = create((set) => ({ + config: null, + setConfig: (config: CaldavConfig | null) => { + set({ config }); + }, +})); diff --git a/apps/client/src/stores/index.ts b/apps/client/src/stores/index.ts index a832c08..bfeda05 100644 --- a/apps/client/src/stores/index.ts +++ b/apps/client/src/stores/index.ts @@ -5,3 +5,4 @@ export { type MessageData, } from "./ChatStore"; export { useEventsStore } from "./EventsStore"; +export { useCaldavConfigStore } from "./CaldavConfigStore"; diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 4d5c62e..3ccba6f 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -83,7 +83,7 @@ app.use( authController, chatController, eventController, - caldavController + caldavController, }), ); diff --git a/apps/server/src/repositories/mongo/MongoCaldavRepository.ts b/apps/server/src/repositories/mongo/MongoCaldavRepository.ts index 87442e4..d65fca2 100644 --- a/apps/server/src/repositories/mongo/MongoCaldavRepository.ts +++ b/apps/server/src/repositories/mongo/MongoCaldavRepository.ts @@ -25,7 +25,7 @@ export class MongoCaldavRepository implements CaldavRepository { } async deleteByUserId(userId: string): Promise { - const result = await CaldavConfigModel.findOneAndDelete({userId}); + const result = await CaldavConfigModel.findOneAndDelete({ userId }); return result !== null; } } diff --git a/apps/server/src/repositories/mongo/MongoEventRepository.ts b/apps/server/src/repositories/mongo/MongoEventRepository.ts index aa765bd..d779975 100644 --- a/apps/server/src/repositories/mongo/MongoEventRepository.ts +++ b/apps/server/src/repositories/mongo/MongoEventRepository.ts @@ -28,7 +28,10 @@ export class MongoEventRepository implements EventRepository { return events.map((e) => e.toJSON() as unknown as CalendarEvent); } - async findByCaldavUUID(userId: string, caldavUUID: string): Promise { + async findByCaldavUUID( + userId: string, + caldavUUID: string, + ): Promise { const event = await EventModel.findOne({ userId, caldavUUID }); if (!event) return null; return event.toJSON() as unknown as CalendarEvent; diff --git a/apps/server/src/repositories/mongo/models/EventModel.ts b/apps/server/src/repositories/mongo/models/EventModel.ts index a5b7beb..cc99284 100644 --- a/apps/server/src/repositories/mongo/models/EventModel.ts +++ b/apps/server/src/repositories/mongo/models/EventModel.ts @@ -2,9 +2,7 @@ import mongoose, { Schema, Document, Model } from "mongoose"; import { CalendarEvent } from "@calchat/shared"; import { IdVirtual } from "./types"; -export interface EventDocument - extends Omit, - Document { +export interface EventDocument extends Omit, Document { toJSON(): CalendarEvent; } diff --git a/apps/server/src/routes/index.ts b/apps/server/src/routes/index.ts index 95ea427..bcad14c 100644 --- a/apps/server/src/routes/index.ts +++ b/apps/server/src/routes/index.ts @@ -6,7 +6,7 @@ import { AuthController, ChatController, EventController, - CaldavController + CaldavController, } from "../controllers"; import { createCaldavRoutes } from "./caldav.routes"; diff --git a/apps/server/src/services/ChatService.ts b/apps/server/src/services/ChatService.ts index 63f135b..739af91 100644 --- a/apps/server/src/services/ChatService.ts +++ b/apps/server/src/services/ChatService.ts @@ -363,9 +363,7 @@ async function getTestResponse( event: { title: sportEvent.title, startTime: exceptionDate, - endTime: new Date( - exceptionDate.getTime() + 90 * 60 * 1000, - ), // +90 min + endTime: new Date(exceptionDate.getTime() + 90 * 60 * 1000), // +90 min description: sportEvent.description, recurrenceRule: sportEvent.recurrenceRule, exceptionDates: sportEvent.exceptionDates, @@ -375,7 +373,8 @@ async function getTestResponse( }; } return { - content: "Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.", + content: + "Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.", }; } @@ -387,13 +386,13 @@ async function getTestResponse( // Calculate UNTIL date: 6 weeks from start const untilDate = new Date(sportEvent.startTime); untilDate.setDate(untilDate.getDate() + 42); // 6 weeks - const untilStr = untilDate.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"; + const untilStr = + untilDate.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"; const newRule = `FREQ=WEEKLY;BYDAY=WE;UNTIL=${untilStr}`; return { - content: - "Alles klar! Ich beende die Sport-Serie nach 6 Wochen:", + content: "Alles klar! Ich beende die Sport-Serie nach 6 Wochen:", proposedChanges: [ { id: "sport-until", @@ -413,7 +412,8 @@ async function getTestResponse( }; } return { - content: "Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.", + content: + "Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.", }; } @@ -440,9 +440,7 @@ async function getTestResponse( event: { title: sportEvent.title, startTime: exceptionDate, - endTime: new Date( - exceptionDate.getTime() + 90 * 60 * 1000, - ), // +90 min + endTime: new Date(exceptionDate.getTime() + 90 * 60 * 1000), // +90 min description: sportEvent.description, recurrenceRule: sportEvent.recurrenceRule, exceptionDates: sportEvent.exceptionDates, @@ -452,7 +450,8 @@ async function getTestResponse( }; } return { - content: "Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.", + content: + "Ich konnte keinen Termin 'Sport' finden. Bitte erstelle ihn zuerst.", }; } @@ -567,7 +566,11 @@ export class ChatService { if (process.env.USE_TEST_RESPONSES === "true") { // Test mode: use static responses - response = await getTestResponse(responseIndex, this.eventService, userId); + response = await getTestResponse( + responseIndex, + this.eventService, + userId, + ); responseIndex++; } else { // Production mode: use real AI @@ -642,7 +645,11 @@ export class ChatService { const createdEvent = await this.eventService.create(userId, event); content = `Der Termin "${createdEvent.title}" wurde erstellt.`; } else if (action === "update" && eventId && updates) { - const updatedEvent = await this.eventService.update(eventId, userId, updates); + const updatedEvent = await this.eventService.update( + eventId, + userId, + updates, + ); content = updatedEvent ? `Der Termin "${updatedEvent.title}" wurde aktualisiert.` : "Termin nicht gefunden."; diff --git a/apps/server/src/services/EventService.ts b/apps/server/src/services/EventService.ts index 47d2048..b8478e2 100644 --- a/apps/server/src/services/EventService.ts +++ b/apps/server/src/services/EventService.ts @@ -24,7 +24,10 @@ export class EventService { return event; } - async findByCaldavUUID(userId: string, caldavUUID: string): Promise { + async findByCaldavUUID( + userId: string, + caldavUUID: string, + ): Promise { return this.eventRepo.findByCaldavUUID(userId, caldavUUID); } diff --git a/apps/server/src/services/interfaces/AIProvider.ts b/apps/server/src/services/interfaces/AIProvider.ts index 870816c..2189ca2 100644 --- a/apps/server/src/services/interfaces/AIProvider.ts +++ b/apps/server/src/services/interfaces/AIProvider.ts @@ -11,7 +11,10 @@ export interface AIContext { currentDate: Date; // Callback to load events from a specific date range // Returns ExpandedEvent[] with occurrenceStart/occurrenceEnd for recurring events - fetchEventsInRange: (startDate: Date, endDate: Date) => Promise; + fetchEventsInRange: ( + startDate: Date, + endDate: Date, + ) => Promise; // Callback to search events by title searchEvents: (query: string) => Promise; // Callback to fetch a single event by ID diff --git a/apps/server/src/services/interfaces/EventRepository.ts b/apps/server/src/services/interfaces/EventRepository.ts index 8a87896..85a3ff7 100644 --- a/apps/server/src/services/interfaces/EventRepository.ts +++ b/apps/server/src/services/interfaces/EventRepository.ts @@ -8,7 +8,10 @@ export interface EventRepository { startDate: Date, endDate: Date, ): Promise; - findByCaldavUUID(userId: string, caldavUUID: string): Promise; + findByCaldavUUID( + userId: string, + caldavUUID: string, + ): Promise; searchByTitle(userId: string, query: string): Promise; create(userId: string, data: CreateEventDTO): Promise; update(id: string, data: UpdateEventDTO): Promise; diff --git a/packages/shared/src/utils/rruleHelpers.ts b/packages/shared/src/utils/rruleHelpers.ts index 0293ade..fd20de3 100644 --- a/packages/shared/src/utils/rruleHelpers.ts +++ b/packages/shared/src/utils/rruleHelpers.ts @@ -36,7 +36,10 @@ const REPEAT_TYPE_SINGULAR: Record = { * @param interval - The interval between repetitions (default: 1) * @returns RRULE string like "FREQ=WEEKLY;INTERVAL=2" */ -export function buildRRule(repeatType: RepeatType, interval: number = 1): string { +export function buildRRule( + repeatType: RepeatType, + interval: number = 1, +): string { const freq = REPEAT_TYPE_TO_FREQ[repeatType]; if (interval <= 1) {