613 lines
21 KiB
TypeScript
613 lines
21 KiB
TypeScript
import { PLAN } from '@cio/utils/plans';
|
|
import * as z from 'zod';
|
|
|
|
export const WIDGET_STATUS_VALUES = ['DRAFT', 'PUBLISHED', 'ARCHIVED'] as const;
|
|
export const WIDGET_LAYOUT_TYPE_VALUES = [
|
|
'card_grid',
|
|
'tag_filter',
|
|
'carousel',
|
|
'primary_course',
|
|
'compact_list',
|
|
'editorial_spotlight',
|
|
'category_shelf'
|
|
] as const;
|
|
export const WIDGET_SELECTION_MODE_VALUES = ['manual', 'published'] as const;
|
|
export const WIDGET_SORT_BY_VALUES = ['manual', 'newest', 'title'] as const;
|
|
export const WIDGET_THEME_PRESET_VALUES = ['classroomio', 'graphite', 'linen', 'spruce'] as const;
|
|
export const WIDGET_SHADOW_SIZE_VALUES = ['none', 'sm', 'md', 'lg'] as const;
|
|
export const WIDGET_TOP_BADGE_STYLE_VALUES = ['solid', 'outline'] as const;
|
|
export const WIDGET_CARD_GRID_COLUMN_VALUES = ['2', '3'] as const;
|
|
export const WIDGET_CAROUSEL_VISIBLE_CARD_VALUES = ['2', '3', '4'] as const;
|
|
export const WIDGET_COMPACT_LIST_DENSITY_VALUES = ['comfortable', 'compact'] as const;
|
|
export const WIDGET_EDITORIAL_TITLE_STYLE_VALUES = ['serif', 'sans'] as const;
|
|
export const WIDGET_TAG_FILTER_SOURCE_VALUES = ['auto', 'manual'] as const;
|
|
|
|
const ZCssColor = z.string().trim().min(1).max(64);
|
|
|
|
export const ZWidgetStatus = z.enum(WIDGET_STATUS_VALUES);
|
|
export const ZWidgetLayoutType = z.enum(WIDGET_LAYOUT_TYPE_VALUES);
|
|
export const ZWidgetSelectionMode = z.enum(WIDGET_SELECTION_MODE_VALUES);
|
|
export const ZWidgetSortBy = z.enum(WIDGET_SORT_BY_VALUES);
|
|
export const ZWidgetThemePreset = z.enum(WIDGET_THEME_PRESET_VALUES);
|
|
export const ZWidgetShadowSize = z.enum(WIDGET_SHADOW_SIZE_VALUES);
|
|
export const ZWidgetTopBadgeStyle = z.enum(WIDGET_TOP_BADGE_STYLE_VALUES);
|
|
export const ZWidgetCardGridColumns = z.enum(WIDGET_CARD_GRID_COLUMN_VALUES);
|
|
export const ZWidgetCarouselVisibleCards = z.enum(WIDGET_CAROUSEL_VISIBLE_CARD_VALUES);
|
|
export const ZWidgetCompactListDensity = z.enum(WIDGET_COMPACT_LIST_DENSITY_VALUES);
|
|
export const ZWidgetEditorialTitleStyle = z.enum(WIDGET_EDITORIAL_TITLE_STYLE_VALUES);
|
|
export const ZWidgetTagFilterSource = z.enum(WIDGET_TAG_FILTER_SOURCE_VALUES);
|
|
|
|
const DEFAULT_WIDGET_CONTENT_CONFIG = {
|
|
loadMoreLabel: 'Load more',
|
|
hideLoadMore: false,
|
|
showBorder: true,
|
|
borderWidth: 1,
|
|
borderRadius: 12,
|
|
shadowSize: 'md' as const,
|
|
shadowColor: 'rgba(15, 23, 42, 0.3)',
|
|
showCourseImage: true,
|
|
showCourseTypeBadge: true,
|
|
showLessonsCount: true,
|
|
showPrice: true,
|
|
showDescriptionExcerpt: true,
|
|
shortenLongDescription: true,
|
|
showRating: true,
|
|
showFeaturedStyle: false,
|
|
ctaLabel: 'View course'
|
|
};
|
|
|
|
const DEFAULT_WIDGET_COLORS_CONFIG = {
|
|
primaryColor: '#0f172a',
|
|
backgroundColor: '#ffffff',
|
|
textColor: '#0f172a',
|
|
badgeColor: '#e2e8f0',
|
|
borderColor: 'rgba(15, 23, 42, 0.3)',
|
|
highlightColor: '#f8fafc'
|
|
};
|
|
|
|
const DEFAULT_WIDGET_TYPOGRAPHY_CONFIG = {
|
|
fontFamily: "'DM Sans', Inter, ui-sans-serif, system-ui, sans-serif",
|
|
fontSizeScale: 1
|
|
};
|
|
|
|
const DEFAULT_WIDGET_FILTERS_CONFIG = {
|
|
includeTagIds: [] as string[],
|
|
excludeTagIds: [] as string[],
|
|
topBadgeStyle: 'solid' as const
|
|
};
|
|
|
|
const DEFAULT_WIDGET_BRANDING_CONFIG = {
|
|
showPoweredBy: true
|
|
};
|
|
|
|
const DEFAULT_WIDGET_ADVANCED_CONFIG = {
|
|
customCss: ''
|
|
};
|
|
|
|
const DEFAULT_LAYOUT_OPTIONS_CARD_GRID = {
|
|
columns: '3' as const,
|
|
maxCount: 9,
|
|
showRating: true
|
|
};
|
|
|
|
const DEFAULT_LAYOUT_OPTIONS_TAG_FILTER = {
|
|
maxCount: 12,
|
|
tagSource: 'auto' as const,
|
|
manualTagIds: [] as string[],
|
|
defaultTagId: null,
|
|
showAllOption: true
|
|
};
|
|
|
|
const DEFAULT_LAYOUT_OPTIONS_CAROUSEL = {
|
|
visibleCards: '3' as const,
|
|
maxCount: 6,
|
|
autoPlay: false,
|
|
autoPlayIntervalMs: 5000,
|
|
showDots: true,
|
|
showArrows: true,
|
|
loop: true
|
|
};
|
|
|
|
const DEFAULT_LAYOUT_OPTIONS_PRIMARY_COURSE = {
|
|
featuredCourseId: null,
|
|
eyebrowLabel: 'Featured course',
|
|
ctaLabel: 'Enroll now',
|
|
secondaryMaxCount: 3
|
|
};
|
|
|
|
const DEFAULT_LAYOUT_OPTIONS_COMPACT_LIST = {
|
|
maxCount: 5,
|
|
density: 'comfortable' as const,
|
|
showThumbnail: true,
|
|
showTags: true
|
|
};
|
|
|
|
const DEFAULT_LAYOUT_OPTIONS_EDITORIAL_SPOTLIGHT = {
|
|
mainCourseId: null,
|
|
secondaryMaxCount: 4,
|
|
titleStyle: 'serif' as const
|
|
};
|
|
|
|
const DEFAULT_LAYOUT_OPTIONS_CATEGORY_SHELF = {
|
|
categoryTagIds: [] as string[],
|
|
defaultCategoryTagId: null,
|
|
showAllTab: true,
|
|
maxPerCategory: 6
|
|
};
|
|
|
|
const DEFAULT_LAYOUT_OPTIONS = {
|
|
cardGrid: DEFAULT_LAYOUT_OPTIONS_CARD_GRID,
|
|
tagFilter: DEFAULT_LAYOUT_OPTIONS_TAG_FILTER,
|
|
carousel: DEFAULT_LAYOUT_OPTIONS_CAROUSEL,
|
|
primaryCourse: DEFAULT_LAYOUT_OPTIONS_PRIMARY_COURSE,
|
|
compactList: DEFAULT_LAYOUT_OPTIONS_COMPACT_LIST,
|
|
editorialSpotlight: DEFAULT_LAYOUT_OPTIONS_EDITORIAL_SPOTLIGHT,
|
|
categoryShelf: DEFAULT_LAYOUT_OPTIONS_CATEGORY_SHELF
|
|
};
|
|
|
|
const DEFAULT_WIDGET_CONFIG = {
|
|
themePreset: 'classroomio' as const,
|
|
sortBy: 'manual' as const,
|
|
content: DEFAULT_WIDGET_CONTENT_CONFIG,
|
|
colors: DEFAULT_WIDGET_COLORS_CONFIG,
|
|
typography: DEFAULT_WIDGET_TYPOGRAPHY_CONFIG,
|
|
filters: DEFAULT_WIDGET_FILTERS_CONFIG,
|
|
branding: DEFAULT_WIDGET_BRANDING_CONFIG,
|
|
advanced: DEFAULT_WIDGET_ADVANCED_CONFIG,
|
|
layoutOptions: DEFAULT_LAYOUT_OPTIONS
|
|
};
|
|
|
|
export const ZWidgetContentConfig = z.object({
|
|
loadMoreLabel: z.string().trim().min(1).max(64).default('Load more'),
|
|
hideLoadMore: z.boolean().default(false),
|
|
showBorder: z.boolean().default(true),
|
|
borderWidth: z.number().min(0).max(8).default(1),
|
|
borderRadius: z.number().min(0).max(32).default(12),
|
|
shadowSize: ZWidgetShadowSize.default('md'),
|
|
shadowColor: ZCssColor.default('rgba(15, 23, 42, 0.3)'),
|
|
showCourseImage: z.boolean().default(true),
|
|
showCourseTypeBadge: z.boolean().default(true),
|
|
showLessonsCount: z.boolean().default(true),
|
|
showPrice: z.boolean().default(true),
|
|
showDescriptionExcerpt: z.boolean().default(true),
|
|
shortenLongDescription: z.boolean().default(true),
|
|
showRating: z.boolean().default(true),
|
|
showFeaturedStyle: z.boolean().default(false),
|
|
ctaLabel: z.string().trim().min(1).max(64).default('View course')
|
|
});
|
|
|
|
export const ZWidgetColorsConfig = z.object({
|
|
primaryColor: ZCssColor.default('#0f172a'),
|
|
backgroundColor: ZCssColor.default('#ffffff'),
|
|
textColor: ZCssColor.default('#0f172a'),
|
|
badgeColor: ZCssColor.default('#e2e8f0'),
|
|
borderColor: ZCssColor.default('rgba(15, 23, 42, 0.3)'),
|
|
highlightColor: ZCssColor.default('#f8fafc')
|
|
});
|
|
|
|
export const ZWidgetTypographyConfig = z.object({
|
|
fontFamily: z.string().trim().min(1).max(120).default("'DM Sans', Inter, ui-sans-serif, system-ui, sans-serif"),
|
|
fontSizeScale: z.number().min(0.8).max(1.4).default(1)
|
|
});
|
|
|
|
export const ZWidgetFiltersConfig = z.object({
|
|
includeTagIds: z.array(z.uuid()).default([]),
|
|
excludeTagIds: z.array(z.uuid()).default([]),
|
|
topBadgeStyle: ZWidgetTopBadgeStyle.default('solid')
|
|
});
|
|
|
|
export const ZWidgetBrandingConfig = z.object({
|
|
showPoweredBy: z.boolean().default(true)
|
|
});
|
|
|
|
export const ZWidgetAdvancedConfig = z.object({
|
|
customCss: z.string().max(5_000).default('')
|
|
});
|
|
|
|
export const ZWidgetCardGridOptions = z.object({
|
|
columns: ZWidgetCardGridColumns.default('3'),
|
|
maxCount: z.number().int().min(3).max(24).default(9),
|
|
showRating: z.boolean().default(true)
|
|
});
|
|
|
|
export const ZWidgetTagFilterOptions = z.object({
|
|
maxCount: z.number().int().min(4).max(24).default(12),
|
|
tagSource: ZWidgetTagFilterSource.default('auto'),
|
|
manualTagIds: z.array(z.uuid()).default([]),
|
|
defaultTagId: z.uuid().nullable().default(null),
|
|
showAllOption: z.boolean().default(true)
|
|
});
|
|
|
|
export const ZWidgetCarouselOptions = z.object({
|
|
visibleCards: ZWidgetCarouselVisibleCards.default('3'),
|
|
maxCount: z.number().int().min(3).max(12).default(6),
|
|
autoPlay: z.boolean().default(false),
|
|
autoPlayIntervalMs: z.number().int().min(3000).max(10000).default(5000),
|
|
showDots: z.boolean().default(true),
|
|
showArrows: z.boolean().default(true),
|
|
loop: z.boolean().default(true)
|
|
});
|
|
|
|
export const ZWidgetPrimaryCourseOptions = z.object({
|
|
featuredCourseId: z.uuid().nullable().default(null),
|
|
eyebrowLabel: z.string().trim().min(1).max(40).default('Featured course'),
|
|
ctaLabel: z.string().trim().min(1).max(40).default('Enroll now'),
|
|
secondaryMaxCount: z.number().int().min(0).max(6).default(3)
|
|
});
|
|
|
|
export const ZWidgetCompactListOptions = z.object({
|
|
maxCount: z.number().int().min(1).max(12).default(5),
|
|
density: ZWidgetCompactListDensity.default('comfortable'),
|
|
showThumbnail: z.boolean().default(true),
|
|
showTags: z.boolean().default(true)
|
|
});
|
|
|
|
export const ZWidgetEditorialSpotlightOptions = z.object({
|
|
mainCourseId: z.uuid().nullable().default(null),
|
|
secondaryMaxCount: z.number().int().min(2).max(4).default(4),
|
|
titleStyle: ZWidgetEditorialTitleStyle.default('serif')
|
|
});
|
|
|
|
export const ZWidgetCategoryShelfOptions = z.object({
|
|
categoryTagIds: z.array(z.uuid()).max(8).default([]),
|
|
defaultCategoryTagId: z.uuid().nullable().default(null),
|
|
showAllTab: z.boolean().default(true),
|
|
maxPerCategory: z.number().int().min(3).max(9).default(6)
|
|
});
|
|
|
|
export const ZWidgetLayoutOptions = z.object({
|
|
cardGrid: ZWidgetCardGridOptions.default(DEFAULT_LAYOUT_OPTIONS_CARD_GRID),
|
|
tagFilter: ZWidgetTagFilterOptions.default(DEFAULT_LAYOUT_OPTIONS_TAG_FILTER),
|
|
carousel: ZWidgetCarouselOptions.default(DEFAULT_LAYOUT_OPTIONS_CAROUSEL),
|
|
primaryCourse: ZWidgetPrimaryCourseOptions.default(DEFAULT_LAYOUT_OPTIONS_PRIMARY_COURSE),
|
|
compactList: ZWidgetCompactListOptions.default(DEFAULT_LAYOUT_OPTIONS_COMPACT_LIST),
|
|
editorialSpotlight: ZWidgetEditorialSpotlightOptions.default(DEFAULT_LAYOUT_OPTIONS_EDITORIAL_SPOTLIGHT),
|
|
categoryShelf: ZWidgetCategoryShelfOptions.default(DEFAULT_LAYOUT_OPTIONS_CATEGORY_SHELF)
|
|
});
|
|
|
|
export const ZWidgetConfig = z.object({
|
|
themePreset: ZWidgetThemePreset.default('classroomio'),
|
|
sortBy: ZWidgetSortBy.default('manual'),
|
|
content: ZWidgetContentConfig.default(DEFAULT_WIDGET_CONTENT_CONFIG),
|
|
colors: ZWidgetColorsConfig.default(DEFAULT_WIDGET_COLORS_CONFIG),
|
|
typography: ZWidgetTypographyConfig.default(DEFAULT_WIDGET_TYPOGRAPHY_CONFIG),
|
|
filters: ZWidgetFiltersConfig.default(DEFAULT_WIDGET_FILTERS_CONFIG),
|
|
branding: ZWidgetBrandingConfig.default(DEFAULT_WIDGET_BRANDING_CONFIG),
|
|
advanced: ZWidgetAdvancedConfig.default(DEFAULT_WIDGET_ADVANCED_CONFIG),
|
|
layoutOptions: ZWidgetLayoutOptions.default(DEFAULT_LAYOUT_OPTIONS)
|
|
});
|
|
|
|
export const ZWidgetPlanGatedFields = z.object({
|
|
isPaidPlan: z.boolean(),
|
|
canUseCustomColors: z.boolean(),
|
|
canUseCustomCss: z.boolean(),
|
|
canToggleBranding: z.boolean(),
|
|
isBrandingForced: z.boolean(),
|
|
availableThemes: z.array(ZWidgetThemePreset),
|
|
selectedTheme: ZWidgetThemePreset
|
|
});
|
|
|
|
export const ZWidgetPayloadCourseTag = z.object({
|
|
id: z.uuid(),
|
|
name: z.string(),
|
|
slug: z.string(),
|
|
color: z.string()
|
|
});
|
|
|
|
export const ZWidgetPayloadCourse = z.object({
|
|
id: z.uuid(),
|
|
slug: z.string(),
|
|
title: z.string(),
|
|
description: z.string().nullable().optional(),
|
|
imageUrl: z.string().nullable().optional(),
|
|
price: z.union([z.string(), z.number()]).nullable().optional(),
|
|
lessonCount: z.number().int().nonnegative(),
|
|
exerciseCount: z.number().int().nonnegative().default(0),
|
|
rating: z.number().min(0).max(5).nullable().optional(),
|
|
ratingCount: z.number().int().nonnegative().default(0),
|
|
courseType: z.string().nullable().optional(),
|
|
createdAt: z.string(),
|
|
tags: z.array(ZWidgetPayloadCourseTag).default([]),
|
|
featured: z.boolean().default(false),
|
|
url: z.string()
|
|
});
|
|
|
|
export const ZWidgetPayloadCategory = z.object({
|
|
tagId: z.uuid(),
|
|
name: z.string(),
|
|
slug: z.string(),
|
|
color: z.string(),
|
|
courseIds: z.array(z.uuid())
|
|
});
|
|
|
|
export const ZWidgetPayloadLabels = z.object({
|
|
loadMoreLabel: z.string(),
|
|
poweredByLabel: z.string()
|
|
});
|
|
|
|
export const ZWidgetPayload = z.object({
|
|
version: z.literal('v1'),
|
|
widgetId: z.uuid(),
|
|
publicKey: z.string(),
|
|
organization: z.object({
|
|
id: z.uuid(),
|
|
name: z.string(),
|
|
siteName: z.string(),
|
|
customDomain: z.string().nullable().optional()
|
|
}),
|
|
layoutType: ZWidgetLayoutType,
|
|
selectionMode: ZWidgetSelectionMode,
|
|
design: ZWidgetConfig,
|
|
planGatedFields: ZWidgetPlanGatedFields,
|
|
labels: ZWidgetPayloadLabels,
|
|
courses: z.array(ZWidgetPayloadCourse),
|
|
tagPool: z.array(ZWidgetPayloadCourseTag).default([]),
|
|
categories: z.array(ZWidgetPayloadCategory).default([]),
|
|
timestamp: z.number().int().nonnegative()
|
|
});
|
|
|
|
export const ZWidgetListItem = z.object({
|
|
id: z.uuid(),
|
|
organizationId: z.uuid(),
|
|
name: z.string(),
|
|
status: ZWidgetStatus,
|
|
layoutType: ZWidgetLayoutType,
|
|
selectionMode: ZWidgetSelectionMode,
|
|
publicKey: z.string(),
|
|
hasUnpublishedChanges: z.boolean().default(false),
|
|
latestPublishedVersionId: z.uuid().nullable().optional(),
|
|
createdAt: z.string(),
|
|
updatedAt: z.string()
|
|
});
|
|
|
|
export const ZWidgetEditorCourse = z.object({
|
|
id: z.uuid(),
|
|
slug: z.string(),
|
|
title: z.string(),
|
|
description: z.string().nullable().optional(),
|
|
imageUrl: z.string().nullable().optional(),
|
|
isPublished: z.boolean().nullable().optional(),
|
|
price: z.union([z.string(), z.number()]).nullable().optional(),
|
|
lessonCount: z.number().int().nonnegative(),
|
|
exerciseCount: z.number().int().nonnegative().default(0),
|
|
courseType: z.string().nullable().optional(),
|
|
createdAt: z.string(),
|
|
tags: z.array(ZWidgetPayloadCourseTag).default([])
|
|
});
|
|
|
|
export const ZWidgetVersionRecord = z.object({
|
|
id: z.uuid(),
|
|
widgetId: z.uuid(),
|
|
version: z.number().int().positive(),
|
|
payloadSnapshot: ZWidgetPayload,
|
|
runtimeManifest: z.record(z.string(), z.unknown()),
|
|
rolledBackFromVersionId: z.uuid().nullable().optional(),
|
|
publishedAt: z.string(),
|
|
publishedByUserId: z.uuid()
|
|
});
|
|
|
|
export const ZWidgetDetail = z.object({
|
|
widget: ZWidgetListItem.extend({
|
|
config: ZWidgetConfig,
|
|
selectedCourseIds: z.array(z.uuid()),
|
|
embedCode: z.string(),
|
|
publicScriptUrl: z.string()
|
|
}),
|
|
organization: z.object({
|
|
id: z.uuid(),
|
|
name: z.string(),
|
|
siteName: z.string(),
|
|
customDomain: z.string().nullable().optional()
|
|
}),
|
|
orgBaseUrl: z.string(),
|
|
availableCourses: z.array(ZWidgetEditorCourse),
|
|
availableTags: z.array(
|
|
z.object({
|
|
id: z.uuid(),
|
|
name: z.string(),
|
|
slug: z.string(),
|
|
color: z.string(),
|
|
groupId: z.uuid()
|
|
})
|
|
),
|
|
versions: z.array(ZWidgetVersionRecord),
|
|
planGatedFields: ZWidgetPlanGatedFields,
|
|
planName: z.string().nullable()
|
|
});
|
|
|
|
/**
|
|
* Per-layout cross-field rules. Keep this in sync with the editor UI gating
|
|
* in `widget-editor.svelte` so users get the same feedback there.
|
|
*/
|
|
function applyLayoutRefinements(
|
|
data: {
|
|
layoutType?: TWidgetLayoutType;
|
|
selectionMode?: TWidgetSelectionMode;
|
|
selectedCourseIds?: string[];
|
|
config?: TWidgetConfig;
|
|
},
|
|
ctx: z.RefinementCtx
|
|
) {
|
|
if (!data.layoutType || !data.config) return;
|
|
|
|
const layoutOptions = data.config.layoutOptions;
|
|
const selectedIds = new Set(data.selectedCourseIds ?? []);
|
|
const isManual = data.selectionMode === 'manual';
|
|
|
|
if (data.layoutType === 'primary_course') {
|
|
const featuredId = layoutOptions.primaryCourse.featuredCourseId;
|
|
if (!featuredId) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['config', 'layoutOptions', 'primaryCourse', 'featuredCourseId'],
|
|
message: 'A featured course is required for the Primary course layout.'
|
|
});
|
|
} else if (isManual && !selectedIds.has(featuredId)) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['config', 'layoutOptions', 'primaryCourse', 'featuredCourseId'],
|
|
message: 'The featured course must be one of the selected courses.'
|
|
});
|
|
}
|
|
}
|
|
|
|
if (data.layoutType === 'editorial_spotlight') {
|
|
const mainId = layoutOptions.editorialSpotlight.mainCourseId;
|
|
if (!mainId) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['config', 'layoutOptions', 'editorialSpotlight', 'mainCourseId'],
|
|
message: 'A main course is required for the Editorial spotlight layout.'
|
|
});
|
|
} else if (isManual && !selectedIds.has(mainId)) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['config', 'layoutOptions', 'editorialSpotlight', 'mainCourseId'],
|
|
message: 'The main course must be one of the selected courses.'
|
|
});
|
|
}
|
|
}
|
|
|
|
if (data.layoutType === 'category_shelf' && !isManual) {
|
|
if (layoutOptions.categoryShelf.categoryTagIds.length === 0) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
path: ['config', 'layoutOptions', 'categoryShelf', 'categoryTagIds'],
|
|
message: 'Add at least one category tag for the Category shelf layout.'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export const ZCreateWidget = z
|
|
.object({
|
|
name: z.string().trim().min(1).max(120),
|
|
layoutType: ZWidgetLayoutType.default('card_grid'),
|
|
selectionMode: ZWidgetSelectionMode.default('manual'),
|
|
config: ZWidgetConfig.default(DEFAULT_WIDGET_CONFIG),
|
|
selectedCourseIds: z.array(z.uuid()).default([])
|
|
})
|
|
.superRefine((data, ctx) => applyLayoutRefinements(data, ctx));
|
|
|
|
export const ZUpdateWidget = z
|
|
.object({
|
|
name: z.string().trim().min(1).max(120).optional(),
|
|
layoutType: ZWidgetLayoutType.optional(),
|
|
selectionMode: ZWidgetSelectionMode.optional(),
|
|
config: ZWidgetConfig.optional(),
|
|
selectedCourseIds: z.array(z.uuid()).optional()
|
|
})
|
|
.superRefine((data, ctx) => applyLayoutRefinements(data, ctx));
|
|
|
|
export const ZRollbackWidget = z.object({
|
|
versionId: z.uuid()
|
|
});
|
|
|
|
export const ZWidgetIdParams = z.object({
|
|
widgetId: z.uuid()
|
|
});
|
|
|
|
export const ZWidgetPublicKeyParams = z.object({
|
|
publicKey: z.string().trim().min(8)
|
|
});
|
|
|
|
export type TWidgetStatus = z.infer<typeof ZWidgetStatus>;
|
|
export type TWidgetLayoutType = z.infer<typeof ZWidgetLayoutType>;
|
|
export type TWidgetSelectionMode = z.infer<typeof ZWidgetSelectionMode>;
|
|
export type TWidgetSortBy = z.infer<typeof ZWidgetSortBy>;
|
|
export type TWidgetThemePreset = z.infer<typeof ZWidgetThemePreset>;
|
|
export type TWidgetConfig = z.infer<typeof ZWidgetConfig>;
|
|
export type TWidgetLayoutOptions = z.infer<typeof ZWidgetLayoutOptions>;
|
|
export type TWidgetCardGridOptions = z.infer<typeof ZWidgetCardGridOptions>;
|
|
export type TWidgetTagFilterOptions = z.infer<typeof ZWidgetTagFilterOptions>;
|
|
export type TWidgetCarouselOptions = z.infer<typeof ZWidgetCarouselOptions>;
|
|
export type TWidgetPrimaryCourseOptions = z.infer<typeof ZWidgetPrimaryCourseOptions>;
|
|
export type TWidgetCompactListOptions = z.infer<typeof ZWidgetCompactListOptions>;
|
|
export type TWidgetEditorialSpotlightOptions = z.infer<typeof ZWidgetEditorialSpotlightOptions>;
|
|
export type TWidgetCategoryShelfOptions = z.infer<typeof ZWidgetCategoryShelfOptions>;
|
|
export type TWidgetPlanGatedFields = z.infer<typeof ZWidgetPlanGatedFields>;
|
|
export type TWidgetPayloadCourse = z.infer<typeof ZWidgetPayloadCourse>;
|
|
export type TWidgetPayloadCategory = z.infer<typeof ZWidgetPayloadCategory>;
|
|
export type TWidgetPayloadCourseTag = z.infer<typeof ZWidgetPayloadCourseTag>;
|
|
export type TWidgetPayload = z.infer<typeof ZWidgetPayload>;
|
|
export type TWidgetListItem = z.infer<typeof ZWidgetListItem>;
|
|
export type TWidgetEditorCourse = z.infer<typeof ZWidgetEditorCourse>;
|
|
export type TWidgetDetail = z.infer<typeof ZWidgetDetail>;
|
|
export type TCreateWidget = z.infer<typeof ZCreateWidget>;
|
|
export type TUpdateWidget = z.infer<typeof ZUpdateWidget>;
|
|
export type TRollbackWidget = z.infer<typeof ZRollbackWidget>;
|
|
|
|
export function getDefaultWidgetConfig(): TWidgetConfig {
|
|
return ZWidgetConfig.parse(DEFAULT_WIDGET_CONFIG);
|
|
}
|
|
|
|
export function isPaidWidgetPlan(planName?: string | null): boolean {
|
|
return planName === PLAN.EARLY_ADOPTER || planName === PLAN.ENTERPRISE;
|
|
}
|
|
|
|
export function resolveWidgetPlanGatedFields(
|
|
planName?: string | null,
|
|
selectedTheme?: TWidgetThemePreset
|
|
): TWidgetPlanGatedFields {
|
|
const isPaidPlan = isPaidWidgetPlan(planName);
|
|
const availableThemes: TWidgetThemePreset[] = isPaidPlan ? [...WIDGET_THEME_PRESET_VALUES] : ['classroomio'];
|
|
const themeCandidate = selectedTheme ?? 'classroomio';
|
|
const selectedResolvedTheme = availableThemes.includes(themeCandidate) ? themeCandidate : 'classroomio';
|
|
|
|
return {
|
|
isPaidPlan,
|
|
canUseCustomColors: isPaidPlan,
|
|
canUseCustomCss: isPaidPlan,
|
|
canToggleBranding: isPaidPlan,
|
|
isBrandingForced: !isPaidPlan,
|
|
availableThemes,
|
|
selectedTheme: selectedResolvedTheme
|
|
};
|
|
}
|
|
|
|
export function normalizeWidgetConfig(config?: Partial<TWidgetConfig> | null, planName?: string | null): TWidgetConfig {
|
|
const parsedConfig = ZWidgetConfig.parse(config ?? {});
|
|
const planGatedFields = resolveWidgetPlanGatedFields(planName, parsedConfig.themePreset);
|
|
|
|
return {
|
|
...parsedConfig,
|
|
themePreset: planGatedFields.selectedTheme,
|
|
branding: {
|
|
...parsedConfig.branding,
|
|
showPoweredBy: planGatedFields.isBrandingForced ? true : parsedConfig.branding.showPoweredBy
|
|
},
|
|
advanced: {
|
|
...parsedConfig.advanced,
|
|
customCss: planGatedFields.canUseCustomCss ? parsedConfig.advanced.customCss : ''
|
|
},
|
|
colors: planGatedFields.canUseCustomColors ? parsedConfig.colors : getDefaultWidgetConfig().colors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum number of courses that should be requested/rendered for a
|
|
* given layout. Mirrors the rules enforced server-side in `buildWidgetPayload`.
|
|
*/
|
|
export function getLayoutCourseLimit(layoutType: TWidgetLayoutType, options: TWidgetLayoutOptions): number {
|
|
switch (layoutType) {
|
|
case 'card_grid':
|
|
return options.cardGrid.maxCount;
|
|
case 'tag_filter':
|
|
return options.tagFilter.maxCount;
|
|
case 'carousel':
|
|
return options.carousel.maxCount;
|
|
case 'primary_course':
|
|
return 1 + options.primaryCourse.secondaryMaxCount;
|
|
case 'compact_list':
|
|
return options.compactList.maxCount;
|
|
case 'editorial_spotlight':
|
|
return 1 + options.editorialSpotlight.secondaryMaxCount;
|
|
case 'category_shelf': {
|
|
const tagCount = Math.max(options.categoryShelf.categoryTagIds.length, 1);
|
|
return tagCount * options.categoryShelf.maxPerCategory;
|
|
}
|
|
default:
|
|
return 24;
|
|
}
|
|
}
|