diff --git a/CLAUDE.md b/CLAUDE.md index 9d57e79..c3419f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,7 +77,7 @@ src/ │ │ ├── _layout.tsx # Tab bar configuration (themed) │ │ ├── chat.tsx # Chat screen (AI conversation) │ │ ├── calendar.tsx # Calendar overview -│ │ └── settings.tsx # Settings screen (theme switcher, logout) +│ │ └── settings.tsx # Settings screen (theme switcher, logout, CalDAV config with feedback) │ ├── editEvent.tsx # Event edit screen (dual-mode: calendar/chat) │ ├── event/ │ │ └── [id].tsx # Event detail screen (dynamic route) @@ -612,7 +612,7 @@ NODE_ENV=development # development = pretty logs, production = JSON - `ThemeStore`: Zustand store with theme state and setTheme() - `Themes.tsx`: THEMES object with defaultLight/defaultDark variants - All components use `useThemeStore()` for reactive theme colors - - Settings screen with theme switcher (light/dark) and CalDAV configuration (url, username, password with save/sync buttons, loads existing config on mount) + - Settings screen with theme switcher (light/dark) and CalDAV configuration (url, username, password with save/sync buttons, loads existing config on mount). Save/Sync buttons show independent feedback via `FeedbackRow` component: spinner + loading text during request, then success (green) or error (red) message that auto-clears after 3s. Both feedbacks can be visible simultaneously. - `BaseButton`: Reusable themed button component - Tab navigation (Chat, Calendar, Settings) implemented with themed UI - Calendar screen fully functional: @@ -639,7 +639,7 @@ NODE_ENV=development # development = pretty logs, production = JSON - `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate) - fully implemented with recurring delete modes - `ChatService`: sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation(), updateProposalEvent() - fully implemented with cursor pagination, recurring delete support, and proposal editing - `CaldavConfigService`: saveConfig(), getConfig(), deleteConfig(), pull(), pushAll(), sync() - CalDAV config management and sync trigger -- `CustomTextInput`: Themed text input with focus border highlight. Props: `text`, `onValueChange`, `placeholder`, `placeholderTextColor`, `secureTextEntry`, `autoCapitalize`, `keyboardType`, `className`, `multiline`. No default padding — callers must set padding via `className` (e.g., `px-3 py-2` or `p-4`) +- `CustomTextInput`: Themed text input with focus border highlight. Props: `text`, `onValueChange`, `placeholder`, `placeholderTextColor`, `secureTextEntry`, `autoCapitalize`, `keyboardType`, `className`, `multiline`. No default padding — callers must set padding via `className` (e.g., `px-3 py-2` or `p-4`). When not focused, cursor is reset to start (`selection={{ start: 0 }}`) to avoid text appearing scrolled to the end. - `CardBase`: Reusable card component with header (title/subtitle), content area, and optional footer button - configurable padding, border, text size via props, ScrollView uses `nestedScrollEnabled` for Android - `ModalBase`: Reusable modal wrapper with backdrop (absolute-positioned behind card), uses CardBase internally - provides click-outside-to-close, Android back button support, and proper scrolling on Android - `EventCardBase`: Event card with date/time/recurring icons - uses CardBase for structure. Accepts `recurrenceRule` string (not boolean) and displays German-formatted recurrence via `formatRecurrenceRule()` diff --git a/apps/client/src/app/(tabs)/settings.tsx b/apps/client/src/app/(tabs)/settings.tsx index 04f2f0e..3b70534 100644 --- a/apps/client/src/app/(tabs)/settings.tsx +++ b/apps/client/src/app/(tabs)/settings.tsx @@ -1,4 +1,4 @@ -import { Text, View } from "react-native"; +import { ActivityIndicator, Text, View } from "react-native"; import BaseBackground from "../../components/BaseBackground"; import BaseButton, { BaseButtonProps } from "../../components/BaseButton"; import { useThemeStore } from "../../stores/ThemeStore"; @@ -8,7 +8,7 @@ import { Ionicons } from "@expo/vector-icons"; import { SimpleHeader } from "../../components/Header"; import { THEMES } from "../../Themes"; import CustomTextInput from "../../components/CustomTextInput"; -import { useState } from "react"; +import { useCallback, useRef, useState } from "react"; import { CaldavConfigService } from "../../services/CaldavConfigService"; import { useCaldavConfigStore } from "../../stores"; @@ -33,25 +33,54 @@ type CaldavTextInputProps = { title: string; value: string; onValueChange: (text: string) => void; + secureTextEntry?: boolean; }; const CaldavTextInput = ({ title, value, onValueChange, + secureTextEntry, }: CaldavTextInputProps) => { + const { theme } = useThemeStore(); return ( - {title}: + {title}: ); }; +type Feedback = { text: string; isError: boolean; loading: boolean }; + +const FeedbackRow = ({ feedback }: { feedback: Feedback | null }) => { + const { theme } = useThemeStore(); + if (!feedback) return null; + return ( + + {feedback.loading && ( + + )} + + {feedback.text} + + + ); +}; + const CaldavSettings = () => { const { theme } = useThemeStore(); const { config, setConfig } = useCaldavConfigStore(); @@ -59,18 +88,51 @@ const CaldavSettings = () => { const [serverUrl, setServerUrl] = useState(config?.serverUrl ?? ""); const [username, setUsername] = useState(config?.username ?? ""); const [password, setPassword] = useState(config?.password ?? ""); + const [saveFeedback, setSaveFeedback] = useState(null); + const [syncFeedback, setSyncFeedback] = useState(null); + const saveTimer = useRef | null>(null); + const syncTimer = useRef | null>(null); + + const showFeedback = useCallback( + ( + setter: typeof setSaveFeedback, + timer: typeof saveTimer, + text: string, + isError: boolean, + loading = false, + ) => { + if (timer.current) clearTimeout(timer.current); + setter({ text, isError, loading }); + if (!loading) { + timer.current = setTimeout(() => setter(null), 3000); + } + }, + [], + ); const saveConfig = async () => { - const saved = await CaldavConfigService.saveConfig( - serverUrl, - username, - password, - ); - setConfig(saved); + showFeedback(setSaveFeedback, saveTimer, "Speichere Konfiguration...", false, true); + try { + const saved = await CaldavConfigService.saveConfig( + serverUrl, + username, + password, + ); + setConfig(saved); + showFeedback(setSaveFeedback, saveTimer, "Konfiguration wurde gespeichert", false); + } catch { + showFeedback(setSaveFeedback, saveTimer, "Fehler beim Speichern der Konfiguration", true); + } }; const sync = async () => { - await CaldavConfigService.sync(); + showFeedback(setSyncFeedback, syncTimer, "Synchronisiere...", false, true); + try { + await CaldavConfigService.sync(); + showFeedback(setSyncFeedback, syncTimer, "Synchronisierung erfolgreich", false); + } catch { + showFeedback(setSyncFeedback, syncTimer, "Fehler beim Synchronisieren", true); + } }; return ( @@ -99,6 +161,7 @@ const CaldavSettings = () => { title="password" value={password} onValueChange={setPassword} + secureTextEntry /> @@ -109,6 +172,8 @@ const CaldavSettings = () => { Sync + + ); diff --git a/apps/client/src/components/CustomTextInput.tsx b/apps/client/src/components/CustomTextInput.tsx index 8fb5c91..efe3ca0 100644 --- a/apps/client/src/components/CustomTextInput.tsx +++ b/apps/client/src/components/CustomTextInput.tsx @@ -30,6 +30,7 @@ const CustomTextInput = (props: CustomTextInputProps) => { secureTextEntry={props.secureTextEntry} autoCapitalize={props.autoCapitalize} keyboardType={props.keyboardType} + selection={!focused ? { start: 0 } : undefined} style={{ backgroundColor: theme.messageBorderBg, color: theme.textPrimary,