Fresh start after embed

This commit is contained in:
Arthur Belleville 2025-10-24 17:06:42 +02:00
parent 21ac949a18
commit c88394c650
No known key found for this signature in database
14 changed files with 9520 additions and 61 deletions

7
apps/external/.env.production vendored Normal file
View file

@ -0,0 +1,7 @@
VITE_SUPABASE_URL=https://mhcafqvzbrrwvahpvvzd.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDEzMjEsImV4cCI6MjA1NjgxNzMyMX0.Otxn5BWCPD2ABlMM59hCgeur9Tf_Q7PndAbTkqXDPtM
VITE_SUPABASE_ID=mhcafqvzbrrwvahpvvzd
VITE_STREAM_CHAT_API_KEY="t5vvvddteapa"
VITE_API_URL=https://xablo-api-636270553187.europe-west1.run.app

View file

@ -3,7 +3,7 @@
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": false },
"files": {
"ignoreUnknown": true,
"includes": ["src/**/*", "*.{ts,tsx,js,jsx,json}"]
"includes": ["src/**/*", "*.{tsx,js,jsx,json}", "vite.config.ts"]
},
"formatter": {
"enabled": true,

View file

@ -4,8 +4,8 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "tsc -b && vite build",
"dev": "vite dev --port 5174",
"build": "tsc -b && vite build --mode production",
"deploy": "wrangler deploy",
"typecheck": "tsc --noEmit",
"lint": "biome check .",
@ -17,6 +17,7 @@
},
"devDependencies": {
"@biomejs/biome": "2.2.5",
"@cloudflare/vite-plugin": "^1.9.4",
"@tailwindcss/vite": "^4.0.14",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",

View file

@ -213,11 +213,7 @@ export function EmbeddedBookingPage() {
const shortUserId = userInfo?.substring(userInfo.lastIndexOf("-") + 1);
const { data: publicSlots } = usePublicSlots(
api,
shortUserId || "",
eventTypeStandardName || ""
);
const { data: publicSlots } = usePublicSlots(api, shortUserId || "", eventTypeStandardName || "");
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api, () => {
handleCloseModal();

View file

@ -119,29 +119,31 @@ export function FloatingBookingWidget() {
const eventTypeStandardName = params.eventTypeStandardName as string;
const shortUserId = userInfo?.substring(userInfo.lastIndexOf("-") + 1);
// Get variants from URL params with fallback to purple
// Get view mode and variants from URL params
const view = searchParams.get("view") || "default"; // 'button', 'modal', or 'default'
const buttonVariant = (searchParams.get("buttonVariant") as ColorVariant) || "purple";
// Get color schemes based on variants
const btnColors = buttonColors[buttonVariant];
const { data: publicSlots } = usePublicSlots(
api,
shortUserId || "",
eventTypeStandardName || ""
);
const { data: publicSlots } = usePublicSlots(api, shortUserId || "", eventTypeStandardName || "");
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api, () => {
handleCloseModal();
setIsWidgetOpen(false);
if (view === "modal") {
// Send message to parent to close the modal
window.parent.postMessage("xtablo:close", "*");
} else {
setIsWidgetOpen(false);
}
});
const userProfile = publicSlots?.user;
const eventType = publicSlots?.eventType;
const slotsData = publicSlots?.slots || {};
// Widget state
const [isWidgetOpen, setIsWidgetOpen] = useState(false);
// Widget state - auto-open if in modal view
const [isWidgetOpen, setIsWidgetOpen] = useState(view === "modal");
// Calendar state
const [currentDate, setCurrentDate] = useState(new Date());
@ -370,27 +372,61 @@ export function FloatingBookingWidget() {
}
};
return (
<div className="fixed inset-0 pointer-events-none">
{/* Floating Button */}
<div className="fixed bottom-6 right-6 z-50 pointer-events-auto">
// If view is 'button', only show the button
if (view === "button") {
return (
<div className="fixed inset-0 flex items-center justify-center">
<Button
size="lg"
className={twMerge(
"rounded-full h-14 w-14 shadow-lg hover:shadow-xl transition-all duration-200",
btnColors.floating,
isWidgetOpen && "scale-0 opacity-0"
"rounded-full h-14 w-14 shadow-lg hover:shadow-xl border-0 transition-all duration-200",
btnColors.floating
)}
onClick={() => setIsWidgetOpen(true)}
onClick={() => window.parent.postMessage("xtablo:open", "*")}
>
<CalendarIcon className="w-6 h-6" />
</Button>
</div>
);
}
return (
<div className="fixed inset-0 pointer-events-none">
{/* Backdrop for modal view */}
{view === "modal" && isWidgetOpen && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm pointer-events-auto animate-in fade-in duration-200"
onClick={() => window.parent.postMessage("xtablo:close", "*")}
/>
)}
{/* Floating Button - only show in default view */}
{view === "default" && (
<div className="fixed bottom-6 right-6 z-50 pointer-events-auto">
<Button
size="lg"
className={twMerge(
"rounded-full h-14 w-14 shadow-lg hover:shadow-xl transition-all duration-200",
btnColors.floating,
isWidgetOpen && "scale-0 opacity-0"
)}
onClick={() => setIsWidgetOpen(true)}
>
<CalendarIcon className="w-6 h-6" />
</Button>
</div>
)}
{/* Floating Widget Popup */}
{isWidgetOpen && (
<div className="fixed bottom-6 right-6 z-50 w-[450px] max-h-[650px] bg-white dark:bg-gray-800 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 flex flex-col overflow-hidden animate-in slide-in-from-bottom-4 duration-300 pointer-events-auto">
<div
className={twMerge(
"z-50 w-[450px] max-h-[650px] bg-white dark:bg-gray-800 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 flex flex-col overflow-hidden pointer-events-auto",
view === "modal"
? "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 animate-in fade-in zoom-in-95 duration-200"
: "fixed bottom-6 right-6 animate-in slide-in-from-bottom-4 duration-300"
)}
>
{/* Header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-start justify-between">
<div className="flex items-center gap-3 flex-1 min-w-0">
@ -419,8 +455,12 @@ export function FloatingBookingWidget() {
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => {
setIsWidgetOpen(false);
setSelectedDate(null);
if (view === "modal") {
window.parent.postMessage("xtablo:close", "*");
} else {
setIsWidgetOpen(false);
setSelectedDate(null);
}
}}
>
<XIcon className="w-4 h-4" />

View file

@ -9,4 +9,3 @@
}
}
}

View file

@ -1,26 +1,44 @@
/// <reference types="vite/client" />
import { cloudflare } from "@cloudflare/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { dirname, resolve } from "path";
import { dirname } from "path";
import { fileURLToPath } from "url";
import { defineConfig } from "vite";
import { defineConfig, PluginOption } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
const __dirname = dirname(fileURLToPath(import.meta.url));
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss(), tsconfigPaths()],
server: {
cors: false,
port: 5174,
},
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
},
export default defineConfig(({ mode }) => {
const plugins: PluginOption[] = [
react(),
// visualizer() as PluginOption,
tailwindcss(),
tsconfigPaths(),
];
// Only include cloudflare plugin when not in test mode
if (mode !== "test" && process.env.VITEST !== "true") {
plugins.push(cloudflare());
}
return {
plugins,
server: {
cors: false,
},
},
define: process.env.VITEST
? {
"import.meta.env.VITE_SUPABASE_URL": JSON.stringify("https://test.supabase.co"),
"import.meta.env.VITE_SUPABASE_ANON_KEY": JSON.stringify("test-anon-key"),
}
: undefined,
test: {
globals: true,
environment: "jsdom",
setupFiles: "./src/setupTests.ts",
},
};
});

9363
apps/external/worker-configuration.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,5 @@ enabled = true
[vars]
PYTHON_VERSION = "3.11.5"
[[routes]]
pattern = "embed.xtablo.com"
custom_domain = true
[env.production]
route = { pattern = "embed.xtablo.com", custom_domain = true }

View file

@ -4,7 +4,7 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite dev",
"dev": "vite dev --port 5173",
"build": "tsc -b && vite build",
"typecheck": "tsc --noEmit",
"lint": "biome check .",

View file

@ -61,17 +61,49 @@ export function EmbedConfigModal({ isOpen, onClose, buildPublicLink }: EmbedConf
if (embedConfig.embedType === "floating") {
return `<!-- Xtablo Floating Widget -->
<div id="xtablo-widget-container"></div>
<div id="xtablo-widget-root"></div>
<script>
(function() {
var container = document.getElementById('xtablo-widget-container');
var iframe = document.createElement('iframe');
iframe.src = '${embedUrl}';
iframe.style.cssText = 'position: fixed; bottom: 0; right: 0; width: 100%; height: 100%; border: none; z-index: 999999; background: transparent;';
iframe.setAttribute('frameborder', '0');
iframe.setAttribute('allow', 'clipboard-write');
var root = document.getElementById('xtablo-widget-root');
var isOpen = false;
var modal = null;
container.appendChild(iframe);
// Create floating button
var button = document.createElement('div');
button.style.cssText = 'position: fixed; bottom: 24px; right: 24px; z-index: 999998; cursor: pointer; width: 60px; height: 60px;';
button.innerHTML = '<iframe src="${embedUrl}&view=button" style="width: 100%; height: 100%; border: none; border-radius: 50%;" frameborder="0" scrolling="no"></iframe>';
// Create modal container (hidden by default)
modal = document.createElement('div');
modal.style.cssText = 'position: fixed; bottom: 0; right: 0; width: 100%; height: 100%; z-index: 999999; display: none; pointer-events: none;';
var modalIframe = document.createElement('iframe');
modalIframe.src = '${embedUrl}&view=modal';
modalIframe.style.cssText = 'position: absolute; bottom: 0; right: 0; width: 100%; height: 100%; border: none; background: transparent; pointer-events: auto;';
modalIframe.setAttribute('frameborder', '0');
modalIframe.setAttribute('allow', 'clipboard-write');
modal.appendChild(modalIframe);
// Toggle modal on button click
button.addEventListener('click', function() {
isOpen = !isOpen;
modal.style.display = isOpen ? 'block' : 'none';
});
// Listen for close messages from iframe
window.addEventListener('message', function(event) {
if (event.data === 'xtablo:close') {
isOpen = false;
modal.style.display = 'none';
} else if (event.data === 'xtablo:open') {
isOpen = true;
modal.style.display = 'block';
}
});
root.appendChild(button);
root.appendChild(modal);
})();
</script>`;
}

View file

@ -19,6 +19,7 @@ import {
import { useState } from "react";
import { match } from "ts-pattern";
import { EventType, EventTypeConfig, useEventTypes } from "../hooks/event-types";
import { isDev } from "../lib/env";
import { useUser } from "../providers/UserStoreProvider";
export function EventTypeCard({
@ -41,16 +42,16 @@ export function EventTypeCard({
const shortUserId = user.id.substring(0, 6);
// Construct the public booking URL
const baseUrl = window.location.origin;
const baseUrl = isDev ? "http://localhost:5174" : "https://embed.xtablo.com";
return match(type)
.with("embed", () => {
return `${baseUrl}/external/?mode=embed&userInfo=${sanitizedUserName}-${shortUserId}&eventTypeStandardName=${standardName}`;
return `${baseUrl}/embed/${sanitizedUserName}-${shortUserId}/${standardName}`;
})
.with("floating", () => {
return `${baseUrl}/external/?mode=widget&userInfo=${sanitizedUserName}-${shortUserId}&eventTypeStandardName=${standardName}`;
return `${baseUrl}/widget/${sanitizedUserName}-${shortUserId}/${standardName}`;
})
.with("normal", () => {
return `${baseUrl}/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
return `${window.location.origin}/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
})
.exhaustive();
};

View file

@ -18,7 +18,7 @@ export default defineConfig(({ mode }) => {
// Only include cloudflare plugin when not in test mode
if (mode !== "test" && process.env.VITEST !== "true") {
plugins.push(cloudflare());
plugins.push(cloudflare({ inspectorPort: 9230 }));
}
return {

View file

@ -61,6 +61,9 @@ importers:
'@biomejs/biome':
specifier: 2.2.5
version: 2.2.5
'@cloudflare/vite-plugin':
specifier: ^1.9.4
version: 1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2))(workerd@1.20251011.0)(wrangler@4.44.0)
'@tailwindcss/vite':
specifier: ^4.0.14
version: 4.1.15(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2))