blocks-dusted 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +967 -3
  2. package/package.json +2 -2
  3. package/src/commands/add.js +143 -45
  4. package/src/commands/list.js +2 -2
  5. package/src/installers/checkRequirements.js +1 -0
  6. package/src/installers/copyTemplateFiles.js +23 -2
  7. package/src/installers/patchPayloadConfigCollections.js +92 -0
  8. package/src/installers/writeInstalledBlockReadme.js +17 -4
  9. package/src/registry/listTemplates.js +16 -7
  10. package/src/registry/loadTemplateManifest.js +20 -11
  11. package/src/registry/validateTemplateManifest.js +6 -5
  12. package/src/utils/paths.js +7 -0
  13. package/templates/blocks/DD-BookCall/Component.tsx +535 -573
  14. package/templates/blocks/DD-CardTemplate01/Component.tsx +45 -49
  15. package/templates/blocks/DD-Carousel-A/Component.tsx +249 -266
  16. package/templates/blocks/DD-Carousel-B/Component.tsx +403 -432
  17. package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -272
  18. package/templates/blocks/DD-Contact/Component.tsx +434 -439
  19. package/templates/blocks/DD-Contact/config.ts +8 -3
  20. package/templates/blocks/DD-FeatCat/Component.tsx +302 -361
  21. package/templates/blocks/DD-FeatCat/config.ts +201 -204
  22. package/templates/blocks/DD-FeatStrip/Component.tsx +171 -201
  23. package/templates/blocks/DD-Hero/Component.tsx +297 -317
  24. package/templates/blocks/DD-HorizontalScroll/Component.tsx +244 -303
  25. package/templates/blocks/DD-MasonaryMedia/Component.tsx +202 -228
  26. package/templates/blocks/DD-MasonaryMedia/config.ts +13 -14
  27. package/templates/blocks/DD-Pricing/Component.tsx +372 -380
  28. package/templates/blocks/DD-Process/Component.tsx +149 -155
  29. package/templates/blocks/DD-Section/Component.tsx +266 -327
  30. package/templates/blocks/DD-Services/Component.tsx +176 -188
  31. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -317
  32. package/templates/blocks/DD-Slider-A/Component.tsx +237 -252
  33. package/templates/blocks/DD-StackCards/Component.tsx +137 -160
  34. package/templates/blocks/DD-Team/Component.tsx +247 -265
  35. package/templates/blocks/DD-TechStack/Component.tsx +57 -72
  36. package/templates/blocks/DD-Testimonials/Component.tsx +147 -147
  37. package/templates/blocks/DD-Testimonials/config.ts +66 -66
  38. package/templates/blocks/DD-Work/Component.tsx +315 -328
  39. package/templates/blocks/DD-Work-B/Component.tsx +294 -320
  40. package/templates/collections/Testimonials/README.md +11 -0
  41. package/templates/collections/Testimonials/Testimonials.ts +161 -0
  42. package/templates/collections/Testimonials/manifest.json +55 -0
  43. package/templates/components/RichText/README.md +13 -0
  44. package/templates/components/RichText/converter/componentConverter/blocks.tsx +43 -0
  45. package/templates/components/RichText/converter/componentConverter/types.ts +11 -0
  46. package/templates/components/RichText/converter/index.tsx +18 -0
  47. package/templates/components/RichText/converter/internalLinks.tsx +45 -0
  48. package/templates/components/RichText/converter/textConverter.tsx +27 -0
  49. package/templates/components/RichText/index.tsx +35 -0
  50. package/templates/components/RichText/manifest.json +80 -0
