From 8e58ab424925e795935eb6fe5e3a8423a0b4df4f Mon Sep 17 00:00:00 2001 From: Linus Waldowsky Date: Mon, 5 Jan 2026 19:27:33 +0100 Subject: [PATCH] refactor: extract shared EventCardBase component - Create EventCardBase with common layout, icons (calendar, clock, repeat), and formatting functions - Refactor EventCard and ProposedEventCard to use EventCardBase - Add event details to delete action responses for better UX - Include event title in delete confirmation message --- CLAUDE.md | 10 +- apps/client/src/components/EventCard.tsx | 120 ++------------- apps/client/src/components/EventCardBase.tsx | 141 +++++++++++++++++ .../src/components/ProposedEventCard.tsx | 142 +++++++++--------- apps/server/src/services/ChatService.ts | 14 +- 5 files changed, 235 insertions(+), 192 deletions(-) create mode 100644 apps/client/src/components/EventCardBase.tsx diff --git a/CLAUDE.md b/CLAUDE.md index d7dd982..7f8b520 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,9 +77,10 @@ src/ ├── components/ │ ├── BaseBackground.tsx # Common screen wrapper │ ├── Header.tsx # Header component -│ ├── EventCard.tsx # Event card for calendar display +│ ├── EventCardBase.tsx # Shared event card layout with icons (used by EventCard & ProposedEventCard) +│ ├── EventCard.tsx # Calendar event card (uses EventCardBase + edit/delete buttons) │ ├── EventConfirmDialog.tsx # AI-proposed event confirmation modal -│ └── ProposedEventCard.tsx # Inline event proposal with confirm/reject buttons +│ └── ProposedEventCard.tsx # Chat event proposal (uses EventCardBase + confirm/reject buttons) ├── Themes.tsx # Centralized color/theme definitions ├── services/ │ ├── index.ts # Re-exports all services @@ -305,8 +306,9 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin - `ApiClient`: get(), post(), put(), delete() implemented - `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete() - fully implemented - `ChatService`: sendMessage(), confirmEvent(convId, msgId, action, event?, eventId?, updates?), rejectEvent() - supports create/update/delete actions -- `EventCard`: Displays event details (title, date, time, duration, recurring indicator) with Feather icons and edit/delete buttons -- `ProposedEventCard`: Displays proposed events (title, date, description, recurring indicator) with confirm/reject buttons +- `EventCardBase`: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCard +- `EventCard`: Uses EventCardBase + edit/delete buttons for calendar display +- `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions) - `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator - `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[] - `ChatStore`: Zustand store with addMessage(), updateMessage(), clearMessages() - persists messages across tab switches diff --git a/apps/client/src/components/EventCard.tsx b/apps/client/src/components/EventCard.tsx index b6b3683..b71ccd5 100644 --- a/apps/client/src/components/EventCard.tsx +++ b/apps/client/src/components/EventCard.tsx @@ -1,7 +1,8 @@ -import { View, Text, Pressable } from "react-native"; +import { View, Pressable } from "react-native"; import { ExpandedEvent } from "@caldav/shared"; import { Feather } from "@expo/vector-icons"; import currentTheme from "../Themes"; +import { EventCardBase } from "./EventCardBase"; type EventCardProps = { event: ExpandedEvent; @@ -9,117 +10,16 @@ type EventCardProps = { onDelete: () => void; }; -function formatDate(date: Date): string { - const d = new Date(date); - return d.toLocaleDateString("de-DE", { - weekday: "short", - day: "2-digit", - month: "2-digit", - year: "numeric", - }); -} - -function formatTime(date: Date): string { - const d = new Date(date); - return d.toLocaleTimeString("de-DE", { - hour: "2-digit", - minute: "2-digit", - }); -} - -function formatDuration(start: Date, end: Date): string { - const startDate = new Date(start); - const endDate = new Date(end); - const diffMs = endDate.getTime() - startDate.getTime(); - const diffMins = Math.round(diffMs / 60000); - - if (diffMins < 60) { - return `${diffMins} min`; - } - - const hours = Math.floor(diffMins / 60); - const mins = diffMins % 60; - - if (mins === 0) { - return hours === 1 ? "1 Std" : `${hours} Std`; - } - - return `${hours} Std ${mins} min`; -} - export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => { return ( - - {/* Header with title */} - + - {event.title} - - - {/* Content */} - - {/* Date */} - - - - {formatDate(event.occurrenceStart)} - - - - {/* Time with duration */} - - - - {formatTime(event.occurrenceStart)} -{" "} - {formatTime(event.occurrenceEnd)} ( - {formatDuration(event.occurrenceStart, event.occurrenceEnd)}) - - - - {/* Recurring indicator */} - {event.isRecurring && ( - - - - Wiederkehrend - - - )} - - {/* Description */} - {event.description && ( - - {event.description} - - )} - {/* Action buttons */} { /> - + ); }; diff --git a/apps/client/src/components/EventCardBase.tsx b/apps/client/src/components/EventCardBase.tsx new file mode 100644 index 0000000..02ec251 --- /dev/null +++ b/apps/client/src/components/EventCardBase.tsx @@ -0,0 +1,141 @@ +import { View, Text } from "react-native"; +import { Feather } from "@expo/vector-icons"; +import { ReactNode } from "react"; +import currentTheme from "../Themes"; + +type EventCardBaseProps = { + className?: string; + title: string; + startTime: Date; + endTime: Date; + description?: string; + isRecurring?: boolean; + children?: ReactNode; +}; + +function formatDate(date: Date): string { + const d = new Date(date); + return d.toLocaleDateString("de-DE", { + weekday: "short", + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +} + +function formatTime(date: Date): string { + const d = new Date(date); + return d.toLocaleTimeString("de-DE", { + hour: "2-digit", + minute: "2-digit", + }); +} + +function formatDuration(start: Date, end: Date): string { + const startDate = new Date(start); + const endDate = new Date(end); + const diffMs = endDate.getTime() - startDate.getTime(); + const diffMins = Math.round(diffMs / 60000); + + if (diffMins < 60) { + return `${diffMins} min`; + } + + const hours = Math.floor(diffMins / 60); + const mins = diffMins % 60; + + if (mins === 0) { + return hours === 1 ? "1 Std" : `${hours} Std`; + } + + return `${hours} Std ${mins} min`; +} + +export const EventCardBase = ({ + className, + title, + startTime, + endTime, + description, + isRecurring, + children, +}: EventCardBaseProps) => { + return ( + + {/* Header with title */} + + {title} + + + {/* Content */} + + {/* Date */} + + + + {formatDate(startTime)} + + + + {/* Time with duration */} + + + + {formatTime(startTime)} - {formatTime(endTime)} ( + {formatDuration(startTime, endTime)}) + + + + {/* Recurring indicator */} + {isRecurring && ( + + + + Wiederkehrend + + + )} + + {/* Description */} + {description && ( + + {description} + + )} + + {/* Action buttons slot */} + {children} + + + ); +}; + +export default EventCardBase; diff --git a/apps/client/src/components/ProposedEventCard.tsx b/apps/client/src/components/ProposedEventCard.tsx index 34d3348..973f11c 100644 --- a/apps/client/src/components/ProposedEventCard.tsx +++ b/apps/client/src/components/ProposedEventCard.tsx @@ -1,6 +1,7 @@ import { View, Text, Pressable } from "react-native"; import { ProposedEventChange } from "@caldav/shared"; import currentTheme from "../Themes"; +import { EventCardBase } from "./EventCardBase"; type ProposedEventCardProps = { proposedChange: ProposedEventChange; @@ -9,17 +10,52 @@ type ProposedEventCardProps = { onReject: () => void; }; -function formatDateTime(date?: Date): string { - if (!date) return ""; - const d = new Date(date); - return d.toLocaleDateString("de-DE", { - weekday: "long", - day: "2-digit", - month: "2-digit", - hour: "2-digit", - minute: "2-digit", - }); -} +const ConfirmRejectButtons = ({ + isDisabled, + respondedAction, + onConfirm, + onReject, +}: { + isDisabled: boolean; + respondedAction?: "confirm" | "reject"; + onConfirm: () => void; + onReject: () => void; +}) => ( + + + + Annehmen + + + + + Ablehnen + + + +); export const ProposedEventCard = ({ proposedChange, @@ -30,71 +66,27 @@ export const ProposedEventCard = ({ const event = proposedChange.event; const isDisabled = !!respondedAction; - return ( - - {/* Event Details */} - {event?.title} - - {formatDateTime(event?.startTime)} - - {event?.description && ( - - {event.description} - - )} - {event?.isRecurring && ( - - Wiederkehrend - - )} + if (!event) { + return null; + } - {/* Buttons */} - - - - Annehmen - - - - - Ablehnen - - - + return ( + + + + ); }; diff --git a/apps/server/src/services/ChatService.ts b/apps/server/src/services/ChatService.ts index 4061842..2d69897 100644 --- a/apps/server/src/services/ChatService.ts +++ b/apps/server/src/services/ChatService.ts @@ -175,11 +175,17 @@ async function getTestResponse( const jensEvent = events.find((e) => e.title === "Meeting mit Jens"); if (jensEvent) { return { - content: - "Alles klar, ich lösche den Termin 'Meeting mit Jens' für dich:", + content: "Soll ich diesen Termin wirklich löschen?", proposedChange: { action: "delete", eventId: jensEvent.id, + event: { + title: jensEvent.title, + startTime: jensEvent.startTime, + endTime: jensEvent.endTime, + description: jensEvent.description, + isRecurring: jensEvent.isRecurring, + }, }, }; } @@ -285,7 +291,9 @@ export class ChatService { : "Termin nicht gefunden."; } else if (action === "delete" && eventId) { await this.eventRepo.delete(eventId); - content = "Der Termin wurde gelöscht."; + content = event?.title + ? `Der Termin "${event.title}" wurde gelöscht.` + : "Der Termin wurde gelöscht."; } else { content = "Ungültige Aktion."; }