Skip to content

Commit 8848568

Browse files
committed
feat: improve discount badge in pricign section
1 parent 76b628d commit 8848568

2 files changed

Lines changed: 34 additions & 20 deletions

File tree

src/components/sections/PricingSection.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { cn } from "@/lib/utils"
1111
import NumberFlow from "@number-flow/react"
1212
import { IconCheck, IconX } from "@tabler/icons-react"
1313
import { BorderBeam } from "border-beam"
14+
import { AnimatePresence, m as motion } from "motion/react"
1415
import { useState } from "react"
1516

1617
type PricingPeriod = "monthly" | "quarterly" | "yearly"
@@ -27,10 +28,12 @@ export function PricingSection({ content, locale, packages, paymentPeriod }: Pri
2728
const [selectedPeriod, setSelectedPeriod] = useState<PricingPeriod>("monthly")
2829
if (!content) return null
2930

31+
const quarterlyDiscount = Math.round((paymentPeriod.quarterlyDiscount ?? 0) * 100)
32+
const yearlyDiscount = Math.round((paymentPeriod.yearlyDiscount ?? 0) * 100)
3033
const periodOptions = [
3134
{ value: "monthly", label: paymentPeriod.monthlyText },
32-
{ value: "quarterly", label: paymentPeriod.quarterlyText },
33-
{ value: "yearly", label: paymentPeriod.yearlyText },
35+
{ value: "quarterly", label: paymentPeriod.quarterlyText, badge: quarterlyDiscount > 0 ? `-${quarterlyDiscount}%` : null },
36+
{ value: "yearly", label: paymentPeriod.yearlyText, badge: yearlyDiscount > 0 ? `-${yearlyDiscount}%` : null },
3437
] as const
3538
const periodSuffix = {
3639
monthly: "/mo",
@@ -81,7 +84,7 @@ export function PricingSection({ content, locale, packages, paymentPeriod }: Pri
8184
className="overflow-visible!"
8285
>
8386
<div className="flex w-full justify-center">
84-
<Switch value={selectedPeriod} options={periodOptions} onChange={setSelectedPeriod} fitContent />
87+
<Switch value={selectedPeriod} options={periodOptions} onChange={setSelectedPeriod} className="[&>div>button]:min-w-34 sm:[&>div>button]:min-w-40" fitContent />
8588
</div>
8689

8790
<StaggerContainer className="grid w-full grid-cols-1 items-stretch gap-6 md:grid-cols-2 lg:grid-cols-3" delayChildren={0.04} staggerChildren={0.08}>
@@ -92,10 +95,12 @@ export function PricingSection({ content, locale, packages, paymentPeriod }: Pri
9295
const buttonUrl = pricingPackage.content?.button?.url?.trim()
9396
const highlighted = pricingPackage.key === "max"
9497
const periodMonths = selectedPeriod === "quarterly" ? 3 : selectedPeriod === "yearly" ? 12 : 1
95-
const discount =
96-
selectedPeriod !== "monthly" && pricingPackage.price !== null && pricingPackage.monthlyPrice !== null && pricingPackage.monthlyPrice > 0
98+
const calculatedDiscount =
99+
selectedPeriod !== "monthly" && pricingPackage.price !== null && pricingPackage.price > 0 && pricingPackage.monthlyPrice !== null && pricingPackage.monthlyPrice > 0
97100
? Math.max(0, Math.round((1 - pricingPackage.price / (pricingPackage.monthlyPrice * periodMonths)) * 100))
98101
: 0
102+
const configuredDiscount = selectedPeriod === "quarterly" ? paymentPeriod.quarterlyDiscount : selectedPeriod === "yearly" ? paymentPeriod.yearlyDiscount : 0
103+
const discount = pricingPackage.price === null ? 0 : calculatedDiscount || Math.round((configuredDiscount ?? 0) * 100)
99104

100105
const card = (
101106
<StaggerItem
@@ -106,15 +111,24 @@ export function PricingSection({ content, locale, packages, paymentPeriod }: Pri
106111
>
107112
{highlighted && <div aria-hidden="true" className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,oklch(1_0_0/0.1),transparent_36%)]" />}
108113
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-linear-to-r from-transparent via-white/30 to-transparent" />
109-
{discount > 0 && (
110-
<StableBadge
111-
border
112-
className="absolute right-4 top-4 z-20 border-brand/25! bg-brand/15! px-3 py-1.5 text-sm font-semibold tracking-wider text-brand! md:right-6 md:top-6"
113-
>
114-
-{discount}%
115-
</StableBadge>
116-
)}
117-
<h3 className={cn("relative z-10 text-2xl font-semibold text-white", discount > 0 && "pr-20")}>{pricingPackage.title}</h3>
114+
<AnimatePresence initial={false}>
115+
{discount > 0 && (
116+
<motion.div
117+
initial={{ opacity: 0, scale: 0.88, y: -4 }}
118+
animate={{ opacity: 1, scale: 1, y: 0 }}
119+
exit={{ opacity: 0, scale: 0.88, y: -4 }}
120+
transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
121+
className="absolute right-4 top-4 z-20 md:right-6 md:top-6"
122+
>
123+
<StableBadge border className="border! border-brand/10! bg-brand/10! px-3 py-1 text-sm font-medium text-brand!">
124+
<span className="inline-flex items-baseline gap-0">
125+
-<NumberFlow value={discount} />%
126+
</span>
127+
</StableBadge>
128+
</motion.div>
129+
)}
130+
</AnimatePresence>
131+
<h3 className={cn("relative z-10 text-2xl font-semibold text-white", pricingPackage.price !== null && "pr-20")}>{pricingPackage.title}</h3>
118132

119133
{pricingPackage.price !== null && (
120134
<div className="relative z-10 mt-2">
@@ -125,7 +139,7 @@ export function PricingSection({ content, locale, packages, paymentPeriod }: Pri
125139
format={{ style: "currency", currency: "EUR", trailingZeroDisplay: "stripIfInteger" }}
126140
className="text-4xl font-semibold text-white"
127141
/>
128-
<span className="text-lg font-semibold text-tertiary">{periodSuffix}</span>
142+
<span className="mt-2 text-lg font-semibold text-tertiary">{periodSuffix}</span>
129143
</div>
130144
</div>
131145
)}

src/components/ui/Switch.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ export function Switch<TValue extends string>({ label, description, value, optio
5454

5555
return (
5656
<div className={cn("space-y-3", className)}>
57-
{label || description ? (
57+
{(label || description) && (
5858
<div>
59-
{label ? <p className="text-base text-secondary">{label}</p> : null}
60-
{description ? <p className="mt-1 text-sm text-tertiary">{description}</p> : null}
59+
{label && <p className="text-base text-secondary">{label}</p>}
60+
{description && <p className="mt-1 text-sm text-tertiary">{description}</p>}
6161
</div>
62-
) : null}
62+
)}
6363
<div
6464
ref={trackRef}
6565
className="relative grid overflow-hidden rounded-2xl border border-white/10 bg-white/3 p-1"
@@ -101,7 +101,7 @@ export function Switch<TValue extends string>({ label, description, value, optio
101101
>
102102
<span className={cn("min-w-0", fitContent ? "whitespace-nowrap" : "max-w-[calc(100%-2.25rem)] truncate")}>{option.label}</span>
103103
{option.badge && (
104-
<span className="absolute left-1/2 top-1/2 ml-6 sm:ml-8 translate-y-[-85%] rounded-full bg-brand/15 px-1 py-0.5 text-[10px] leading-none tracking-wider text-brand">
104+
<span className="absolute left-1/2 top-1/2 ml-6 sm:ml-8 translate-y-[-85%] rounded-full bg-brand/10 border border-brand/10 px-1 py-0.5 text-[10px] leading-none tracking-wider text-brand">
105105
{option.badge}
106106
</span>
107107
)}

0 commit comments

Comments
 (0)