2025-12-21 00:15:29 +01:00
|
|
|
"use client";
|
|
|
|
|
|
2025-12-21 17:27:23 +01:00
|
|
|
import React from "react";
|
2025-12-21 00:15:29 +01:00
|
|
|
import { useEffect, useState, useCallback } from "react";
|
|
|
|
|
import { Permission } from "../auth-context";
|
|
|
|
|
import { api } from "../api";
|
|
|
|
|
import { Header } from "../components/Header";
|
|
|
|
|
import { useRequireAuth } from "../hooks/useRequireAuth";
|
|
|
|
|
import { components } from "../generated/api";
|
|
|
|
|
import constants from "../../../shared/constants.json";
|
2025-12-21 17:54:49 +01:00
|
|
|
import { formatDate, formatTime, getDateRange } from "../utils/date";
|
2025-12-21 18:01:30 +01:00
|
|
|
import { sharedStyles } from "../styles/shared";
|
2025-12-21 00:15:29 +01:00
|
|
|
|
|
|
|
|
const { slotDurationMinutes, maxAdvanceDays, minAdvanceDays, noteMaxLength } = constants.booking;
|
|
|
|
|
|
|
|
|
|
type BookableSlot = components["schemas"]["BookableSlot"];
|
|
|
|
|
type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"];
|
|
|
|
|
type AppointmentResponse = components["schemas"]["AppointmentResponse"];
|
|
|
|
|
|
2025-12-21 18:01:30 +01:00
|
|
|
const pageStyles: Record<string, React.CSSProperties> = {
|
2025-12-21 17:27:23 +01:00
|
|
|
main: {
|
|
|
|
|
minHeight: "100vh",
|
|
|
|
|
background: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b4e 100%)",
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
},
|
|
|
|
|
loader: {
|
|
|
|
|
flex: 1,
|
|
|
|
|
display: "flex",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
color: "rgba(255, 255, 255, 0.5)",
|
|
|
|
|
},
|
|
|
|
|
content: {
|
|
|
|
|
flex: 1,
|
|
|
|
|
padding: "2rem",
|
|
|
|
|
maxWidth: "900px",
|
|
|
|
|
margin: "0 auto",
|
|
|
|
|
width: "100%",
|
|
|
|
|
},
|
|
|
|
|
pageTitle: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
fontSize: "1.75rem",
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
color: "#fff",
|
|
|
|
|
marginBottom: "0.5rem",
|
|
|
|
|
},
|
|
|
|
|
pageSubtitle: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
color: "rgba(255, 255, 255, 0.5)",
|
|
|
|
|
fontSize: "0.9rem",
|
|
|
|
|
marginBottom: "1.5rem",
|
|
|
|
|
},
|
|
|
|
|
successBanner: {
|
|
|
|
|
background: "rgba(34, 197, 94, 0.15)",
|
|
|
|
|
border: "1px solid rgba(34, 197, 94, 0.3)",
|
|
|
|
|
color: "#4ade80",
|
|
|
|
|
padding: "1rem",
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
marginBottom: "1rem",
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
fontSize: "0.875rem",
|
|
|
|
|
},
|
|
|
|
|
section: {
|
|
|
|
|
marginBottom: "2rem",
|
|
|
|
|
},
|
|
|
|
|
sectionTitle: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
fontSize: "1.1rem",
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
color: "#fff",
|
|
|
|
|
marginBottom: "1rem",
|
|
|
|
|
},
|
|
|
|
|
dateGrid: {
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexWrap: "wrap",
|
|
|
|
|
gap: "0.5rem",
|
|
|
|
|
},
|
|
|
|
|
dateButton: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
padding: "0.75rem 1rem",
|
|
|
|
|
background: "rgba(255, 255, 255, 0.03)",
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
|
|
|
borderRadius: "10px",
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
minWidth: "90px",
|
|
|
|
|
textAlign: "center" as const,
|
|
|
|
|
transition: "all 0.2s",
|
|
|
|
|
},
|
|
|
|
|
dateButtonSelected: {
|
|
|
|
|
background: "rgba(167, 139, 250, 0.15)",
|
2025-12-21 18:08:49 +01:00
|
|
|
border: "1px solid #a78bfa",
|
2025-12-21 17:27:23 +01:00
|
|
|
},
|
|
|
|
|
dateWeekday: {
|
|
|
|
|
color: "#fff",
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
fontSize: "0.875rem",
|
|
|
|
|
marginBottom: "0.25rem",
|
|
|
|
|
},
|
|
|
|
|
dateDay: {
|
|
|
|
|
color: "rgba(255, 255, 255, 0.5)",
|
|
|
|
|
fontSize: "0.8rem",
|
|
|
|
|
},
|
|
|
|
|
slotGrid: {
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexWrap: "wrap",
|
|
|
|
|
gap: "0.5rem",
|
|
|
|
|
},
|
|
|
|
|
slotButton: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
padding: "0.6rem 1.25rem",
|
|
|
|
|
background: "rgba(255, 255, 255, 0.03)",
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
color: "#fff",
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
fontSize: "0.9rem",
|
|
|
|
|
transition: "all 0.2s",
|
|
|
|
|
},
|
|
|
|
|
slotButtonSelected: {
|
|
|
|
|
background: "rgba(167, 139, 250, 0.15)",
|
2025-12-21 18:08:49 +01:00
|
|
|
border: "1px solid #a78bfa",
|
2025-12-21 17:27:23 +01:00
|
|
|
},
|
|
|
|
|
emptyState: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
color: "rgba(255, 255, 255, 0.4)",
|
|
|
|
|
padding: "1rem 0",
|
|
|
|
|
},
|
|
|
|
|
confirmCard: {
|
|
|
|
|
background: "rgba(255, 255, 255, 0.03)",
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
|
|
|
borderRadius: "12px",
|
|
|
|
|
padding: "1.5rem",
|
|
|
|
|
maxWidth: "400px",
|
|
|
|
|
},
|
|
|
|
|
confirmTitle: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
fontSize: "1.1rem",
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
color: "#fff",
|
|
|
|
|
marginBottom: "1rem",
|
|
|
|
|
},
|
|
|
|
|
confirmTime: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
color: "rgba(255, 255, 255, 0.7)",
|
|
|
|
|
marginBottom: "1rem",
|
|
|
|
|
},
|
|
|
|
|
inputLabel: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
display: "block",
|
|
|
|
|
color: "rgba(255, 255, 255, 0.7)",
|
|
|
|
|
fontSize: "0.875rem",
|
|
|
|
|
marginBottom: "0.5rem",
|
|
|
|
|
},
|
|
|
|
|
textarea: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
width: "100%",
|
|
|
|
|
padding: "0.75rem",
|
|
|
|
|
background: "rgba(255, 255, 255, 0.05)",
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
color: "#fff",
|
|
|
|
|
fontSize: "0.875rem",
|
|
|
|
|
minHeight: "80px",
|
|
|
|
|
resize: "vertical" as const,
|
|
|
|
|
},
|
|
|
|
|
charCount: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
fontSize: "0.75rem",
|
|
|
|
|
color: "rgba(255, 255, 255, 0.4)",
|
|
|
|
|
textAlign: "right" as const,
|
|
|
|
|
marginTop: "0.25rem",
|
|
|
|
|
},
|
|
|
|
|
charCountWarning: {
|
|
|
|
|
color: "#f87171",
|
|
|
|
|
},
|
|
|
|
|
buttonRow: {
|
|
|
|
|
display: "flex",
|
|
|
|
|
gap: "0.75rem",
|
|
|
|
|
marginTop: "1.5rem",
|
|
|
|
|
},
|
|
|
|
|
bookButton: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
flex: 1,
|
|
|
|
|
padding: "0.75rem",
|
|
|
|
|
background: "linear-gradient(135deg, #a78bfa 0%, #7c3aed 100%)",
|
|
|
|
|
border: "none",
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
color: "#fff",
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
transition: "all 0.2s",
|
|
|
|
|
},
|
|
|
|
|
bookButtonDisabled: {
|
|
|
|
|
opacity: 0.5,
|
|
|
|
|
cursor: "not-allowed",
|
|
|
|
|
},
|
|
|
|
|
cancelButton: {
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
padding: "0.75rem 1.25rem",
|
|
|
|
|
background: "rgba(255, 255, 255, 0.05)",
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
color: "rgba(255, 255, 255, 0.7)",
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
transition: "all 0.2s",
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-21 18:01:30 +01:00
|
|
|
const styles = { ...sharedStyles, ...pageStyles };
|
|
|
|
|
|
2025-12-21 00:15:29 +01:00
|
|
|
export default function BookingPage() {
|
|
|
|
|
const { user, isLoading, isAuthorized } = useRequireAuth({
|
|
|
|
|
requiredPermission: Permission.BOOK_APPOINTMENT,
|
|
|
|
|
fallbackRedirect: "/",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
|
|
|
|
const [availableSlots, setAvailableSlots] = useState<BookableSlot[]>([]);
|
|
|
|
|
const [selectedSlot, setSelectedSlot] = useState<BookableSlot | null>(null);
|
|
|
|
|
const [note, setNote] = useState("");
|
|
|
|
|
const [isLoadingSlots, setIsLoadingSlots] = useState(false);
|
|
|
|
|
const [isBooking, setIsBooking] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
|
|
|
|
|
2025-12-21 17:54:49 +01:00
|
|
|
const dates = getDateRange(minAdvanceDays, maxAdvanceDays);
|
2025-12-21 00:15:29 +01:00
|
|
|
|
|
|
|
|
const fetchSlots = useCallback(async (date: Date) => {
|
|
|
|
|
setIsLoadingSlots(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
setAvailableSlots([]);
|
|
|
|
|
setSelectedSlot(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const dateStr = formatDate(date);
|
|
|
|
|
const data = await api.get<AvailableSlotsResponse>(`/api/booking/slots?date=${dateStr}`);
|
|
|
|
|
setAvailableSlots(data.slots);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to fetch slots:", err);
|
|
|
|
|
setError("Failed to load available slots");
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoadingSlots(false);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (selectedDate && user && isAuthorized) {
|
|
|
|
|
fetchSlots(selectedDate);
|
|
|
|
|
}
|
|
|
|
|
}, [selectedDate, user, isAuthorized, fetchSlots]);
|
|
|
|
|
|
|
|
|
|
const handleDateSelect = (date: Date) => {
|
|
|
|
|
setSelectedDate(date);
|
|
|
|
|
setSuccessMessage(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSlotSelect = (slot: BookableSlot) => {
|
|
|
|
|
setSelectedSlot(slot);
|
|
|
|
|
setNote("");
|
|
|
|
|
setError(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBook = async () => {
|
|
|
|
|
if (!selectedSlot) return;
|
|
|
|
|
|
|
|
|
|
setIsBooking(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const appointment = await api.post<AppointmentResponse>("/api/booking", {
|
|
|
|
|
slot_start: selectedSlot.start_time,
|
|
|
|
|
note: note || null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setSuccessMessage(`Appointment booked for ${formatTime(appointment.slot_start)} - ${formatTime(appointment.slot_end)}`);
|
|
|
|
|
setSelectedSlot(null);
|
|
|
|
|
setNote("");
|
|
|
|
|
|
|
|
|
|
// Refresh slots to show the booked one is gone
|
|
|
|
|
if (selectedDate) {
|
|
|
|
|
await fetchSlots(selectedDate);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2025-12-21 17:50:52 +01:00
|
|
|
setError(err instanceof Error ? err.message : "Failed to book appointment");
|
2025-12-21 00:15:29 +01:00
|
|
|
} finally {
|
|
|
|
|
setIsBooking(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const cancelSlotSelection = () => {
|
|
|
|
|
setSelectedSlot(null);
|
|
|
|
|
setNote("");
|
|
|
|
|
setError(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
2025-12-21 17:27:23 +01:00
|
|
|
<main style={styles.main}>
|
|
|
|
|
<div style={styles.loader}>Loading...</div>
|
|
|
|
|
</main>
|
2025-12-21 00:15:29 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isAuthorized) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-21 17:27:23 +01:00
|
|
|
<main style={styles.main}>
|
2025-12-21 00:15:29 +01:00
|
|
|
<Header currentPage="booking" />
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.content}>
|
|
|
|
|
<h1 style={styles.pageTitle}>Book an Appointment</h1>
|
|
|
|
|
<p style={styles.pageSubtitle}>
|
2025-12-21 00:15:29 +01:00
|
|
|
Select a date to see available {slotDurationMinutes}-minute slots
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{successMessage && (
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.successBanner}>{successMessage}</div>
|
2025-12-21 00:15:29 +01:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{error && (
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.errorBanner}>{error}</div>
|
2025-12-21 00:15:29 +01:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Date Selection */}
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.section}>
|
|
|
|
|
<h2 style={styles.sectionTitle}>Select a Date</h2>
|
|
|
|
|
<div style={styles.dateGrid}>
|
2025-12-21 00:15:29 +01:00
|
|
|
{dates.map((date) => {
|
|
|
|
|
const isSelected = selectedDate && formatDate(selectedDate) === formatDate(date);
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={formatDate(date)}
|
|
|
|
|
onClick={() => handleDateSelect(date)}
|
|
|
|
|
style={{
|
2025-12-21 17:27:23 +01:00
|
|
|
...styles.dateButton,
|
|
|
|
|
...(isSelected ? styles.dateButtonSelected : {}),
|
2025-12-21 00:15:29 +01:00
|
|
|
}}
|
|
|
|
|
>
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.dateWeekday}>
|
2025-12-21 00:15:29 +01:00
|
|
|
{date.toLocaleDateString("en-US", { weekday: "short" })}
|
|
|
|
|
</div>
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.dateDay}>
|
2025-12-21 00:15:29 +01:00
|
|
|
{date.toLocaleDateString("en-US", { month: "short", day: "numeric" })}
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Available Slots */}
|
|
|
|
|
{selectedDate && (
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.section}>
|
|
|
|
|
<h2 style={styles.sectionTitle}>
|
2025-12-21 00:15:29 +01:00
|
|
|
Available Slots for {selectedDate.toLocaleDateString("en-US", {
|
|
|
|
|
weekday: "long",
|
|
|
|
|
month: "long",
|
|
|
|
|
day: "numeric"
|
|
|
|
|
})}
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
{isLoadingSlots ? (
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.emptyState}>Loading slots...</div>
|
2025-12-21 00:15:29 +01:00
|
|
|
) : availableSlots.length === 0 ? (
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.emptyState}>No available slots for this date</div>
|
2025-12-21 00:15:29 +01:00
|
|
|
) : (
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.slotGrid}>
|
2025-12-21 00:15:29 +01:00
|
|
|
{availableSlots.map((slot) => {
|
|
|
|
|
const isSelected = selectedSlot?.start_time === slot.start_time;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={slot.start_time}
|
|
|
|
|
onClick={() => handleSlotSelect(slot)}
|
|
|
|
|
style={{
|
2025-12-21 17:27:23 +01:00
|
|
|
...styles.slotButton,
|
|
|
|
|
...(isSelected ? styles.slotButtonSelected : {}),
|
2025-12-21 00:15:29 +01:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{formatTime(slot.start_time)}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Booking Form */}
|
|
|
|
|
{selectedSlot && (
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.confirmCard}>
|
|
|
|
|
<h3 style={styles.confirmTitle}>Confirm Booking</h3>
|
|
|
|
|
<p style={styles.confirmTime}>
|
2025-12-21 00:15:29 +01:00
|
|
|
<strong>Time:</strong> {formatTime(selectedSlot.start_time)} - {formatTime(selectedSlot.end_time)}
|
|
|
|
|
</p>
|
|
|
|
|
|
2025-12-21 17:27:23 +01:00
|
|
|
<div>
|
|
|
|
|
<label style={styles.inputLabel}>
|
2025-12-21 00:15:29 +01:00
|
|
|
Note (optional, max {noteMaxLength} chars)
|
|
|
|
|
</label>
|
|
|
|
|
<textarea
|
|
|
|
|
value={note}
|
|
|
|
|
onChange={(e) => setNote(e.target.value.slice(0, noteMaxLength))}
|
|
|
|
|
placeholder="Add a note about your appointment..."
|
2025-12-21 17:27:23 +01:00
|
|
|
style={styles.textarea}
|
2025-12-21 00:15:29 +01:00
|
|
|
/>
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={{
|
|
|
|
|
...styles.charCount,
|
|
|
|
|
...(note.length >= noteMaxLength ? styles.charCountWarning : {}),
|
2025-12-21 00:15:29 +01:00
|
|
|
}}>
|
|
|
|
|
{note.length}/{noteMaxLength}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-21 17:27:23 +01:00
|
|
|
<div style={styles.buttonRow}>
|
2025-12-21 00:15:29 +01:00
|
|
|
<button
|
|
|
|
|
onClick={handleBook}
|
|
|
|
|
disabled={isBooking}
|
|
|
|
|
style={{
|
2025-12-21 17:27:23 +01:00
|
|
|
...styles.bookButton,
|
|
|
|
|
...(isBooking ? styles.bookButtonDisabled : {}),
|
2025-12-21 00:15:29 +01:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isBooking ? "Booking..." : "Book Appointment"}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={cancelSlotSelection}
|
|
|
|
|
disabled={isBooking}
|
2025-12-21 17:27:23 +01:00
|
|
|
style={styles.cancelButton}
|
2025-12-21 00:15:29 +01:00
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-21 17:27:23 +01:00
|
|
|
</div>
|
|
|
|
|
</main>
|
2025-12-21 00:15:29 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|