Files
calchat/apps/client/src/components/ProposedEventCard.tsx
Linus Waldowsky 27602aee4c Add E2E testing infrastructure with WebdriverIO + Appium
Set up E2E test framework for Android using WebdriverIO, Appium, and
UiAutomator2. Add testID props to key components (AuthButton, BaseButton,
ChatBubble, CustomTextInput, ProposedEventCard) and apply testIDs to
login screen, chat screen, tab bar, and settings. Include initial tests
for app launch detection and login flow validation. Update CLAUDE.md
with E2E docs.
2026-02-26 21:37:40 +01:00

190 lines
5.7 KiB
TypeScript

import { View, Text, Pressable } from "react-native";
import { Feather, Ionicons } from "@expo/vector-icons";
import { ProposedEventChange, formatDate, formatTime } from "@calchat/shared";
import { rrulestr } from "rrule";
import { useThemeStore } from "../stores/ThemeStore";
import { EventCardBase } from "./EventCardBase";
type ProposedEventCardProps = {
proposedChange: ProposedEventChange;
onConfirm: (proposal: ProposedEventChange) => void;
onReject: () => void;
onEdit?: (proposal: ProposedEventChange) => void;
};
const ActionButtons = ({
isDisabled,
respondedAction,
showEdit,
onConfirm,
onReject,
onEdit,
}: {
isDisabled: boolean;
respondedAction?: "confirm" | "reject";
showEdit: boolean;
onConfirm: () => void;
onReject: () => void;
onEdit?: () => void;
}) => {
const { theme } = useThemeStore();
return (
<View className="flex-row mt-3 gap-2">
<Pressable
testID="event-accept-button"
onPress={onConfirm}
disabled={isDisabled}
className="flex-1 py-2 rounded-lg items-center"
style={{
backgroundColor: isDisabled
? theme.disabledButton
: theme.confirmButton,
borderWidth: respondedAction === "confirm" ? 2 : 0,
borderColor: theme.confirmButton,
}}
>
<Text style={{ color: theme.buttonText }} className="font-medium">
Annehmen
</Text>
</Pressable>
<Pressable
testID="event-reject-button"
onPress={onReject}
disabled={isDisabled}
className="flex-1 py-2 rounded-lg items-center"
style={{
backgroundColor: isDisabled
? theme.disabledButton
: theme.rejectButton,
borderWidth: respondedAction === "reject" ? 2 : 0,
borderColor: theme.rejectButton,
}}
>
<Text style={{ color: theme.buttonText }} className="font-medium">
Ablehnen
</Text>
</Pressable>
{showEdit && onEdit && (
<Pressable
onPress={onEdit}
className="py-2 px-3 rounded-lg items-center"
style={{
backgroundColor: theme.secondaryBg,
borderWidth: 1,
borderColor: theme.borderPrimary,
}}
>
<Feather name="edit-2" size={18} color={theme.textPrimary} />
</Pressable>
)}
</View>
);
};
export const ProposedEventCard = ({
proposedChange,
onConfirm,
onReject,
onEdit,
}: ProposedEventCardProps) => {
const { theme } = useThemeStore();
const event = proposedChange.event;
const isDisabled = !!proposedChange.respondedAction;
// For delete/single action, the occurrenceDate becomes a new exception
const newExceptionDate =
proposedChange.action === "delete" &&
proposedChange.deleteMode === "single" &&
proposedChange.occurrenceDate;
// For update actions, check if a new UNTIL date is being set
const newUntilDate =
proposedChange.action === "update" &&
event?.recurrenceRule &&
rrulestr(event.recurrenceRule).options.until;
if (!event) {
return null;
}
return (
<View className="mt-2">
<EventCardBase
className="m-2"
title={event.title}
startTime={event.startTime}
endTime={event.endTime}
description={event.description}
recurrenceRule={event.recurrenceRule}
>
{/* Show new exception date for delete/single actions */}
{newExceptionDate && (
<View className="flex-row items-center mb-2">
<Feather
name="plus-circle"
size={16}
color={theme.confirmButton}
style={{ marginRight: 8 }}
/>
<Text
style={{ color: theme.confirmButton }}
className="font-medium"
>
Neue Ausnahme:{" "}
{formatDate(new Date(proposedChange.occurrenceDate!))}
</Text>
</View>
)}
{/* Show new UNTIL date for update actions */}
{newUntilDate && (
<View className="flex-row items-center mb-2">
<Feather
name="plus-circle"
size={16}
color={theme.confirmButton}
style={{ marginRight: 8 }}
/>
<Text
style={{ color: theme.confirmButton }}
className="font-medium"
>
Neues Ende: {formatDate(newUntilDate)}
</Text>
</View>
)}
{/* Show conflicting events warning */}
{proposedChange.conflictingEvents &&
proposedChange.conflictingEvents.length > 0 && (
<View className="mb-2">
{proposedChange.conflictingEvents.map((conflict, index) => (
<View key={index} className="flex-row items-center mt-1">
<Ionicons
name="alert-circle"
size={16}
color={theme.rejectButton}
style={{ marginRight: 8 }}
/>
<Text
style={{ color: theme.rejectButton }}
className="text-sm flex-1"
>
Konflikt: {conflict.title} ({formatTime(conflict.startTime)}{" "}
- {formatTime(conflict.endTime)})
</Text>
</View>
))}
</View>
)}
<ActionButtons
isDisabled={isDisabled}
respondedAction={proposedChange.respondedAction}
showEdit={proposedChange.action !== "delete" && !isDisabled}
onConfirm={() => onConfirm(proposedChange)}
onReject={onReject}
onEdit={onEdit ? () => onEdit(proposedChange) : undefined}
/>
</EventCardBase>
</View>
);
};