refactor: add CardBase and ModalBase components
- Add CardBase: reusable card with header, content, footer - Configurable via props: padding, border, text size, background - Add ModalBase: modal wrapper using CardBase internally - Provides backdrop, click-outside-to-close, Android back button - Refactor EventCardBase to use CardBase - Refactor DeleteEventModal to use ModalBase - Refactor EventOverlay (calendar.tsx) to use ModalBase - Update CLAUDE.md with component documentation
This commit is contained in:
65
CLAUDE.md
65
CLAUDE.md
@@ -86,13 +86,15 @@ src/
|
|||||||
│ ├── BaseButton.tsx # Reusable button component (themed, supports children)
|
│ ├── BaseButton.tsx # Reusable button component (themed, supports children)
|
||||||
│ ├── Header.tsx # Header component (themed)
|
│ ├── Header.tsx # Header component (themed)
|
||||||
│ ├── AuthButton.tsx # Reusable button for auth screens (themed, with shadow)
|
│ ├── AuthButton.tsx # Reusable button for auth screens (themed, with shadow)
|
||||||
|
│ ├── CardBase.tsx # Reusable card component (header + content + optional footer)
|
||||||
|
│ ├── ModalBase.tsx # Reusable modal with backdrop (uses CardBase, click-outside-to-close)
|
||||||
│ ├── ChatBubble.tsx # Reusable chat bubble component (used by ChatMessage & TypingIndicator)
|
│ ├── ChatBubble.tsx # Reusable chat bubble component (used by ChatMessage & TypingIndicator)
|
||||||
│ ├── TypingIndicator.tsx # Animated typing indicator (. .. ...) shown while waiting for AI response
|
│ ├── TypingIndicator.tsx # Animated typing indicator (. .. ...) shown while waiting for AI response
|
||||||
│ ├── EventCardBase.tsx # Shared event card layout with icons (used by EventCard & ProposedEventCard)
|
│ ├── EventCardBase.tsx # Event card layout with icons (uses CardBase)
|
||||||
│ ├── EventCard.tsx # Calendar event card (uses EventCardBase + edit/delete buttons)
|
│ ├── EventCard.tsx # Calendar event card (uses EventCardBase + edit/delete buttons)
|
||||||
│ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal (skeleton)
|
│ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal (skeleton)
|
||||||
│ ├── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject buttons)
|
│ ├── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject buttons)
|
||||||
│ └── DeleteEventModal.tsx # Unified delete confirmation modal (recurring: 3 options, non-recurring: simple confirm)
|
│ └── DeleteEventModal.tsx # Delete confirmation modal (uses ModalBase)
|
||||||
├── Themes.tsx # Theme definitions: THEMES object with defaultLight/defaultDark, Theme type
|
├── Themes.tsx # Theme definitions: THEMES object with defaultLight/defaultDark, Theme type
|
||||||
├── logging/
|
├── logging/
|
||||||
│ ├── index.ts # Re-exports
|
│ ├── index.ts # Re-exports
|
||||||
@@ -157,6 +159,58 @@ setTheme("defaultDark"); // or "defaultLight"
|
|||||||
|
|
||||||
**Note:** `shadowColor` only works on iOS. Android uses `elevation` with system-defined shadow colors.
|
**Note:** `shadowColor` only works on iOS. Android uses `elevation` with system-defined shadow colors.
|
||||||
|
|
||||||
|
### Base Components (CardBase & ModalBase)
|
||||||
|
|
||||||
|
Reusable base components for cards and modals with consistent styling.
|
||||||
|
|
||||||
|
**CardBase** - Card structure with header, content, and optional footer:
|
||||||
|
```typescript
|
||||||
|
<CardBase
|
||||||
|
title="Title"
|
||||||
|
subtitle="Optional subtitle"
|
||||||
|
footer={{ label: "Button", onPress: () => {} }}
|
||||||
|
// Styling props (all optional):
|
||||||
|
headerPadding={4} // p-{n}, default: px-3 py-2
|
||||||
|
contentPadding={4} // p-{n}, default: px-3 py-2
|
||||||
|
headerTextSize="text-lg" // "text-sm" | "text-base" | "text-lg" | "text-xl"
|
||||||
|
borderWidth={2} // outer border, default: 2
|
||||||
|
headerBorderWidth={3} // header bottom border, default: borderWidth
|
||||||
|
contentBg={theme.primeBg} // content background color, default: theme.secondaryBg
|
||||||
|
scrollable={true} // wrap content in ScrollView
|
||||||
|
maxContentHeight={400} // for scrollable content
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CardBase>
|
||||||
|
```
|
||||||
|
|
||||||
|
**ModalBase** - Modal with backdrop using CardBase internally:
|
||||||
|
```typescript
|
||||||
|
<ModalBase
|
||||||
|
visible={isVisible}
|
||||||
|
onClose={() => setVisible(false)}
|
||||||
|
title="Modal Title"
|
||||||
|
subtitle="Optional"
|
||||||
|
footer={{ label: "Close", onPress: onClose }}
|
||||||
|
scrollable={true}
|
||||||
|
maxContentHeight={400}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ModalBase>
|
||||||
|
```
|
||||||
|
|
||||||
|
ModalBase provides: transparent Modal + backdrop (click-outside-to-close) + Android back button support.
|
||||||
|
|
||||||
|
**Component Hierarchy:**
|
||||||
|
```
|
||||||
|
CardBase
|
||||||
|
├── ModalBase (uses CardBase)
|
||||||
|
│ ├── DeleteEventModal
|
||||||
|
│ └── EventOverlay (in calendar.tsx)
|
||||||
|
└── EventCardBase (uses CardBase)
|
||||||
|
├── EventCard
|
||||||
|
└── ProposedEventCard
|
||||||
|
```
|
||||||
|
|
||||||
### Backend Architecture (apps/server)
|
### Backend Architecture (apps/server)
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -461,10 +515,13 @@ NODE_ENV=development # development = pretty logs, production = JSON
|
|||||||
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
||||||
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate) - fully implemented with recurring delete modes
|
- `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
|
- `ChatService`: sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation() - fully implemented with cursor pagination and recurring delete support
|
||||||
- `EventCardBase`: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCard
|
- `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
|
||||||
|
- `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 for calendar display
|
||||||
- `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions with deleteMode display)
|
- `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions with deleteMode display)
|
||||||
- `DeleteEventModal`: Unified delete confirmation modal - shows three options for recurring events (single/future/all), simple confirm for non-recurring
|
- `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.)
|
- `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[]
|
||||||
- `ChatStore`: Zustand store with addMessage(), addMessages(), updateMessage(), clearMessages(), isWaitingForResponse/setWaitingForResponse() for typing indicator - loads from server on mount and persists across tab switches
|
- `ChatStore`: Zustand store with addMessage(), addMessages(), updateMessage(), clearMessages(), isWaitingForResponse/setWaitingForResponse() for typing indicator - loads from server on mount and persists across tab switches
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { Animated, Modal, Pressable, Text, View } from "react-native";
|
||||||
Animated,
|
|
||||||
Modal,
|
|
||||||
Pressable,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
ScrollView,
|
|
||||||
} from "react-native";
|
|
||||||
import {
|
import {
|
||||||
DAYS,
|
DAYS,
|
||||||
MONTHS,
|
MONTHS,
|
||||||
@@ -16,6 +9,7 @@ import {
|
|||||||
import Header from "../../components/Header";
|
import Header from "../../components/Header";
|
||||||
import { EventCard } from "../../components/EventCard";
|
import { EventCard } from "../../components/EventCard";
|
||||||
import { DeleteEventModal } from "../../components/DeleteEventModal";
|
import { DeleteEventModal } from "../../components/DeleteEventModal";
|
||||||
|
import { ModalBase } from "../../components/ModalBase";
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -263,7 +257,6 @@ const EventOverlay = ({
|
|||||||
onEditEvent,
|
onEditEvent,
|
||||||
onDeleteEvent,
|
onDeleteEvent,
|
||||||
}: EventOverlayProps) => {
|
}: EventOverlayProps) => {
|
||||||
const { theme } = useThemeStore();
|
|
||||||
if (!date) return null;
|
if (!date) return null;
|
||||||
|
|
||||||
const dateString = date.toLocaleDateString("de-DE", {
|
const dateString = date.toLocaleDateString("de-DE", {
|
||||||
@@ -273,49 +266,18 @@ const EventOverlay = ({
|
|||||||
year: "numeric",
|
year: "numeric",
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
const subtitle = `${events.length} ${events.length === 1 ? "Termin" : "Termine"}`;
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
transparent={true}
|
|
||||||
animationType="fade"
|
|
||||||
onRequestClose={onClose}
|
|
||||||
>
|
|
||||||
<Pressable
|
|
||||||
className="flex-1 justify-center items-center"
|
|
||||||
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
|
||||||
onPress={onClose}
|
|
||||||
>
|
|
||||||
<Pressable
|
|
||||||
className="w-11/12 max-h-3/4 rounded-2xl overflow-hidden"
|
|
||||||
style={{
|
|
||||||
backgroundColor: theme.primeBg,
|
|
||||||
borderWidth: 4,
|
|
||||||
borderColor: theme.borderPrimary,
|
|
||||||
}}
|
|
||||||
onPress={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<View
|
|
||||||
className="px-4 py-3"
|
|
||||||
style={{
|
|
||||||
backgroundColor: theme.chatBot,
|
|
||||||
borderBottomWidth: 3,
|
|
||||||
borderBottomColor: theme.borderPrimary,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
className="font-bold text-lg"
|
|
||||||
style={{ color: theme.textPrimary }}
|
|
||||||
>
|
|
||||||
{dateString}
|
|
||||||
</Text>
|
|
||||||
<Text style={{ color: theme.textPrimary }}>
|
|
||||||
{events.length} {events.length === 1 ? "Termin" : "Termine"}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Events List */}
|
return (
|
||||||
<ScrollView className="p-4" style={{ maxHeight: 400 }}>
|
<ModalBase
|
||||||
|
visible={visible}
|
||||||
|
onClose={onClose}
|
||||||
|
title={dateString}
|
||||||
|
subtitle={subtitle}
|
||||||
|
footer={{ label: "Schliessen", onPress: onClose }}
|
||||||
|
scrollable={true}
|
||||||
|
maxContentHeight={400}
|
||||||
|
>
|
||||||
{events.map((event, index) => (
|
{events.map((event, index) => (
|
||||||
<EventCard
|
<EventCard
|
||||||
key={`${event.id}-${index}`}
|
key={`${event.id}-${index}`}
|
||||||
@@ -324,24 +286,7 @@ const EventOverlay = ({
|
|||||||
onDelete={() => onDeleteEvent(event)}
|
onDelete={() => onDeleteEvent(event)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ModalBase>
|
||||||
|
|
||||||
{/* Close button */}
|
|
||||||
<Pressable
|
|
||||||
onPress={onClose}
|
|
||||||
className="py-3 items-center"
|
|
||||||
style={{
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: theme.placeholderBg,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: theme.primeFg }} className="font-bold">
|
|
||||||
Schließen
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</Pressable>
|
|
||||||
</Pressable>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -114,9 +114,7 @@ const Chat = () => {
|
|||||||
const message = messages.find((m) => m.id === messageId);
|
const message = messages.find((m) => m.id === messageId);
|
||||||
if (message?.proposedChanges) {
|
if (message?.proposedChanges) {
|
||||||
const updatedProposals = message.proposedChanges.map((p) =>
|
const updatedProposals = message.proposedChanges.map((p) =>
|
||||||
p.id === proposalId
|
p.id === proposalId ? { ...p, respondedAction: action } : p,
|
||||||
? { ...p, respondedAction: action }
|
|
||||||
: p,
|
|
||||||
);
|
);
|
||||||
updateMessage(messageId, { proposedChanges: updatedProposals });
|
updateMessage(messageId, { proposedChanges: updatedProposals });
|
||||||
}
|
}
|
||||||
|
|||||||
113
apps/client/src/components/CardBase.tsx
Normal file
113
apps/client/src/components/CardBase.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { View, Text, Pressable, ScrollView } from "react-native";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { useThemeStore } from "../stores/ThemeStore";
|
||||||
|
|
||||||
|
type TextSize = "text-sm" | "text-base" | "text-lg" | "text-xl";
|
||||||
|
|
||||||
|
type CardBaseProps = {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
footer?: {
|
||||||
|
label: string;
|
||||||
|
onPress: () => void;
|
||||||
|
};
|
||||||
|
className?: string;
|
||||||
|
scrollable?: boolean;
|
||||||
|
maxContentHeight?: number;
|
||||||
|
borderWidth?: number;
|
||||||
|
headerBorderWidth?: number;
|
||||||
|
headerPadding?: number;
|
||||||
|
contentPadding?: number;
|
||||||
|
headerTextSize?: TextSize;
|
||||||
|
contentBg?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CardBase = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
className = "",
|
||||||
|
scrollable = false,
|
||||||
|
maxContentHeight,
|
||||||
|
borderWidth = 2,
|
||||||
|
headerBorderWidth,
|
||||||
|
headerPadding,
|
||||||
|
contentPadding,
|
||||||
|
headerTextSize = "text-base",
|
||||||
|
contentBg,
|
||||||
|
}: CardBaseProps) => {
|
||||||
|
const { theme } = useThemeStore();
|
||||||
|
const effectiveHeaderBorderWidth = headerBorderWidth ?? borderWidth;
|
||||||
|
|
||||||
|
const headerPaddingClass = headerPadding ? `p-${headerPadding}` : "px-3 py-2";
|
||||||
|
const contentPaddingClass = contentPadding
|
||||||
|
? `p-${contentPadding}`
|
||||||
|
: "px-3 py-2";
|
||||||
|
|
||||||
|
const contentElement = (
|
||||||
|
<View
|
||||||
|
className={contentPaddingClass}
|
||||||
|
style={{ backgroundColor: contentBg ?? theme.secondaryBg }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
className={`rounded-xl overflow-hidden ${className}`}
|
||||||
|
style={{ borderWidth, borderColor: theme.borderPrimary }}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<View
|
||||||
|
className={headerPaddingClass}
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.chatBot,
|
||||||
|
borderBottomWidth: effectiveHeaderBorderWidth,
|
||||||
|
borderBottomColor: theme.borderPrimary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className={`font-bold ${headerTextSize}`}
|
||||||
|
style={{ color: theme.textPrimary }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
{subtitle && (
|
||||||
|
<Text style={{ color: theme.primeFg }} numberOfLines={1}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{scrollable ? (
|
||||||
|
<ScrollView style={{ maxHeight: maxContentHeight }}>
|
||||||
|
{contentElement}
|
||||||
|
</ScrollView>
|
||||||
|
) : (
|
||||||
|
contentElement
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer (optional) */}
|
||||||
|
{footer && (
|
||||||
|
<Pressable
|
||||||
|
onPress={footer.onPress}
|
||||||
|
className="py-3 items-center"
|
||||||
|
style={{
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: theme.placeholderBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: theme.primeFg }} className="font-bold">
|
||||||
|
{footer.label}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardBase;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Modal, Pressable, Text, View } from "react-native";
|
import { Pressable, Text, View } from "react-native";
|
||||||
import { RecurringDeleteMode } from "@calchat/shared";
|
import { RecurringDeleteMode } from "@calchat/shared";
|
||||||
import { useThemeStore } from "../stores/ThemeStore";
|
import { useThemeStore } from "../stores/ThemeStore";
|
||||||
|
import { ModalBase } from "./ModalBase";
|
||||||
|
|
||||||
type DeleteEventModalProps = {
|
type DeleteEventModalProps = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -13,24 +14,20 @@ type DeleteEventModalProps = {
|
|||||||
type DeleteOption = {
|
type DeleteOption = {
|
||||||
mode: RecurringDeleteMode;
|
mode: RecurringDeleteMode;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RECURRING_DELETE_OPTIONS: DeleteOption[] = [
|
const RECURRING_DELETE_OPTIONS: DeleteOption[] = [
|
||||||
{
|
{
|
||||||
mode: "single",
|
mode: "single",
|
||||||
label: "Nur dieses Vorkommen",
|
label: "Nur dieses Vorkommen",
|
||||||
description: "Nur der ausgewaehlte Termin wird entfernt",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mode: "future",
|
mode: "future",
|
||||||
label: "Dieses und zukuenftige",
|
label: "Dieses und zukünftige",
|
||||||
description: "Dieser und alle folgenden Termine werden entfernt",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mode: "all",
|
mode: "all",
|
||||||
label: "Alle Vorkommen",
|
label: "Alle Vorkommen",
|
||||||
description: "Die gesamte Terminserie wird geloescht",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -43,51 +40,18 @@ export const DeleteEventModal = ({
|
|||||||
}: DeleteEventModalProps) => {
|
}: DeleteEventModalProps) => {
|
||||||
const { theme } = useThemeStore();
|
const { theme } = useThemeStore();
|
||||||
|
|
||||||
return (
|
const title = isRecurring
|
||||||
<Modal
|
? "Wiederkehrenden Termin löschen"
|
||||||
visible={visible}
|
: "Termin loeschen";
|
||||||
transparent={true}
|
|
||||||
animationType="fade"
|
|
||||||
onRequestClose={onCancel}
|
|
||||||
>
|
|
||||||
<Pressable
|
|
||||||
className="flex-1 justify-center items-center"
|
|
||||||
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
|
||||||
onPress={onCancel}
|
|
||||||
>
|
|
||||||
<Pressable
|
|
||||||
className="w-11/12 rounded-2xl overflow-hidden"
|
|
||||||
style={{
|
|
||||||
backgroundColor: theme.primeBg,
|
|
||||||
borderWidth: 4,
|
|
||||||
borderColor: theme.borderPrimary,
|
|
||||||
}}
|
|
||||||
onPress={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<View
|
|
||||||
className="px-4 py-3"
|
|
||||||
style={{
|
|
||||||
backgroundColor: theme.chatBot,
|
|
||||||
borderBottomWidth: 3,
|
|
||||||
borderBottomColor: theme.borderPrimary,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
className="font-bold text-lg"
|
|
||||||
style={{ color: theme.textPrimary }}
|
|
||||||
>
|
|
||||||
{isRecurring
|
|
||||||
? "Wiederkehrenden Termin loeschen"
|
|
||||||
: "Termin loeschen"}
|
|
||||||
</Text>
|
|
||||||
<Text style={{ color: theme.textSecondary }} numberOfLines={1}>
|
|
||||||
{eventTitle}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Content */}
|
return (
|
||||||
<View className="p-4">
|
<ModalBase
|
||||||
|
visible={visible}
|
||||||
|
onClose={onCancel}
|
||||||
|
title={title}
|
||||||
|
subtitle={eventTitle}
|
||||||
|
footer={{ label: "Abbrechen", onPress: onCancel }}
|
||||||
|
>
|
||||||
{isRecurring ? (
|
{isRecurring ? (
|
||||||
// Recurring event: show three options
|
// Recurring event: show three options
|
||||||
RECURRING_DELETE_OPTIONS.map((option) => (
|
RECURRING_DELETE_OPTIONS.map((option) => (
|
||||||
@@ -107,22 +71,13 @@ export const DeleteEventModal = ({
|
|||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
|
||||||
className="text-sm mt-1"
|
|
||||||
style={{ color: theme.textMuted }}
|
|
||||||
>
|
|
||||||
{option.description}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
// Non-recurring event: simple confirmation
|
// Non-recurring event: simple confirmation
|
||||||
<View>
|
<View>
|
||||||
<Text
|
<Text className="text-base mb-4" style={{ color: theme.textPrimary }}>
|
||||||
className="text-base mb-4"
|
Moechtest du diesen Termin wirklich loeschen?
|
||||||
style={{ color: theme.textPrimary }}
|
|
||||||
>
|
|
||||||
Möchtest du diesen Termin wirklich löschen?
|
|
||||||
</Text>
|
</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => onConfirm("all")}
|
onPress={() => onConfirm("all")}
|
||||||
@@ -140,23 +95,6 @@ export const DeleteEventModal = ({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</ModalBase>
|
||||||
|
|
||||||
{/* Cancel button */}
|
|
||||||
<Pressable
|
|
||||||
onPress={onCancel}
|
|
||||||
className="py-3 items-center"
|
|
||||||
style={{
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: theme.placeholderBg,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: theme.primeFg }} className="font-bold">
|
|
||||||
Abbrechen
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</Pressable>
|
|
||||||
</Pressable>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { View, Text } from "react-native";
|
|||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { useThemeStore } from "../stores/ThemeStore";
|
import { useThemeStore } from "../stores/ThemeStore";
|
||||||
|
import { CardBase } from "./CardBase";
|
||||||
|
|
||||||
type EventCardBaseProps = {
|
type EventCardBaseProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -61,33 +62,9 @@ export const EventCardBase = ({
|
|||||||
children,
|
children,
|
||||||
}: EventCardBaseProps) => {
|
}: EventCardBaseProps) => {
|
||||||
const { theme } = useThemeStore();
|
const { theme } = useThemeStore();
|
||||||
return (
|
|
||||||
<View
|
|
||||||
className={`rounded-xl overflow-hidden ${className}`}
|
|
||||||
style={{ borderWidth: 2, borderColor: theme.borderPrimary }}
|
|
||||||
>
|
|
||||||
{/* Header with title */}
|
|
||||||
<View
|
|
||||||
className="px-3 py-2"
|
|
||||||
style={{
|
|
||||||
backgroundColor: theme.chatBot,
|
|
||||||
borderBottomWidth: 2,
|
|
||||||
borderBottomColor: theme.borderPrimary,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
className="font-bold text-base"
|
|
||||||
style={{ color: theme.textPrimary }}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Content */}
|
return (
|
||||||
<View
|
<CardBase title={title} className={className} borderWidth={2}>
|
||||||
className="px-3 py-2"
|
|
||||||
style={{ backgroundColor: theme.secondaryBg }}
|
|
||||||
>
|
|
||||||
{/* Date */}
|
{/* Date */}
|
||||||
<View className="flex-row items-center mb-1">
|
<View className="flex-row items-center mb-1">
|
||||||
<Feather
|
<Feather
|
||||||
@@ -137,8 +114,7 @@ export const EventCardBase = ({
|
|||||||
|
|
||||||
{/* Action buttons slot */}
|
{/* Action buttons slot */}
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</CardBase>
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
74
apps/client/src/components/ModalBase.tsx
Normal file
74
apps/client/src/components/ModalBase.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { Modal, Pressable } from "react-native";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { useThemeStore } from "../stores/ThemeStore";
|
||||||
|
import { CardBase } from "./CardBase";
|
||||||
|
|
||||||
|
type ModalBaseProps = {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
footer?: {
|
||||||
|
label: string;
|
||||||
|
onPress: () => void;
|
||||||
|
};
|
||||||
|
scrollable?: boolean;
|
||||||
|
maxContentHeight?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModalBase = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
scrollable,
|
||||||
|
maxContentHeight,
|
||||||
|
}: ModalBaseProps) => {
|
||||||
|
const { theme } = useThemeStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
transparent={true}
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
className="flex-1 justify-center items-center"
|
||||||
|
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
||||||
|
onPress={onClose}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
className="w-11/12 rounded-2xl overflow-hidden"
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.primeBg,
|
||||||
|
borderWidth: 4,
|
||||||
|
borderColor: theme.borderPrimary,
|
||||||
|
}}
|
||||||
|
onPress={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<CardBase
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
footer={footer}
|
||||||
|
scrollable={scrollable}
|
||||||
|
maxContentHeight={maxContentHeight}
|
||||||
|
borderWidth={0}
|
||||||
|
headerBorderWidth={3}
|
||||||
|
headerPadding={4}
|
||||||
|
contentPadding={4}
|
||||||
|
headerTextSize="text-lg"
|
||||||
|
contentBg={theme.primeBg}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CardBase>
|
||||||
|
</Pressable>
|
||||||
|
</Pressable>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalBase;
|
||||||
Reference in New Issue
Block a user