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) {