Files
cal.diy/companion/components/screens/AddGuestsScreen.tsx
T
Dhairyashil Shinde b8e34bdc06 feat(companion): new booking details page and long press feature for ios (#26264)
* longpress ios - event type, booking, availability

* new booking details page for ios

* remove comments

* address cubics comments

* use lowercase type same as api v2
2025-12-29 18:57:48 +05:30

224 lines
7.6 KiB
TypeScript

/**
* AddGuestsScreen Component
*
* Screen content for adding guests to a booking.
* Used with the add-guests route that has native Stack.Header.
*/
import { Ionicons } from "@expo/vector-icons";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import {
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { Booking } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import { safeLogError } from "@/utils/safeLogger";
export interface AddGuestsScreenProps {
booking: Booking | null;
onSuccess: () => void;
onSavingChange?: (isSaving: boolean) => void;
}
// Handle type for parent component to call submit
export interface AddGuestsScreenHandle {
submit: () => void;
}
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email.trim());
}
export const AddGuestsScreen = forwardRef<AddGuestsScreenHandle, AddGuestsScreenProps>(
function AddGuestsScreen({ booking, onSuccess, onSavingChange }, ref) {
const insets = useSafeAreaInsets();
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [guests, setGuests] = useState<{ email: string; name?: string }[]>([]);
const [isSaving, setIsSaving] = useState(false);
// Notify parent of saving state changes
useEffect(() => {
onSavingChange?.(isSaving);
}, [isSaving, onSavingChange]);
const handleAddGuest = useCallback(() => {
const trimmedEmail = email.trim();
if (!trimmedEmail) {
Alert.alert("Error", "Please enter an email address");
return;
}
if (!isValidEmail(trimmedEmail)) {
Alert.alert("Error", "Please enter a valid email address");
return;
}
if (guests.some((g) => g.email.toLowerCase() === trimmedEmail.toLowerCase())) {
Alert.alert("Error", "This guest has already been added");
return;
}
setGuests([...guests, { email: trimmedEmail, name: name.trim() || undefined }]);
setEmail("");
setName("");
}, [email, name, guests]);
const handleRemoveGuest = useCallback(
(index: number) => {
setGuests(guests.filter((_, i) => i !== index));
},
[guests]
);
const handleSubmit = useCallback(async () => {
if (!booking) return;
if (guests.length === 0) {
Alert.alert("Error", "Please add at least one guest");
return;
}
setIsSaving(true);
try {
await CalComAPIService.addGuests(booking.uid, guests);
Alert.alert("Success", "Guests added successfully", [{ text: "OK", onPress: onSuccess }]);
setIsSaving(false);
} catch (error) {
safeLogError("[AddGuestsScreen] Failed to add guests:", error);
Alert.alert("Error", "Failed to add guests. Please try again.");
setIsSaving(false);
}
}, [booking, guests, onSuccess]);
// Expose submit function to parent via ref (same pattern as senior's actionHandlersRef)
useImperativeHandle(
ref,
() => ({
submit: handleSubmit,
}),
[handleSubmit]
);
if (!booking) {
return (
<View className="flex-1 items-center justify-center bg-[#F2F2F7]">
<Text className="text-gray-500">No booking data</Text>
</View>
);
}
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
className="flex-1 bg-[#F2F2F7]"
>
<ScrollView
className="flex-1"
contentContainerStyle={{ padding: 16, paddingBottom: insets.bottom + 16 }}
keyboardShouldPersistTaps="handled"
>
{/* Info note */}
<View className="mb-4 flex-row items-start rounded-xl bg-[#E3F2FD] p-4">
<Ionicons name="information-circle" size={20} color="#1976D2" />
<Text className="ml-3 flex-1 text-[15px] leading-5 text-[#1565C0]">
Guests will receive an email notification about this booking.
</Text>
</View>
{/* Form Card */}
<View className="mb-4 overflow-hidden rounded-xl bg-white">
{/* Email input */}
<View className="border-b border-gray-100 px-4 py-3">
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">Email *</Text>
<TextInput
className="h-10 text-[17px] text-[#000]"
placeholder="guest@example.com"
placeholderTextColor="#9CA3AF"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
editable={!isSaving}
/>
</View>
{/* Name input */}
<View className="px-4 py-3">
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">Name (optional)</Text>
<TextInput
className="h-10 text-[17px] text-[#000]"
placeholder="Guest Name"
placeholderTextColor="#9CA3AF"
value={name}
onChangeText={setName}
editable={!isSaving}
/>
</View>
</View>
{/* Add button */}
<TouchableOpacity
className="mb-6 flex-row items-center justify-center rounded-xl bg-white py-3"
onPress={handleAddGuest}
activeOpacity={0.7}
disabled={isSaving}
>
<Ionicons name="add-circle" size={22} color="#007AFF" />
<Text className="ml-2 text-[17px] font-medium text-[#007AFF]">Add Guest</Text>
</TouchableOpacity>
{/* Guest list */}
{guests.length > 0 && (
<View>
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-gray-500">
Guests to add ({guests.length})
</Text>
<View className="overflow-hidden rounded-xl bg-white">
{guests.map((guest, index) => (
<View
key={guest.email}
className={`flex-row items-center px-4 py-3 ${
index < guests.length - 1 ? "border-b border-gray-100" : ""
}`}
>
<View className="mr-3 h-10 w-10 items-center justify-center rounded-full bg-[#E8E8ED]">
<Ionicons name="person" size={20} color="#6B7280" />
</View>
<View className="flex-1">
<Text className="text-[17px] text-[#000]">{guest.email}</Text>
{guest.name && (
<Text className="mt-0.5 text-[15px] text-gray-500">{guest.name}</Text>
)}
</View>
<TouchableOpacity
onPress={() => handleRemoveGuest(index)}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
disabled={isSaving}
>
<Ionicons name="close-circle-outline" size={24} color="#800020" />
</TouchableOpacity>
</View>
))}
</View>
</View>
)}
</ScrollView>
</KeyboardAvoidingView>
);
}
);
// Default export for React Native compatibility
export default AddGuestsScreen;