diff --git a/CLAUDE.md b/CLAUDE.md index 79466d8..8c6cccd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -200,6 +200,11 @@ Reusable base components for cards and modals with consistent styling. ModalBase provides: transparent Modal + backdrop (click-outside-to-close) + Android back button support. +**ModalBase Architecture Note:** Uses absolute-positioned backdrop behind the card content (not nested Pressables). This approach: +- Fixes modal stacking issues on web (React Native Web renders modals as DOM portals) +- Allows proper scrolling on Android (no touch event conflicts) +- Card naturally blocks touches from reaching backdrop due to z-order + **Component Hierarchy:** ``` CardBase @@ -502,6 +507,7 @@ NODE_ENV=development # development = pretty logs, production = JSON - Supports events from adjacent months visible in grid - Uses `useFocusEffect` for automatic reload on tab focus - DeleteEventModal integration for recurring event deletion with three modes + - EventOverlay hides when DeleteEventModal is open (fixes modal stacking on web) - Chat screen fully functional with FlashList, message sending, and event confirm/reject - **Multiple event proposals**: AI can propose multiple events in one response - Arrow navigation between proposals with "Event X von Y" counter @@ -515,10 +521,10 @@ NODE_ENV=development # development = pretty logs, production = JSON - keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled" - `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate) - fully implemented with recurring delete modes - `ChatService`: sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation() - fully implemented with cursor pagination and recurring delete support -- `CardBase`: Reusable card component with header (title/subtitle), content area, and optional footer button - configurable padding, border, text size via props -- `ModalBase`: Reusable modal wrapper with backdrop, uses CardBase internally - provides click-outside-to-close and Android back button support +- `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 -- `EventCard`: Uses EventCardBase + edit/delete buttons for calendar display +- `EventCard`: Uses EventCardBase + edit/delete buttons (TouchableOpacity with delayPressIn for scroll-friendly touch handling) - `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions with deleteMode display) - `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 diff --git a/apps/client/src/app/(tabs)/calendar.tsx b/apps/client/src/app/(tabs)/calendar.tsx index 4a08ab1..092d64b 100644 --- a/apps/client/src/app/(tabs)/calendar.tsx +++ b/apps/client/src/app/(tabs)/calendar.tsx @@ -188,14 +188,14 @@ const Calendar = () => { } } catch (error) { console.error("Failed to delete event:", error); - } finally { - setEventToDelete(null); } + // Note: Don't clear eventToDelete here - it will be overwritten when opening a new modal. + // Clearing it during fade-out animation causes the modal content to flash from recurring to single. }; const handleDeleteCancel = () => { setDeleteModalVisible(false); - setEventToDelete(null); + // Note: Don't clear eventToDelete - keeps modal content stable during fade-out animation }; // Get events for selected date @@ -222,7 +222,7 @@ const Calendar = () => { onDayPress={handleDayPress} /> + {contentElement} ) : ( diff --git a/apps/client/src/components/DeleteEventModal.tsx b/apps/client/src/components/DeleteEventModal.tsx index acdb28c..2b103a3 100644 --- a/apps/client/src/components/DeleteEventModal.tsx +++ b/apps/client/src/components/DeleteEventModal.tsx @@ -77,7 +77,7 @@ export const DeleteEventModal = ({ // Non-recurring event: simple confirmation - Moechtest du diesen Termin wirklich loeschen? + Möchtest du diesen Termin wirklich löschen? onConfirm("all")} @@ -90,7 +90,7 @@ export const DeleteEventModal = ({ className="font-medium text-base text-center" style={{ color: theme.buttonText }} > - Loeschen + Löschen diff --git a/apps/client/src/components/EventCard.tsx b/apps/client/src/components/EventCard.tsx index f92df45..5d4b9da 100644 --- a/apps/client/src/components/EventCard.tsx +++ b/apps/client/src/components/EventCard.tsx @@ -1,4 +1,4 @@ -import { View, Pressable } from "react-native"; +import { View, TouchableOpacity } from "react-native"; import { ExpandedEvent } from "@calchat/shared"; import { Feather } from "@expo/vector-icons"; import { useThemeStore } from "../stores/ThemeStore"; @@ -21,10 +21,12 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => { description={event.description} isRecurring={event.isRecurring} > - {/* Action buttons */} + {/* Action buttons - TouchableOpacity with delayPressIn allows ScrollView to detect scroll gestures */} - { }} > - - + { }} > - + diff --git a/apps/client/src/components/ModalBase.tsx b/apps/client/src/components/ModalBase.tsx index 16c9a37..9d589a9 100644 --- a/apps/client/src/components/ModalBase.tsx +++ b/apps/client/src/components/ModalBase.tsx @@ -1,4 +1,4 @@ -import { Modal, Pressable } from "react-native"; +import { Modal, Pressable, View } from "react-native"; import { ReactNode } from "react"; import { useThemeStore } from "../stores/ThemeStore"; import { CardBase } from "./CardBase"; @@ -36,19 +36,21 @@ export const ModalBase = ({ animationType="fade" onRequestClose={onClose} > - + + {/* Backdrop - absolute positioned behind the card */} + {/* Card content - on top, naturally blocks touches to backdrop */} + e.stopPropagation()} > {children} - - + + ); };