Added RSVP context to manage current guest and RSVP data, ensuring dynamic updates based on state changes. Adjusted styling to improve button responsiveness and removed redundant borders for a cleaner UI. These updates enhance functionality and provide a more seamless user experience.
515 lines
16 KiB
TypeScript
515 lines
16 KiB
TypeScript
"use client";
|
|
import React, { useState, useEffect } from "react";
|
|
import { useGuests } from "@/context/guest-context";
|
|
import { useEventThemes } from "@/context/event-theme-context";
|
|
import { useEvents } from "@/context/event-context";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectTrigger,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { RsvpStatus } from "@/client/types.gen";
|
|
import { useSearchParams, useParams } from "next/navigation";
|
|
import { Loader2, Plus, Minus } from "lucide-react";
|
|
import { motion } from "framer-motion";
|
|
import { useTheme } from "next-themes";
|
|
import { useRSVPs } from "@/context/rsvp-context";
|
|
|
|
interface RSVPProps {
|
|
eventId: string;
|
|
onRSVPSuccess?: () => void;
|
|
}
|
|
|
|
export const RSVP: React.FC<RSVPProps> = ({ eventId, onRSVPSuccess }) => {
|
|
const {
|
|
guests,
|
|
isLoadingGuests,
|
|
findGuestByInvitationCode,
|
|
submitGuestRsvp,
|
|
} = useGuests();
|
|
|
|
const { rsvps } = useRSVPs();
|
|
const searchParams = useSearchParams();
|
|
const { slug } = useParams<{ slug: string }>();
|
|
const { event, fetchEventBySlug, isLoadingEvent } = useEvents();
|
|
const { themes, isLoadingThemes } = useEventThemes();
|
|
|
|
const [status, setStatus] = useState<RsvpStatus>(RsvpStatus.ATTENDING);
|
|
const [number_of_guests, setNumberOfGuests] = useState<number>(1);
|
|
const [response_message, setResponseMessage] = useState("");
|
|
const [dietary_requirements, setDietaryRequirements] = useState("");
|
|
const [guestId, setGuestId] = useState<string | null>(null);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState<boolean>(false);
|
|
const [currentGuest, setCurrentGuest] = useState<any | null>(null);
|
|
const [currentRSVP, setCurrentRSVP] = useState<any | null>(null);
|
|
|
|
const { setTheme } = useTheme();
|
|
|
|
useEffect(() => {
|
|
setTheme("light");
|
|
}, [setTheme]);
|
|
|
|
// Load event data based on slug or event ID
|
|
useEffect(() => {
|
|
if (slug) {
|
|
fetchEventBySlug(slug);
|
|
}
|
|
}, [slug, fetchEventBySlug]);
|
|
|
|
useEffect(() => {
|
|
if (rsvps && currentGuest) {
|
|
setCurrentRSVP(rsvps.find((rsvp) => rsvp.guest_id === currentGuest?.id));
|
|
}
|
|
}, [rsvps, currentGuest]);
|
|
|
|
useEffect(() => {
|
|
if (currentRSVP) {
|
|
setStatus(currentRSVP.status);
|
|
setNumberOfGuests(currentRSVP.number_of_guests);
|
|
setResponseMessage(currentRSVP.response_message);
|
|
setDietaryRequirements(currentRSVP.dietary_requirements);
|
|
}
|
|
}, [currentRSVP]);
|
|
|
|
// Find the theme for this event
|
|
const eventTheme =
|
|
event && themes?.find((theme) => theme.id === event.theme_id);
|
|
|
|
// Enhanced color palette that works well with safari animals
|
|
const colors = eventTheme?.color_palette || {
|
|
primary: "#90B77D", // Soft jungle green
|
|
secondary: "#D2AB67", // Warm giraffe yellow
|
|
accent: "#B5A9EA", // Soft hippo purple
|
|
accent2: "#8FBDD3", // Elephant blue
|
|
accent3: "#E8B87D", // Lion tan
|
|
background: "#F9F5F0", // Cream paper texture
|
|
backgroundDark: "#F0E9D6", // Slightly darker cream for panels
|
|
text: "#5B4B49", // Warm dark brown
|
|
textLight: "#7D6D6B", // Lighter text variant
|
|
gold: "#D4AF37", // Gold accent for special elements
|
|
};
|
|
|
|
// Font selections
|
|
const fonts = eventTheme?.fonts || {
|
|
heading: "Georgia, serif",
|
|
body: "Arial, sans-serif",
|
|
};
|
|
|
|
// Animation variants
|
|
const formVariants = {
|
|
hidden: { opacity: 0, y: 20 },
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
transition: { duration: 0.4 },
|
|
},
|
|
};
|
|
|
|
const itemVariants = {
|
|
hidden: { opacity: 0, y: 10 },
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
transition: { duration: 0.3 },
|
|
},
|
|
};
|
|
|
|
// Extract invitation code from URL parameters
|
|
useEffect(() => {
|
|
const invitationCode = searchParams.get("code");
|
|
if (!invitationCode) {
|
|
setError(
|
|
"Codice invito mancante. Si prega di utilizzare il link fornito nell'invito.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Find the guest with matching invitation code
|
|
if (guests) {
|
|
const matchingGuest = findGuestByInvitationCode(invitationCode);
|
|
setCurrentGuest(matchingGuest);
|
|
if (matchingGuest) {
|
|
console.log("matchingGuest ", matchingGuest);
|
|
setGuestId(matchingGuest.id);
|
|
setError(null);
|
|
} else {
|
|
setError("Codice invito non valido. Controlla il link dell'invito.");
|
|
}
|
|
}
|
|
}, [searchParams, guests, findGuestByInvitationCode]);
|
|
|
|
// Handler for incrementing the number of guests
|
|
const incrementGuests = () => {
|
|
setNumberOfGuests((prev) => prev + 1);
|
|
};
|
|
|
|
// Handler for decrementing the number of guests
|
|
const decrementGuests = () => {
|
|
setNumberOfGuests((prev) => Math.max(1, prev - 1));
|
|
};
|
|
|
|
// Handler for direct number input
|
|
const handleGuestsInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value === "" ? 1 : parseInt(e.target.value, 10);
|
|
if (!isNaN(value)) {
|
|
setNumberOfGuests(Math.max(1, value));
|
|
}
|
|
};
|
|
|
|
const submitRsvp = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!guestId) {
|
|
setError(
|
|
"Impossibile identificare il tuo invito. Controlla il link o contatta l'organizzatore.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
setIsProcessing(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Use the combined endpoint to update both RSVP and Guest status
|
|
await submitGuestRsvp(guestId, {
|
|
event_id: eventId,
|
|
guest_id: guestId,
|
|
status,
|
|
number_of_guests,
|
|
response_message,
|
|
dietary_requirements,
|
|
});
|
|
|
|
setSuccess(true);
|
|
|
|
if (onRSVPSuccess) {
|
|
setTimeout(() => {
|
|
onRSVPSuccess();
|
|
}, 2000);
|
|
}
|
|
} catch (err) {
|
|
console.error("Error submitting RSVP:", err);
|
|
setError(
|
|
"Impossibile inviare la tua risposta. Riprova o contatta l'organizzatore.",
|
|
);
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
// Show loading state while checking invitation code
|
|
if (isLoadingGuests || isLoadingEvent || isLoadingThemes) {
|
|
return (
|
|
<div
|
|
className="w-full rounded-lg p-6 flex justify-center items-center min-h-[300px]"
|
|
style={{
|
|
backgroundColor: colors.background,
|
|
borderColor: colors.backgroundDark,
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
<Loader2
|
|
className="h-8 w-8 animate-spin"
|
|
style={{ color: colors.primary }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Show error if no valid invitation code
|
|
if (error) {
|
|
return (
|
|
<div
|
|
className="w-full rounded-lg border shadow-sm"
|
|
style={{
|
|
backgroundColor: colors.background,
|
|
borderColor: colors.backgroundDark,
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
<div className="p-6 flex flex-col gap-6">
|
|
<div>
|
|
<h2
|
|
className="text-lg font-medium"
|
|
style={{
|
|
color: "#EF4444",
|
|
fontFamily: fonts.heading,
|
|
}}
|
|
>
|
|
Errore Invito
|
|
</h2>
|
|
<p className="mt-2" style={{ color: colors.textLight }}>
|
|
{error}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
onClick={() => (window.location.href = "/")}
|
|
className="w-full md:w-auto self-end"
|
|
style={{
|
|
backgroundColor: colors.primary,
|
|
color: "white",
|
|
}}
|
|
>
|
|
Torna alla Home
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Show success message
|
|
if (success) {
|
|
return (
|
|
<motion.div
|
|
className="w-full rounded-lg shadow-sm p-8 text-center"
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.5 }}
|
|
style={{
|
|
backgroundColor: colors.background,
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div
|
|
className="w-16 h-16 rounded-full flex items-center justify-center mb-2"
|
|
style={{ backgroundColor: colors.primary + "20" }}
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-8 w-8"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
style={{ color: colors.primary }}
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M5 13l4 4L19 7"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<h2
|
|
className="text-xl font-bold"
|
|
style={{
|
|
fontFamily: fonts.heading,
|
|
color: colors.secondary,
|
|
}}
|
|
>
|
|
Grazie per la tua risposta!
|
|
</h2>
|
|
<p style={{ color: colors.textLight }}>
|
|
La tua presenza è stata registrata con successo.
|
|
</p>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<motion.div
|
|
className="w-full rounded-lg shadow-sm"
|
|
initial="hidden"
|
|
animate="visible"
|
|
variants={formVariants}
|
|
style={{
|
|
backgroundColor: colors.background,
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
<div className="p-6 flex flex-col gap-6">
|
|
<div>
|
|
<h2
|
|
className="text-xl font-bold mb-1"
|
|
style={{
|
|
fontFamily: fonts.heading,
|
|
color: colors.secondary,
|
|
}}
|
|
>
|
|
Conferma la tua presenza
|
|
</h2>
|
|
<p style={{ color: colors.textLight }}>
|
|
Ti preghiamo di farci sapere se potrai partecipare
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={submitRsvp} className="space-y-5">
|
|
<motion.div variants={itemVariants}>
|
|
<Label
|
|
htmlFor="status"
|
|
className="mb-2 block font-medium"
|
|
style={{ color: colors.text }}
|
|
>
|
|
La tua Partecipazione
|
|
</Label>
|
|
<Select
|
|
defaultValue={status}
|
|
onValueChange={(val) => setStatus(val as any)}
|
|
>
|
|
<SelectTrigger
|
|
id="status"
|
|
className="w-full"
|
|
style={{
|
|
borderColor: colors.background,
|
|
backgroundColor: "white",
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
<SelectValue>
|
|
{status === "attending" && "Parteciperò"}
|
|
{status === "not_attending" && "Non parteciperò"}
|
|
{status === "maybe" && "Forse"}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent className="data-[radix-select-content]:rounded-md data-[radix-select-content]:bg-[--event-background] data-[radix-select-content]:border-[--event-backgroundDark]">
|
|
<SelectItem value="attending">Parteciperò</SelectItem>
|
|
<SelectItem value="not_attending">Non parteciperò</SelectItem>
|
|
<SelectItem value="maybe">Forse</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</motion.div>
|
|
|
|
<motion.div variants={itemVariants}>
|
|
<Label
|
|
htmlFor="number_of_guests"
|
|
className="mb-2 block font-medium"
|
|
style={{ color: colors.text }}
|
|
>
|
|
Numero di Ospiti
|
|
</Label>
|
|
|
|
{/* Custom number input with buttons for better mobile experience */}
|
|
<div className="flex items-center">
|
|
<Button
|
|
type="button"
|
|
onClick={decrementGuests}
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-10 w-10 rounded-r-none"
|
|
style={{
|
|
borderColor: colors.backgroundDark,
|
|
backgroundColor: "white",
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
<Minus className="h-4 w-4" />
|
|
</Button>
|
|
<Input
|
|
id="number_of_guests"
|
|
type="text"
|
|
inputMode="numeric"
|
|
pattern="[0-9]*"
|
|
value={number_of_guests}
|
|
onChange={handleGuestsInputChange}
|
|
className="h-10 rounded-none text-center"
|
|
style={{
|
|
borderColor: colors.backgroundDark,
|
|
backgroundColor: "white",
|
|
color: colors.text,
|
|
}}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
onClick={incrementGuests}
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-10 w-10 rounded-l-none"
|
|
style={{
|
|
borderColor: colors.backgroundDark,
|
|
backgroundColor: "white",
|
|
color: colors.text,
|
|
}}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
<p className="text-sm mt-1" style={{ color: colors.textLight }}>
|
|
Quante persone (incluso te) parteciperanno?
|
|
</p>
|
|
</motion.div>
|
|
|
|
<motion.div variants={itemVariants}>
|
|
<Label
|
|
htmlFor="response_message"
|
|
className="mb-2 block font-medium"
|
|
style={{ color: colors.text }}
|
|
>
|
|
Messaggio agli Organizzatori (opzionale)
|
|
</Label>
|
|
<Textarea
|
|
id="response_message"
|
|
value={response_message}
|
|
onChange={(e) => setResponseMessage(e.target.value)}
|
|
placeholder="Scrivi un breve messaggio per gli organizzatori"
|
|
rows={3}
|
|
style={{
|
|
borderColor: colors.backgroundDark,
|
|
backgroundColor: "white",
|
|
color: colors.text,
|
|
}}
|
|
/>
|
|
</motion.div>
|
|
|
|
<motion.div variants={itemVariants}>
|
|
<Label
|
|
htmlFor="dietary_requirements"
|
|
className="mb-2 block font-medium"
|
|
style={{ color: colors.text }}
|
|
>
|
|
Esigenze Alimentari
|
|
</Label>
|
|
<Textarea
|
|
id="dietary_requirements"
|
|
value={dietary_requirements}
|
|
onChange={(e) => setDietaryRequirements(e.target.value)}
|
|
placeholder="Eventuali restrizioni alimentari o allergie?"
|
|
rows={2}
|
|
style={{
|
|
borderColor: colors.backgroundDark,
|
|
backgroundColor: "white",
|
|
color: colors.text,
|
|
}}
|
|
/>
|
|
</motion.div>
|
|
|
|
<motion.div className="flex justify-end mt-6" variants={itemVariants}>
|
|
<Button
|
|
type="submit"
|
|
className="w-full md:w-auto text-white transition-colors duration-300"
|
|
disabled={isProcessing}
|
|
style={{ backgroundColor: colors.accent }}
|
|
onMouseEnter={(e) =>
|
|
(e.currentTarget.style.backgroundColor = colors.accent2)
|
|
}
|
|
onMouseLeave={(e) =>
|
|
(e.currentTarget.style.backgroundColor = colors.accent)
|
|
}
|
|
>
|
|
{isProcessing ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Invio in corso...
|
|
</>
|
|
) : (
|
|
"Invia Risposta"
|
|
)}
|
|
</Button>
|
|
</motion.div>
|
|
</form>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
export default RSVP;
|