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 <[email protected]> * 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 <[email protected]> * fix(ui): prefix group class for landing section hover badge Co-authored-by: Rotimi Best <[email protected]> * fix(ui): remove unused formatCourseCurrency helper Co-authored-by: Rotimi Best <[email protected]> * 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 <[email protected]> ---------
This commit is contained in:
co-authored by
Rotimi Best
parent
6c3a982daf
commit
0822f5f5a3
+1
-1
@@ -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}
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
export * from './array';
|
||||
export * from './currency';
|
||||
export * from './fileValidation';
|
||||
export * from './sanitize';
|
||||
export * from './slug';
|
||||
|
||||
Reference in New Issue
Block a user