fix(dashboard): restore paid course payment modal on landing page (#737)
* fix(dashboard): restore paid course payment modal on landing page Wire the existing PaymentModal back into the theme-based course landing page. Paid course CTAs now open the name/email modal and redirect to the configured payment link instead of navigating to the blocked enroll route. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): reset payment modal on close without effect loop Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * docs: add Svelte 5 rerender and effect loop guidance to AGENTS.md Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): avoid double-applying discount on course landing pricing Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
co-authored by
Rotimi Best
Cursor Agent
parent
fd8bec3e70
commit
46feccd0fd
+17
-4
@@ -32,9 +32,24 @@
|
||||
|
||||
let step = $state(STEPS.STEP_1);
|
||||
|
||||
function resetForm() {
|
||||
step = STEPS.STEP_1;
|
||||
fields.fullname = '';
|
||||
fields.email = '';
|
||||
errors.fullname = '';
|
||||
errors.email = '';
|
||||
}
|
||||
|
||||
function handleOpenChange(isOpen: boolean) {
|
||||
open = isOpen;
|
||||
|
||||
if (!isOpen) {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
errors = coursePaymentValidation(fields);
|
||||
console.log('coursePayment errors', errors);
|
||||
|
||||
if (Object.keys(errors).length) {
|
||||
return;
|
||||
@@ -51,13 +66,11 @@
|
||||
}
|
||||
|
||||
function onClickPaymentLink() {
|
||||
fields.fullname = '';
|
||||
fields.email = '';
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Root {open} onOpenChange={handleOpenChange}>
|
||||
<Dialog.Content class="w-96">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Process course payment</Dialog.Title>
|
||||
|
||||
@@ -11,8 +11,11 @@
|
||||
import type { Course } from '$features/course/utils/types';
|
||||
import type { AccountOrg } from '$features/app/types';
|
||||
import UploadWidget from '$features/ui/upload-widget/upload-widget.svelte';
|
||||
import PaymentModal from './components/payment-modal.svelte';
|
||||
import { buildCourseLandingPageProps } from './utils';
|
||||
import { handleOpenWidget } from './store';
|
||||
import { calcCourseDiscount, isCourseFree } from '$lib/utils/functions/course';
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
|
||||
interface Props {
|
||||
editMode?: boolean;
|
||||
@@ -24,6 +27,8 @@
|
||||
|
||||
let { editMode = false, courseData = $bindable(), org = null, themeComponent = null }: Props = $props();
|
||||
|
||||
let openPaymentModal = $state(false);
|
||||
|
||||
const activeOrg = $derived(org ?? $currentOrg);
|
||||
const landingSettings = $derived(normalizeLandingPageSettings(activeOrg.landingpage));
|
||||
|
||||
@@ -47,11 +52,34 @@
|
||||
const enrollmentsOpen = $derived(get(courseData, 'metadata.allowNewStudent') === true);
|
||||
const enrollDisabled = $derived(editMode || !enrollmentsOpen);
|
||||
|
||||
const discount = $derived(get(courseData, 'metadata.discount', 0));
|
||||
const showDiscount = $derived(get(courseData, 'metadata.showDiscount', false));
|
||||
const calculatedCost = $derived(calcCourseDiscount(discount, courseData.cost || 0, !!showDiscount));
|
||||
const isFree = $derived(isCourseFree(calculatedCost));
|
||||
|
||||
function handlePaidEnrollClick(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
if (editMode || !$currentOrg.siteName) {
|
||||
return;
|
||||
}
|
||||
|
||||
capturePosthogEvent('join_course', {
|
||||
course_id: courseData.id,
|
||||
course_title: courseData.title,
|
||||
course_cost: courseData.cost,
|
||||
course_free: isFree
|
||||
});
|
||||
|
||||
openPaymentModal = true;
|
||||
}
|
||||
|
||||
const landingProps = $derived(
|
||||
buildCourseLandingPageProps(courseData, activeOrg, {
|
||||
enrollHref,
|
||||
enrollDisabled,
|
||||
authAction
|
||||
authAction,
|
||||
onPaidEnrollClick: handlePaidEnrollClick
|
||||
})
|
||||
);
|
||||
|
||||
@@ -77,6 +105,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<PaymentModal
|
||||
bind:open={openPaymentModal}
|
||||
paymentLink={get(courseData, 'metadata.paymentLink', '')}
|
||||
courseId={courseData.id}
|
||||
/>
|
||||
|
||||
{#if ThemeComponent}
|
||||
{#if editMode && $handleOpenWidget.open}
|
||||
<div class="mx-auto w-full max-w-7xl px-6 py-3">
|
||||
|
||||
@@ -7,6 +7,8 @@ import get from 'lodash/get';
|
||||
import type { AccountOrg } from '$features/app/types';
|
||||
import { normalizeLandingPageSettings } from '$features/org/utils/landing-page';
|
||||
import type { CourseLandingPageProps, OrgLandingPageTheme } from '@cio/ui/custom/org-landing-page';
|
||||
import { calcCourseDiscount, isCourseFree } from '$lib/utils/functions/course';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
export type LandingPageLesson = {
|
||||
id: string;
|
||||
@@ -116,6 +118,7 @@ export function buildCourseLandingPageProps(
|
||||
enrollHref: string;
|
||||
enrollDisabled: boolean;
|
||||
authAction?: { label: string; href: string };
|
||||
onPaidEnrollClick?: (event: MouseEvent) => void;
|
||||
}
|
||||
): CourseLandingPageProps {
|
||||
const landing = normalizeLandingPageSettings(org.landingpage);
|
||||
@@ -161,7 +164,14 @@ export function buildCourseLandingPageProps(
|
||||
hasCertificate ? { label: 'Certificate', value: 'Included' } : null
|
||||
].filter(Boolean) as Array<{ label: string; value: string }>;
|
||||
|
||||
const pricingCtaLabel = course.cost && course.cost > 0 ? 'Enrol now' : 'Start free';
|
||||
const discount = metadata?.discount ?? 0;
|
||||
const showDiscount = metadata?.showDiscount ?? false;
|
||||
const calculatedCost = calcCourseDiscount(discount, course.cost ?? 0, !!showDiscount);
|
||||
const isFree = isCourseFree(calculatedCost);
|
||||
const pricingCtaLabel = isFree
|
||||
? t.get('course.navItem.landing_page.pricing_section.enroll')
|
||||
: t.get('course.navItem.landing_page.pricing_section.buy');
|
||||
const paidEnrollClick = !isFree && !options.enrollDisabled ? options.onPaidEnrollClick : undefined;
|
||||
|
||||
return {
|
||||
theme: landing.theme as OrgLandingPageTheme,
|
||||
@@ -174,7 +184,8 @@ export function buildCourseLandingPageProps(
|
||||
subheading: course.description ?? landing.hero.subheading,
|
||||
primaryAction: {
|
||||
label: pricingCtaLabel,
|
||||
href: options.enrollHref,
|
||||
href: paidEnrollClick ? undefined : options.enrollHref,
|
||||
onclick: paidEnrollClick,
|
||||
disabled: options.enrollDisabled
|
||||
},
|
||||
secondaryAction: { label: 'View curriculum', href: '#curriculum' },
|
||||
@@ -237,7 +248,8 @@ export function buildCourseLandingPageProps(
|
||||
discount: metadata?.discount,
|
||||
showDiscount: metadata?.showDiscount,
|
||||
ctaLabel: pricingCtaLabel,
|
||||
ctaHref: options.enrollHref,
|
||||
ctaHref: paidEnrollClick ? undefined : options.enrollHref,
|
||||
ctaOnclick: paidEnrollClick,
|
||||
reward: metadata?.reward?.show ? { show: true, description: metadata.reward.description ?? '' } : undefined
|
||||
},
|
||||
footer: landing.footer
|
||||
|
||||
Reference in New Issue
Block a user