feat: add hosted embed URL functionality and enhance widget embed options with tabbed interface
This commit is contained in:
@@ -171,6 +171,16 @@ export function getWidgetEmbedCode(publicKey: string): string {
|
|||||||
return `<div data-cio-widget="course-widget" data-widget-key="${publicKey}" data-api-base-url="${apiBaseUrl}"></div>\n<script async type="module" src="${scriptUrl}"></script>`;
|
return `<div data-cio-widget="course-widget" data-widget-key="${publicKey}" data-api-base-url="${apiBaseUrl}"></div>\n<script async type="module" src="${scriptUrl}"></script>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getWidgetHostedEmbedUrl(publicKey: string): string {
|
||||||
|
const embedBaseUrl = (
|
||||||
|
env.PUBLIC_EMBED_BASE_URL ||
|
||||||
|
(env.NODE_ENV === 'development' ? DEFAULT_LOCAL_EMBED_BASE_URL : DEFAULT_EMBED_CDN_BASE_URL)
|
||||||
|
).replace(/\/$/, '');
|
||||||
|
const apiBaseUrl = getWidgetApiBaseUrl();
|
||||||
|
|
||||||
|
return `${embedBaseUrl}/embeds/course-widget/embed.html?key=${encodeURIComponent(publicKey)}&api=${encodeURIComponent(apiBaseUrl)}`;
|
||||||
|
}
|
||||||
|
|
||||||
type OrgCourse = Awaited<ReturnType<typeof getOrgCourses>>['items'][number];
|
type OrgCourse = Awaited<ReturnType<typeof getOrgCourses>>['items'][number];
|
||||||
|
|
||||||
export function formatCourseForWidget(
|
export function formatCourseForWidget(
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
getCourseBaseUrl,
|
getCourseBaseUrl,
|
||||||
getCourseWidgetScriptUrl,
|
getCourseWidgetScriptUrl,
|
||||||
getWidgetEmbedCode,
|
getWidgetEmbedCode,
|
||||||
|
getWidgetHostedEmbedUrl,
|
||||||
listWidgetAvailableEditorData
|
listWidgetAvailableEditorData
|
||||||
} from './widget-payload';
|
} from './widget-payload';
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ export async function getOrganizationWidgetDetail(orgId: string, widgetId: strin
|
|||||||
config: normalizeWidgetConfig(widget.config as Record<string, unknown>, editorData.planName),
|
config: normalizeWidgetConfig(widget.config as Record<string, unknown>, editorData.planName),
|
||||||
selectedCourseIds: selectedCourses.map((course) => course.courseId),
|
selectedCourseIds: selectedCourses.map((course) => course.courseId),
|
||||||
embedCode: getWidgetEmbedCode(widget.publicKey),
|
embedCode: getWidgetEmbedCode(widget.publicKey),
|
||||||
|
hostedEmbedUrl: getWidgetHostedEmbedUrl(widget.publicKey),
|
||||||
publicScriptUrl: getCourseWidgetScriptUrl()
|
publicScriptUrl: getCourseWidgetScriptUrl()
|
||||||
},
|
},
|
||||||
organization: {
|
organization: {
|
||||||
@@ -227,6 +229,7 @@ export async function publishOrganizationWidget(orgId: string, widgetId: string,
|
|||||||
version,
|
version,
|
||||||
payload,
|
payload,
|
||||||
embedCode: getWidgetEmbedCode(widget.publicKey),
|
embedCode: getWidgetEmbedCode(widget.publicKey),
|
||||||
|
hostedEmbedUrl: getWidgetHostedEmbedUrl(widget.publicKey),
|
||||||
publicScriptUrl: getCourseWidgetScriptUrl()
|
publicScriptUrl: getCourseWidgetScriptUrl()
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -66,6 +66,19 @@
|
|||||||
let lastStepsCount = $state(0);
|
let lastStepsCount = $state(0);
|
||||||
let lastCurrentActionSig = $state('');
|
let lastCurrentActionSig = $state('');
|
||||||
let lastStreamingSig = $state(0);
|
let lastStreamingSig = $state(0);
|
||||||
|
// True when the user is at (or very near) the bottom. Streaming auto-scroll is gated on this
|
||||||
|
// so scrolling up doesn't get clobbered by the next token.
|
||||||
|
let isPinnedToBottom = $state(true);
|
||||||
|
|
||||||
|
const SCROLL_PIN_THRESHOLD = 60;
|
||||||
|
|
||||||
|
function handleScroll() {
|
||||||
|
if (!messagesContainer) return;
|
||||||
|
|
||||||
|
const distanceFromBottom =
|
||||||
|
messagesContainer.scrollHeight - messagesContainer.scrollTop - messagesContainer.clientHeight;
|
||||||
|
isPinnedToBottom = distanceFromBottom <= SCROLL_PIN_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
const isEmpty = $derived(messages.length === 0);
|
const isEmpty = $derived(messages.length === 0);
|
||||||
|
|
||||||
@@ -101,6 +114,8 @@
|
|||||||
if (messages.length === lastMessageCount) return;
|
if (messages.length === lastMessageCount) return;
|
||||||
|
|
||||||
lastMessageCount = messages.length;
|
lastMessageCount = messages.length;
|
||||||
|
// A new message (typically the user's) — force scroll and re-pin.
|
||||||
|
isPinnedToBottom = true;
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,6 +125,9 @@
|
|||||||
if (streamingContentSig === lastStreamingSig) return;
|
if (streamingContentSig === lastStreamingSig) return;
|
||||||
|
|
||||||
lastStreamingSig = streamingContentSig;
|
lastStreamingSig = streamingContentSig;
|
||||||
|
|
||||||
|
if (!isPinnedToBottom) return;
|
||||||
|
|
||||||
// Use 'auto' during streaming — smooth scrolls queue up per token and lag behind the cursor.
|
// Use 'auto' during streaming — smooth scrolls queue up per token and lag behind the cursor.
|
||||||
scrollToBottom('auto');
|
scrollToBottom('auto');
|
||||||
});
|
});
|
||||||
@@ -120,7 +138,7 @@
|
|||||||
const stepsCount = planExecutionState.steps.length;
|
const stepsCount = planExecutionState.steps.length;
|
||||||
if (stepsCount !== lastStepsCount) {
|
if (stepsCount !== lastStepsCount) {
|
||||||
lastStepsCount = stepsCount;
|
lastStepsCount = stepsCount;
|
||||||
scrollToBottom();
|
if (isPinnedToBottom) scrollToBottom();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,12 +146,12 @@
|
|||||||
|
|
||||||
if (actionSig !== lastCurrentActionSig) {
|
if (actionSig !== lastCurrentActionSig) {
|
||||||
lastCurrentActionSig = actionSig;
|
lastCurrentActionSig = actionSig;
|
||||||
scrollToBottom();
|
if (isPinnedToBottom) scrollToBottom();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-4" bind:this={messagesContainer}>
|
<div class="flex-1 overflow-y-auto p-4" bind:this={messagesContainer} onscroll={handleScroll}>
|
||||||
{#if isEmpty}
|
{#if isEmpty}
|
||||||
<!-- Empty state with quick actions -->
|
<!-- Empty state with quick actions -->
|
||||||
<div class="flex h-full flex-col items-center justify-center gap-4">
|
<div class="flex h-full flex-col items-center justify-center gap-4">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '@cio/ui/base/button';
|
import { Button } from '@cio/ui/base/button';
|
||||||
|
import * as Tabs from '@cio/ui/base/tabs';
|
||||||
import { TextareaField } from '@cio/ui/custom/textarea-field';
|
import { TextareaField } from '@cio/ui/custom/textarea-field';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import type { WidgetDetail } from '../utils/types';
|
import type { WidgetDetail } from '../utils/types';
|
||||||
@@ -11,15 +12,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { detail, onRollback, onDelete }: Props = $props();
|
let { detail, onRollback, onDelete }: Props = $props();
|
||||||
|
|
||||||
|
let activeFormat = $state<'html' | 'url'>('html');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<TextareaField label={$t('widgets.form.embed_code')} value={detail.widget.embedCode} rows={6} readonly />
|
<Tabs.Root bind:value={activeFormat} class="w-full">
|
||||||
<div class="flex flex-wrap gap-2">
|
<Tabs.List class="inline-flex w-auto">
|
||||||
<Button variant="outline" onclick={() => navigator.clipboard.writeText(detail.widget.embedCode)}>
|
<Tabs.Trigger value="html">{$t('widgets.embed.tabs.html')}</Tabs.Trigger>
|
||||||
{$t('widgets.actions.copy_embed')}
|
<Tabs.Trigger value="url">{$t('widgets.embed.tabs.url')}</Tabs.Trigger>
|
||||||
</Button>
|
</Tabs.List>
|
||||||
</div>
|
|
||||||
|
<Tabs.Content value="html" class="mt-4 space-y-3">
|
||||||
|
<TextareaField label={$t('widgets.form.embed_code')} value={detail.widget.embedCode} rows={6} readonly />
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<Button variant="outline" onclick={() => navigator.clipboard.writeText(detail.widget.embedCode)}>
|
||||||
|
{$t('widgets.actions.copy_embed')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="url" class="mt-4 space-y-3">
|
||||||
|
<TextareaField label={$t('widgets.embed.url_label')} value={detail.widget.hostedEmbedUrl} rows={2} readonly />
|
||||||
|
<p class="ui:text-muted-foreground text-xs">{$t('widgets.embed.url_helper')}</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<Button variant="outline" onclick={() => navigator.clipboard.writeText(detail.widget.hostedEmbedUrl)}>
|
||||||
|
{$t('widgets.actions.copy_url')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
|
||||||
{#if detail.versions.length > 0}
|
{#if detail.versions.length > 0}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|||||||
@@ -4010,9 +4010,18 @@
|
|||||||
"archive": "Arkiv",
|
"archive": "Arkiv",
|
||||||
"restore": "Gendan",
|
"restore": "Gendan",
|
||||||
"copy_embed": "Kopiér indlejring",
|
"copy_embed": "Kopiér indlejring",
|
||||||
|
"copy_url": "Kopiér URL",
|
||||||
"rename": "Omdøb",
|
"rename": "Omdøb",
|
||||||
"delete": "Slet"
|
"delete": "Slet"
|
||||||
},
|
},
|
||||||
|
"embed": {
|
||||||
|
"tabs": {
|
||||||
|
"html": "HTML",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"url_label": "Hostet embed-URL",
|
||||||
|
"url_helper": "Brug denne URL på platforme som Notion eller Confluence, der kun accepterer links."
|
||||||
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"heading": "Forhåndsvisning",
|
"heading": "Forhåndsvisning",
|
||||||
"saved": "Gemt",
|
"saved": "Gemt",
|
||||||
|
|||||||
@@ -4010,9 +4010,18 @@
|
|||||||
"archive": "Archiv",
|
"archive": "Archiv",
|
||||||
"restore": "Wiederherstellen",
|
"restore": "Wiederherstellen",
|
||||||
"copy_embed": "Einbetten kopieren",
|
"copy_embed": "Einbetten kopieren",
|
||||||
|
"copy_url": "URL kopieren",
|
||||||
"rename": "Umbenennen",
|
"rename": "Umbenennen",
|
||||||
"delete": "Löschen"
|
"delete": "Löschen"
|
||||||
},
|
},
|
||||||
|
"embed": {
|
||||||
|
"tabs": {
|
||||||
|
"html": "HTML",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"url_label": "Gehostete Embed-URL",
|
||||||
|
"url_helper": "Verwenden Sie diese URL auf Plattformen wie Notion oder Confluence, die nur Links akzeptieren."
|
||||||
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"heading": "Vorschau",
|
"heading": "Vorschau",
|
||||||
"saved": "Gespeichert",
|
"saved": "Gespeichert",
|
||||||
|
|||||||
@@ -1343,9 +1343,18 @@
|
|||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
"restore": "Restore",
|
"restore": "Restore",
|
||||||
"copy_embed": "Copy embed",
|
"copy_embed": "Copy embed",
|
||||||
|
"copy_url": "Copy URL",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"delete": "Delete"
|
"delete": "Delete"
|
||||||
},
|
},
|
||||||
|
"embed": {
|
||||||
|
"tabs": {
|
||||||
|
"html": "HTML",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"url_label": "Hosted embed URL",
|
||||||
|
"url_helper": "Use this URL on platforms like Notion or Confluence that only accept a link."
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"description": "Give your widget a name to get started."
|
"description": "Give your widget a name to get started."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4010,9 +4010,18 @@
|
|||||||
"archive": "Archivo",
|
"archive": "Archivo",
|
||||||
"restore": "Restaurar",
|
"restore": "Restaurar",
|
||||||
"copy_embed": "Copiar incrustado",
|
"copy_embed": "Copiar incrustado",
|
||||||
|
"copy_url": "Copiar URL",
|
||||||
"rename": "Cambiar nombre",
|
"rename": "Cambiar nombre",
|
||||||
"delete": "Eliminar"
|
"delete": "Eliminar"
|
||||||
},
|
},
|
||||||
|
"embed": {
|
||||||
|
"tabs": {
|
||||||
|
"html": "HTML",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"url_label": "URL de incrustación alojada",
|
||||||
|
"url_helper": "Usa esta URL en plataformas como Notion o Confluence que solo aceptan enlaces."
|
||||||
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"heading": "Vista previa",
|
"heading": "Vista previa",
|
||||||
"saved": "Guardado",
|
"saved": "Guardado",
|
||||||
|
|||||||
@@ -4010,9 +4010,18 @@
|
|||||||
"archive": "Archiver",
|
"archive": "Archiver",
|
||||||
"restore": "Restaurer",
|
"restore": "Restaurer",
|
||||||
"copy_embed": "Copier l'intégration",
|
"copy_embed": "Copier l'intégration",
|
||||||
|
"copy_url": "Copier l'URL",
|
||||||
"rename": "Renommer",
|
"rename": "Renommer",
|
||||||
"delete": "Supprimer"
|
"delete": "Supprimer"
|
||||||
},
|
},
|
||||||
|
"embed": {
|
||||||
|
"tabs": {
|
||||||
|
"html": "HTML",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"url_label": "URL d'intégration hébergée",
|
||||||
|
"url_helper": "Utilisez cette URL sur des plateformes comme Notion ou Confluence qui n'acceptent que des liens."
|
||||||
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"heading": "Aperçu",
|
"heading": "Aperçu",
|
||||||
"saved": "Enregistré",
|
"saved": "Enregistré",
|
||||||
|
|||||||
@@ -4010,9 +4010,18 @@
|
|||||||
"archive": "पुरालेख",
|
"archive": "पुरालेख",
|
||||||
"restore": "पुनर्स्थापित करें",
|
"restore": "पुनर्स्थापित करें",
|
||||||
"copy_embed": "एंबेड कॉपी करें",
|
"copy_embed": "एंबेड कॉपी करें",
|
||||||
|
"copy_url": "URL कॉपी करें",
|
||||||
"rename": "नाम बदलें",
|
"rename": "नाम बदलें",
|
||||||
"delete": "हटाएँ"
|
"delete": "हटाएँ"
|
||||||
},
|
},
|
||||||
|
"embed": {
|
||||||
|
"tabs": {
|
||||||
|
"html": "HTML",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"url_label": "होस्ट किया गया एम्बेड URL",
|
||||||
|
"url_helper": "Notion या Confluence जैसी प्लेटफ़ॉर्म पर इस URL का उपयोग करें जो केवल लिंक स्वीकार करती हैं।"
|
||||||
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"heading": "पूर्वावलोकन",
|
"heading": "पूर्वावलोकन",
|
||||||
"saved": "सहेजा गया",
|
"saved": "सहेजा गया",
|
||||||
|
|||||||
@@ -4010,9 +4010,18 @@
|
|||||||
"archive": "Archiwum",
|
"archive": "Archiwum",
|
||||||
"restore": "Przywróć",
|
"restore": "Przywróć",
|
||||||
"copy_embed": "Skopiuj osadzenie",
|
"copy_embed": "Skopiuj osadzenie",
|
||||||
|
"copy_url": "Kopiuj URL",
|
||||||
"rename": "Zmień nazwę",
|
"rename": "Zmień nazwę",
|
||||||
"delete": "Usuń"
|
"delete": "Usuń"
|
||||||
},
|
},
|
||||||
|
"embed": {
|
||||||
|
"tabs": {
|
||||||
|
"html": "HTML",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"url_label": "Hostowany URL osadzania",
|
||||||
|
"url_helper": "Użyj tego adresu URL na platformach takich jak Notion lub Confluence, które akceptują tylko linki."
|
||||||
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"heading": "Podgląd",
|
"heading": "Podgląd",
|
||||||
"saved": "Zapisano",
|
"saved": "Zapisano",
|
||||||
|
|||||||
@@ -4010,9 +4010,18 @@
|
|||||||
"archive": "Arquivo",
|
"archive": "Arquivo",
|
||||||
"restore": "Restaurar",
|
"restore": "Restaurar",
|
||||||
"copy_embed": "Copiar incorporação",
|
"copy_embed": "Copiar incorporação",
|
||||||
|
"copy_url": "Copiar URL",
|
||||||
"rename": "Renomear",
|
"rename": "Renomear",
|
||||||
"delete": "Excluir"
|
"delete": "Excluir"
|
||||||
},
|
},
|
||||||
|
"embed": {
|
||||||
|
"tabs": {
|
||||||
|
"html": "HTML",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"url_label": "URL de incorporação hospedada",
|
||||||
|
"url_helper": "Use esta URL em plataformas como Notion ou Confluence que só aceitam links."
|
||||||
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"heading": "Visualização",
|
"heading": "Visualização",
|
||||||
"saved": "Salvo",
|
"saved": "Salvo",
|
||||||
|
|||||||
@@ -4010,9 +4010,18 @@
|
|||||||
"archive": "Архив",
|
"archive": "Архив",
|
||||||
"restore": "Восстановить",
|
"restore": "Восстановить",
|
||||||
"copy_embed": "Копировать встроить",
|
"copy_embed": "Копировать встроить",
|
||||||
|
"copy_url": "Копировать URL",
|
||||||
"rename": "Переименовать",
|
"rename": "Переименовать",
|
||||||
"delete": "Удалить"
|
"delete": "Удалить"
|
||||||
},
|
},
|
||||||
|
"embed": {
|
||||||
|
"tabs": {
|
||||||
|
"html": "HTML",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"url_label": "URL встраивания",
|
||||||
|
"url_helper": "Используйте этот URL на платформах вроде Notion или Confluence, которые принимают только ссылки."
|
||||||
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"heading": "Предварительный просмотр",
|
"heading": "Предварительный просмотр",
|
||||||
"saved": "Сохранено",
|
"saved": "Сохранено",
|
||||||
|
|||||||
@@ -4010,9 +4010,18 @@
|
|||||||
"archive": "Lưu trữ",
|
"archive": "Lưu trữ",
|
||||||
"restore": "Khôi phục",
|
"restore": "Khôi phục",
|
||||||
"copy_embed": "Sao chép nhúng",
|
"copy_embed": "Sao chép nhúng",
|
||||||
|
"copy_url": "Sao chép URL",
|
||||||
"rename": "Đổi tên",
|
"rename": "Đổi tên",
|
||||||
"delete": "Xóa"
|
"delete": "Xóa"
|
||||||
},
|
},
|
||||||
|
"embed": {
|
||||||
|
"tabs": {
|
||||||
|
"html": "HTML",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
|
"url_label": "URL nhúng được lưu trữ",
|
||||||
|
"url_helper": "Sử dụng URL này trên các nền tảng như Notion hoặc Confluence chỉ chấp nhận liên kết."
|
||||||
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"heading": "Xem trước",
|
"heading": "Xem trước",
|
||||||
"saved": "Đã lưu",
|
"saved": "Đã lưu",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"dev": "vite dev --port 5180",
|
"dev": "vite dev --port 5180",
|
||||||
"prebuild": "pnpm --filter @cio/ui run build",
|
"prebuild": "pnpm --filter @cio/ui run build",
|
||||||
"build:question-type-picker": "vite build --mode question-type-picker",
|
"build:question-type-picker": "vite build --mode question-type-picker",
|
||||||
"build:course-widget": "vite build --mode course-widget",
|
"build:course-widget": "vite build --mode course-widget && cp src/widgets/course-widget/embed.html dist/course-widget/embed.html",
|
||||||
"build": "rm -rf dist && pnpm build:question-type-picker && pnpm build:course-widget",
|
"build": "rm -rf dist && pnpm build:question-type-picker && pnpm build:course-widget",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"preview": "vite preview --port 4180",
|
"preview": "vite preview --port 4180",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<title>ClassroomIO Course Widget</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="cio-mount"></div>
|
||||||
|
<script type="module">
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const key = params.get('key');
|
||||||
|
const apiBaseUrl = params.get('api');
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
const note = document.createElement('p');
|
||||||
|
note.textContent = 'Missing widget key. Append ?key=<your-widget-key> to the URL.';
|
||||||
|
note.style.cssText = 'font:14px system-ui;padding:16px;color:#475569;';
|
||||||
|
document.body.appendChild(note);
|
||||||
|
} else {
|
||||||
|
const mount = document.getElementById('cio-mount');
|
||||||
|
const widgetDiv = document.createElement('div');
|
||||||
|
widgetDiv.setAttribute('data-cio-widget', 'course-widget');
|
||||||
|
widgetDiv.setAttribute('data-widget-key', key);
|
||||||
|
if (apiBaseUrl) {
|
||||||
|
widgetDiv.setAttribute('data-api-base-url', apiBaseUrl);
|
||||||
|
}
|
||||||
|
mount.appendChild(widgetDiv);
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.async = true;
|
||||||
|
script.type = 'module';
|
||||||
|
script.src = './course-widget.js';
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -13,7 +13,8 @@ const embedEntries = {
|
|||||||
} as const;
|
} as const;
|
||||||
const embedDevEntrypoints = {
|
const embedDevEntrypoints = {
|
||||||
'/embeds/question-type-picker/question-type-picker.js': '/src/widgets/question-type-picker/bootstrap.ts',
|
'/embeds/question-type-picker/question-type-picker.js': '/src/widgets/question-type-picker/bootstrap.ts',
|
||||||
'/embeds/course-widget/course-widget.js': '/src/widgets/course-widget/bootstrap.ts'
|
'/embeds/course-widget/course-widget.js': '/src/widgets/course-widget/bootstrap.ts',
|
||||||
|
'/embeds/course-widget/embed.html': '/src/widgets/course-widget/embed.html'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function embedDevEntryPlugin() {
|
function embedDevEntryPlugin() {
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ export const ZWidgetDetail = z.object({
|
|||||||
config: ZWidgetConfig,
|
config: ZWidgetConfig,
|
||||||
selectedCourseIds: z.array(z.uuid()),
|
selectedCourseIds: z.array(z.uuid()),
|
||||||
embedCode: z.string(),
|
embedCode: z.string(),
|
||||||
|
hostedEmbedUrl: z.string(),
|
||||||
publicScriptUrl: z.string()
|
publicScriptUrl: z.string()
|
||||||
}),
|
}),
|
||||||
organization: z.object({
|
organization: z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user