@@ -1,380 +1,372 @@
1
- "use client";
2
-
3
- import React, { useState } from "react";
4
- import Link from "next/link";
5
- import { Check, X } from "lucide-react";
6
-
7
- export type BillingCycle = "monthly" | "annual";
8
-
9
- interface PricingFeature {
10
- id?: string | null;
11
- label?: string | null;
12
- }
13
-
14
- interface PricingTier {
15
- id?: string | null;
16
- name?: string | null;
17
- tagline?: string | null;
18
- monthlyPrice?: number | null;
19
- annualPrice?: number | null;
20
- cta?: string | null;
21
- ctaLink?: string | null;
22
- highlight?: boolean | null;
23
- badge?: string | null;
24
- features?: PricingFeature[] | null;
25
- missing?: PricingFeature[] | null;
26
- }
27
-
28
- interface StudioPricingBlockProps {
29
- eyebrow?: string | null;
30
- heading?: string | null;
31
- description?: string | null;
32
- defaultBilling?: BillingCycle | null;
33
- annualDiscountLabel?: string | null;
34
- monthlyLabel?: string | null;
35
- annualLabel?: string | null;
36
- tiers?: PricingTier[] | null;
37
- guaranteeText?: string | null;
38
- guaranteeHighlight?: string | null;
39
- sectionId?: string | null;
40
- customCSS?: string | null;
41
- [key: string]: unknown;
42
- }
43
-
44
- const DEFAULT_TIERS: PricingTier[] = [
45
- {
46
- id: "free",
47
- name: "Free",
48
- tagline: "Start building today",
49
- monthlyPrice: 0,
50
- annualPrice: 0,
51
- cta: "Get Started Free",
52
- ctaLink: "/account",
53
- highlight: false,
54
- badge: null,
55
- features: [
56
- { label: "Access to all free components" },
57
- { label: "Access to free templates" },
58
- { label: "Community Discord access" },
59
- { label: "MIT license for personal use" },
60
- { label: "Basic documentation" },
61
- { label: "Copy JSX code" },
62
- ],
63
- missing: [
64
- { label: "Pro components & templates" },
65
- { label: "Source files (Figma, PSD)" },
66
- { label: "Payload CMS blocks" },
67
- { label: "Priority support" },
68
- { label: "Commercial license" },
69
- ],
70
- },
71
- {
72
- id: "pro",
73
- name: "Pro",
74
- tagline: "For individual developers",
75
- monthlyPrice: 49,
76
- annualPrice: 39,
77
- cta: "Get Pro Access",
78
- ctaLink: "/account",
79
- highlight: true,
80
- badge: "Most Popular",
81
- features: [
82
- { label: "Everything in Free" },
83
- { label: "120+ premium components" },
84
- { label: "24+ full website templates" },
85
- { label: "Payload CMS v3 blocks" },
86
- { label: "Figma source files" },
87
- { label: "Commercial license (1 project)" },
88
- { label: "Priority email support" },
89
- { label: "Lifetime updates" },
90
- { label: "Early access to new releases" },
91
- ],
92
- missing: [
93
- { label: "Unlimited commercial projects" },
94
- { label: "Team seats" },
95
- { label: "White-label license" },
96
- ],
97
- },
98
- {
99
- id: "team",
100
- name: "Team",
101
- tagline: "For studios & agencies",
102
- monthlyPrice: 129,
103
- annualPrice: 99,
104
- cta: "Get Team Access",
105
- ctaLink: "/account",
106
- highlight: false,
107
- badge: null,
108
- features: [
109
- { label: "Everything in Pro" },
110
- { label: "Unlimited commercial projects" },
111
- { label: "Up to 10 team seats" },
112
- { label: "White-label license" },
113
- { label: "Custom Payload CMS schemas" },
114
- { label: "Dedicated Slack channel" },
115
- { label: "1-on-1 onboarding call" },
116
- { label: "Invoice & billing management" },
117
- { label: "SLA support guarantee" },
118
- ],
119
- missing: [],
120
- },
121
- ];
122
-
123
- export function DDPricing({
124
- eyebrow,
125
- heading,
126
- description,
127
- defaultBilling = "monthly",
128
- annualDiscountLabel = "-20%",
129
- monthlyLabel = "Pay Monthly",
130
- annualLabel = "Pay Annually",
131
- tiers,
132
- guaranteeText = "All paid plans include a",
133
- guaranteeHighlight = "14-day money-back guarantee",
134
- sectionId,
135
- }: StudioPricingBlockProps) {
136
- const [billing, setBilling] = useState<BillingCycle>(
137
- defaultBilling === "annual" ? "annual" : "monthly",
138
- );
139
- const pricingTiers = tiers && tiers.length > 0 ? tiers : DEFAULT_TIERS;
140
-
141
- return (
142
- <section
143
- id={sectionId ?? undefined}
144
- className="studio-pricing-block px-6 pb-20 pt-32 text-stone-700 dark:text-stone-50"
145
- >
146
- <div className="mx-auto max-w-3xl text-center">
147
- {eyebrow && (
148
- <p className="mb-4 text-xs font-semibold uppercase tracking-[0.28em] text-stone-700 dark:text-stone-500">
149
- {eyebrow}
150
- </p>
151
- )}
152
-
153
- {heading && (
154
- <h2 className="mx-auto mb-4 max-w-3xl text-4xl font-black tracking-tight text-stone-700 dark:text-stone-50 md:text-5xl">
155
- {heading}
156
- </h2>
157
- )}
158
-
159
- {description && (
160
- <p className="mx-auto mb-10 max-w-2xl text-sm leading-relaxed text-stone-500 md:text-base">
161
- {description}
162
- </p>
163
- )}
164
-
165
- <div className="inline-flex items-center gap-1 rounded-xl border border-stone-100 dark:border-stone-800 bg-stone-200 dark:bg-stone-900 p-1">
166
- <button
167
- type="button"
168
- onClick={() => setBilling("monthly")}
169
- className={`cursor-pointer whitespace-nowrap rounded-lg px-5 py-2 text-sm font-medium transition-all ${
170
- billing === "monthly"
171
- ? "bg-stone-50 text-stone-900"
172
- : "text-stone-500 "
173
- }`}
174
- >
175
- {monthlyLabel}
176
- </button>
177
-
178
- <button
179
- type="button"
180
- onClick={() => setBilling("annual")}
181
- className={`flex cursor-pointer items-center gap-2 whitespace-nowrap rounded-lg px-5 py-2 text-sm font-medium transition-all ${
182
- billing === "annual"
183
- ? "bg-stone-50 text-stone-900"
184
- : "text-stone-500 hover:text-stone-700"
185
- }`}
186
- >
187
- {annualLabel}
188
- {annualDiscountLabel && (
189
- <span
190
- className={`rounded-full px-1.5 py-0.5 text-[10px] font-bold ${
191
- billing === "annual"
192
- ? "bg-lime-500 text-stone-50"
193
- : "bg-lime-900/60 text-lime-400"
194
- }`}
195
- >
196
- {annualDiscountLabel}
197
- </span>
198
- )}
199
- </button>
200
- </div>
201
- </div>
202
-
203
- <div className="mx-auto mt-16 max-w-6xl">
204
- <div className="grid grid-cols-1 items-start gap-5 md:grid-cols-3">
205
- {pricingTiers.map((tier, index) => {
206
- const isHighlighted = tier.highlight === true;
207
- const monthlyPrice =
208
- typeof tier.monthlyPrice === "number" ? tier.monthlyPrice : 0;
209
- const annualPrice =
210
- typeof tier.annualPrice === "number"
211
- ? tier.annualPrice
212
- : monthlyPrice;
213
- const price = billing === "annual" ? annualPrice : monthlyPrice;
214
- const tierId = tier.id ?? tier.name ?? String(index);
215
-
216
- return (
217
- <div
218
- key={tierId}
219
- className={`relative flex flex-col rounded-2xl border p-7 ${
220
- isHighlighted
221
- ? "dark:border-white dark:bg-stone-50/50"
222
- : "border-stone-200 dark:border-stone-800 bg-stone-50/50 dark:bg-stone-900/50"
223
- }`}
224
- style={{ cornerShape: "squircle" } as React.CSSProperties}
225
- >
226
- {tier.badge && (
227
- <div className="absolute -top-3 left-1/2 -translate-x-1/2">
228
- <span className="whitespace-nowrap rounded-full border border-indigo-400 bg-indigo-900/90 px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-stone-300 shadow-md">
229
- {tier.badge}
230
- </span>
231
- </div>
232
- )}
233
-
234
- <div className="mb-6">
235
- {tier.name && (
236
- <p className="mb-1 text-base font-semibold uppercase tracking-widest text-stone-500">
237
- {tier.name}
238
- </p>
239
- )}
240
-
241
- {tier.tagline && (
242
- <p className="mb-5 text-[11.5px] text-stone-500">
243
- {tier.tagline}
244
- </p>
245
- )}
246
-
247
- <div className="flex items-end gap-1">
248
- <span
249
- className={`text-5xl text-stone-900 dark:text-stone-50 font-bold tracking-tight ${
250
- isHighlighted
251
- ? "text-stone-900 dark:text-stone-50"
252
- : "text-stone-50 dark:text-stone-50"
253
- }`}
254
- >
255
- {price === 0 ? "Free" : `$${price}`}
256
- </span>
257
-
258
- {price > 0 && (
259
- <span className="mb-2 text-sm text-stone-500">
260
- /{billing === "annual" ? "mo, billed annually" : "mo"}
261
- </span>
262
- )}
263
- </div>
264
-
265
- {billing === "annual" && price > 0 && (
266
- <p
267
- className={`mt-1 text-xs ${isHighlighted ? "text-stone-800 dark:text-stone-500" : "text-stone-800 dark:text-stone-600"}`}
268
- >
269
- ${monthlyPrice}/mo if billed monthly
270
- </p>
271
- )}
272
- </div>
273
-
274
- <Link
275
- href={tier.ctaLink ?? "#"}
276
- className={`mb-7 flex w-full cursor-pointer items-center justify-center rounded-xl py-3 text-sm font-semibold transition-all hover:shadow-2xl ${
277
- isHighlighted
278
- ? "bg-indigo-900 text-stone-50 hover:bg-indigo-800"
279
- : tierId === "free"
280
- ? "border border-indigo-700 bg-indigo-800 text-stone-200 hover:bg-indigo-700"
281
- : "bg-indigo-50 text-indigo-900 hover:bg-indigo-100"
282
- }`}
283
- style={{ cornerShape: "squircle" } as React.CSSProperties}
284
- >
285
- {tier.cta ?? "Get Started"}
286
- </Link>
287
-
288
- <div
289
- className={`mb-6 h-px ${isHighlighted ? "bg-stone-800 dark:bg-stone-200" : "bg-stone-200 dark:bg-stone-800"}`}
290
- />
291
-
292
- <ul className="flex-1 space-y-3">
293
- {(tier.features ?? []).map((feature, featureIndex) => {
294
- const label = feature.label;
295
- if (!label) return null;
296
-
297
- return (
298
- <li
299
- key={feature.id ?? `${label}-${featureIndex}`}
300
- className="flex items-start gap-2.5"
301
- >
302
- <div
303
- className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full ${
304
- isHighlighted
305
- ? "bg-stone-800 dark:bg-stone-200"
306
- : "bg-stone-200 dark:bg-stone-800"
307
- }`}
308
- >
309
- <Check
310
- className={`h-2.5 w-2.5 ${
311
- isHighlighted
312
- ? "text-stone-700 dark:text-stone-200"
313
- : "text-stone-800 dark:text-stone-300"
314
- }`}
315
- />
316
- </div>
317
- <span
318
- className={`text-sm leading-snug ${
319
- isHighlighted
320
- ? "text-stone-700 dark:text-stone-200"
321
- : "text-stone-800 dark:text-stone-300"
322
- }`}
323
- >
324
- {label}
325
- </span>
326
- </li>
327
- );
328
- })}
329
-
330
- {(tier.missing ?? []).map((feature, featureIndex) => {
331
- const label = feature.label;
332
- if (!label) return null;
333
-
334
- return (
335
- <li
336
- key={feature.id ?? `${label}-${featureIndex}`}
337
- className="flex items-start gap-2.5 opacity-40"
338
- >
339
- <div
340
- className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full ${
341
- isHighlighted
342
- ? "bg-stone-800 dark:bg-stone-200"
343
- : "bg-stone-200 dark:bg-stone-800"
344
- }`}
345
- >
346
- <X className="h-2.5 w-2.5 text-stone-700 dark:text-stone-400" />
347
- </div>
348
- <span
349
- className={`text-sm leading-snug ${
350
- isHighlighted
351
- ? "text-stone-700 dark:text-stone-200"
352
- : "text-stone-800 dark:text-stone-300"
353
- }`}
354
- >
355
- {label}
356
- </span>
357
- </li>
358
- );
359
- })}
360
- </ul>
361
- </div>
362
- );
363
- })}
364
- </div>
365
-
366
- {(guaranteeText || guaranteeHighlight) && (
367
- <p className="mt-16 text-center text-xs text-stone-500 dark:text-stone-600">
368
- {guaranteeText}{" "}
369
- {guaranteeHighlight && (
370
- <strong className="text-stone-500">{guaranteeHighlight}</strong>
371
- )}
372
- . No questions asked.
373
- </p>
374
- )}
375
- </div>
376
- </section>
377
- );
378
- }
379
-
380
- export default DDPricing;
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { Check, X } from 'lucide-react'
6
+
7
+ export type BillingCycle = 'monthly' | 'annual'
8
+
9
+ interface PricingFeature {
10
+ id?: string | null
11
+ label?: string | null
12
+ }
13
+
14
+ interface PricingTier {
15
+ id?: string | null
16
+ name?: string | null
17
+ tagline?: string | null
18
+ monthlyPrice?: number | null
19
+ annualPrice?: number | null
20
+ cta?: string | null
21
+ ctaLink?: string | null
22
+ highlight?: boolean | null
23
+ badge?: string | null
24
+ features?: PricingFeature[] | null
25
+ missing?: PricingFeature[] | null
26
+ }
27
+
28
+ interface StudioPricingBlockProps {
29
+ eyebrow?: string | null
30
+ heading?: string | null
31
+ description?: string | null
32
+ defaultBilling?: BillingCycle | null
33
+ annualDiscountLabel?: string | null
34
+ monthlyLabel?: string | null
35
+ annualLabel?: string | null
36
+ tiers?: PricingTier[] | null
37
+ guaranteeText?: string | null
38
+ guaranteeHighlight?: string | null
39
+ sectionId?: string | null
40
+ customCSS?: string | null
41
+ [key: string]: unknown
42
+ }
43
+
44
+ const DEFAULT_TIERS: PricingTier[] = [
45
+ {
46
+ id: 'free',
47
+ name: 'Free',
48
+ tagline: 'Start building today',
49
+ monthlyPrice: 0,
50
+ annualPrice: 0,
51
+ cta: 'Get Started Free',
52
+ ctaLink: '/account',
53
+ highlight: false,
54
+ badge: null,
55
+ features: [
56
+ { label: 'Access to all free components' },
57
+ { label: 'Access to free templates' },
58
+ { label: 'Community Discord access' },
59
+ { label: 'MIT license for personal use' },
60
+ { label: 'Basic documentation' },
61
+ { label: 'Copy JSX code' },
62
+ ],
63
+ missing: [
64
+ { label: 'Pro components & templates' },
65
+ { label: 'Source files (Figma, PSD)' },
66
+ { label: 'Payload CMS blocks' },
67
+ { label: 'Priority support' },
68
+ { label: 'Commercial license' },
69
+ ],
70
+ },
71
+ {
72
+ id: 'pro',
73
+ name: 'Pro',
74
+ tagline: 'For individual developers',
75
+ monthlyPrice: 49,
76
+ annualPrice: 39,
77
+ cta: 'Get Pro Access',
78
+ ctaLink: '/account',
79
+ highlight: true,
80
+ badge: 'Most Popular',
81
+ features: [
82
+ { label: 'Everything in Free' },
83
+ { label: '120+ premium components' },
84
+ { label: '24+ full website templates' },
85
+ { label: 'Payload CMS v3 blocks' },
86
+ { label: 'Figma source files' },
87
+ { label: 'Commercial license (1 project)' },
88
+ { label: 'Priority email support' },
89
+ { label: 'Lifetime updates' },
90
+ { label: 'Early access to new releases' },
91
+ ],
92
+ missing: [
93
+ { label: 'Unlimited commercial projects' },
94
+ { label: 'Team seats' },
95
+ { label: 'White-label license' },
96
+ ],
97
+ },
98
+ {
99
+ id: 'team',
100
+ name: 'Team',
101
+ tagline: 'For studios & agencies',
102
+ monthlyPrice: 129,
103
+ annualPrice: 99,
104
+ cta: 'Get Team Access',
105
+ ctaLink: '/account',
106
+ highlight: false,
107
+ badge: null,
108
+ features: [
109
+ { label: 'Everything in Pro' },
110
+ { label: 'Unlimited commercial projects' },
111
+ { label: 'Up to 10 team seats' },
112
+ { label: 'White-label license' },
113
+ { label: 'Custom Payload CMS schemas' },
114
+ { label: 'Dedicated Slack channel' },
115
+ { label: '1-on-1 onboarding call' },
116
+ { label: 'Invoice & billing management' },
117
+ { label: 'SLA support guarantee' },
118
+ ],
119
+ missing: [],
120
+ },
121
+ ]
122
+
123
+ export function DDPricing({
124
+ eyebrow,
125
+ heading,
126
+ description,
127
+ defaultBilling = 'monthly',
128
+ annualDiscountLabel = '-20%',
129
+ monthlyLabel = 'Pay Monthly',
130
+ annualLabel = 'Pay Annually',
131
+ tiers,
132
+ guaranteeText = 'All paid plans include a',
133
+ guaranteeHighlight = '14-day money-back guarantee',
134
+ sectionId,
135
+ }: StudioPricingBlockProps) {
136
+ const [billing, setBilling] = useState<BillingCycle>(
137
+ defaultBilling === 'annual' ? 'annual' : 'monthly',
138
+ )
139
+ const pricingTiers = tiers && tiers.length > 0 ? tiers : DEFAULT_TIERS
140
+
141
+ return (
142
+ <section
143
+ id={sectionId ?? undefined}
144
+ className="studio-pricing-block px-6 pb-20 pt-32 text-stone-700 dark:text-stone-50"
145
+ >
146
+ <div className="mx-auto max-w-3xl text-center">
147
+ #BlockName-DDPricing
148
+ {eyebrow && (
149
+ <p className="mb-4 text-xs font-semibold uppercase tracking-[0.28em] text-stone-700 dark:text-stone-500">
150
+ {eyebrow}
151
+ </p>
152
+ )}
153
+
154
+ {heading && (
155
+ <h2 className="mx-auto mb-4 max-w-3xl text-4xl font-black tracking-tight text-stone-700 dark:text-stone-50 md:text-5xl">
156
+ {heading}
157
+ </h2>
158
+ )}
159
+
160
+ {description && (
161
+ <p className="mx-auto mb-10 max-w-2xl text-sm leading-relaxed text-stone-500 md:text-base">
162
+ {description}
163
+ </p>
164
+ )}
165
+
166
+ <div className="inline-flex items-center gap-1 rounded-xl border border-stone-100 dark:border-stone-800 bg-stone-200 dark:bg-stone-900 p-1">
167
+ <button
168
+ type="button"
169
+ onClick={() => setBilling('monthly')}
170
+ className={`cursor-pointer whitespace-nowrap rounded-lg px-5 py-2 text-sm font-medium transition-all ${
171
+ billing === 'monthly' ? 'bg-stone-50 text-stone-900' : 'text-stone-500 '
172
+ }`}
173
+ >
174
+ {monthlyLabel}
175
+ </button>
176
+
177
+ <button
178
+ type="button"
179
+ onClick={() => setBilling('annual')}
180
+ className={`flex cursor-pointer items-center gap-2 whitespace-nowrap rounded-lg px-5 py-2 text-sm font-medium transition-all ${
181
+ billing === 'annual'
182
+ ? 'bg-stone-50 text-stone-900'
183
+ : 'text-stone-500 hover:text-stone-700'
184
+ }`}
185
+ >
186
+ {annualLabel}
187
+ {annualDiscountLabel && (
188
+ <span
189
+ className={`rounded-full px-1.5 py-0.5 text-[10px] font-bold ${
190
+ billing === 'annual'
191
+ ? 'bg-lime-500 text-stone-50'
192
+ : 'bg-lime-900/60 text-lime-400'
193
+ }`}
194
+ >
195
+ {annualDiscountLabel}
196
+ </span>
197
+ )}
198
+ </button>
199
+ </div>
200
+ </div>
201
+
202
+ <div className="mx-auto mt-16 max-w-6xl">
203
+ <div className="grid grid-cols-1 items-start gap-5 md:grid-cols-3">
204
+ {pricingTiers.map((tier, index) => {
205
+ const isHighlighted = tier.highlight === true
206
+ const monthlyPrice = typeof tier.monthlyPrice === 'number' ? tier.monthlyPrice : 0
207
+ const annualPrice =
208
+ typeof tier.annualPrice === 'number' ? tier.annualPrice : monthlyPrice
209
+ const price = billing === 'annual' ? annualPrice : monthlyPrice
210
+ const tierId = tier.id ?? tier.name ?? String(index)
211
+
212
+ return (
213
+ <div
214
+ key={tierId}
215
+ className={`relative flex flex-col rounded-2xl border p-7 ${
216
+ isHighlighted
217
+ ? 'dark:border-white dark:bg-stone-50/50'
218
+ : 'border-stone-200 dark:border-stone-800 bg-stone-50/50 dark:bg-stone-900/50'
219
+ }`}
220
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
221
+ >
222
+ {tier.badge && (
223
+ <div className="absolute -top-3 left-1/2 -translate-x-1/2">
224
+ <span className="whitespace-nowrap rounded-full border border-indigo-400 bg-indigo-900/90 px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-stone-300 shadow-md">
225
+ {tier.badge}
226
+ </span>
227
+ </div>
228
+ )}
229
+
230
+ <div className="mb-6">
231
+ {tier.name && (
232
+ <p className="mb-1 text-base font-semibold uppercase tracking-widest text-stone-500">
233
+ {tier.name}
234
+ </p>
235
+ )}
236
+
237
+ {tier.tagline && (
238
+ <p className="mb-5 text-[11.5px] text-stone-500">{tier.tagline}</p>
239
+ )}
240
+
241
+ <div className="flex items-end gap-1">
242
+ <span
243
+ className={`text-5xl text-stone-900 dark:text-stone-50 font-bold tracking-tight ${
244
+ isHighlighted
245
+ ? 'text-stone-900 dark:text-stone-50'
246
+ : 'text-stone-50 dark:text-stone-50'
247
+ }`}
248
+ >
249
+ {price === 0 ? 'Free' : `$${price}`}
250
+ </span>
251
+
252
+ {price > 0 && (
253
+ <span className="mb-2 text-sm text-stone-500">
254
+ /{billing === 'annual' ? 'mo, billed annually' : 'mo'}
255
+ </span>
256
+ )}
257
+ </div>
258
+
259
+ {billing === 'annual' && price > 0 && (
260
+ <p
261
+ className={`mt-1 text-xs ${isHighlighted ? 'text-stone-800 dark:text-stone-500' : 'text-stone-800 dark:text-stone-600'}`}
262
+ >
263
+ ${monthlyPrice}/mo if billed monthly
264
+ </p>
265
+ )}
266
+ </div>
267
+
268
+ <Link
269
+ href={tier.ctaLink ?? '#'}
270
+ className={`mb-7 flex w-full cursor-pointer items-center justify-center rounded-xl py-3 text-sm font-semibold transition-all hover:shadow-2xl ${
271
+ isHighlighted
272
+ ? 'bg-indigo-900 text-stone-50 hover:bg-indigo-800'
273
+ : tierId === 'free'
274
+ ? 'border border-indigo-700 bg-indigo-800 text-stone-200 hover:bg-indigo-700'
275
+ : 'bg-indigo-50 text-indigo-900 hover:bg-indigo-100'
276
+ }`}
277
+ style={{ cornerShape: 'squircle' } as React.CSSProperties}
278
+ >
279
+ {tier.cta ?? 'Get Started'}
280
+ </Link>
281
+
282
+ <div
283
+ className={`mb-6 h-px ${isHighlighted ? 'bg-stone-800 dark:bg-stone-200' : 'bg-stone-200 dark:bg-stone-800'}`}
284
+ />
285
+
286
+ <ul className="flex-1 space-y-3">
287
+ {(tier.features ?? []).map((feature, featureIndex) => {
288
+ const label = feature.label
289
+ if (!label) return null
290
+
291
+ return (
292
+ <li
293
+ key={feature.id ?? `${label}-${featureIndex}`}
294
+ className="flex items-start gap-2.5"
295
+ >
296
+ <div
297
+ className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full ${
298
+ isHighlighted
299
+ ? 'bg-stone-800 dark:bg-stone-200'
300
+ : 'bg-stone-200 dark:bg-stone-800'
301
+ }`}
302
+ >
303
+ <Check
304
+ className={`h-2.5 w-2.5 ${
305
+ isHighlighted
306
+ ? 'text-stone-700 dark:text-stone-200'
307
+ : 'text-stone-800 dark:text-stone-300'
308
+ }`}
309
+ />
310
+ </div>
311
+ <span
312
+ className={`text-sm leading-snug ${
313
+ isHighlighted
314
+ ? 'text-stone-700 dark:text-stone-200'
315
+ : 'text-stone-800 dark:text-stone-300'
316
+ }`}
317
+ >
318
+ {label}
319
+ </span>
320
+ </li>
321
+ )
322
+ })}
323
+
324
+ {(tier.missing ?? []).map((feature, featureIndex) => {
325
+ const label = feature.label
326
+ if (!label) return null
327
+
328
+ return (
329
+ <li
330
+ key={feature.id ?? `${label}-${featureIndex}`}
331
+ className="flex items-start gap-2.5 opacity-40"
332
+ >
333
+ <div
334
+ className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full ${
335
+ isHighlighted
336
+ ? 'bg-stone-800 dark:bg-stone-200'
337
+ : 'bg-stone-200 dark:bg-stone-800'
338
+ }`}
339
+ >
340
+ <X className="h-2.5 w-2.5 text-stone-700 dark:text-stone-400" />
341
+ </div>
342
+ <span
343
+ className={`text-sm leading-snug ${
344
+ isHighlighted
345
+ ? 'text-stone-700 dark:text-stone-200'
346
+ : 'text-stone-800 dark:text-stone-300'
347
+ }`}
348
+ >
349
+ {label}
350
+ </span>
351
+ </li>
352
+ )
353
+ })}
354
+ </ul>
355
+ </div>
356
+ )
357
+ })}
358
+ </div>
359
+
360
+ {(guaranteeText || guaranteeHighlight) && (
361
+ <p className="mt-16 text-center text-xs text-stone-500 dark:text-stone-600">
362
+ {guaranteeText}{' '}
363
+ {guaranteeHighlight && <strong className="text-stone-500">{guaranteeHighlight}</strong>}
364
+ . No questions asked.
365
+ </p>
366
+ )}
367
+ </div>
368
+ </section>
369
+ )
370
+ }
371
+
372
+ export default DDPricing