diff --git a/CLAUDE.md b/CLAUDE.md index 7c233f3..4f5c0a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,7 +76,7 @@ src/ │ ├── (tabs)/ # Tab navigation group │ │ ├── _layout.tsx # Tab bar configuration (themed) │ │ ├── chat.tsx # Chat screen (AI conversation) -│ │ ├── calendar.tsx # Calendar overview +│ │ ├── calendar.tsx # Calendar overview (with CalendarToolbar: sync + logout) │ │ └── settings.tsx # Settings screen (theme switcher, logout, CalDAV config with feedback) │ ├── editEvent.tsx # Event edit screen (dual-mode: calendar/chat) │ ├── event/ @@ -637,6 +637,7 @@ NODE_ENV=development # development = pretty logs, production = JSON - `EventCard`: Uses EventCardBase + edit/delete buttons (TouchableOpacity with delayPressIn for scroll-friendly touch handling) - `ProposedEventCard`: Uses EventCardBase + confirm/reject/edit buttons for chat proposals, displays green highlighted text for new changes ("Neue Ausnahme: [date]" for single delete, "Neues Ende: [date]" for UNTIL updates), shows yellow conflict warnings when proposed time overlaps with existing events. Edit button allows modifying proposals before confirming. - `DeleteEventModal`: Delete confirmation modal using ModalBase - shows three options for recurring events (single/future/all), simple confirm for non-recurring +- `CalendarToolbar` (in calendar.tsx): Toolbar between header and weekdays with Sync button (CalDAV sync with spinner/green checkmark/red X feedback, disabled without config) and Logout button - `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[], preloaded by AuthGuard diff --git a/apps/client/src/app/(tabs)/calendar.tsx b/apps/client/src/app/(tabs)/calendar.tsx index ce0453b..c72ff14 100644 --- a/apps/client/src/app/(tabs)/calendar.tsx +++ b/apps/client/src/app/(tabs)/calendar.tsx @@ -1,4 +1,4 @@ -import { Pressable, Text, View } from "react-native"; +import { ActivityIndicator, Pressable, Text, View } from "react-native"; import { DAYS, MONTHS, @@ -16,9 +16,11 @@ import { router, useFocusEffect } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; import { useThemeStore } from "../../stores/ThemeStore"; import BaseBackground from "../../components/BaseBackground"; -import { EventService } from "../../services"; +import { AuthService, EventService } from "../../services"; import { useEventsStore } from "../../stores"; import { useDropdownPosition } from "../../hooks/useDropdownPosition"; +import { CaldavConfigService } from "../../services/CaldavConfigService"; +import { useCaldavConfigStore } from "../../stores/CaldavConfigStore"; // MonthSelector types and helpers type MonthItem = { @@ -239,6 +241,7 @@ const Calendar = () => { setMonthIndex={setMonthIndex} setYear={setCurrentYear} /> + { ); }; +type CalendarToolbarProps = { + loadEvents: () => Promise; +}; + +const CalendarToolbar = ({ loadEvents }: CalendarToolbarProps) => { + const { theme } = useThemeStore(); + const { config } = useCaldavConfigStore(); + const [isSyncing, setIsSyncing] = useState(false); + const [syncResult, setSyncResult] = useState<"success" | "error" | null>( + null, + ); + + const handleSync = async () => { + if (!config || isSyncing) return; + setSyncResult(null); + setIsSyncing(true); + try { + await CaldavConfigService.sync(); + await loadEvents(); + setSyncResult("success"); + } catch (error) { + console.error("CalDAV sync failed:", error); + setSyncResult("error"); + } finally { + setIsSyncing(false); + } + }; + + useEffect(() => { + if (!syncResult) return; + const timer = setTimeout(() => setSyncResult(null), 3000); + return () => clearTimeout(timer); + }, [syncResult]); + + const handleLogout = async () => { + await AuthService.logout(); + router.replace("/login"); + }; + + const syncIcon = () => { + if (isSyncing) { + return ; + } + if (syncResult === "success") { + return ( + + ); + } + if (syncResult === "error") { + return ( + + ); + } + return ( + + ); + }; + + const buttonStyle = { + backgroundColor: theme.chatBot, + borderColor: theme.borderPrimary, + borderWidth: 1, + shadowColor: theme.shadowColor, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 4, + }; + + const className = "flex flex-row items-center gap-2 px-3 py-1 rounded-lg" + + return ( + + + {syncIcon()} + + Sync + + + + + + + Logout + + + + ); +}; + const WeekDaysLine = () => { const { theme } = useThemeStore(); return (