Skip to content

Commit 29cfab2

Browse files
committed
feat: compare application section & block
1 parent e86d36d commit 29cfab2

12 files changed

Lines changed: 17780 additions & 449 deletions

scripts/check-migrations.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ const payloadBin = resolve(projectRoot, "node_modules/payload/bin.js")
1111
const schemaCheckSecret = "migration-schema-check-only-not-for-runtime"
1212

1313
function normalizedFile(path) {
14-
return readFileSync(path, "utf8").replaceAll("\r\n", "\n").trim()
14+
return readFileSync(path, "utf8")
15+
.replaceAll("\r\n", "\n")
16+
.split("\n")
17+
.map((line) => line.trimEnd())
18+
.join("\n")
19+
.trim()
1520
}
1621

1722
try {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { buttonField } from "@/fields/buttonField"
2+
import { gradientFields } from "@/fields/gradientFields"
3+
import { sectionFields } from "@/fields/sectionFields"
4+
import type { Block } from "payload"
5+
6+
export const CompareApplicationBlock: Block = {
7+
slug: "compareApplication",
8+
labels: {
9+
singular: "Compare Application",
10+
plural: "Compare Applications",
11+
},
12+
fields: [
13+
sectionFields(),
14+
{
15+
name: "apps",
16+
label: "Applications",
17+
type: "array",
18+
required: true,
19+
minRows: 2,
20+
fields: [
21+
{
22+
name: "logo",
23+
type: "upload",
24+
relationTo: "media",
25+
required: true,
26+
},
27+
{
28+
name: "name",
29+
type: "text",
30+
required: true,
31+
localized: true,
32+
},
33+
{
34+
name: "features",
35+
label: "Features",
36+
type: "array",
37+
required: false,
38+
fields: [
39+
{
40+
name: "title",
41+
type: "text",
42+
required: true,
43+
localized: true,
44+
},
45+
{
46+
name: "exists",
47+
label: "Exists",
48+
type: "checkbox",
49+
defaultValue: true,
50+
},
51+
],
52+
},
53+
],
54+
},
55+
{
56+
name: "showIcon",
57+
label: "Show Icons",
58+
type: "checkbox",
59+
defaultValue: true,
60+
},
61+
...gradientFields(),
62+
buttonField(1),
63+
],
64+
}

src/collections/pages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { FlowExampleBlock } from "@/blocks/FlowExampleBlock"
3131
import { SubscriptionConfiguratorBlock } from "@/blocks/SubscriptionConfiguratorBlock"
3232
import { PricingBlock } from "@/blocks/PricingBlock"
3333
import { SmallPricingBlock } from "@/blocks/SmallPricingBlock"
34+
import { CompareApplicationBlock } from "@/blocks/CompareApplicationBlock"
3435

3536
const RESERVED_PAGE_SLUGS = [
3637
"main",
@@ -189,6 +190,7 @@ export const Pages: CollectionConfig = {
189190
SubscriptionConfiguratorBlock,
190191
PricingBlock,
191192
SmallPricingBlock,
193+
CompareApplicationBlock,
192194
],
193195
required: false,
194196
localized: true,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Card } from "@/components/ui/Card"
2+
import { HapticButtonLink } from "@/components/ui/HapticButtonLink"
3+
import { Section } from "@/components/ui/Section"
4+
import type { CompareApplicationLayoutBlock } from "@/lib/cms"
5+
import { getMediaUrl } from "@/lib/media"
6+
import { cn } from "@/lib/utils"
7+
import type { Media } from "@/payload-types"
8+
import { IconCheck, IconX } from "@tabler/icons-react"
9+
import Image from "next/image"
10+
11+
interface CompareApplicationSectionProps {
12+
content?: CompareApplicationLayoutBlock | null
13+
}
14+
15+
export function CompareApplicationSection({ content }: CompareApplicationSectionProps) {
16+
const apps = content?.apps?.filter((app) => Boolean(app.name)) ?? []
17+
const buttons = content?.buttons?.filter((button) => Boolean(button.label && button.url)) ?? []
18+
const button = buttons[0]
19+
const showIcon = content?.showIcon !== false
20+
21+
if (!content || apps.length === 0) return null
22+
23+
return (
24+
<Section
25+
heading={content.sectionHeading}
26+
description={content.sectionDescription}
27+
linkButton={content.sectionLinkButton}
28+
funnelType={content.sectionLayout ?? "center"}
29+
animation={{ preset: "none" }}
30+
>
31+
<Card size="lg" variant="light" radialGradient={content.gradient} gradientDirection={content.gradientDirection} className="w-full p-0!">
32+
<div className="relative z-10 overflow-x-auto rounded-[inherit]">
33+
<table className="w-full table-fixed border-separate border-spacing-0" style={{ minWidth: `${Math.max(apps.length, 2) * 15}rem` }}>
34+
<thead>
35+
<tr>
36+
{apps.map((app, index) => {
37+
const logo = typeof app.logo === "object" ? (app.logo as Media) : null
38+
const logoUrl = getMediaUrl(logo?.url)
39+
40+
return (
41+
<th
42+
scope="col"
43+
className={cn("border-b border-white/10 p-5 text-left align-top", index > 0 && "border-l")}
44+
key={app.id ?? `${app.name}-${index}`}
45+
>
46+
<div className="flex items-center gap-3">
47+
<div className="relative flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-white/10 bg-white/5 p-2">
48+
{logoUrl && (
49+
<Image
50+
src={logoUrl}
51+
alt={logo?.alt || app.name}
52+
fill
53+
sizes="44px"
54+
className="object-contain p-2"
55+
/>
56+
)}
57+
</div>
58+
<span className="text-lg font-semibold text-white">{app.name}</span>
59+
</div>
60+
</th>
61+
)
62+
})}
63+
</tr>
64+
</thead>
65+
<tbody>
66+
<tr>
67+
{apps.map((app, index) => {
68+
const features = app.features?.filter((feature) => Boolean(feature.title)) ?? []
69+
70+
return (
71+
<td className={cn("p-5 align-top", index > 0 && "border-l border-white/10")} key={app.id ?? `${app.name}-${index}`}>
72+
<div className="flex h-full flex-col">
73+
<ul className="flex flex-col gap-2">
74+
{features.map((feature, featureIndex) => (
75+
<li
76+
className={cn("flex items-start text-sm", showIcon && "gap-2", feature.exists === false ? "text-tertiary" : "text-white")}
77+
key={feature.id ?? `feature-${featureIndex}`}
78+
>
79+
{showIcon &&
80+
(feature.exists === false ? (
81+
<IconX size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
82+
) : (
83+
<IconCheck size={16} className="mt-0.5 shrink-0 text-brand" aria-hidden="true" />
84+
))}
85+
<span>{feature.title}</span>
86+
</li>
87+
))}
88+
</ul>
89+
90+
{index === 0 && button && (
91+
<div className="mt-auto pt-6">
92+
<HapticButtonLink
93+
href={button.url}
94+
variant={button.variant ?? "normal"}
95+
className={cn("w-full!", button.variant === "filled" && "bg-white/80! text-primary! hover:bg-white!")}
96+
>
97+
{button.label}
98+
</HapticButtonLink>
99+
</div>
100+
)}
101+
</div>
102+
</td>
103+
)
104+
})}
105+
</tr>
106+
</tbody>
107+
</table>
108+
</div>
109+
</Card>
110+
</Section>
111+
)
112+
}

src/components/sections/PricingSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export function PricingSection({ content, locale, packages, paymentPeriod }: Pri
177177
)
178178

179179
return highlighted ? (
180-
<BorderBeam key={pricingPackage.key} size="pulse-inner" colorVariant="colorful" strength={0.7} className="h-full">
180+
<BorderBeam key={pricingPackage.key} size="md" colorVariant="colorful" strength={0.7} className="h-full">
181181
{card}
182182
</BorderBeam>
183183
) : (

src/components/ui/PageBlockRenderer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { SubscriptionConfiguratorSection, type SubscriptionIcons } from "../sect
3030
import { PricingSection } from "../sections/PricingSection"
3131
import { SmallPricingSection } from "../sections/SmallPricingSection"
3232
import { ContactSection } from "../sections/ContactSection"
33+
import { CompareApplicationSection } from "../sections/CompareApplicationSection"
3334
import { getIcon } from "@/components/ui/IconRenderer"
3435
import type { ActionItem, SubscriptionConfigData, SubscriptionConfiguratorBlockData } from "@/lib/cms"
3536

@@ -185,6 +186,7 @@ const pageBlockRenderers: Partial<Record<PageBlock["blockType"], BlockRenderer>>
185186
return <SmallPricingSection content={block as Extract<PageBlock, { blockType: "smallPricing" }>} locale={options.locale ?? "en"} packages={config.packages} />
186187
},
187188
contact: (block, options) => <ContactSection content={block as Extract<PageBlock, { blockType: "contact" }>} locale={options.locale ?? "en"} />,
189+
compareApplication: (block) => <CompareApplicationSection content={block as Extract<PageBlock, { blockType: "compareApplication" }>} />,
188190
}
189191

190192
function renderPageBlock(block: PageBlock, options: PageBlockRenderOptions) {

src/lib/cms.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type StatsLayoutBlock = Extract<PageLayoutBlock, { blockType: "stats" }>
3737
export type FlowExampleLayoutBlock = Extract<PageLayoutBlock, { blockType: "flowExample" }>
3838
export type PricingLayoutBlock = Extract<PageLayoutBlock, { blockType: "pricing" }>
3939
export type SmallPricingLayoutBlock = Extract<PageLayoutBlock, { blockType: "smallPricing" }>
40+
export type CompareApplicationLayoutBlock = Extract<PageLayoutBlock, { blockType: "compareApplication" }>
4041

4142
export type ActionItem = Pick<Action, "id" | "identifier" | "module" | "tags" | "createdAt"> & {
4243
slug: string

0 commit comments

Comments
 (0)