Skip to content

Commit 2a5ce63

Browse files
committed
feat: add configurable highlighted card styling
1 parent 9561627 commit 2a5ce63

10 files changed

Lines changed: 33918 additions & 15 deletions

src/blocks/PricingBlock.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { sectionFields } from "@/fields/sectionFields"
2+
import { colorField } from "@mvriu5/payload-color-picker"
23
import { iconField } from "@mvriu5/payload-icon-picker"
34
import { pricingPackageFields } from "@/fields/pricingPackageFields"
45
import type { Block } from "payload"
@@ -61,6 +62,32 @@ export const PricingBlock: Block = {
6162
localized: true,
6263
defaultValue: "What's included",
6364
},
65+
colorField({
66+
name: "titleColor",
67+
label: "Card Title Color",
68+
required: false,
69+
admin: {
70+
description: "Optional color applied to all card titles. Leave empty to use white.",
71+
},
72+
}),
73+
{
74+
name: "highlightedCardColor",
75+
label: "Highlighted Card Color",
76+
type: "select",
77+
required: false,
78+
admin: {
79+
description: "Optional accent color for the highlighted card background. Leave empty to use the white gradient.",
80+
},
81+
options: [
82+
{ label: "Brand", value: "brand" },
83+
{ label: "Pink", value: "pink" },
84+
{ label: "Yellow", value: "yellow" },
85+
{ label: "Aqua", value: "aqua" },
86+
{ label: "Blue", value: "blue" },
87+
{ label: "Lime", value: "lime" },
88+
{ label: "Magenta", value: "magenta" },
89+
],
90+
},
6491
pricingPackageFields("pro", "Pro"),
6592
pricingPackageFields("max", "Max"),
6693
pricingPackageFields("custom", "Custom"),

src/components/animations/Stagger.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client"
22

33
import { m as motion, type Variants } from "motion/react"
4-
import type { ReactNode } from "react"
4+
import type { CSSProperties, ReactNode } from "react"
55

