Make configurable embed
This commit is contained in:
parent
e6b85c62c1
commit
ea673e50ad
3 changed files with 442 additions and 45 deletions
181
ui/src/components/EmbedConfigModal.tsx
Normal file
181
ui/src/components/EmbedConfigModal.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/components/ui/dialog";
|
||||
import { Label } from "@ui/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/ui/select";
|
||||
import { CopyButton } from "@ui/components/ui/clipboard";
|
||||
import { useState } from "react";
|
||||
import { TypographyMuted } from "@ui/components/ui/typography";
|
||||
|
||||
type ColorVariant = "black" | "white" | "blue" | "purple" | "green" | "orange";
|
||||
|
||||
interface EmbedConfig {
|
||||
backgroundVariant: ColorVariant;
|
||||
buttonVariant: ColorVariant;
|
||||
}
|
||||
|
||||
interface EmbedConfigModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
baseEmbedUrl: string;
|
||||
}
|
||||
|
||||
export function EmbedConfigModal({ isOpen, onClose, baseEmbedUrl }: EmbedConfigModalProps) {
|
||||
const [embedConfig, setEmbedConfig] = useState<EmbedConfig>({
|
||||
backgroundVariant: "purple",
|
||||
buttonVariant: "purple",
|
||||
});
|
||||
|
||||
const getEmbedUrl = () => {
|
||||
const params = new URLSearchParams({
|
||||
backgroundVariant: embedConfig.backgroundVariant,
|
||||
buttonVariant: embedConfig.buttonVariant,
|
||||
});
|
||||
return `${baseEmbedUrl}?${params.toString()}`;
|
||||
};
|
||||
|
||||
const generateEmbedCode = () => {
|
||||
const embedUrl = getEmbedUrl();
|
||||
|
||||
return `<iframe
|
||||
src="${embedUrl}"
|
||||
width="1130"
|
||||
height="700"
|
||||
frameborder="0"
|
||||
style="border: none; border-radius: 8px;"
|
||||
></iframe>`;
|
||||
};
|
||||
|
||||
const colorOptions: { value: ColorVariant; label: string; color: string }[] = [
|
||||
{ value: "black", label: "Noir", color: "bg-gray-900" },
|
||||
{ value: "white", label: "Blanc", color: "bg-white" },
|
||||
{ value: "blue", label: "Bleu", color: "bg-blue-600" },
|
||||
{ value: "purple", label: "Violet", color: "bg-purple-600" },
|
||||
{ value: "green", label: "Vert", color: "bg-green-600" },
|
||||
{ value: "orange", label: "Orange", color: "bg-orange-600" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Configurer l'intégration</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 overflow-hidden">
|
||||
{/* Configuration Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Couleur de fond</Label>
|
||||
<Select
|
||||
value={embedConfig.backgroundVariant}
|
||||
onValueChange={(value) =>
|
||||
setEmbedConfig({ ...embedConfig, backgroundVariant: value as ColorVariant })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{colorOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-4 h-4 rounded ${option.color}`}></div>
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Couleur des boutons</Label>
|
||||
<Select
|
||||
value={embedConfig.buttonVariant}
|
||||
onValueChange={(value) =>
|
||||
setEmbedConfig({ ...embedConfig, buttonVariant: value as ColorVariant })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{colorOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-4 h-4 rounded ${option.color}`}></div>
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview Link */}
|
||||
<div className="space-y-2 pt-4 border-t">
|
||||
<Label>Lien d'aperçu</Label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={getEmbedUrl()}
|
||||
className="flex-1 min-w-0 px-3 py-2 text-sm bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md overflow-hidden text-ellipsis"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-shrink-0"
|
||||
onClick={() => window.open(getEmbedUrl(), "_blank")}
|
||||
>
|
||||
Aperçu
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Embed Code */}
|
||||
<div className="space-y-2 min-w-0">
|
||||
<Label>Code d'intégration</Label>
|
||||
<TypographyMuted className="text-xs">
|
||||
Copiez ce code pour intégrer le formulaire de réservation sur votre site web
|
||||
</TypographyMuted>
|
||||
<div className="relative min-w-0">
|
||||
<div className="overflow-auto max-w-full">
|
||||
<pre className="p-4 pr-16 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md text-xs whitespace-pre-wrap break-words w-full">
|
||||
<code className="break-all">{generateEmbedCode()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div className="absolute top-2 right-2">
|
||||
<CopyButton
|
||||
copyValue={generateEmbedCode()}
|
||||
label="Copier"
|
||||
labelAfterCopied="Copié"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Fermer
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,9 +7,17 @@ import {
|
|||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/components/ui/card";
|
||||
import { CopyButton } from "@ui/components/ui/clipboard";
|
||||
import { EmbedConfigModal } from "@ui/components/EmbedConfigModal";
|
||||
import { EventType, EventTypeConfig, useEventTypes } from "@ui/hooks/event-types";
|
||||
import { CheckIcon, EditIcon, ExternalLinkIcon, TrashIcon, XIcon } from "lucide-react";
|
||||
import {
|
||||
CheckIcon,
|
||||
EditIcon,
|
||||
ExternalLinkIcon,
|
||||
SettingsIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useUser } from "src/providers/UserStoreProvider";
|
||||
|
||||
export function EventTypeCard({
|
||||
|
|
@ -21,7 +29,9 @@ export function EventTypeCard({
|
|||
}) {
|
||||
const { toggleEventType, deleteEventType } = useEventTypes();
|
||||
const user = useUser();
|
||||
const getPublicLink = (standardName: string | null) => {
|
||||
const [isEmbedModalOpen, setIsEmbedModalOpen] = useState(false);
|
||||
|
||||
const getPublicLink = (standardName: string | null, isEmbed: boolean = false) => {
|
||||
// Sanitize user name for URL (replace spaces with hyphens, lowercase, remove special chars)
|
||||
const sanitizedUserName = user.name
|
||||
?.toLowerCase()
|
||||
|
|
@ -31,10 +41,12 @@ export function EventTypeCard({
|
|||
const shortUserId = user.id.substring(0, 6);
|
||||
// Construct the public booking URL
|
||||
const baseUrl = window.location.origin;
|
||||
const publicUrl = `${baseUrl}/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
|
||||
|
||||
return publicUrl;
|
||||
if (isEmbed) {
|
||||
return `${baseUrl}/embed/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
|
||||
}
|
||||
return `${baseUrl}/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={eventType.id}
|
||||
|
|
@ -44,6 +56,14 @@ export function EventTypeCard({
|
|||
<CardTitle className="text-lg">{eventType.name}</CardTitle>
|
||||
<CardAction>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsEmbedModalOpen(true)}
|
||||
aria-label="Configurer l'intégration"
|
||||
>
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
|
@ -52,11 +72,6 @@ export function EventTypeCard({
|
|||
>
|
||||
<ExternalLinkIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<CopyButton
|
||||
copyValue={getPublicLink(eventType.standardName ?? null)}
|
||||
label="Copier le lien"
|
||||
labelAfterCopied="Lien copié"
|
||||
></CopyButton>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
|
@ -129,6 +144,12 @@ export function EventTypeCard({
|
|||
{eventType.isActive ? "Actif" : "Inactif"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
<EmbedConfigModal
|
||||
isOpen={isEmbedModalOpen}
|
||||
onClose={() => setIsEmbedModalOpen(false)}
|
||||
baseEmbedUrl={getPublicLink(eventType.standardName ?? null, true)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,19 +20,173 @@ import {
|
|||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type ColorVariant = "black" | "white" | "blue" | "purple" | "green" | "orange";
|
||||
|
||||
// Color scheme configurations
|
||||
const backgroundColors = {
|
||||
black: {
|
||||
gradient: "from-gray-900 via-gray-800 to-gray-900",
|
||||
overlay: "from-gray-600/5 via-transparent to-gray-600/10",
|
||||
iconBg: "bg-gray-700/50",
|
||||
iconBorder: "border-gray-500/20",
|
||||
iconText: "text-gray-400",
|
||||
borderColor: "border-gray-700/50",
|
||||
linkColor: "text-gray-400/60",
|
||||
avatarBorder: "border-gray-500/30",
|
||||
},
|
||||
white: {
|
||||
gradient: "from-gray-50 via-white to-gray-50",
|
||||
overlay: "from-gray-200/5 via-transparent to-gray-200/10",
|
||||
iconBg: "bg-gray-100/50",
|
||||
iconBorder: "border-gray-300/20",
|
||||
iconText: "text-gray-600",
|
||||
borderColor: "border-gray-200/50",
|
||||
linkColor: "text-gray-500/60",
|
||||
avatarBorder: "border-gray-300/30",
|
||||
},
|
||||
blue: {
|
||||
gradient: "from-blue-900 via-blue-800 to-blue-900",
|
||||
overlay: "from-blue-600/5 via-transparent to-blue-600/10",
|
||||
iconBg: "bg-blue-700/50",
|
||||
iconBorder: "border-blue-500/20",
|
||||
iconText: "text-blue-400",
|
||||
borderColor: "border-blue-700/50",
|
||||
linkColor: "text-blue-400/60",
|
||||
avatarBorder: "border-blue-500/30",
|
||||
},
|
||||
purple: {
|
||||
gradient: "from-gray-900 via-gray-800 to-gray-900",
|
||||
overlay: "from-purple-600/5 via-transparent to-purple-600/10",
|
||||
iconBg: "bg-gray-700/50",
|
||||
iconBorder: "border-purple-500/20",
|
||||
iconText: "text-purple-400",
|
||||
borderColor: "border-gray-700/50",
|
||||
linkColor: "text-purple-400/60",
|
||||
avatarBorder: "border-purple-500/30",
|
||||
},
|
||||
green: {
|
||||
gradient: "from-green-900 via-green-800 to-green-900",
|
||||
overlay: "from-green-600/5 via-transparent to-green-600/10",
|
||||
iconBg: "bg-green-700/50",
|
||||
iconBorder: "border-green-500/20",
|
||||
iconText: "text-green-400",
|
||||
borderColor: "border-green-700/50",
|
||||
linkColor: "text-green-400/60",
|
||||
avatarBorder: "border-green-500/30",
|
||||
},
|
||||
orange: {
|
||||
gradient: "from-orange-900 via-orange-800 to-orange-900",
|
||||
overlay: "from-orange-600/5 via-transparent to-orange-600/10",
|
||||
iconBg: "bg-orange-700/50",
|
||||
iconBorder: "border-orange-500/20",
|
||||
iconText: "text-orange-400",
|
||||
borderColor: "border-orange-700/50",
|
||||
linkColor: "text-orange-400/60",
|
||||
avatarBorder: "border-orange-500/30",
|
||||
},
|
||||
};
|
||||
|
||||
const buttonColors = {
|
||||
black: {
|
||||
selected: "bg-gray-900 dark:bg-white text-white dark:text-gray-900",
|
||||
ring: "ring-gray-500/50",
|
||||
todayBorder: "border-gray-500/30",
|
||||
hoverBorder: "hover:border-gray-500/50",
|
||||
slotHover:
|
||||
"hover:bg-gray-900 dark:hover:bg-white hover:text-white dark:hover:text-gray-900 hover:border-gray-500/50",
|
||||
modalBorder: "border-gray-500/20",
|
||||
modalIcon: "text-gray-600 dark:text-gray-400",
|
||||
},
|
||||
white: {
|
||||
selected: "bg-white dark:bg-gray-100 text-gray-900 dark:text-gray-900",
|
||||
ring: "ring-gray-300/50",
|
||||
todayBorder: "border-gray-300/30",
|
||||
hoverBorder: "hover:border-gray-300/50",
|
||||
slotHover:
|
||||
"hover:bg-white dark:hover:bg-gray-100 hover:text-gray-900 dark:hover:text-gray-900 hover:border-gray-300/50",
|
||||
modalBorder: "border-gray-300/20",
|
||||
modalIcon: "text-gray-600 dark:text-gray-500",
|
||||
},
|
||||
blue: {
|
||||
selected: "bg-blue-600 dark:bg-blue-500 text-white dark:text-white",
|
||||
ring: "ring-blue-500/50",
|
||||
todayBorder: "border-blue-500/30",
|
||||
hoverBorder: "hover:border-blue-500/50",
|
||||
slotHover:
|
||||
"hover:bg-blue-600 dark:hover:bg-blue-500 hover:text-white dark:hover:text-white hover:border-blue-500/50",
|
||||
modalBorder: "border-blue-500/20",
|
||||
modalIcon: "text-blue-600 dark:text-blue-400",
|
||||
},
|
||||
purple: {
|
||||
selected: "bg-purple-600 dark:bg-purple-500 text-white dark:text-white",
|
||||
ring: "ring-purple-500/50",
|
||||
todayBorder: "border-purple-500/30",
|
||||
hoverBorder: "hover:border-purple-500/50",
|
||||
slotHover:
|
||||
"hover:bg-purple-600 dark:hover:bg-purple-500 hover:text-white dark:hover:text-white hover:border-purple-500/50",
|
||||
modalBorder: "border-purple-500/20",
|
||||
modalIcon: "text-purple-600 dark:text-purple-400",
|
||||
},
|
||||
green: {
|
||||
selected: "bg-green-600 dark:bg-green-500 text-white dark:text-white",
|
||||
ring: "ring-green-500/50",
|
||||
todayBorder: "border-green-500/30",
|
||||
hoverBorder: "hover:border-green-500/50",
|
||||
slotHover:
|
||||
"hover:bg-green-600 dark:hover:bg-green-500 hover:text-white dark:hover:text-white hover:border-green-500/50",
|
||||
modalBorder: "border-green-500/20",
|
||||
modalIcon: "text-green-600 dark:text-green-400",
|
||||
},
|
||||
orange: {
|
||||
selected: "bg-orange-600 dark:bg-orange-500 text-white dark:text-white",
|
||||
ring: "ring-orange-500/50",
|
||||
todayBorder: "border-orange-500/30",
|
||||
hoverBorder: "hover:border-orange-500/50",
|
||||
slotHover:
|
||||
"hover:bg-orange-600 dark:hover:bg-orange-500 hover:text-white dark:hover:text-white hover:border-orange-500/50",
|
||||
modalBorder: "border-orange-500/20",
|
||||
modalIcon: "text-orange-600 dark:text-orange-400",
|
||||
},
|
||||
};
|
||||
|
||||
// Automatically infer text color based on background luminance
|
||||
const getTextColorFromBackground = (variant: ColorVariant): string => {
|
||||
// Dark backgrounds need light text, light backgrounds need dark text
|
||||
const darkBackgrounds = ["black", "blue", "purple", "green", "orange"];
|
||||
return darkBackgrounds.includes(variant) ? "text-white" : "text-gray-900";
|
||||
};
|
||||
|
||||
// Automatically infer muted text color based on background luminance
|
||||
const getMutedTextColorFromBackground = (variant: ColorVariant): string => {
|
||||
// Dark backgrounds need lighter muted text, light backgrounds need darker muted text
|
||||
const darkBackgrounds = ["black", "blue", "purple", "green", "orange"];
|
||||
return darkBackgrounds.includes(variant) ? "text-gray-400" : "text-gray-600";
|
||||
};
|
||||
|
||||
export function EmbeddedBookingPage() {
|
||||
const { user_info, event_type_standard_name } = useParams<{
|
||||
user_info: string;
|
||||
event_type_standard_name: string;
|
||||
}>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { mutateAsync: signUpWithoutPassword } = useSignUpWithoutPassword();
|
||||
const { session } = useSession();
|
||||
const user = useMaybeUser();
|
||||
const shortUserId = user_info?.substring(user_info.lastIndexOf("-") + 1);
|
||||
|
||||
// Get variants from URL params or props, with fallback to purple
|
||||
const backgroundVariant = (searchParams.get("backgroundVariant") as ColorVariant) || "black";
|
||||
const buttonVariant = (searchParams.get("buttonVariant") as ColorVariant) || "purple";
|
||||
|
||||
// Get color schemes based on variants
|
||||
const bgColors = backgroundColors[backgroundVariant];
|
||||
const btnColors = buttonColors[buttonVariant];
|
||||
const txtColor = getTextColorFromBackground(backgroundVariant);
|
||||
const mutedTxtColor = getMutedTextColorFromBackground(backgroundVariant);
|
||||
|
||||
const { data: publicSlots, isLoading: isLoadingSlots } = usePublicSlots(
|
||||
shortUserId || "",
|
||||
event_type_standard_name || ""
|
||||
|
|
@ -41,7 +195,6 @@ export function EmbeddedBookingPage() {
|
|||
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner();
|
||||
|
||||
const userProfile = publicSlots?.user;
|
||||
console.log(userProfile);
|
||||
const eventType = publicSlots?.eventType;
|
||||
const slotsData = publicSlots?.slots || {};
|
||||
|
||||
|
|
@ -285,25 +438,39 @@ export function EmbeddedBookingPage() {
|
|||
<div className="w-[1130px] h-[700px] p-6 bg-gray-50 dark:bg-gray-900 overflow-hidden">
|
||||
<div className="h-full bg-white dark:bg-gray-800 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 flex overflow-hidden">
|
||||
{/* Left Side - Event Details */}
|
||||
<div className="w-[400px] bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 p-8 flex flex-col text-white relative overflow-hidden">
|
||||
{/* Subtle purple accent overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-600/5 via-transparent to-purple-600/10 pointer-events-none"></div>
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-[400px] bg-gradient-to-br p-8 flex flex-col relative overflow-hidden",
|
||||
bgColors.gradient,
|
||||
txtColor
|
||||
)}
|
||||
>
|
||||
{/* Subtle accent overlay */}
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute inset-0 bg-gradient-to-br pointer-events-none",
|
||||
bgColors.overlay
|
||||
)}
|
||||
></div>
|
||||
<div className="relative z-10 flex flex-col h-full">
|
||||
{/* Logo */}
|
||||
<div className="mb-8">
|
||||
<img src="/logo_white.png" alt="Xtablo" className="h-10 w-auto" />
|
||||
</div>
|
||||
|
||||
{/* User Profile */}
|
||||
<div className="mb-8">
|
||||
{(userProfile as { name: string; avatar_url?: string })?.avatar_url ? (
|
||||
<img
|
||||
src={(userProfile as { name: string; avatar_url?: string }).avatar_url}
|
||||
alt={userProfile?.name || "Profile"}
|
||||
className="w-20 h-20 rounded-full object-cover border-4 border-purple-500/30 mb-4"
|
||||
className={twMerge(
|
||||
"w-20 h-20 rounded-full object-cover border-4 mb-4",
|
||||
bgColors.avatarBorder
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-20 h-20 rounded-full bg-gray-700 flex items-center justify-center border-4 border-purple-500/30 mb-4">
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-20 h-20 rounded-full bg-gray-700 flex items-center justify-center border-4 mb-4",
|
||||
bgColors.avatarBorder
|
||||
)}
|
||||
>
|
||||
<UserIcon className="w-10 h-10 text-gray-300" />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -315,46 +482,62 @@ export function EmbeddedBookingPage() {
|
|||
<h3 className="text-xl font-bold mb-3">{eventType?.name || "Type d'événement"}</h3>
|
||||
|
||||
{eventType?.description && (
|
||||
<p className="text-white/90 mb-6 text-sm leading-relaxed">
|
||||
<TypographyMuted className={twMerge("mb-6 text-sm leading-relaxed", mutedTxtColor)}>
|
||||
{eventType.description}
|
||||
</p>
|
||||
</TypographyMuted>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{eventType?.duration && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-700/50 border border-purple-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<ClockIcon className="w-5 h-5 text-purple-400" />
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-10 h-10 rounded-lg border flex items-center justify-center flex-shrink-0",
|
||||
bgColors.iconBg,
|
||||
bgColors.iconBorder
|
||||
)}
|
||||
>
|
||||
<ClockIcon className={twMerge("w-5 h-5", bgColors.iconText)} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-400">Durée</p>
|
||||
<p className="font-semibold text-white">
|
||||
{formatDuration(eventType.duration)}
|
||||
</p>
|
||||
<p className="font-semibold">{formatDuration(eventType.duration)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{eventType?.price && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-700/50 border border-purple-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-xl font-bold text-purple-400">€</span>
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-10 h-10 rounded-lg border flex items-center justify-center flex-shrink-0",
|
||||
bgColors.iconBg,
|
||||
bgColors.iconBorder
|
||||
)}
|
||||
>
|
||||
<span className={twMerge("text-xl font-bold", bgColors.iconText)}>€</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-400">Prix</p>
|
||||
<p className="font-semibold text-white">{eventType.price}€</p>
|
||||
<p className="font-semibold">{eventType.price}€</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{eventType?.location && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gray-700/50 border border-purple-500/20 flex items-center justify-center flex-shrink-0">
|
||||
<MapPinIcon className="w-5 h-5 text-purple-400" />
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-10 h-10 rounded-lg border flex items-center justify-center flex-shrink-0",
|
||||
bgColors.iconBg,
|
||||
bgColors.iconBorder
|
||||
)}
|
||||
>
|
||||
<MapPinIcon className={twMerge("w-5 h-5", bgColors.iconText)} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-400">Lieu</p>
|
||||
<p className="font-semibold text-white text-sm">{eventType.location}</p>
|
||||
<p className="font-semibold text-sm">{eventType.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -362,14 +545,18 @@ export function EmbeddedBookingPage() {
|
|||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-auto pt-6 border-t border-gray-700/50">
|
||||
<div className={twMerge("mt-auto pt-6 border-t", bgColors.borderColor)}>
|
||||
{/* Logo */}
|
||||
<div className="mb-4">
|
||||
<img src="/logo_white.png" alt="Xtablo" className="h-8 w-auto" />
|
||||
</div>
|
||||
<TypographyMuted className="text-xs text-gray-500">
|
||||
Powered by{" "}
|
||||
<a
|
||||
href="https://www.xtablo.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-purple-400/60 hover:underline"
|
||||
className={twMerge("hover:underline", bgColors.linkColor)}
|
||||
>
|
||||
XTablo
|
||||
</a>
|
||||
|
|
@ -434,11 +621,11 @@ export function EmbeddedBookingPage() {
|
|||
isPastDate(date)
|
||||
? "text-gray-300 dark:text-gray-600 cursor-not-allowed"
|
||||
: selectedDate?.toDateString() === date.toDateString()
|
||||
? "bg-gray-900 dark:bg-white text-white dark:text-gray-900 font-semibold shadow-md ring-2 ring-purple-500/50"
|
||||
? `${btnColors.selected} font-semibold shadow-md ring-2 ${btnColors.ring}`
|
||||
: isToday(date)
|
||||
? "bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white font-semibold border border-purple-500/30"
|
||||
? `bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white font-semibold border ${btnColors.todayBorder}`
|
||||
: hasAvailableSlots(date)
|
||||
? "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-purple-500/50 border border-gray-200 dark:border-gray-600"
|
||||
? `text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 ${btnColors.hoverBorder} border border-gray-200 dark:border-gray-600`
|
||||
: "text-gray-400 dark:text-gray-500 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
|
|
@ -480,7 +667,10 @@ export function EmbeddedBookingPage() {
|
|||
key={index}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-center text-sm py-2 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600 hover:bg-gray-900 dark:hover:bg-white hover:text-white dark:hover:text-gray-900 hover:border-purple-500/50 transition-all"
|
||||
className={twMerge(
|
||||
"w-full justify-center text-sm py-2 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600 transition-all",
|
||||
btnColors.slotHover
|
||||
)}
|
||||
onClick={() => handleSlotClick(selectedDate, slot)}
|
||||
>
|
||||
{slot.time}
|
||||
|
|
@ -516,9 +706,14 @@ export function EmbeddedBookingPage() {
|
|||
width="md"
|
||||
>
|
||||
{selectedSlot && (
|
||||
<div className="mb-6 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg border border-purple-500/20">
|
||||
<div
|
||||
className={twMerge(
|
||||
"mb-6 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg border",
|
||||
btnColors.modalBorder
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-gray-900 dark:text-gray-100">
|
||||
<CalendarIcon className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<CalendarIcon className={twMerge("w-4 h-4", btnColors.modalIcon)} />
|
||||
<Text className="font-medium">
|
||||
{selectedSlot.date.toLocaleDateString("fr-FR", {
|
||||
weekday: "long",
|
||||
|
|
@ -528,7 +723,7 @@ export function EmbeddedBookingPage() {
|
|||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-900 dark:text-gray-100 mt-1">
|
||||
<ClockIcon className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<ClockIcon className={twMerge("w-4 h-4", btnColors.modalIcon)} />
|
||||
<Text className="font-medium">{selectedSlot.slot.time}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue