@@ -11,6 +11,7 @@ import { cn } from "@/lib/utils"
1111import NumberFlow from "@number-flow/react"
1212import { IconCheck , IconX } from "@tabler/icons-react"
1313import { BorderBeam } from "border-beam"
14+ import { AnimatePresence , m as motion } from "motion/react"
1415import { useState } from "react"
1516
1617type 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 ) }
0 commit comments