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,