Fresh start after embed
This commit is contained in:
parent
21ac949a18
commit
c88394c650
14 changed files with 9520 additions and 61 deletions
7
apps/external/.env.production
vendored
Normal file
7
apps/external/.env.production
vendored
Normal 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
|
||||
2
apps/external/biome.json
vendored
2
apps/external/biome.json
vendored
|
|
@ -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,
|
||||
|
|
|
|||
5
apps/external/package.json
vendored
5
apps/external/package.json
vendored
|
|
@ -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",
|
||||
|
|
|
|||
6
apps/external/src/EmbeddedBookingPage.tsx
vendored
6
apps/external/src/EmbeddedBookingPage.tsx
vendored
|
|
@ -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();
|
||||
|
|
|
|||
82
apps/external/src/FloatingBookingWidget.tsx
vendored
82
apps/external/src/FloatingBookingWidget.tsx
vendored
|
|
@ -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" />
|
||||
|
|
|
|||
1
apps/external/turbo.json
vendored
1
apps/external/turbo.json
vendored
|
|
@ -9,4 +9,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
46
apps/external/vite.config.ts
vendored
46
apps/external/vite.config.ts
vendored
|
|
@ -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
9363
apps/external/worker-configuration.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
5
apps/external/wrangler.toml
vendored
5
apps/external/wrangler.toml
vendored
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 .",
|
||||
|
|
|
|||
|
|
@ -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>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue