fix(widget): powered toggle persistence, copy buttons, version dates, showLessonsCount, color pickers, and loading spinner
This commit is contained in:
@@ -23,7 +23,7 @@ import {
|
|||||||
type TUpdateWidget
|
type TUpdateWidget
|
||||||
} from '@cio/utils/validation/widget';
|
} from '@cio/utils/validation/widget';
|
||||||
|
|
||||||
import { getOrganizationById } from '@cio/db/queries/organization';
|
import { getActiveOrganizationPlan, getOrganizationById } from '@cio/db/queries/organization';
|
||||||
import {
|
import {
|
||||||
buildWidgetPayload,
|
buildWidgetPayload,
|
||||||
generateWidgetPublicKey,
|
generateWidgetPublicKey,
|
||||||
@@ -48,13 +48,14 @@ export async function listOrganizationWidgets(orgId: string) {
|
|||||||
|
|
||||||
export async function createOrganizationWidget(orgId: string, userId: string, data: TCreateWidget) {
|
export async function createOrganizationWidget(orgId: string, userId: string, data: TCreateWidget) {
|
||||||
try {
|
try {
|
||||||
|
const activePlan = await getActiveOrganizationPlan(orgId);
|
||||||
const widget = await createWidget({
|
const widget = await createWidget({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
layoutType: data.layoutType,
|
layoutType: data.layoutType,
|
||||||
selectionMode: data.selectionMode,
|
selectionMode: data.selectionMode,
|
||||||
publicKey: generateWidgetPublicKey(),
|
publicKey: generateWidgetPublicKey(),
|
||||||
config: normalizeWidgetConfig(data.config ?? getDefaultWidgetConfig()),
|
config: normalizeWidgetConfig(data.config ?? getDefaultWidgetConfig(), activePlan?.planName ?? null),
|
||||||
createdByUserId: userId,
|
createdByUserId: userId,
|
||||||
updatedByUserId: userId
|
updatedByUserId: userId
|
||||||
});
|
});
|
||||||
@@ -132,7 +133,10 @@ export async function getOrganizationWidgetDetail(orgId: string, widgetId: strin
|
|||||||
|
|
||||||
export async function updateOrganizationWidget(orgId: string, widgetId: string, userId: string, data: TUpdateWidget) {
|
export async function updateOrganizationWidget(orgId: string, widgetId: string, userId: string, data: TUpdateWidget) {
|
||||||
try {
|
try {
|
||||||
const existingWidget = await getWidgetById(orgId, widgetId);
|
const [existingWidget, activePlan] = await Promise.all([
|
||||||
|
getWidgetById(orgId, widgetId),
|
||||||
|
getActiveOrganizationPlan(orgId)
|
||||||
|
]);
|
||||||
if (!existingWidget) {
|
if (!existingWidget) {
|
||||||
throw new AppError('Widget not found', ErrorCodes.WIDGET_NOT_FOUND, 404);
|
throw new AppError('Widget not found', ErrorCodes.WIDGET_NOT_FOUND, 404);
|
||||||
}
|
}
|
||||||
@@ -141,7 +145,10 @@ export async function updateOrganizationWidget(orgId: string, widgetId: string,
|
|||||||
name: data.name ?? existingWidget.name,
|
name: data.name ?? existingWidget.name,
|
||||||
layoutType: data.layoutType ?? existingWidget.layoutType,
|
layoutType: data.layoutType ?? existingWidget.layoutType,
|
||||||
selectionMode: data.selectionMode ?? existingWidget.selectionMode,
|
selectionMode: data.selectionMode ?? existingWidget.selectionMode,
|
||||||
config: normalizeWidgetConfig((data.config ?? existingWidget.config) as Record<string, unknown>),
|
config: normalizeWidgetConfig(
|
||||||
|
(data.config ?? existingWidget.config) as Record<string, unknown>,
|
||||||
|
activePlan?.planName ?? null
|
||||||
|
),
|
||||||
hasUnpublishedChanges: true,
|
hasUnpublishedChanges: true,
|
||||||
updatedByUserId: userId
|
updatedByUserId: userId
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const handle: Handle = async (args) => {
|
|||||||
|
|
||||||
const cookieString = event.cookies.getAll().map((c) => `${c.name}=${c.value}`);
|
const cookieString = event.cookies.getAll().map((c) => `${c.name}=${c.value}`);
|
||||||
console.log('event.cookies', cookieString);
|
console.log('event.cookies', cookieString);
|
||||||
|
console.log('page path', event.url.pathname);
|
||||||
const sessionData = await getSessionData(event.cookies);
|
const sessionData = await getSessionData(event.cookies);
|
||||||
|
|
||||||
if (sessionData) {
|
if (sessionData) {
|
||||||
|
|||||||
@@ -49,14 +49,15 @@
|
|||||||
checkoutURL.searchParams.set('products', isYearlyPlan ? plan.CTA.PRODUCT_ID_YEARLY : plan.CTA.PRODUCT_ID);
|
checkoutURL.searchParams.set('products', isYearlyPlan ? plan.CTA.PRODUCT_ID_YEARLY : plan.CTA.PRODUCT_ID);
|
||||||
checkoutURL.searchParams.set('orgId', $currentOrg.id);
|
checkoutURL.searchParams.set('orgId', $currentOrg.id);
|
||||||
|
|
||||||
window.location.href = checkoutURL.toString();
|
requestAnimationFrame(() => {
|
||||||
|
window.location.href = checkoutURL.toString();
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
isLoadingPlan = null;
|
||||||
console.error('Error subscribing', error);
|
console.error('Error subscribing', error);
|
||||||
|
|
||||||
snackbar.error('snackbar.upgrade.failed');
|
snackbar.error('snackbar.upgrade.failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingPlan = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUpgrade() {
|
function onUpgrade() {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
isDirty: boolean;
|
isDirty: boolean;
|
||||||
layoutValidationError: string | null;
|
layoutValidationError: string | null;
|
||||||
editingName: boolean;
|
editingName: boolean;
|
||||||
|
saving: boolean;
|
||||||
|
publishing: boolean;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
onPublish: () => void;
|
onPublish: () => void;
|
||||||
onDiscard: () => void;
|
onDiscard: () => void;
|
||||||
@@ -29,6 +31,8 @@
|
|||||||
isDirty,
|
isDirty,
|
||||||
layoutValidationError,
|
layoutValidationError,
|
||||||
editingName,
|
editingName,
|
||||||
|
saving,
|
||||||
|
publishing,
|
||||||
onSave,
|
onSave,
|
||||||
onPublish,
|
onPublish,
|
||||||
onDiscard,
|
onDiscard,
|
||||||
@@ -103,11 +107,12 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!isDirty || Boolean(layoutValidationError)}
|
disabled={!isDirty || Boolean(layoutValidationError)}
|
||||||
|
loading={saving}
|
||||||
onclick={onSave}
|
onclick={onSave}
|
||||||
>
|
>
|
||||||
{$t('widgets.editor.save_changes')}
|
{$t('widgets.editor.save_changes')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" type="button" disabled={Boolean(layoutValidationError)} onclick={onPublish}>
|
<Button size="sm" type="button" disabled={Boolean(layoutValidationError)} loading={publishing} onclick={onPublish}>
|
||||||
{$t('widgets.actions.publish')}
|
{$t('widgets.actions.publish')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
isDirty={store.isDirty}
|
isDirty={store.isDirty}
|
||||||
layoutValidationError={store.layoutValidationError}
|
layoutValidationError={store.layoutValidationError}
|
||||||
editingName={store.editingName}
|
editingName={store.editingName}
|
||||||
|
saving={store.saving}
|
||||||
|
publishing={store.publishing}
|
||||||
onSave={() => store.saveWidget()}
|
onSave={() => store.saveWidget()}
|
||||||
onPublish={() => store.publishWidget()}
|
onPublish={() => store.publishWidget()}
|
||||||
onDiscard={() => store.resetDraft()}
|
onDiscard={() => store.resetDraft()}
|
||||||
|
|||||||
@@ -45,33 +45,65 @@
|
|||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
</Field.Field>
|
</Field.Field>
|
||||||
<InputField
|
<Field.Field>
|
||||||
label={$t('widgets.form.primary_color')}
|
<Field.Label>{$t('widgets.form.primary_color')}</Field.Label>
|
||||||
bind:value={draftConfig.colors.primaryColor}
|
<div class="ui:flex ui:items-center ui:gap-2">
|
||||||
maxLength={64}
|
<input
|
||||||
errorMessage={errFor('config.colors.primaryColor')}
|
type="color"
|
||||||
/>
|
bind:value={draftConfig.colors.primaryColor}
|
||||||
<InputField
|
class="ui:h-9 ui:w-9 ui:cursor-pointer ui:rounded ui:border ui:bg-transparent ui:p-0.5"
|
||||||
label={$t('widgets.form.background_color')}
|
/>
|
||||||
bind:value={draftConfig.colors.backgroundColor}
|
<span class="ui:text-muted-foreground ui:font-mono ui:text-sm">{draftConfig.colors.primaryColor}</span>
|
||||||
isDisabled={!canUseCustomColors}
|
</div>
|
||||||
maxLength={64}
|
{#if errFor('config.colors.primaryColor')}
|
||||||
errorMessage={errFor('config.colors.backgroundColor')}
|
<Field.Error>{errFor('config.colors.primaryColor')}</Field.Error>
|
||||||
/>
|
{/if}
|
||||||
<InputField
|
</Field.Field>
|
||||||
label={$t('widgets.form.text_color')}
|
<Field.Field>
|
||||||
bind:value={draftConfig.colors.textColor}
|
<Field.Label>{$t('widgets.form.background_color')}</Field.Label>
|
||||||
isDisabled={!canUseCustomColors}
|
<div class="ui:flex ui:items-center ui:gap-2">
|
||||||
maxLength={64}
|
<input
|
||||||
errorMessage={errFor('config.colors.textColor')}
|
type="color"
|
||||||
/>
|
bind:value={draftConfig.colors.backgroundColor}
|
||||||
<InputField
|
disabled={!canUseCustomColors}
|
||||||
label={$t('widgets.form.border_color')}
|
class="ui:h-9 ui:w-9 ui:cursor-pointer ui:rounded ui:border ui:bg-transparent ui:p-0.5 disabled:ui:cursor-not-allowed disabled:ui:opacity-50"
|
||||||
bind:value={draftConfig.colors.borderColor}
|
/>
|
||||||
isDisabled={!canUseCustomColors}
|
<span class="ui:text-muted-foreground ui:font-mono ui:text-sm">{draftConfig.colors.backgroundColor}</span>
|
||||||
maxLength={64}
|
</div>
|
||||||
errorMessage={errFor('config.colors.borderColor')}
|
{#if errFor('config.colors.backgroundColor')}
|
||||||
/>
|
<Field.Error>{errFor('config.colors.backgroundColor')}</Field.Error>
|
||||||
|
{/if}
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label>{$t('widgets.form.text_color')}</Field.Label>
|
||||||
|
<div class="ui:flex ui:items-center ui:gap-2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
bind:value={draftConfig.colors.textColor}
|
||||||
|
disabled={!canUseCustomColors}
|
||||||
|
class="ui:h-9 ui:w-9 ui:cursor-pointer ui:rounded ui:border ui:bg-transparent ui:p-0.5 disabled:ui:cursor-not-allowed disabled:ui:opacity-50"
|
||||||
|
/>
|
||||||
|
<span class="ui:text-muted-foreground ui:font-mono ui:text-sm">{draftConfig.colors.textColor}</span>
|
||||||
|
</div>
|
||||||
|
{#if errFor('config.colors.textColor')}
|
||||||
|
<Field.Error>{errFor('config.colors.textColor')}</Field.Error>
|
||||||
|
{/if}
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label>{$t('widgets.form.border_color')}</Field.Label>
|
||||||
|
<div class="ui:flex ui:items-center ui:gap-2">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
bind:value={draftConfig.colors.borderColor}
|
||||||
|
disabled={!canUseCustomColors}
|
||||||
|
class="ui:h-9 ui:w-9 ui:cursor-pointer ui:rounded ui:border ui:bg-transparent ui:p-0.5 disabled:ui:cursor-not-allowed disabled:ui:opacity-50"
|
||||||
|
/>
|
||||||
|
<span class="ui:text-muted-foreground ui:font-mono ui:text-sm">{draftConfig.colors.borderColor}</span>
|
||||||
|
</div>
|
||||||
|
{#if errFor('config.colors.borderColor')}
|
||||||
|
<Field.Error>{errFor('config.colors.borderColor')}</Field.Error>
|
||||||
|
{/if}
|
||||||
|
</Field.Field>
|
||||||
<InputField
|
<InputField
|
||||||
type="number"
|
type="number"
|
||||||
label={$t('widgets.form.border_radius')}
|
label={$t('widgets.form.border_radius')}
|
||||||
|
|||||||
@@ -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 { CopyButton } from '@cio/ui/base/copy-button';
|
||||||
import * as Tabs from '@cio/ui/base/tabs';
|
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';
|
||||||
@@ -26,9 +27,9 @@
|
|||||||
<Tabs.Content value="html" class="mt-4 space-y-3">
|
<Tabs.Content value="html" class="mt-4 space-y-3">
|
||||||
<TextareaField label={$t('widgets.form.embed_code')} value={detail.widget.embedCode} rows={6} readonly />
|
<TextareaField label={$t('widgets.form.embed_code')} value={detail.widget.embedCode} rows={6} readonly />
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<Button variant="outline" onclick={() => navigator.clipboard.writeText(detail.widget.embedCode)}>
|
<CopyButton text={detail.widget.embedCode} variant="outline">
|
||||||
{$t('widgets.actions.copy_embed')}
|
{$t('widgets.actions.copy_embed')}
|
||||||
</Button>
|
</CopyButton>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
||||||
@@ -36,9 +37,9 @@
|
|||||||
<TextareaField label={$t('widgets.embed.url_label')} value={detail.widget.hostedEmbedUrl} rows={2} readonly />
|
<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>
|
<p class="ui:text-muted-foreground text-xs">{$t('widgets.embed.url_helper')}</p>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<Button variant="outline" onclick={() => navigator.clipboard.writeText(detail.widget.hostedEmbedUrl)}>
|
<CopyButton text={detail.widget.hostedEmbedUrl} variant="outline">
|
||||||
{$t('widgets.actions.copy_url')}
|
{$t('widgets.actions.copy_url')}
|
||||||
</Button>
|
</CopyButton>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
@@ -50,7 +51,15 @@
|
|||||||
<div class="ui:border-border ui:bg-card/50 flex items-center justify-between rounded-2xl border p-3">
|
<div class="ui:border-border ui:bg-card/50 flex items-center justify-between rounded-2xl border p-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium">v{version.version}</div>
|
<div class="font-medium">v{version.version}</div>
|
||||||
<div class="ui:text-muted-foreground text-xs">{version.publishedAt}</div>
|
<div class="ui:text-muted-foreground text-xs">
|
||||||
|
{new Date(version.publishedAt).toLocaleString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onclick={() => onRollback(version.id)}>
|
<Button variant="outline" size="sm" onclick={() => onRollback(version.id)}>
|
||||||
{$t('widgets.actions.restore')}
|
{$t('widgets.actions.restore')}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class WidgetEditorStore {
|
|||||||
selectedCourseIds = $state<string[]>([]);
|
selectedCourseIds = $state<string[]>([]);
|
||||||
|
|
||||||
// ── UI state ───────────────────────────────────────────────────────────────
|
// ── UI state ───────────────────────────────────────────────────────────────
|
||||||
|
saving = $state(false);
|
||||||
|
publishing = $state(false);
|
||||||
activePanel = $state<PanelId>('select-courses');
|
activePanel = $state<PanelId>('select-courses');
|
||||||
editingName = $state(false);
|
editingName = $state(false);
|
||||||
courseSearch = $state('');
|
courseSearch = $state('');
|
||||||
@@ -180,43 +182,53 @@ class WidgetEditorStore {
|
|||||||
async saveWidget(silent = false): Promise<boolean> {
|
async saveWidget(silent = false): Promise<boolean> {
|
||||||
if (!this.detail) return false;
|
if (!this.detail) return false;
|
||||||
|
|
||||||
|
this.saving = true;
|
||||||
widgetApi.resetErrors();
|
widgetApi.resetErrors();
|
||||||
|
|
||||||
const updatedWidget = await widgetApi.updateWidget(
|
try {
|
||||||
this.detail.widget.id,
|
const updatedWidget = await widgetApi.updateWidget(
|
||||||
{
|
this.detail.widget.id,
|
||||||
name: this.draftName,
|
{
|
||||||
layoutType: this.draftLayoutType,
|
name: this.draftName,
|
||||||
selectionMode: this.draftSelectionMode,
|
layoutType: this.draftLayoutType,
|
||||||
config: this.draftConfig,
|
selectionMode: this.draftSelectionMode,
|
||||||
selectedCourseIds: this.selectedCourseIds
|
config: this.draftConfig,
|
||||||
},
|
selectedCourseIds: this.selectedCourseIds
|
||||||
silent
|
},
|
||||||
);
|
silent
|
||||||
|
);
|
||||||
|
|
||||||
if (!updatedWidget) return false;
|
if (!updatedWidget) return false;
|
||||||
|
|
||||||
await widgetApi.getWidget(this.detail.widget.id);
|
await widgetApi.getWidget(this.detail.widget.id);
|
||||||
if (widgetApi.widgetDetail) {
|
if (widgetApi.widgetDetail) {
|
||||||
this.applyDetail(widgetApi.widgetDetail);
|
this.applyDetail(widgetApi.widgetDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async publishWidget() {
|
async publishWidget() {
|
||||||
const saved = await this.saveWidget(true);
|
this.publishing = true;
|
||||||
if (!saved || !this.detail) return;
|
try {
|
||||||
|
const saved = await this.saveWidget(true);
|
||||||
|
if (!saved || !this.detail) return;
|
||||||
|
|
||||||
const published = await widgetApi.publishWidget(this.detail.widget.id);
|
const published = await widgetApi.publishWidget(this.detail.widget.id);
|
||||||
if (!published) return;
|
if (!published) return;
|
||||||
|
|
||||||
await widgetApi.getWidget(this.detail.widget.id);
|
await widgetApi.getWidget(this.detail.widget.id);
|
||||||
if (widgetApi.widgetDetail) {
|
if (widgetApi.widgetDetail) {
|
||||||
this.applyDetail(widgetApi.widgetDetail);
|
this.applyDetail(widgetApi.widgetDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activePanel = 'embed';
|
||||||
|
} finally {
|
||||||
|
this.publishing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activePanel = 'embed';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async rollback(versionId: string) {
|
async rollback(versionId: string) {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<span></span>
|
<span></span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="cio-cg-meta">{formatLessonsLine(course)}</span>
|
<span class="cio-cg-meta">{formatLessonsLine(course, payload.design.content.showLessonsCount)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
<TypeBadge courseType={course.courseType} />
|
<TypeBadge courseType={course.courseType} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="cio-cr-meta">{formatLessonsLine(course)}</div>
|
<div class="cio-cr-meta">{formatLessonsLine(course, payload.design.content.showLessonsCount)}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import Stars from './atoms/stars.svelte';
|
import Stars from './atoms/stars.svelte';
|
||||||
import TypeBadge from './atoms/type-badge.svelte';
|
import TypeBadge from './atoms/type-badge.svelte';
|
||||||
import Thumb from './atoms/thumb.svelte';
|
import Thumb from './atoms/thumb.svelte';
|
||||||
|
import { formatLessonsLine } from './utils';
|
||||||
|
|
||||||
let { payload }: { payload: TWidgetPayload } = $props();
|
let { payload }: { payload: TWidgetPayload } = $props();
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@
|
|||||||
<div class="cio-cl-top">
|
<div class="cio-cl-top">
|
||||||
<span class="cio-cl-title">{course.title}</span>
|
<span class="cio-cl-title">{course.title}</span>
|
||||||
{#if payload.design.content.showLessonsCount}
|
{#if payload.design.content.showLessonsCount}
|
||||||
<span class="cio-cl-lessons">{course.lessonCount} lessons</span>
|
<span class="cio-cl-lessons">{formatLessonsLine(course, payload.design.content.showLessonsCount)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="cio-cl-mid">
|
<div class="cio-cl-mid">
|
||||||
|
|||||||
@@ -22,15 +22,23 @@
|
|||||||
if (!payload || !rootEl) return;
|
if (!payload || !rootEl) return;
|
||||||
ensureWidgetGoogleFontsLoaded(rootEl, payload.design);
|
ensureWidgetGoogleFontsLoaded(rootEl, payload.design);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!rootEl) return;
|
||||||
|
const existing = rootEl.querySelector<HTMLStyleElement>('[data-cio-widget-custom-css]');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
if (!customCss) return;
|
||||||
|
const styleEl = document.createElement('style');
|
||||||
|
styleEl.setAttribute('data-cio-widget-custom-css', '');
|
||||||
|
styleEl.textContent = customCss;
|
||||||
|
rootEl.appendChild(styleEl);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !payload}
|
{#if !payload}
|
||||||
<div class="cio-widget cio-widget--empty">No widget payload provided.</div>
|
<div class="cio-widget cio-widget--empty">No widget payload provided.</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div bind:this={rootEl} class="cio-widget" style={cssVars} data-cio-layout={payload.layoutType}>
|
<div bind:this={rootEl} class="cio-widget" style={cssVars} data-cio-layout={payload.layoutType}>
|
||||||
{#if customCss}
|
|
||||||
<svelte:element this={'style'} data-cio-widget-custom-css="">{customCss}</svelte:element>
|
|
||||||
{/if}
|
|
||||||
{#if payload.layoutType === 'card_grid'}
|
{#if payload.layoutType === 'card_grid'}
|
||||||
<CardGrid {payload} />
|
<CardGrid {payload} />
|
||||||
{:else if payload.layoutType === 'tag_filter'}
|
{:else if payload.layoutType === 'tag_filter'}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<span class="cio-ph-sep">·</span>
|
<span class="cio-ph-sep">·</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="cio-ph-meta">{formatLessonsLine(featured)}</span>
|
<span class="cio-ph-meta">{formatLessonsLine(featured, payload.design.content.showLessonsCount)}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if payload.design.content.showCourseTypeBadge || featured.tags.length > 0}
|
{#if payload.design.content.showCourseTypeBadge || featured.tags.length > 0}
|
||||||
<div class="cio-ph-badges">
|
<div class="cio-ph-badges">
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ export function getCourseExcerpt(course: TWidgetPayloadCourse, maxLength = 160):
|
|||||||
return `${flat.slice(0, maxLength - 1).trim()}…`;
|
return `${flat.slice(0, maxLength - 1).trim()}…`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatLessonsLine(course: TWidgetPayloadCourse): string {
|
export function formatLessonsLine(course: TWidgetPayloadCourse, showLessonsCount?: boolean): string {
|
||||||
|
if (showLessonsCount === false) return '';
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (course.lessonCount) parts.push(`${course.lessonCount} lessons`);
|
if (course.lessonCount) parts.push(`${course.lessonCount} lessons`);
|
||||||
if (course.exerciseCount) parts.push(`${course.exerciseCount} exercises`);
|
if (course.exerciseCount) parts.push(`${course.exerciseCount} exercises`);
|
||||||
|
|||||||
Reference in New Issue
Block a user