fix(course): make coursesNo and reward description optional; render landing HTML safely
- Allow coursesNo to be omitted in instructor metadata (not required)
- Allow reward description to be omitted when reward object is present
- Replace raw {@html} with SafeHtmlContent in course-pricing,
course-info-blocks, and embed landing-page components
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import { courseLandingTokens } from './course-landing-page.tokens';
|
import { courseLandingTokens } from './course-landing-page.tokens';
|
||||||
import EditableLandingSection from './editable-section.svelte';
|
import EditableLandingSection from './editable-section.svelte';
|
||||||
import AwardIcon from '@lucide/svelte/icons/award';
|
import AwardIcon from '@lucide/svelte/icons/award';
|
||||||
|
import SafeHtmlContent from '../safe-html-content/safe-html-content.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
variant: OrgLandingPageTheme;
|
variant: OrgLandingPageTheme;
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
<article id={block.id} class={t.infoBlock}>
|
<article id={block.id} class={t.infoBlock}>
|
||||||
<span class={t.infoBlockEyebrow}>{block.eyebrow}</span>
|
<span class={t.infoBlockEyebrow}>{block.eyebrow}</span>
|
||||||
<h2 class={t.infoBlockHeading}>{block.heading}</h2>
|
<h2 class={t.infoBlockHeading}>{block.heading}</h2>
|
||||||
<div class={t.infoBlockBody}>{@html block.html}</div>
|
<div class={t.infoBlockBody}><SafeHtmlContent content={block.html} /></div>
|
||||||
</article>
|
</article>
|
||||||
</EditableLandingSection>
|
</EditableLandingSection>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -91,7 +92,7 @@
|
|||||||
<article id={block.id} class={t.infoBlock}>
|
<article id={block.id} class={t.infoBlock}>
|
||||||
<span class={t.infoBlockEyebrow}>{block.eyebrow}</span>
|
<span class={t.infoBlockEyebrow}>{block.eyebrow}</span>
|
||||||
<h2 class={t.infoBlockHeading}>{block.heading}</h2>
|
<h2 class={t.infoBlockHeading}>{block.heading}</h2>
|
||||||
<div class={t.infoBlockBody}>{@html block.html}</div>
|
<div class={t.infoBlockBody}><SafeHtmlContent content={block.html} /></div>
|
||||||
</article>
|
</article>
|
||||||
</EditableLandingSection>
|
</EditableLandingSection>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -114,7 +115,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ui:p-6">
|
<div class="ui:p-6">
|
||||||
<h2 class={`${t.infoBlockHeading} ui:mb-3`}>{block.heading}</h2>
|
<h2 class={`${t.infoBlockHeading} ui:mb-3`}>{block.heading}</h2>
|
||||||
<div class={t.infoBlockBody}>{@html block.html}</div>
|
<div class={t.infoBlockBody}><SafeHtmlContent content={block.html} /></div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</EditableLandingSection>
|
</EditableLandingSection>
|
||||||
@@ -138,7 +139,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class={`${t.infoBlockHeading} ui:mb-4`}>{block.heading}</h2>
|
<h2 class={`${t.infoBlockHeading} ui:mb-4`}>{block.heading}</h2>
|
||||||
<div class={t.infoBlockBody}>{@html block.html}</div>
|
<div class={t.infoBlockBody}><SafeHtmlContent content={block.html} /></div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</EditableLandingSection>
|
</EditableLandingSection>
|
||||||
@@ -153,7 +154,7 @@
|
|||||||
<span class={t.infoBlockEyebrow}>{block.eyebrow}</span>
|
<span class={t.infoBlockEyebrow}>{block.eyebrow}</span>
|
||||||
<h2 class={`${t.infoBlockHeading} ui:mt-2`}>{block.heading}</h2>
|
<h2 class={`${t.infoBlockHeading} ui:mt-2`}>{block.heading}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class={t.infoBlockBody}>{@html block.html}</div>
|
<div class={t.infoBlockBody}><SafeHtmlContent content={block.html} /></div>
|
||||||
</article>
|
</article>
|
||||||
</EditableLandingSection>
|
</EditableLandingSection>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import LandingButton from './landing-button.svelte';
|
import LandingButton from './landing-button.svelte';
|
||||||
import CheckIcon from '@lucide/svelte/icons/check';
|
import CheckIcon from '@lucide/svelte/icons/check';
|
||||||
import GiftIcon from '@lucide/svelte/icons/gift';
|
import GiftIcon from '@lucide/svelte/icons/gift';
|
||||||
|
import SafeHtmlContent from '../safe-html-content/safe-html-content.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
variant: OrgLandingPageTheme;
|
variant: OrgLandingPageTheme;
|
||||||
@@ -80,7 +81,7 @@
|
|||||||
<GiftIcon class="ui:size-4 ui:text-[var(--landing-accent)]" />
|
<GiftIcon class="ui:size-4 ui:text-[var(--landing-accent)]" />
|
||||||
<span class={t.pricingRewardLabel}>{labels?.pricingRewardLabel ?? 'Bonus'}</span>
|
<span class={t.pricingRewardLabel}>{labels?.pricingRewardLabel ?? 'Bonus'}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{pricing.reward.description}</p>
|
<div><SafeHtmlContent content={pricing.reward.description} /></div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type { OrgLandingPageLabels, OrgLandingPageTheme, LandingPageEmbed } from './types';
|
import type { OrgLandingPageLabels, OrgLandingPageTheme, LandingPageEmbed } from './types';
|
||||||
import SecondaryActionButton from './secondary-action-button.svelte';
|
import SecondaryActionButton from './secondary-action-button.svelte';
|
||||||
import EditableLandingSection from './editable-section.svelte';
|
import EditableLandingSection from './editable-section.svelte';
|
||||||
|
import SafeHtmlContent from '../safe-html-content/safe-html-content.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
embed,
|
embed,
|
||||||
@@ -51,7 +52,7 @@
|
|||||||
style="border: 1px solid #262a35; background: #0f1218; box-shadow: 0 30px 90px -30px rgba(0,0,0,0.7);"
|
style="border: 1px solid #262a35; background: #0f1218; box-shadow: 0 30px 90px -30px rgba(0,0,0,0.7);"
|
||||||
>
|
>
|
||||||
<div class="ui:p-5 ui:overflow-x-auto" style="color: #e9eaed;">
|
<div class="ui:p-5 ui:overflow-x-auto" style="color: #e9eaed;">
|
||||||
{@html embed.code}
|
<SafeHtmlContent content={embed.code} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -76,7 +77,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if embed.code}
|
{#if embed.code}
|
||||||
<div class="ui:flex ui:w-full ui:justify-center">
|
<div class="ui:flex ui:w-full ui:justify-center">
|
||||||
{@html embed.code}
|
<SafeHtmlContent content={embed.code} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +96,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if embed.code}
|
{#if embed.code}
|
||||||
<div class="ui:flex ui:w-full ui:justify-center">
|
<div class="ui:flex ui:w-full ui:justify-center">
|
||||||
{@html embed.code}
|
<SafeHtmlContent content={embed.code} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export type TCourseCreate = z.infer<typeof ZCourseCreate>;
|
|||||||
|
|
||||||
export const ZCourseReward = z.object({
|
export const ZCourseReward = z.object({
|
||||||
show: z.boolean(),
|
show: z.boolean(),
|
||||||
description: z.string()
|
description: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const METADATA_NULLABLE_STRING_KEYS = ['requirements', 'description', 'goals', 'videoUrl', 'paymentLink'] as const;
|
const METADATA_NULLABLE_STRING_KEYS = ['requirements', 'description', 'goals', 'videoUrl', 'paymentLink'] as const;
|
||||||
@@ -320,7 +320,7 @@ function preprocessCourseMetadata(value: unknown): unknown {
|
|||||||
export const ZCourseInstructor = z.object({
|
export const ZCourseInstructor = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.string(),
|
role: z.string(),
|
||||||
coursesNo: z.coerce.number(),
|
coursesNo: z.coerce.number().optional(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
imgUrl: z.string()
|
imgUrl: z.string()
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user