Beautiful loading state

This commit is contained in:
Arthur Belleville 2025-10-26 10:15:10 +01:00
parent ba30079d03
commit 3149928484
No known key found for this signature in database
2 changed files with 76 additions and 7 deletions

View file

@ -136,6 +136,10 @@ tabloRouter.post("/create-and-invite", async (c) => {
email: string;
};
if (ownerId === user.id) {
return c.json({ error: "You cannot create a tablo with yourself" }, 400);
}
// TODO: Verify that the event start and end correspond to a slot
// Check if there's already a tablo between the owner and the invited user

View file

@ -18,6 +18,7 @@ import {
TypographyMuted,
} from "@xtablo/ui/components/typography";
import {
CalendarCheck2,
CalendarIcon,
ChevronLeftIcon,
ChevronRightIcon,
@ -28,7 +29,7 @@ import {
SunIcon,
UserIcon,
} from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { api } from "../lib/api";
@ -53,12 +54,13 @@ export function PublicBookingPage() {
event_type_standard_name || ""
);
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api, (data) => {
queryClient.invalidateQueries({ queryKey: ["tablos"] });
invalidatePublicSlots();
navigate(`/chat/${data.id}`, { replace: true });
navigate(0);
});
const { mutateAsync: createTabloWithOwner, isPending: isCreatingTabloWithOwner } =
useCreateTabloWithOwner(api, (data) => {
queryClient.invalidateQueries({ queryKey: ["tablos"] });
invalidatePublicSlots();
navigate(`/chat/${data.id}`, { replace: true });
navigate(0);
});
const userProfile = publicSlots?.user;
const eventType = publicSlots?.eventType;
@ -83,6 +85,26 @@ export function PublicBookingPage() {
name: "",
});
// Loading messages rotation
const loadingMessages = [
"Nous créons votre rendez-vous, veuillez patienter",
"Préparation de votre réservation...",
"Configuration de votre appel...",
"Finalisation de votre créneau...",
];
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
useEffect(() => {
setCurrentMessageIndex(0);
if (isCreatingTabloWithOwner) {
const interval = setInterval(() => {
setCurrentMessageIndex((prev) => (prev + 1) % loadingMessages.length);
}, 1000);
return () => clearInterval(interval);
}
}, [isCreatingTabloWithOwner]);
// Theme
const { theme, setTheme } = useTheme();
@ -566,6 +588,49 @@ export function PublicBookingPage() {
</div>
</div>
{/* Loading Overlay */}
{isCreatingTabloWithOwner && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl p-8 w-md mx-4 border border-gray-200 dark:border-gray-700">
<div className="flex flex-col items-center gap-6">
{/* Animated Icon */}
<div className="relative">
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center animate-pulse">
<CalendarCheck2 className="w-10 h-10 text-white" />
</div>
<div className="absolute inset-0 rounded-full border-4 border-purple-500/30 animate-ping"></div>
</div>
{/* Text */}
<div className="text-center space-y-2">
<TypographyH3 className="text-gray-900 dark:text-white">
Réservation en cours...
</TypographyH3>
<TypographyMuted className="text-gray-600 dark:text-gray-400">
{loadingMessages[currentMessageIndex]}
</TypographyMuted>
</div>
{/* Loading Spinner */}
<div className="flex gap-2">
<div
className="w-3 h-3 rounded-full bg-purple-500 animate-bounce"
style={{ animationDelay: "0ms" }}
></div>
<div
className="w-3 h-3 rounded-full bg-purple-500 animate-bounce"
style={{ animationDelay: "150ms" }}
></div>
<div
className="w-3 h-3 rounded-full bg-purple-500 animate-bounce"
style={{ animationDelay: "300ms" }}
></div>
</div>
</div>
</div>
</div>
)}
{/* Booking Modal */}
<CustomModal
isOpen={isModalOpen}