From 5a0314ae879ca46e1e158a998569a21c4d4b5264 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 26 Oct 2025 09:46:11 +0100 Subject: [PATCH 1/4] Fix copy text --- apps/main/src/locales/fr/pages.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/main/src/locales/fr/pages.json b/apps/main/src/locales/fr/pages.json index 876ba4f..2fe04a9 100644 --- a/apps/main/src/locales/fr/pages.json +++ b/apps/main/src/locales/fr/pages.json @@ -44,10 +44,10 @@ }, "events": { "title": "Mes Événements", - "subtitle": "Gérez vos événements, réservations et types d'événements", + "subtitle": "Gérez vos événements, réservations et types d'appels", "tabs": { "events": "Événements", - "eventTypes": "Types d'événements" + "eventTypes": "Types d'appels" }, "createEvent": "Nouvel événement", "createEventType": "Nouveau type", From ba30079d03a1656326b221938a1db6a622a16381 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 26 Oct 2025 09:46:26 +0100 Subject: [PATCH 2/4] Improve public page --- apps/main/src/pages/PublicBookingPage.tsx | 466 +++++++++++----------- packages/shared/src/hooks/public.ts | 2 +- 2 files changed, 235 insertions(+), 233 deletions(-) diff --git a/apps/main/src/pages/PublicBookingPage.tsx b/apps/main/src/pages/PublicBookingPage.tsx index 6e86861..6d6db6e 100644 --- a/apps/main/src/pages/PublicBookingPage.tsx +++ b/apps/main/src/pages/PublicBookingPage.tsx @@ -11,7 +11,12 @@ import { Button } from "@xtablo/ui/components/button"; import { FieldError } from "@xtablo/ui/components/field"; import { Input } from "@xtablo/ui/components/input"; import { Label } from "@xtablo/ui/components/label"; -import { Strong, Text } from "@xtablo/ui/components/typography"; +import { + Text, + TypographyH3, + TypographyH4, + TypographyMuted, +} from "@xtablo/ui/components/typography"; import { CalendarIcon, ChevronLeftIcon, @@ -25,6 +30,7 @@ import { } from "lucide-react"; import { useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { twMerge } from "tailwind-merge"; import { api } from "../lib/api"; import { supabase } from "../lib/supabase"; import { useMaybeUser } from "../providers/UserStoreProvider"; @@ -81,13 +87,7 @@ export function PublicBookingPage() { const { theme, setTheme } = useTheme(); const toggleTheme = () => { - if (theme === "light") { - setTheme("dark"); - } else if (theme === "dark") { - setTheme("system"); - } else { - setTheme("light"); - } + setTheme(theme === "light" ? "dark" : "light"); }; const getThemeIcon = () => { @@ -321,244 +321,246 @@ export function PublicBookingPage() { }; return ( -
- {/* Header */} -
-
-
- {/* Xtablo Logo */} -
- Xtablo -
- - {/* Avatar */} - {/*
- {userProfile.avatar_url ? ( - {userProfile.name - ) : ( -
- -
- )} -
*/} - - {/* User Info */} -
-

- {userProfile?.name || "Professionnel"} -

-
- - {/* Theme Toggle */} -
- -
-
+
+
+ {/* Theme Toggle - Floating */} +
+
-
- {/* Main Content */} -
-
- {/* Left Sidebar - Event Type Info */} -
-
-

- {eventType?.name || "Type d'appel"} -

+ {/* Main Card */} +
+
+ {/* Left Panel - User Profile & Event Details */} +
+ {/* Subtle accent overlay */} +
- {eventType?.description && ( - - {eventType.description} - - )} - -
- {eventType?.duration && ( -
- -
- - Durée: {formatDuration(eventType.duration)} - +
+ {/* User Profile */} +
+ {userProfile?.avatar_url ? ( + {userProfile.name + ) : ( +
+
-
- )} - - {eventType?.price && ( -
- - € - -
- - {eventType.price}€ - -
-
- )} - {eventType?.location && ( -
- -
- - {eventType.location} - -
-
- )} - {eventType?.requiresApproval && ( -
- -
- - Approbation requise - -
-
- )} -
-
-
- - {/* Center - Calendar */} -
-
- {/* Calendar Header */} -
-

- {formatMonthYear(currentDate)} -

-
- - + )} +

+ {userProfile?.name || "Professionnel"} +

-
- {/* Calendar Grid */} -
- {["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"].map((day) => ( -
- {day} -
- ))} -
+ {/* Event Type Info */} +
+

+ {eventType?.name || "Type d'appel"} +

-
- {getDaysInMonth(currentDate).map((date, index) => ( -
- {date ? ( - - ) : ( -
+ {eventType?.description && ( + + {eventType.description} + + )} + +
+ {eventType?.duration && ( +
+
+ +
+
+

Durée

+

+ {formatDuration(eventType.duration)} +

+
+
+ )} + + {eventType?.price && ( +
+
+ +
+
+

Prix

+

{eventType.price}€

+
+
+ )} + + {eventType?.location && ( +
+
+ +
+
+

Lieu

+

{eventType.location}

+
+
)}
- ))} +
+ + {/* Footer */} +
+
+ Xtablo +
+ + Propulsé par{" "} + + XTablo + + +
-
- {/* Right Sidebar - Available Slots */} -
-
-

- {selectedDate ? ( - <> - Créneaux disponibles -
- - {selectedDate.toLocaleDateString("fr-FR", { - weekday: "long", - day: "numeric", - month: "long", - })} - - - ) : ( - "Sélectionnez une date" - )} -

+ {/* Right Panel - Calendar & Time Slots */} +
+
+ {/* Calendar */} +
+
+ + {formatMonthYear(currentDate)} + +
+ + +
+
- {selectedDate ? ( -
- {getAvailableSlots(selectedDate).map((slot, index) => ( - - ))} + {/* Calendar Grid */} +
+ {["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"].map((day) => ( +
+ {day} +
+ ))} +
- {getAvailableSlots(selectedDate).length === 0 && ( +
+ {getDaysInMonth(currentDate).map((date, index) => ( +
+ {date ? ( + + ) : ( +
+ )} +
+ ))} +
+
+ + {/* Time Slots */} +
+ + {selectedDate ? ( + <> + Créneaux disponibles +
+ + {selectedDate.toLocaleDateString("fr-FR", { + weekday: "long", + day: "numeric", + month: "long", + })} + + + ) : ( + "Sélectionnez une date" + )} +
+ + {selectedDate ? ( +
+ {getAvailableSlots(selectedDate).map((slot, index) => ( + + ))} + + {getAvailableSlots(selectedDate).length === 0 && ( +
+ + Aucun créneau disponible ce jour + +
+ )} +
+ ) : (
- - Aucun créneau disponible ce jour + + + Choisissez une date dans le calendrier
)}
- ) : ( -
- - - Choisissez une date dans le calendrier pour voir les créneaux disponibles - -
- )} +
@@ -572,9 +574,9 @@ export function PublicBookingPage() { width="md" > {selectedSlot && ( -
-
- +
+
+ {selectedSlot.date.toLocaleDateString("fr-FR", { weekday: "long", @@ -583,8 +585,8 @@ export function PublicBookingPage() { })}
-
- +
+ {selectedSlot.slot.time}
diff --git a/packages/shared/src/hooks/public.ts b/packages/shared/src/hooks/public.ts index 43c5b64..5e17b48 100644 --- a/packages/shared/src/hooks/public.ts +++ b/packages/shared/src/hooks/public.ts @@ -25,7 +25,7 @@ export type TimeSlot = { export function usePublicSlots(api: AxiosInstance, shortUserId: string, standardName: string) { return useQuery<{ - user: { name: string }; + user: { name: string; avatar_url?: string }; eventType: EventTypeConfig; slots: { [date: string]: TimeSlot[] }; availableSlots: TimeSlot[]; From 31499284848d61e1032fb22571f2e1c4a6f963df Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 26 Oct 2025 10:15:10 +0100 Subject: [PATCH 3/4] Beautiful loading state --- api/src/tablo.ts | 4 ++ apps/main/src/pages/PublicBookingPage.tsx | 79 +++++++++++++++++++++-- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/api/src/tablo.ts b/api/src/tablo.ts index 754186f..5640f17 100644 --- a/api/src/tablo.ts +++ b/api/src/tablo.ts @@ -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 diff --git a/apps/main/src/pages/PublicBookingPage.tsx b/apps/main/src/pages/PublicBookingPage.tsx index 6d6db6e..02afc67 100644 --- a/apps/main/src/pages/PublicBookingPage.tsx +++ b/apps/main/src/pages/PublicBookingPage.tsx @@ -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() {
+ {/* Loading Overlay */} + {isCreatingTabloWithOwner && ( +
+
+
+ {/* Animated Icon */} +
+
+ +
+
+
+ + {/* Text */} +
+ + Réservation en cours... + + + {loadingMessages[currentMessageIndex]} + +
+ + {/* Loading Spinner */} +
+
+
+
+
+
+
+
+ )} + {/* Booking Modal */} Date: Sun, 26 Oct 2025 13:14:44 +0100 Subject: [PATCH 4/4] Improve public booking page --- apps/main/src/App.tsx | 31 ++++++++------ apps/main/src/lib/publicRoutes.tsx | 22 ++++++++++ apps/main/src/lib/routes.tsx | 18 -------- apps/main/src/pages/PublicBookingPage.tsx | 51 +++++++++++++---------- apps/main/stats.html | 2 +- packages/shared/src/lib/api.ts | 2 +- 6 files changed, 73 insertions(+), 53 deletions(-) create mode 100644 apps/main/src/lib/publicRoutes.tsx diff --git a/apps/main/src/App.tsx b/apps/main/src/App.tsx index 3033d57..2f8feff 100644 --- a/apps/main/src/App.tsx +++ b/apps/main/src/App.tsx @@ -3,6 +3,7 @@ import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext"; import { Toaster } from "@xtablo/ui/components/sonner"; import { BrowserRouter as Router, useRoutes } from "react-router-dom"; import { routes } from "./lib/routes"; +import { publicRoutes } from "./lib/publicRoutes"; import { supabase } from "./lib/supabase"; import { DatadogRumProvider } from "./providers/DatadogRumProvider"; import { UserStoreProvider } from "./providers/UserStoreProvider"; @@ -12,18 +13,25 @@ const AppRoutes = () => { return element; }; +const PublicRoutes = () => { + const element = useRoutes(publicRoutes); + return element; +}; + export const App = () => { return ( - - - - -
+ + + +
+ + - -
-
-
- + +
+
+
); diff --git a/apps/main/src/lib/publicRoutes.tsx b/apps/main/src/lib/publicRoutes.tsx new file mode 100644 index 0000000..53dd6c1 --- /dev/null +++ b/apps/main/src/lib/publicRoutes.tsx @@ -0,0 +1,22 @@ +import { RouteObject } from "react-router-dom"; +import { LandingPage } from "../pages/landing"; +import { PublicBookingPage } from "../pages/PublicBookingPage"; +import { PublicNotePage } from "../pages/PublicNotePage"; + +export const publicRoutes: RouteObject[] = [ + // Landing page + { + path: "/landing", + element: , + }, + // Public booking routes + { + path: "/book/:user_info/:event_type_standard_name", + element: , + }, + // Public notes route (unauthenticated access) + { + path: "/notes/public/:noteId", + element: , + }, +]; diff --git a/apps/main/src/lib/routes.tsx b/apps/main/src/lib/routes.tsx index 1df0551..fe6d761 100644 --- a/apps/main/src/lib/routes.tsx +++ b/apps/main/src/lib/routes.tsx @@ -9,13 +9,10 @@ import { ChatPage } from "../pages/chat"; import { EventsPage } from "../pages/events"; import { FeedbackPage } from "../pages/feedback"; import { JoinPage } from "../pages/join"; -import { LandingPage } from "../pages/landing"; import { LoginPage } from "../pages/login"; import { NotFoundPage } from "../pages/NotFoundPage"; import NotesPage from "../pages/notes"; import { OAuthSigninPage } from "../pages/oauth-signin"; -import { PublicBookingPage } from "../pages/PublicBookingPage"; -import { PublicNotePage } from "../pages/PublicNotePage"; import { PlanningPage } from "../pages/planning"; import { ResetPasswordPage } from "../pages/reset-password"; import SettingsPage from "../pages/settings"; @@ -126,21 +123,6 @@ export const routes: RouteObject[] = [ path: "/login-with-oauth", element: , }, - // Landing page - { - path: "/landing", - element: , - }, - // Public booking routes - { - path: "/book/:user_info/:event_type_standard_name", - element: , - }, - // Public notes route (unauthenticated access) - { - path: "/notes/public/:noteId", - element: , - }, // Authentication pages (redirected to "/" if user is authenticated) { path: "/", diff --git a/apps/main/src/pages/PublicBookingPage.tsx b/apps/main/src/pages/PublicBookingPage.tsx index 02afc67..0e07236 100644 --- a/apps/main/src/pages/PublicBookingPage.tsx +++ b/apps/main/src/pages/PublicBookingPage.tsx @@ -1,6 +1,5 @@ import { useQueryClient } from "@tanstack/react-query"; import { CustomModal } from "@ui/components/CustomModal"; -import { LoadingSpinner } from "@ui/components/LoadingSpinner"; import { useCreateTabloWithOwner } from "@xtablo/shared"; import { useSession } from "@xtablo/shared/contexts/SessionContext"; import { useTheme } from "@xtablo/shared/contexts/ThemeContext"; @@ -34,7 +33,6 @@ import { useNavigate, useParams } from "react-router-dom"; import { twMerge } from "tailwind-merge"; import { api } from "../lib/api"; import { supabase } from "../lib/supabase"; -import { useMaybeUser } from "../providers/UserStoreProvider"; export function PublicBookingPage() { const { user_info, event_type_standard_name } = useParams<{ @@ -43,9 +41,11 @@ export function PublicBookingPage() { }>(); const queryClient = useQueryClient(); const navigate = useNavigate(); - const { mutateAsync: signUpWithoutPassword } = useSignUpWithoutPassword(supabase, api); + const { mutateAsync: signUpWithoutPassword, isPending: isSigningUpWithoutPassword } = + useSignUpWithoutPassword(supabase, api); const { session } = useSession(); - const user = useMaybeUser(); + const user = session ? session.user : null; + const shortUserId = user_info?.substring(user_info.lastIndexOf("-") + 1); const { data: publicSlots, isLoading: isLoadingSlots } = usePublicSlots( @@ -58,10 +58,12 @@ export function PublicBookingPage() { useCreateTabloWithOwner(api, (data) => { queryClient.invalidateQueries({ queryKey: ["tablos"] }); invalidatePublicSlots(); - navigate(`/chat/${data.id}`, { replace: true }); + navigate(`/tablos/${data.id}`, { replace: true }); navigate(0); }); + const isPending = isSigningUpWithoutPassword || isCreatingTabloWithOwner; + const userProfile = publicSlots?.user; const eventType = publicSlots?.eventType; const slotsData = publicSlots?.slots || {}; @@ -88,6 +90,7 @@ export function PublicBookingPage() { // Loading messages rotation const loadingMessages = [ "Nous créons votre rendez-vous, veuillez patienter", + "Creation de votre compte, ...", "Préparation de votre réservation...", "Configuration de votre appel...", "Finalisation de votre créneau...", @@ -96,14 +99,14 @@ export function PublicBookingPage() { useEffect(() => { setCurrentMessageIndex(0); - if (isCreatingTabloWithOwner) { + if (isPending) { const interval = setInterval(() => { setCurrentMessageIndex((prev) => (prev + 1) % loadingMessages.length); }, 1000); return () => clearInterval(interval); } - }, [isCreatingTabloWithOwner]); + }, [isPending]); // Theme const { theme, setTheme } = useTheme(); @@ -217,17 +220,6 @@ export function PublicBookingPage() { return date.toLocaleDateString("fr-FR", { month: "long", year: "numeric" }); }; - if (isLoadingSlots) { - return ( -
-
- -

Chargement des disponibilités...

-
-
- ); - } - const formatDuration = (minutes: number) => { if (minutes < 60) { return `${minutes} min`; @@ -459,7 +451,20 @@ export function PublicBookingPage() {
{/* Right Panel - Calendar & Time Slots */} -
+
+ {/* Loading Overlay for Calendar/Slots */} + {isLoadingSlots && ( +
+
+
+ + Chargement des disponibilités... + +
+
+
+ )} +
{/* Calendar */}
@@ -589,7 +594,7 @@ export function PublicBookingPage() {
{/* Loading Overlay */} - {isCreatingTabloWithOwner && ( + {isPending && (
@@ -666,7 +671,11 @@ export function PublicBookingPage() { id="name" type="text" placeholder="Votre nom complet" - value={user?.name || formData.name} + value={ + user + ? `${user.user_metadata.first_name} ${user.user_metadata.last_name}` + : formData.name + } onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))} disabled={!!user} /> diff --git a/apps/main/stats.html b/apps/main/stats.html index 8e3ac52..5c4a58a 100644 --- a/apps/main/stats.html +++ b/apps/main/stats.html @@ -4929,7 +4929,7 @@ var drawChart = (function (exports) {