diff --git a/apps/dashboard/src/lib/features/ui/course-landing-page/components/editor/pricing-form.svelte b/apps/dashboard/src/lib/features/ui/course-landing-page/components/editor/pricing-form.svelte index 3ff1906f..5367a37a 100644 --- a/apps/dashboard/src/lib/features/ui/course-landing-page/components/editor/pricing-form.svelte +++ b/apps/dashboard/src/lib/features/ui/course-landing-page/components/editor/pricing-form.svelte @@ -7,6 +7,7 @@ import type { Course } from '$features/course/utils/types'; import { t } from '$lib/utils/functions/translations'; import { isCourseFree } from '$lib/utils/functions/course'; + import { toFiniteNumber } from '@cio/utils/functions'; import { InputField } from '@cio/ui/custom/input-field'; import { TextEditor } from '$features/ui'; @@ -18,10 +19,10 @@ let { course = $bindable(), setter }: Props = $props(); - let discount = $derived(get(course, 'metadata.discount', 0)); let paymentLink = $derived(get(course, 'metadata.paymentLink', '')); - let showDiscount = $derived(get(course, 'metadata.showDiscount', false)); - let giftToggled = $derived(get(course, 'metadata.reward.show', false)); + let showDiscount = $derived(Boolean(get(course, 'metadata.showDiscount', false))); + let discount = $derived(toFiniteNumber(get(course, 'metadata.discount', 0)) ?? 0); + let giftToggled = $derived(Boolean(get(course, 'metadata.reward.show', false))); function handleChange(content: string) { setter(content, 'metadata.reward.description'); @@ -34,7 +35,7 @@ setter(paymentLink, 'metadata.paymentLink'); }); $effect(() => { - setter(discount, 'metadata.discount'); + setter(toFiniteNumber(discount) ?? 0, 'metadata.discount'); }); $effect(() => { setter(giftToggled, 'metadata.reward.show'); diff --git a/packages/utils/src/functions/index.ts b/packages/utils/src/functions/index.ts index c1c70430..8b1c5698 100644 --- a/packages/utils/src/functions/index.ts +++ b/packages/utils/src/functions/index.ts @@ -1,6 +1,7 @@ export * from './array'; export * from './currency'; export * from './fileValidation'; +export * from './number'; export * from './sanitize'; export * from './slug'; export * from './transcript-vtt'; diff --git a/packages/utils/src/functions/number.ts b/packages/utils/src/functions/number.ts new file mode 100644 index 00000000..f9a0cfee --- /dev/null +++ b/packages/utils/src/functions/number.ts @@ -0,0 +1,19 @@ +/** + * Coerces a number or numeric string to a finite number. + * Does not coerce booleans, arrays, or other types (unlike bare `Number()`). + * Empty, null, and invalid values return undefined. + */ +export function toFiniteNumber(value: unknown): number | undefined { + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + + if (typeof value === 'string' && value.trim()) { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed; + } + } + + return undefined; +} diff --git a/packages/utils/src/validation/course/course.ts b/packages/utils/src/validation/course/course.ts index 90604126..10efaf21 100644 --- a/packages/utils/src/validation/course/course.ts +++ b/packages/utils/src/validation/course/course.ts @@ -1,5 +1,6 @@ import * as z from 'zod'; +import { toFiniteNumber } from '../../functions/number'; import { ALLOWED_CONTENT_TYPES, ALLOWED_DOCUMENT_TYPES } from '../constants'; import { ZCourseCalloutInput } from './callout'; import { ZCourseType } from './course-type'; @@ -321,6 +322,15 @@ function preprocessCourseMetadata(value: unknown): unknown { metadata.allowNewStudent = true; } + if (metadata.discount === null || metadata.discount === '') { + metadata.discount = undefined; + } else if (typeof metadata.discount === 'string') { + const parsedDiscount = toFiniteNumber(metadata.discount); + if (parsedDiscount !== undefined) { + metadata.discount = parsedDiscount; + } + } + return metadata; }