feat: widgets and ai chat
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
import type { TWidgetLayoutType } from '@cio/utils/validation/widget';
|
||||
|
||||
/** Served from `apps/dashboard/static/templates/widget-layouts/*.png` (~720px max edge for sharp downscaling). */
|
||||
export const WIDGET_LAYOUT_THUMBNAIL_BASE = '/templates/widget-layouts';
|
||||
|
||||
export function widgetLayoutThumbnailSrc(layoutType: TWidgetLayoutType): string {
|
||||
return `${WIDGET_LAYOUT_THUMBNAIL_BASE}/${layoutType}.png`;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { TWidgetPayload } from '@cio/utils/validation/widget';
|
||||
|
||||
// Parent → iframe messages
|
||||
export type WidgetPreviewParentMessage =
|
||||
| { type: 'WIDGET_PREVIEW_RENDER'; payload: TWidgetPayload; requestId: string }
|
||||
| { type: 'WIDGET_PREVIEW_PING' };
|
||||
|
||||
// Iframe → parent messages
|
||||
export type WidgetPreviewIframeMessage =
|
||||
| { type: 'WIDGET_PREVIEW_READY'; iframeId: string; timestamp: number }
|
||||
| { type: 'WIDGET_PREVIEW_RENDERED'; requestId: string; timestamp: number }
|
||||
| { type: 'WIDGET_PREVIEW_ERROR'; requestId: string; error: string; timestamp: number }
|
||||
| { type: 'WIDGET_PREVIEW_RESIZE'; height: number };
|
||||
|
||||
export type WidgetPreviewMessage = WidgetPreviewParentMessage | WidgetPreviewIframeMessage;
|
||||
|
||||
const VALID_PARENT_TYPES = new Set(['WIDGET_PREVIEW_RENDER', 'WIDGET_PREVIEW_PING']);
|
||||
const VALID_IFRAME_TYPES = new Set([
|
||||
'WIDGET_PREVIEW_READY',
|
||||
'WIDGET_PREVIEW_RENDERED',
|
||||
'WIDGET_PREVIEW_ERROR',
|
||||
'WIDGET_PREVIEW_RESIZE'
|
||||
]);
|
||||
|
||||
export function isValidParentMessage(data: unknown): data is WidgetPreviewParentMessage {
|
||||
if (!data || typeof data !== 'object' || !('type' in data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return VALID_PARENT_TYPES.has((data as { type: string }).type);
|
||||
}
|
||||
|
||||
export function isValidIframeMessage(data: unknown): data is WidgetPreviewIframeMessage {
|
||||
if (!data || typeof data !== 'object' || !('type' in data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return VALID_IFRAME_TYPES.has((data as { type: string }).type);
|
||||
}
|
||||
|
||||
export function getAllowedOrigins(): string[] {
|
||||
const origins: string[] = [];
|
||||
const dashboardUrl = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
|
||||
if (dashboardUrl) {
|
||||
origins.push(dashboardUrl);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
|
||||
origins.push(`http://localhost:${window.location.port}`);
|
||||
}
|
||||
|
||||
return origins;
|
||||
}
|
||||
|
||||
export function isAllowedOrigin(origin: string): boolean {
|
||||
if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
return url.hostname === 'localhost';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return origin === window.location.origin;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { classroomio, type InferResponseType } from '$lib/utils/services/api';
|
||||
import type {
|
||||
TCreateWidget,
|
||||
TPreviewWidget,
|
||||
TUpdateWidget,
|
||||
TWidgetConfig,
|
||||
TWidgetDetail,
|
||||
TWidgetPayload
|
||||
} from '@cio/utils/validation/widget';
|
||||
|
||||
export type GetWidgetsRequest = (typeof classroomio.organization)['widgets']['$get'];
|
||||
export type GetWidgetDetailRequest = (typeof classroomio.organization)['widgets'][':widgetId']['$get'];
|
||||
export type CreateWidgetRequest = (typeof classroomio.organization)['widgets']['$post'];
|
||||
export type UpdateWidgetRequest = (typeof classroomio.organization)['widgets'][':widgetId']['$put'];
|
||||
export type PreviewWidgetRequest = (typeof classroomio.organization)['widgets'][':widgetId']['preview']['$post'];
|
||||
export type PublishWidgetRequest = (typeof classroomio.organization)['widgets'][':widgetId']['publish']['$post'];
|
||||
export type RollbackWidgetRequest = (typeof classroomio.organization)['widgets'][':widgetId']['rollback']['$post'];
|
||||
export type DeleteWidgetRequest = (typeof classroomio.organization)['widgets'][':widgetId']['$delete'];
|
||||
|
||||
export type GetWidgetsSuccess = Extract<InferResponseType<GetWidgetsRequest>, { success: true }>;
|
||||
export type WidgetListItem = GetWidgetsSuccess['data'][number];
|
||||
|
||||
export type GetWidgetDetailSuccess = Extract<InferResponseType<GetWidgetDetailRequest>, { success: true }>;
|
||||
export type WidgetDetailResponse = GetWidgetDetailSuccess['data'];
|
||||
export type WidgetDetail = TWidgetDetail & WidgetDetailResponse;
|
||||
|
||||
export type PublishWidgetSuccess = Extract<InferResponseType<PublishWidgetRequest>, { success: true }>;
|
||||
export type RollbackWidgetSuccess = Extract<InferResponseType<RollbackWidgetRequest>, { success: true }>;
|
||||
|
||||
export type WidgetConfig = TWidgetConfig;
|
||||
export type WidgetPayload = TWidgetPayload;
|
||||
export type CreateWidgetInput = TCreateWidget;
|
||||
export type UpdateWidgetInput = TUpdateWidget;
|
||||
export type PreviewWidgetInput = TPreviewWidget;
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { WidgetDetail } from './types';
|
||||
|
||||
/** Plain deep clone — `structuredClone` throws on Svelte `$state` proxies. */
|
||||
export function deepPlain<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
export type FilterPublished = 'all' | 'published' | 'unpublished';
|
||||
|
||||
export function buildCourseFilterPredicate(opts: {
|
||||
courseSearch: string;
|
||||
filterPublished: FilterPublished;
|
||||
filterCourseType: string;
|
||||
filterTagSlugs: string[];
|
||||
}) {
|
||||
return (course: WidgetDetail['availableCourses'][number]): boolean => {
|
||||
if (opts.courseSearch && !course.title.toLowerCase().includes(opts.courseSearch.toLowerCase())) return false;
|
||||
if (opts.filterPublished === 'published' && !course.isPublished) return false;
|
||||
if (opts.filterPublished === 'unpublished' && course.isPublished) return false;
|
||||
if (opts.filterCourseType !== 'all' && course.courseType !== opts.filterCourseType) return false;
|
||||
if (
|
||||
opts.filterTagSlugs.length > 0 &&
|
||||
!opts.filterTagSlugs.some((slug) => course.tags.some((tag) => tag.slug === slug))
|
||||
)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user