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 <[email protected]> * fix(dashboard): reset payment modal on close without effect loop Co-authored-by: Rotimi Best <[email protected]> * docs: add Svelte 5 rerender and effect loop guidance to AGENTS.md Co-authored-by: Rotimi Best <[email protected]> * fix(dashboard): avoid double-applying discount on course landing pricing Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
Cursor Agent
parent
fd8bec3e70
commit
46feccd0fd
@@ -2,7 +2,7 @@
|
||||
import type { OrgLandingPageProps } from '../types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SecondaryActionButton from '../secondary-action-button.svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import CoursePrimaryAction from '../course-primary-action.svelte';
|
||||
import EditableLandingSection from '../editable-section.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -29,14 +29,11 @@
|
||||
{/if}
|
||||
{#if showActions}
|
||||
<div class="ui:flex ui:flex-wrap ui:gap-4">
|
||||
<Button
|
||||
href={hero.primaryAction.href}
|
||||
disabled={hero.primaryAction.disabled ?? false}
|
||||
<CoursePrimaryAction
|
||||
action={hero.primaryAction}
|
||||
size="lg"
|
||||
class="ui:rounded-xl ui:px-8 ui:py-6 ui:text-base ui:font-bold ui:shadow-lg ui:shadow-[var(--landing-accent)]/20 ui:hover:-translate-y-1 ui:transition-all"
|
||||
>
|
||||
{hero.primaryAction.label}
|
||||
</Button>
|
||||
/>
|
||||
{#if hero.secondaryAction}
|
||||
<SecondaryActionButton href={hero.secondaryAction.href} label={hero.secondaryAction.label} variant="bold" />
|
||||
{/if}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { OrgLandingPageProps } from '../types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SecondaryActionButton from '../secondary-action-button.svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import CoursePrimaryAction from '../course-primary-action.svelte';
|
||||
import { BlurIn } from '../../animation/blurin';
|
||||
import { DotPattern } from '../../animation/dot-pattern';
|
||||
import EditableLandingSection from '../editable-section.svelte';
|
||||
@@ -41,14 +41,7 @@
|
||||
{/if}
|
||||
{#if showActions}
|
||||
<div class="ui:flex ui:flex-col ui:sm:flex-row ui:justify-center ui:gap-4">
|
||||
<Button
|
||||
href={hero.primaryAction.href}
|
||||
disabled={hero.primaryAction.disabled ?? false}
|
||||
size="lg"
|
||||
class="ui:px-8 ui:font-semibold"
|
||||
>
|
||||
{hero.primaryAction.label}
|
||||
</Button>
|
||||
<CoursePrimaryAction action={hero.primaryAction} size="lg" class="ui:px-8 ui:font-semibold" />
|
||||
{#if hero.secondaryAction}
|
||||
<SecondaryActionButton
|
||||
href={hero.secondaryAction.href}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { OrgLandingPageProps } from '../types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SecondaryActionButton from '../secondary-action-button.svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import CoursePrimaryAction from '../course-primary-action.svelte';
|
||||
import EditableLandingSection from '../editable-section.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -57,13 +57,7 @@
|
||||
{/if}
|
||||
{#if showActions}
|
||||
<div class="ui:flex ui:flex-wrap ui:items-center ui:gap-3">
|
||||
<Button
|
||||
href={hero.primaryAction.href}
|
||||
disabled={hero.primaryAction.disabled ?? false}
|
||||
class="ui:rounded-none ui:px-6 ui:font-medium"
|
||||
>
|
||||
{hero.primaryAction.label}
|
||||
</Button>
|
||||
<CoursePrimaryAction action={hero.primaryAction} class="ui:rounded-none ui:px-6 ui:font-medium" />
|
||||
{#if hero.secondaryAction}
|
||||
<SecondaryActionButton
|
||||
href={hero.secondaryAction.href}
|
||||
|
||||
@@ -8,7 +8,8 @@ export function alignHeroCtaWithPricing(
|
||||
...hero,
|
||||
primaryAction: {
|
||||
label: pricing.ctaLabel,
|
||||
href: pricing.ctaHref ?? '#pricing',
|
||||
href: pricing.ctaOnclick ? undefined : (pricing.ctaHref ?? '#pricing'),
|
||||
onclick: pricing.ctaOnclick,
|
||||
disabled: hero.primaryAction.disabled
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,7 +71,13 @@
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<LandingButton variant="primary" size="lg" href={pricing.ctaHref ?? '#enroll'} class={t.pricingCta}>
|
||||
<LandingButton
|
||||
variant="primary"
|
||||
size="lg"
|
||||
href={pricing.ctaOnclick ? undefined : (pricing.ctaHref ?? '#enroll')}
|
||||
onclick={pricing.ctaOnclick}
|
||||
class={t.pricingCta}
|
||||
>
|
||||
{pricing.ctaLabel}
|
||||
</LandingButton>
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Button, type ButtonVariant } from '../../base/button';
|
||||
import type { LandingPrimaryAction } from './types';
|
||||
|
||||
interface Props {
|
||||
action: LandingPrimaryAction;
|
||||
size?: 'default' | 'sm' | 'lg';
|
||||
variant?: ButtonVariant;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { action, size = 'default', variant = 'default', class: className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Button
|
||||
href={action.onclick ? undefined : action.href}
|
||||
onclick={action.onclick}
|
||||
disabled={action.disabled ?? false}
|
||||
{size}
|
||||
{variant}
|
||||
class={className}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { OrgLandingPageProps } from '../types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SecondaryActionButton from '../secondary-action-button.svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import CoursePrimaryAction from '../course-primary-action.svelte';
|
||||
import EditableLandingSection from '../editable-section.svelte';
|
||||
import { getCourseTypeLandingMeta } from '../landing-page-utils';
|
||||
|
||||
@@ -83,14 +83,11 @@
|
||||
{/if}
|
||||
{#if showActions}
|
||||
<div class="ui:flex ui:flex-wrap ui:items-center ui:gap-2.5 {featured ? '' : 'ui:justify-center'}">
|
||||
<Button
|
||||
href={hero.primaryAction.href}
|
||||
disabled={hero.primaryAction.disabled ?? false}
|
||||
<CoursePrimaryAction
|
||||
action={hero.primaryAction}
|
||||
size="lg"
|
||||
class="ui:rounded-full ui:px-6 ui:font-medium ui:bg-[var(--landing-fg)] ui:text-[var(--landing-bg)] ui:hover:opacity-90"
|
||||
>
|
||||
{hero.primaryAction.label}
|
||||
</Button>
|
||||
/>
|
||||
{#if hero.secondaryAction}
|
||||
<SecondaryActionButton
|
||||
href={hero.secondaryAction.href}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { OrgLandingPageProps } from '../types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SecondaryActionButton from '../secondary-action-button.svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import CoursePrimaryAction from '../course-primary-action.svelte';
|
||||
import { DotPattern } from '../../animation/dot-pattern';
|
||||
import { ShinnyText } from '../../animation/shinny-text';
|
||||
import EditableLandingSection from '../editable-section.svelte';
|
||||
@@ -55,14 +55,7 @@
|
||||
{/if}
|
||||
{#if showActions}
|
||||
<div class="ui:flex ui:items-center ui:justify-center ui:gap-4">
|
||||
<Button
|
||||
href={hero.primaryAction.href}
|
||||
disabled={hero.primaryAction.disabled ?? false}
|
||||
size="lg"
|
||||
class="ui:rounded-full"
|
||||
>
|
||||
{hero.primaryAction.label}
|
||||
</Button>
|
||||
<CoursePrimaryAction action={hero.primaryAction} size="lg" class="ui:rounded-full" />
|
||||
{#if hero.secondaryAction}
|
||||
<SecondaryActionButton
|
||||
href={hero.secondaryAction.href}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { OrgLandingPageProps } from '../types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SecondaryActionButton from '../secondary-action-button.svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import CoursePrimaryAction from '../course-primary-action.svelte';
|
||||
import EditableLandingSection from '../editable-section.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -44,14 +44,7 @@
|
||||
{/if}
|
||||
{#if showActions}
|
||||
<div class="ui:flex ui:items-center ui:justify-center ui:gap-2.5">
|
||||
<Button
|
||||
href={hero.primaryAction.href}
|
||||
disabled={hero.primaryAction.disabled ?? false}
|
||||
size="lg"
|
||||
class="ui:rounded-full ui:px-6"
|
||||
>
|
||||
{hero.primaryAction.label}
|
||||
</Button>
|
||||
<CoursePrimaryAction action={hero.primaryAction} size="lg" class="ui:rounded-full ui:px-6" />
|
||||
{#if hero.secondaryAction}
|
||||
<SecondaryActionButton href={hero.secondaryAction.href} label={hero.secondaryAction.label} variant="saas" />
|
||||
{/if}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { OrgLandingPageProps } from '../types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SecondaryActionButton from '../secondary-action-button.svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import CoursePrimaryAction from '../course-primary-action.svelte';
|
||||
import EditableLandingSection from '../editable-section.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -70,13 +70,7 @@
|
||||
{/if}
|
||||
{#if showActions}
|
||||
<div class="ui:flex ui:flex-wrap ui:items-center ui:gap-2">
|
||||
<Button
|
||||
href={hero.primaryAction.href}
|
||||
disabled={hero.primaryAction.disabled ?? false}
|
||||
class="ui:rounded-md ui:px-5"
|
||||
>
|
||||
{hero.primaryAction.label}
|
||||
</Button>
|
||||
<CoursePrimaryAction action={hero.primaryAction} class="ui:rounded-md ui:px-5" />
|
||||
{#if hero.secondaryAction}
|
||||
<SecondaryActionButton
|
||||
href={hero.secondaryAction.href}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { OrgLandingPageProps } from '../types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SecondaryActionButton from '../secondary-action-button.svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import CoursePrimaryAction from '../course-primary-action.svelte';
|
||||
import EditableLandingSection from '../editable-section.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -57,15 +57,12 @@
|
||||
{/if}
|
||||
{#if showActions}
|
||||
<div class="ui:flex ui:flex-wrap ui:items-center ui:gap-3">
|
||||
<Button
|
||||
href={hero.primaryAction.href}
|
||||
disabled={hero.primaryAction.disabled ?? false}
|
||||
<CoursePrimaryAction
|
||||
action={hero.primaryAction}
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
class="ui:rounded-none ui:px-7 ui:font-semibold ui:bg-[var(--landing-accent-fg)] ui:text-[var(--landing-accent)] ui:hover:bg-[var(--landing-accent-fg)]/90"
|
||||
>
|
||||
{hero.primaryAction.label}
|
||||
</Button>
|
||||
/>
|
||||
{#if hero.secondaryAction}
|
||||
<SecondaryActionButton
|
||||
href={hero.secondaryAction.href}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { OrgLandingPageProps, CourseItem } from '../types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import CoursePrimaryAction from '../course-primary-action.svelte';
|
||||
import EditableLandingSection from '../editable-section.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -107,14 +108,11 @@
|
||||
{hero.secondaryAction.label}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
href={hero.primaryAction.href}
|
||||
disabled={hero.primaryAction.disabled ?? false}
|
||||
<CoursePrimaryAction
|
||||
action={hero.primaryAction}
|
||||
size="sm"
|
||||
class="ui:rounded-full ui:px-4 ui:font-medium ui:bg-[#f4f5f7] ui:text-[#0a0b0e] ui:hover:bg-white"
|
||||
>
|
||||
{hero.primaryAction.label}
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -214,6 +214,13 @@ export type CourseInstructor = {
|
||||
coursesNo?: number;
|
||||
};
|
||||
|
||||
export type LandingPrimaryAction = {
|
||||
label: string;
|
||||
href?: string;
|
||||
disabled?: boolean;
|
||||
onclick?: (event: MouseEvent) => void;
|
||||
};
|
||||
|
||||
export type CoursePricing = {
|
||||
cost: number;
|
||||
currency: string;
|
||||
@@ -221,6 +228,7 @@ export type CoursePricing = {
|
||||
showDiscount?: boolean;
|
||||
ctaLabel: string;
|
||||
ctaHref?: string;
|
||||
ctaOnclick?: (event: MouseEvent) => void;
|
||||
features?: string[];
|
||||
reward?: {
|
||||
show: boolean;
|
||||
@@ -307,7 +315,7 @@ export interface CourseLandingPageProps {
|
||||
hero: {
|
||||
heading: string;
|
||||
subheading: string;
|
||||
primaryAction: { label: string; href: string; disabled?: boolean };
|
||||
primaryAction: LandingPrimaryAction;
|
||||
secondaryAction?: { label: string; href: string };
|
||||
image?: string;
|
||||
stats?: Array<{ label: string; value: string }>;
|
||||
@@ -339,7 +347,7 @@ export interface OrgLandingPageProps {
|
||||
hero: {
|
||||
heading: string;
|
||||
subheading: string;
|
||||
primaryAction: { label: string; href: string; disabled?: boolean };
|
||||
primaryAction: LandingPrimaryAction;
|
||||
secondaryAction?: { label: string; href: string };
|
||||
image?: string;
|
||||
stats?: Array<{ label: string; value: string }>;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { OrgLandingPageProps } from '../types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import SecondaryActionButton from '../secondary-action-button.svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import CoursePrimaryAction from '../course-primary-action.svelte';
|
||||
import EditableLandingSection from '../editable-section.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -45,13 +45,10 @@
|
||||
{/if}
|
||||
{#if showActions}
|
||||
<div class="ui:inline-flex ui:flex-wrap ui:justify-center ui:items-center ui:gap-3">
|
||||
<Button
|
||||
href={hero.primaryAction.href}
|
||||
disabled={hero.primaryAction.disabled ?? false}
|
||||
<CoursePrimaryAction
|
||||
action={hero.primaryAction}
|
||||
class="ui:rounded-md ui:px-7 ui:py-3.5 ui:text-base ui:font-medium ui:bg-[var(--landing-accent)] ui:text-[var(--landing-accent-fg)] ui:hover:opacity-90"
|
||||
>
|
||||
{hero.primaryAction.label}
|
||||
</Button>
|
||||
/>
|
||||
{#if hero.secondaryAction}
|
||||
<SecondaryActionButton
|
||||
href={hero.secondaryAction.href}
|
||||
|
||||
Reference in New Issue
Block a user