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/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/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",
diff --git a/apps/main/src/pages/PublicBookingPage.tsx b/apps/main/src/pages/PublicBookingPage.tsx
index 6e86861..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";
@@ -11,8 +10,14 @@ 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 {
+ CalendarCheck2,
CalendarIcon,
ChevronLeftIcon,
ChevronRightIcon,
@@ -23,11 +28,11 @@ 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";
import { supabase } from "../lib/supabase";
-import { useMaybeUser } from "../providers/UserStoreProvider";
export function PublicBookingPage() {
const { user_info, event_type_standard_name } = useParams<{
@@ -36,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(
@@ -47,12 +54,15 @@ 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(`/tablos/${data.id}`, { replace: true });
+ navigate(0);
+ });
+
+ const isPending = isSigningUpWithoutPassword || isCreatingTabloWithOwner;
const userProfile = publicSlots?.user;
const eventType = publicSlots?.eventType;
@@ -77,17 +87,32 @@ export function PublicBookingPage() {
name: "",
});
+ // 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...",
+ ];
+ const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
+
+ useEffect(() => {
+ setCurrentMessageIndex(0);
+ if (isPending) {
+ const interval = setInterval(() => {
+ setCurrentMessageIndex((prev) => (prev + 1) % loadingMessages.length);
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }
+ }, [isPending]);
+
// Theme
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 = () => {
@@ -195,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`;
@@ -321,249 +335,307 @@ export function PublicBookingPage() {
};
return (
-
- {/* Header */}
-
-
-
- {/* Xtablo Logo */}
-
-

-
-
- {/* Avatar */}
- {/*
- {userProfile.avatar_url ? (
-

- ) : (
-
-
-
- )}
-
*/}
-
- {/* 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 ? (
+

+ ) : (
+
+
-
- )}
-
- {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 */}
+
-
- {/* 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 */}
+
+ {/* Loading Overlay for Calendar/Slots */}
+ {isLoadingSlots && (
+
+
+
+
+ Chargement des disponibilités...
+
+
+
+
+ )}
- {selectedDate ? (
-
- {getAvailableSlots(selectedDate).map((slot, index) => (
-
- ))}
+
+ {/* Calendar */}
+
+
+
+ {formatMonthYear(currentDate)}
+
+
+
+
+
+
- {getAvailableSlots(selectedDate).length === 0 && (
+ {/* Calendar Grid */}
+
+ {["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"].map((day) => (
+
+ {day}
+
+ ))}
+
+
+
+ {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
-
-
- )}
+
+ {/* Loading Overlay */}
+ {isPending && (
+
+
+
+ {/* Animated Icon */}
+
+
+ {/* Text */}
+
+
+ Réservation en cours...
+
+
+ {loadingMessages[currentMessageIndex]}
+
+
+
+ {/* Loading Spinner */}
+
+
+
+
+ )}
+
{/* Booking Modal */}
{selectedSlot && (
-
-
-
+
+
+
{selectedSlot.date.toLocaleDateString("fr-FR", {
weekday: "long",
@@ -583,8 +655,8 @@ export function PublicBookingPage() {
})}
-
-
+
+
{selectedSlot.slot.time}
@@ -599,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) {