66
interface StaggerContainerProps {
77
children: ReactNode
@@ -17,6 +17,7 @@ interface StaggerItemProps {
1717
as?: "div" | "h1" | "h2" | "p" | "ul"
1818
y?: number
1919
duration?: number
20+
style?: CSSProperties
2021
}
2122

2223
export function StaggerContainer({ children, className, delayChildren = 0, staggerChildren = 0.08, disabled = false }: StaggerContainerProps) {
@@ -26,21 +27,27 @@ export function StaggerContainer({ children, className, delayChildren = 0, stagg
2627
}
2728

2829
return (
29-
<motion.div className={className} variants={variants} initial={disabled ? false : "hidden"} whileInView={disabled ? undefined : "show"} viewport={disabled ? undefined : { once: true, amount: 0.25 }}>
30+
<motion.div
31+
className={className}
32+
variants={variants}
33+
initial={disabled ? false : "hidden"}
34+
whileInView={disabled ? undefined : "show"}
35+
viewport={disabled ? undefined : { once: true, amount: 0.25 }}
36+
>
3037
{children}
3138
</motion.div>
3239
)
3340
}
3441

35-
export function StaggerItem({ children, className, as = "div", y = 12, duration = 0.3 }: StaggerItemProps) {
42+
export function StaggerItem({ children, className, as = "div", y = 12, duration = 0.3, style }: StaggerItemProps) {
3643
const Component = motion[as]
3744
const variants: Variants = {
3845
hidden: { opacity: 0, y },
3946
show: { opacity: 1, y: 0, transition: { duration, ease: [0.22, 1, 0.36, 1] } },
4047
}
4148

4249
return (
43-
<Component variants={variants} className={className}>
50+
<Component variants={variants} className={className} style={style}>
4451
{children}
4552
</Component>
4653
)

src/components/sections/PricingSection.tsx

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

1718
type PricingPeriod = "monthly" | "quarterly" | "yearly"
@@ -27,6 +28,16 @@ const popularPillColorClasses = {
2728
magenta: "border-magenta/10! bg-magenta/10! text-magenta!",
2829
} as const
2930

31+
const highlightedCardColors = {
32+
brand: "var(--bg-brand)",
33+
pink: "var(--bg-pink)",
34+
yellow: "var(--bg-yellow)",
35+
aqua: "var(--bg-aqua)",
36+
blue: "var(--bg-blue)",
37+
lime: "var(--bg-lime)",
38+
magenta: "var(--bg-magenta)",
39+
} as const
40+
3041
interface PricingSectionProps {
3142
content?: PricingLayoutBlock | null
3243
locale: AppLocale
@@ -105,6 +116,7 @@ export function PricingSection({ content, locale, packages, paymentPeriod }: Pri
105116
const buttonUrl = pricingPackage.content?.button?.url?.trim()
106117
const highlighted = pricingPackage.key === "max"
107118
const popularPillColor = content.popularPill?.color ?? "brand"
119+
const highlightedCardColor = content.highlightedCardColor ? highlightedCardColors[content.highlightedCardColor] : null
108120
const periodMonths = selectedPeriod === "quarterly" ? 3 : selectedPeriod === "yearly" ? 12 : 1
109121
const configuredDiscount = selectedPeriod === "quarterly" ? paymentPeriod.quarterlyDiscount : selectedPeriod === "yearly" ? paymentPeriod.yearlyDiscount : 0
110122
const priceWithoutPeriodDiscount = pricingPackage.price !== null && configuredDiscount > 0 && configuredDiscount < 1 ? pricingPackage.price / (1 - configuredDiscount) : null
@@ -120,6 +132,13 @@ export function PricingSection({ content, locale, packages, paymentPeriod }: Pri
120132
y={14}
121133
duration={0.42}
122134
className="relative flex h-full min-w-0 flex-col overflow-hidden rounded-3xl border border-white/5 bg-[linear-gradient(160deg,rgba(255,255,255,0.08),rgba(255,255,255,0.02)_28%,rgba(8,10,20,0.6)_100%)] p-6 md:p-8"
135+
style={
136+
highlighted && highlightedCardColor
137+
? {
138+
background: `linear-gradient(160deg, color-mix(in oklch, ${highlightedCardColor} 8%, transparent), color-mix(in oklch, ${highlightedCardColor} 2%, transparent) 28%, rgba(8, 10, 20, 0.6) 100%)`,
139+
}
140+
: undefined
141+
}
123142
>
124143
{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%)]" />}
125144
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-linear-to-r from-transparent via-white/30 to-transparent" />
@@ -133,21 +152,35 @@ export function PricingSection({ content, locale, packages, paymentPeriod }: Pri
133152
</StableBadge>
134153
</div>
135154
)}
136-
<h3 className={cn("relative z-10 text-2xl font-semibold text-white", pricingPackage.price !== null && "pr-20")}>{pricingPackage.title}</h3>
155+
<h3
156+
className={cn("relative z-10 text-2xl font-semibold text-white", pricingPackage.price !== null && "pr-20")}
157+
style={highlighted && content.titleColor ? { color: content.titleColor } : undefined}
158+
>
159+
{pricingPackage.title}
160+
</h3>
137161
{pricingPackage.description && <p className="relative z-10 mt-1 leading-6 text-secondary">{pricingPackage.description}</p>}
138162

139163
{pricingPackage.price !== null && (
140164
<div className="relative z-10 mt-6 flex flex-col items-start gap-0">
141-
{hasDiscount && (
142-
<span className="relative -mb-1 inline-flex text-sm leading-none font-medium text-tertiary after:absolute after:top-1/2 after:right-0 after:left-0 after:z-10 after:h-px after:bg-current after:content-['']">
143-
{regularPrice.toLocaleString(locale === "de" ? "de-DE" : "en-US", {
144-
style: "currency",
145-
currency: "EUR",
146-
minimumFractionDigits: 0,
147-
maximumFractionDigits: 2,
148-
})}
149-
</span>
150-
)}
165+
<AnimatePresence initial={false}>
166+
{hasDiscount && (
167+
<motion.span
168+
key={selectedPeriod}
169+
initial={{ opacity: 0, y: 4 }}
170+
animate={{ opacity: 1, y: 0 }}
171+
exit={{ opacity: 0, y: 4 }}
172+
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
173+
className="relative -mb-1 inline-flex text-sm leading-none font-medium text-tertiary after:absolute after:top-1/2 after:right-0 after:left-0 after:z-10 after:h-px after:bg-current after:content-['']"
174+
>
175+
{regularPrice.toLocaleString(locale === "de" ? "de-DE" : "en-US", {
176+
style: "currency",
177+
currency: "EUR",
178+
minimumFractionDigits: 0,
179+
maximumFractionDigits: 2,
180+
})}
181+
</motion.span>
182+
)}
183+
</AnimatePresence>
151184
<div className="flex flex-wrap items-center gap-2">
152185
<NumberFlow
153186
value={pricingPackage.price}

0 commit comments

Comments
 (0)