fix(dashboard): course landing page editor section and currency bugs (#768)

* fix(dashboard): course landing page editor section and currency bugs

- Raise currency select z-index so the dropdown is clickable in the
  full-screen editor shell
- Show dashed top/bottom borders and section title on hover and when
  active in EditableLandingSection
- Map hero/header section keys in the course editor preview context

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): format course landing page pricing with Intl currency

Use getCourseCurrencyFormatter (matching dashboard getCurrencyFormatter)
so NGN and USD prices render as proper locale currency strings instead of
raw numbers with a currency code suffix.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): prefix group class for landing section hover badge

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): remove unused formatCourseCurrency helper

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* refactor: move currency formatter to @cio/utils

Shared course price formatting belongs in @cio/utils, not the UI
package. Dashboard re-exports the helper to keep existing imports.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------
This commit is contained in:
Rotimi Best
2026-07-19 12:09:38 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 6c3a982daf
commit 0822f5f5a3
7 changed files with 43 additions and 38 deletions
@@ -48,7 +48,7 @@
<Select.Trigger class="w-full">
<p>{course.currency === 'NGN' ? 'NGN' : 'USD'}</p>
</Select.Trigger>
<Select.Content>
<Select.Content class="z-menu-elevated">
<Select.Item value="NGN">NGN</Select.Item>
<Select.Item value="USD">USD</Select.Item>
</Select.Content>
@@ -1,8 +1,2 @@
export default (currency: string | undefined) => {
const locale = currency == 'NGN' ? 'en-NG' : 'en-US';
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
minimumFractionDigits: 2
});
};
export { getCurrencyFormatter } from '@cio/utils/functions';
export { getCurrencyFormatter as default } from '@cio/utils/functions';
@@ -44,10 +44,13 @@
};
setLandingPageEditContext({
selectedKey: () => selectedSectionKey,
selectKey: (key) => (selectedSectionKey = key),
labelFor: (key) => t.get(`course.navItem.landing_page.editor.title.${key}` as never) || key,
iconFor: (key) => sectionIcons[key] ?? HeaderIcon
selectedKey: () => (selectedSectionKey === 'header' ? 'hero' : selectedSectionKey),
selectKey: (key) => (selectedSectionKey = key === 'hero' ? 'header' : key),
labelFor: (key) => {
const sectionKey = key === 'hero' ? 'header' : key;
return t.get(`course.navItem.landing_page.editor.title.${sectionKey}` as never) || key;
},
iconFor: (key) => sectionIcons[key === 'hero' ? 'header' : key] ?? HeaderIcon
});
function syncCourseStore(_courseData: Course) {
@@ -1,6 +1,8 @@
<script lang="ts">
import type { CoursePricing, CourseLandingPageLabels, OrgLandingPageTheme } from './types';
import { courseLandingTokens } from './course-landing-page.tokens';
import { calcCourseDiscount } from './landing-page-utils';
import { getCurrencyFormatter } from '@cio/utils/functions';
import EditableLandingSection from './editable-section.svelte';
import LandingButton from './landing-button.svelte';
import CheckIcon from '@lucide/svelte/icons/check';
@@ -19,17 +21,17 @@
const isFree = $derived(!pricing.cost || pricing.cost <= 0);
const currencySymbol = $derived(
pricing.currency === 'USD' ? '$' : pricing.currency === 'EUR' ? '€' : pricing.currency === 'GBP' ? '£' : ''
);
const currencyFormatter = $derived(getCurrencyFormatter(pricing.currency ?? 'USD'));
const discountedAmount = $derived(
pricing.showDiscount && pricing.discount && pricing.cost
? pricing.cost - (pricing.cost * pricing.discount) / 100
? calcCourseDiscount(pricing.discount, pricing.cost, true)
: null
);
const displayAmount = $derived(discountedAmount ?? pricing.cost ?? 0);
const formattedDisplayAmount = $derived(currencyFormatter.format(displayAmount));
const formattedOriginalAmount = $derived(currencyFormatter.format(pricing.cost ?? 0));
</script>
<EditableLandingSection sectionKey="pricing">
@@ -42,14 +44,9 @@
<span class={t.pricingAmount}>{labels?.freeLabel ?? 'Free'}</span>
{:else}
<div class="ui:flex ui:items-baseline ui:gap-3">
<span class={t.pricingAmount}>
{currencySymbol}{displayAmount.toFixed(0)}
<span class={t.pricingCurrency}>{currencySymbol ? '' : pricing.currency}</span>
</span>
<span class={t.pricingAmount}>{formattedDisplayAmount}</span>
{#if discountedAmount !== null}
<span class={t.pricingDiscount}>
{currencySymbol}{(pricing.cost ?? 0).toFixed(0)}
</span>
<span class={t.pricingDiscount}>{formattedOriginalAmount}</span>
{/if}
</div>
{#if pricing.discount && pricing.showDiscount}
@@ -42,23 +42,22 @@
onclick={handleClick}
onkeydown={handleKeydown}
class={cn(
'ui:relative ui:outline-none ui:cursor-pointer ui:transition-shadow',
selected
? 'ui:ring-2 ui:ring-primary ui:ring-inset'
: 'ui:hover:ring-2 ui:hover:ring-primary/40 ui:hover:ring-inset',
'ui:group ui:relative ui:outline-none ui:cursor-pointer ui:border-y-2 ui:border-dashed ui:transition-colors',
selected ? 'ui:border-primary' : 'ui:border-transparent ui:hover:border-primary/40',
className
)}
>
{#if selected}
<div
class="ui:absolute ui:top-0 ui:left-0 ui:z-30 ui:-translate-y-full ui:flex ui:items-center ui:gap-1.5 ui:rounded-t-md ui:bg-primary ui:px-2 ui:py-1 ui:text-xs ui:font-medium ui:text-primary-foreground ui:shadow-sm ui:pointer-events-none"
>
{#if Icon}
<Icon size={14} class="ui:size-3.5" />
{/if}
<span>{ctx.labelFor(sectionKey)}</span>
</div>
{/if}
<div
class={cn(
'ui:absolute ui:top-0 ui:left-0 ui:z-30 ui:-translate-y-full ui:flex ui:items-center ui:gap-1.5 ui:rounded-t-md ui:bg-primary ui:px-2 ui:py-1 ui:text-xs ui:font-medium ui:text-primary-foreground ui:shadow-sm ui:pointer-events-none ui:transition-opacity',
selected ? 'ui:opacity-100' : 'ui:opacity-0 ui:group-hover:opacity-100'
)}
>
{#if Icon}
<Icon size={14} class="ui:size-3.5" />
{/if}
<span>{ctx.labelFor(sectionKey)}</span>
</div>
{@render children()}
</div>
{/if}
+11
View File
@@ -0,0 +1,11 @@
/** Locale-aware currency formatter for course prices (NGN and USD). */
export function getCurrencyFormatter(currency: string | undefined = 'USD') {
const normalizedCurrency = currency === 'NGN' ? 'NGN' : 'USD';
const locale = normalizedCurrency === 'NGN' ? 'en-NG' : 'en-US';
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: normalizedCurrency,
minimumFractionDigits: 2
});
}
+1
View File
@@ -1,4 +1,5 @@
export * from './array';
export * from './currency';
export * from './fileValidation';
export * from './sanitize';
export * from './slug';