blocks-dusted 0.1.11 → 0.1.12
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.
- package/package.json +5 -2
- package/src/cli.js +39 -3
- package/src/commands/shop.js +59 -0
- package/src/commands/warehouse.js +73 -0
- package/templates/blocks/DD-BookCall/Component.tsx +535 -534
- package/templates/blocks/DD-CardTemplate01/Component.tsx +140 -139
- package/templates/blocks/DD-Carousel-A/Component.tsx +1 -1
- package/templates/blocks/DD-Carousel-B/Component.tsx +1 -1
- package/templates/blocks/DD-Chip/Component.tsx +14 -14
- package/templates/blocks/DD-ComparisonTable/Component.tsx +1 -1
- package/templates/blocks/DD-Contact/Component.tsx +8 -7
- package/templates/blocks/DD-Counter/Component.tsx +10 -10
- package/templates/blocks/DD-FeatCat/Component.tsx +1 -1
- package/templates/blocks/DD-FeatStrip/Component.tsx +1 -1
- package/templates/blocks/DD-Hero/Component.tsx +297 -320
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +936 -938
- package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +8 -10
- package/templates/blocks/DD-Pricing/Component.tsx +369 -372
- package/templates/blocks/DD-Process/Component.tsx +147 -149
- package/templates/blocks/DD-Progress/Component.tsx +156 -141
- package/templates/blocks/DD-ProjSlider/config.ts +2 -2
- package/templates/blocks/DD-Section/Component.tsx +1 -1
- package/templates/blocks/DD-Services/Component.tsx +1 -1
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +13 -13
- package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
- package/templates/blocks/DD-StackCards/Component.tsx +473 -473
- package/templates/blocks/DD-Team/Component.tsx +1 -1
- package/templates/blocks/DD-TechStack/Component.tsx +22 -28
- package/templates/blocks/DD-Testimonials/Component.tsx +35 -43
- package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
- package/templates/blocks/DD-Thanks/Component.tsx +501 -500
- package/templates/blocks/DD-Work/Component.tsx +4 -4
- package/templates/blocks/DD-Work-B/Component.tsx +1 -1
- package/templates/blocks/DDHG-CTASection2/Component.tsx +6 -6
- package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -243
- package/templates/blocks/DDHG-LogoMarquee/Component.tsx +143 -93
- package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
- package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +227 -213
- package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
- package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
- package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +24 -28
- package/templates/blocks/DDHG-ServiceHero/Component.tsx +1 -1
- package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
- package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
- package/templates/blocks/DDHG-TeamCards/Component.tsx +169 -131
|
@@ -1,500 +1,501 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
-
import Link from 'next/link'
|
|
5
|
-
import {
|
|
6
|
-
Check,
|
|
7
|
-
DownloadCloud,
|
|
8
|
-
Home,
|
|
9
|
-
LayoutGrid,
|
|
10
|
-
MailCheck,
|
|
11
|
-
ShieldCheck,
|
|
12
|
-
Sparkles
|
|
13
|
-
} from 'lucide-react'
|
|
14
|
-
import { cn } from '@/utilities/ui'
|
|
15
|
-
|
|
16
|
-
type InfoPill = {
|
|
17
|
-
id?: string | null
|
|
18
|
-
label?: string | null
|
|
19
|
-
icon?: 'mail' | 'download' | 'shield' | null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type CtaLink = {
|
|
23
|
-
label?: string | null
|
|
24
|
-
href?: string | null
|
|
25
|
-
variant?: 'primary' | 'secondary' | null
|
|
26
|
-
icon?: 'home' | 'grid' | 'download' | null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type ThanksConfettiProps = {
|
|
30
|
-
sectionId?: string | null
|
|
31
|
-
customCSS?: string | null
|
|
32
|
-
headline?: string | null
|
|
33
|
-
description?: string | null
|
|
34
|
-
subDescription?: string | null
|
|
35
|
-
replayLabel?: string | null
|
|
36
|
-
brandLabel?: string | null
|
|
37
|
-
showBrandLabel?: boolean | null
|
|
38
|
-
showReplayButton?: boolean | null
|
|
39
|
-
autoPlayConfetti?: boolean | null
|
|
40
|
-
confettiDuration?: number | null
|
|
41
|
-
infoPills?: InfoPill[] | null
|
|
42
|
-
ctaLinks?: CtaLink[] | null
|
|
43
|
-
className?: string
|
|
44
|
-
disableInnerContainer?: boolean
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type ConfettiParticle = {
|
|
48
|
-
x: number
|
|
49
|
-
y: number
|
|
50
|
-
vx: number
|
|
51
|
-
vy: number
|
|
52
|
-
size: number
|
|
53
|
-
rotation: number
|
|
54
|
-
rotationSpeed: number
|
|
55
|
-
color: string
|
|
56
|
-
shape: 'circle' | 'square'
|
|
57
|
-
life: number
|
|
58
|
-
maxLife: number
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const CONFETTI_COLORS = ['#d6d3d1', '#a8a29e', '#10b981', '#6ee7b7', '#ffffff', '#f5f5f4']
|
|
62
|
-
|
|
63
|
-
const defaultInfoPills: Required<Pick<InfoPill, 'label' | 'icon'>>[] = [
|
|
64
|
-
{ icon: 'mail', label: 'Confirmation sent' },
|
|
65
|
-
{ icon: 'download', label: 'Files ready' },
|
|
66
|
-
{ icon: 'shield', label: 'License activated' }
|
|
67
|
-
]
|
|
68
|
-
|
|
69
|
-
const defaultCtaLinks: Required<Pick<CtaLink, 'label' | 'href' | 'variant' | 'icon'>>[] = [
|
|
70
|
-
{ label: 'Back to Home', href: '/', variant: 'primary', icon: 'home' },
|
|
71
|
-
{ label: 'Continue Browsing', href: '/components', variant: 'secondary', icon: 'grid' }
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
function PillIcon({ icon }: { icon?: InfoPill['icon'] }) {
|
|
75
|
-
if (icon === 'download') return <DownloadCloud className="h-3.5 w-3.5" />
|
|
76
|
-
if (icon === 'shield') return <ShieldCheck className="h-3.5 w-3.5" />
|
|
77
|
-
|
|
78
|
-
return <MailCheck className="h-3.5 w-3.5" />
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function CtaIcon({ icon }: { icon?: CtaLink['icon'] }) {
|
|
82
|
-
if (icon === 'grid') return <LayoutGrid className="h-4 w-4" />
|
|
83
|
-
if (icon === 'download') return <DownloadCloud className="h-4 w-4" />
|
|
84
|
-
|
|
85
|
-
return <Home className="h-4 w-4" />
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function getSafeDuration(duration?: number | null) {
|
|
89
|
-
if (typeof duration !== 'number' || !Number.isFinite(duration)) return 3000
|
|
90
|
-
return Math.min(Math.max(duration, 1000), 8000)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function createParticles({
|
|
94
|
-
amount,
|
|
95
|
-
originX,
|
|
96
|
-
originY,
|
|
97
|
-
angle,
|
|
98
|
-
spread,
|
|
99
|
-
velocity,
|
|
100
|
-
width,
|
|
101
|
-
height
|
|
102
|
-
}: {
|
|
103
|
-
amount: number
|
|
104
|
-
originX: number
|
|
105
|
-
originY: number
|
|
106
|
-
angle?: number
|
|
107
|
-
spread: number
|
|
108
|
-
velocity: number
|
|
109
|
-
width: number
|
|
110
|
-
height: number
|
|
111
|
-
}): ConfettiParticle[] {
|
|
112
|
-
return Array.from({ length: amount }, () => {
|
|
113
|
-
const baseAngle = typeof angle === 'number' ? angle : Math.random() * 360
|
|
114
|
-
const randomAngle = (baseAngle - spread / 2 + Math.random() * spread) * (Math.PI / 180)
|
|
115
|
-
const speed = velocity * (0.45 + Math.random() * 0.75)
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
x: originX * width,
|
|
119
|
-
y: originY * height,
|
|
120
|
-
vx: Math.cos(randomAngle) * speed,
|
|
121
|
-
vy: Math.sin(randomAngle) * speed,
|
|
122
|
-
size: 4 + Math.random() * 7,
|
|
123
|
-
rotation: Math.random() * Math.PI,
|
|
124
|
-
rotationSpeed: -0.22 + Math.random() * 0.44,
|
|
125
|
-
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)] ?? '#ffffff',
|
|
126
|
-
shape: Math.random() > 0.5 ? 'circle' : 'square',
|
|
127
|
-
life: 0,
|
|
128
|
-
maxLife: 90 + Math.random() * 70
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function DDThanks({
|
|
134
|
-
sectionId,
|
|
135
|
-
headline = 'Thank you! 🎉',
|
|
136
|
-
description = 'Your order has been confirmed and is on its way.',
|
|
137
|
-
subDescription = "We've sent a confirmation email with your download links and license keys. Check your inbox — and maybe your spam folder too.",
|
|
138
|
-
replayLabel = 'Replay confetti',
|
|
139
|
-
brandLabel = 'DesignsDusted',
|
|
140
|
-
showBrandLabel = true,
|
|
141
|
-
showReplayButton = true,
|
|
142
|
-
autoPlayConfetti = true,
|
|
143
|
-
confettiDuration = 3000,
|
|
144
|
-
infoPills,
|
|
145
|
-
ctaLinks,
|
|
146
|
-
className
|
|
147
|
-
}: ThanksConfettiProps) {
|
|
148
|
-
const hasRun = useRef(false)
|
|
149
|
-
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
|
150
|
-
const animationRef = useRef<number | null>(null)
|
|
151
|
-
const particlesRef = useRef<ConfettiParticle[]>([])
|
|
152
|
-
const [visible, setVisible] = useState(false)
|
|
153
|
-
|
|
154
|
-
const pills = useMemo(() => {
|
|
155
|
-
const source = infoPills?.length ? infoPills : defaultInfoPills
|
|
156
|
-
return source.filter((pill) => pill.label)
|
|
157
|
-
}, [infoPills])
|
|
158
|
-
|
|
159
|
-
const links = useMemo(() => {
|
|
160
|
-
const source = ctaLinks?.length ? ctaLinks : defaultCtaLinks
|
|
161
|
-
return source.filter((link) => link.label && link.href)
|
|
162
|
-
}, [ctaLinks])
|
|
163
|
-
|
|
164
|
-
const stopAnimation = useCallback(() => {
|
|
165
|
-
if (animationRef.current) {
|
|
166
|
-
cancelAnimationFrame(animationRef.current)
|
|
167
|
-
animationRef.current = null
|
|
168
|
-
}
|
|
169
|
-
}, [])
|
|
170
|
-
|
|
171
|
-
const fireConfetti = useCallback(() => {
|
|
172
|
-
const canvas = canvasRef.current
|
|
173
|
-
const ctx = canvas?.getContext('2d')
|
|
174
|
-
if (!canvas || !ctx) return
|
|
175
|
-
|
|
176
|
-
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
177
|
-
if (reducedMotion) return
|
|
178
|
-
|
|
179
|
-
const pixelRatio = Math.min(window.devicePixelRatio || 1, 2)
|
|
180
|
-
const width = window.innerWidth
|
|
181
|
-
const height = window.innerHeight
|
|
182
|
-
|
|
183
|
-
canvas.width = width * pixelRatio
|
|
184
|
-
canvas.height = height * pixelRatio
|
|
185
|
-
canvas.style.width = `${width}px`
|
|
186
|
-
canvas.style.height = `${height}px`
|
|
187
|
-
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0)
|
|
188
|
-
|
|
189
|
-
const duration = getSafeDuration(confettiDuration)
|
|
190
|
-
const end = performance.now() + duration
|
|
191
|
-
|
|
192
|
-
particlesRef.current = [
|
|
193
|
-
...particlesRef.current,
|
|
194
|
-
...createParticles({
|
|
195
|
-
amount: 120,
|
|
196
|
-
originX: 0.5,
|
|
197
|
-
originY: 0.5,
|
|
198
|
-
spread: 100,
|
|
199
|
-
velocity: 20,
|
|
200
|
-
width,
|
|
201
|
-
height
|
|
202
|
-
}),
|
|
203
|
-
...createParticles({
|
|
204
|
-
amount: 60,
|
|
205
|
-
originX: 0,
|
|
206
|
-
originY: 0.6,
|
|
207
|
-
angle: -35,
|
|
208
|
-
spread: 70,
|
|
209
|
-
velocity: 18,
|
|
210
|
-
width,
|
|
211
|
-
height
|
|
212
|
-
}),
|
|
213
|
-
...createParticles({
|
|
214
|
-
amount: 60,
|
|
215
|
-
originX: 1,
|
|
216
|
-
originY: 0.6,
|
|
217
|
-
angle: 215,
|
|
218
|
-
spread: 70,
|
|
219
|
-
velocity: 18,
|
|
220
|
-
width,
|
|
221
|
-
height
|
|
222
|
-
})
|
|
223
|
-
]
|
|
224
|
-
|
|
225
|
-
stopAnimation()
|
|
226
|
-
|
|
227
|
-
const frame = (now: number) => {
|
|
228
|
-
ctx.clearRect(0, 0, width, height)
|
|
229
|
-
|
|
230
|
-
if (now < end && Math.random() > 0.35) {
|
|
231
|
-
particlesRef.current.push(
|
|
232
|
-
...createParticles({
|
|
233
|
-
amount: 2,
|
|
234
|
-
originX: 0,
|
|
235
|
-
originY: 0.3,
|
|
236
|
-
angle: -25,
|
|
237
|
-
spread: 55,
|
|
238
|
-
velocity: 10,
|
|
239
|
-
width,
|
|
240
|
-
height
|
|
241
|
-
}),
|
|
242
|
-
...createParticles({
|
|
243
|
-
amount: 2,
|
|
244
|
-
originX: 1,
|
|
245
|
-
originY: 0.3,
|
|
246
|
-
angle: 205,
|
|
247
|
-
spread: 55,
|
|
248
|
-
velocity: 10,
|
|
249
|
-
width,
|
|
250
|
-
height
|
|
251
|
-
})
|
|
252
|
-
)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
particlesRef.current = particlesRef.current.filter((particle) => {
|
|
256
|
-
particle.life += 1
|
|
257
|
-
particle.x += particle.vx
|
|
258
|
-
particle.y += particle.vy
|
|
259
|
-
particle.vy += 0.28
|
|
260
|
-
particle.vx *= 0.992
|
|
261
|
-
particle.rotation += particle.rotationSpeed
|
|
262
|
-
|
|
263
|
-
const opacity = Math.max(0, 1 - particle.life / particle.maxLife)
|
|
264
|
-
if (opacity <= 0 || particle.y > height + 40) return false
|
|
265
|
-
|
|
266
|
-
ctx.save()
|
|
267
|
-
ctx.globalAlpha = opacity
|
|
268
|
-
ctx.translate(particle.x, particle.y)
|
|
269
|
-
ctx.rotate(particle.rotation)
|
|
270
|
-
ctx.fillStyle = particle.color
|
|
271
|
-
|
|
272
|
-
if (particle.shape === 'circle') {
|
|
273
|
-
ctx.beginPath()
|
|
274
|
-
ctx.arc(0, 0, particle.size * 0.5, 0, Math.PI * 2)
|
|
275
|
-
ctx.fill()
|
|
276
|
-
} else {
|
|
277
|
-
ctx.fillRect(-particle.size / 2, -particle.size / 2, particle.size, particle.size)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
ctx.restore()
|
|
281
|
-
return true
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
if (particlesRef.current.length || now < end) {
|
|
285
|
-
animationRef.current = requestAnimationFrame(frame)
|
|
286
|
-
} else {
|
|
287
|
-
ctx.clearRect(0, 0, width, height)
|
|
288
|
-
animationRef.current = null
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
animationRef.current = requestAnimationFrame(frame)
|
|
293
|
-
}, [confettiDuration, stopAnimation])
|
|
294
|
-
|
|
295
|
-
useEffect(() => {
|
|
296
|
-
if (hasRun.current) return
|
|
297
|
-
hasRun.current = true
|
|
298
|
-
|
|
299
|
-
const timer = window.setTimeout(() => {
|
|
300
|
-
setVisible(true)
|
|
301
|
-
if (autoPlayConfetti !== false) fireConfetti()
|
|
302
|
-
}, 200)
|
|
303
|
-
|
|
304
|
-
return () => window.clearTimeout(timer)
|
|
305
|
-
}, [autoPlayConfetti, fireConfetti])
|
|
306
|
-
|
|
307
|
-
useEffect(() => {
|
|
308
|
-
return () => {
|
|
309
|
-
stopAnimation()
|
|
310
|
-
particlesRef.current = []
|
|
311
|
-
}
|
|
312
|
-
}, [stopAnimation])
|
|
313
|
-
|
|
314
|
-
return (
|
|
315
|
-
<section
|
|
316
|
-
id={sectionId ?? undefined}
|
|
317
|
-
className={cn(
|
|
318
|
-
'thanks-confetti-section relative flex min-h-screen w-full flex-col items-center justify-center overflow-hidden bg-stone-950 px-4 py-24 text-center',
|
|
319
|
-
className
|
|
320
|
-
)}
|
|
321
|
-
>
|
|
322
|
-
<canvas
|
|
323
|
-
ref={canvasRef}
|
|
324
|
-
aria-hidden="true"
|
|
325
|
-
className="pointer-events-none fixed inset-0 z-
|
|
326
|
-
/>
|
|
327
|
-
|
|
328
|
-
<div className="pointer-events-none absolute inset-0">
|
|
329
|
-
<div
|
|
330
|
-
className="absolute inset-0 opacity-20"
|
|
331
|
-
style={{
|
|
332
|
-
backgroundImage:
|
|
333
|
-
'radial-gradient(circle at 20% 50%, rgba(16,185,129,0.12) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255,255,255,0.05) 0%, transparent 50%)'
|
|
334
|
-
}}
|
|
335
|
-
/>
|
|
336
|
-
<div
|
|
337
|
-
className="absolute inset-0 opacity-5"
|
|
338
|
-
style={{
|
|
339
|
-
backgroundImage:
|
|
340
|
-
'linear-gradient(rgba(255,255,255,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.5) 1px, transparent 1px)',
|
|
341
|
-
backgroundSize: '60px 60px'
|
|
342
|
-
}}
|
|
343
|
-
/>
|
|
344
|
-
</div>
|
|
345
|
-
|
|
346
|
-
<div
|
|
347
|
-
className="relative z-10 flex w-full max-w-lg flex-col items-center px-2"
|
|
348
|
-
style={{
|
|
349
|
-
opacity: visible ? 1 : 0,
|
|
350
|
-
transform: visible ? 'translateY(0) scale(1)' : 'translateY(32px) scale(0.96)',
|
|
351
|
-
transition:
|
|
352
|
-
'opacity 0.7s cubic-bezier(0.34,1.56,0.64,1), transform 0.7s cubic-bezier(0.34,1.56,0.64,1)'
|
|
353
|
-
}}
|
|
354
|
-
>
|
|
355
|
-
<div className="relative mb-8">
|
|
356
|
-
<div
|
|
357
|
-
className="flex h-24 w-24 items-center justify-center rounded-full"
|
|
358
|
-
style={{
|
|
359
|
-
background: 'linear-gradient(135deg, #10b981 0%, #6ee7b7 100%)',
|
|
360
|
-
boxShadow: '0 0 60px rgba(16,185,129,0.35), 0 0 120px rgba(16,185,129,0.15)',
|
|
361
|
-
opacity: visible ? 1 : 0,
|
|
362
|
-
transform: visible ? 'scale(1)' : 'scale(0.5)',
|
|
363
|
-
transition:
|
|
364
|
-
'opacity 0.5s ease 0.3s, transform 0.6s cubic-bezier(0.34,1.56,0.64,1) 0.3s'
|
|
365
|
-
}}
|
|
366
|
-
>
|
|
367
|
-
<Check className="h-11 w-11 stroke-[3px] text-white" />
|
|
368
|
-
</div>
|
|
369
|
-
<div
|
|
370
|
-
className="absolute inset-0 rounded-full bg-emerald-500/20"
|
|
371
|
-
style={{
|
|
372
|
-
animation: visible
|
|
373
|
-
? 'thanks-confetti-ping 2s cubic-bezier(0,0,0.2,1) infinite 0.8s'
|
|
374
|
-
: 'none'
|
|
375
|
-
}}
|
|
376
|
-
/>
|
|
377
|
-
</div>
|
|
378
|
-
|
|
379
|
-
<div
|
|
380
|
-
style={{
|
|
381
|
-
opacity: visible ? 1 : 0,
|
|
382
|
-
transform: visible ? 'translateY(0)' : 'translateY(20px)',
|
|
383
|
-
transition: 'opacity 0.6s ease 0.5s, transform 0.6s ease 0.5s'
|
|
384
|
-
}}
|
|
385
|
-
>
|
|
386
|
-
{headline && (
|
|
387
|
-
<h1 className="mb-3 text-4xl font-bold tracking-tight text-white md:text-5xl">
|
|
388
|
-
{headline}
|
|
389
|
-
</h1>
|
|
390
|
-
)}
|
|
391
|
-
|
|
392
|
-
{description && (
|
|
393
|
-
<p className="mb-2 text-lg leading-relaxed text-stone-400">{description}</p>
|
|
394
|
-
)}
|
|
395
|
-
|
|
396
|
-
{subDescription && (
|
|
397
|
-
<p className="mb-10 text-sm leading-relaxed text-stone-500">{subDescription}</p>
|
|
398
|
-
)}
|
|
399
|
-
</div>
|
|
400
|
-
|
|
401
|
-
{pills.length > 0 && (
|
|
402
|
-
<div
|
|
403
|
-
className="mb-10 flex flex-wrap items-center justify-center gap-3"
|
|
404
|
-
style={{
|
|
405
|
-
opacity: visible ? 1 : 0,
|
|
406
|
-
transform: visible ? 'translateY(0)' : 'translateY(16px)',
|
|
407
|
-
transition: 'opacity 0.6s ease 0.7s, transform 0.6s ease 0.7s'
|
|
408
|
-
}}
|
|
409
|
-
>
|
|
410
|
-
{pills.map((item, index) => (
|
|
411
|
-
<div
|
|
412
|
-
key={
|
|
413
|
-
'id' in item ? (item.id ?? `${item.label}-${index}`) : `${item.label}-${index}`
|
|
414
|
-
}
|
|
415
|
-
className="flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-2"
|
|
416
|
-
>
|
|
417
|
-
<span className="flex h-4 w-4 items-center justify-center text-emerald-400">
|
|
418
|
-
<PillIcon icon={item.icon} />
|
|
419
|
-
</span>
|
|
420
|
-
<span className="whitespace-nowrap text-sm text-stone-400">{item.label}</span>
|
|
421
|
-
</div>
|
|
422
|
-
))}
|
|
423
|
-
</div>
|
|
424
|
-
)}
|
|
425
|
-
|
|
426
|
-
{links.length > 0 && (
|
|
427
|
-
<div
|
|
428
|
-
className="flex w-full flex-col gap-3 sm:flex-row"
|
|
429
|
-
style={{
|
|
430
|
-
opacity: visible ? 1 : 0,
|
|
431
|
-
transform: visible ? 'translateY(0)' : 'translateY(16px)',
|
|
432
|
-
transition: 'opacity 0.6s ease 0.9s, transform 0.6s ease 0.9s'
|
|
433
|
-
}}
|
|
434
|
-
>
|
|
435
|
-
{links.map((link, index) => {
|
|
436
|
-
const isPrimary =
|
|
437
|
-
(link.variant ?? (index === 0 ? 'primary' : 'secondary')) === 'primary'
|
|
438
|
-
|
|
439
|
-
return (
|
|
440
|
-
<Link
|
|
441
|
-
key={`${link.href}-${index}`}
|
|
442
|
-
href={link.href ?? '#'}
|
|
443
|
-
className={cn(
|
|
444
|
-
'inline-flex flex-1 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg px-6 py-3.5 text-sm font-semibold transition-colors',
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
{link.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import {
|
|
6
|
+
Check,
|
|
7
|
+
DownloadCloud,
|
|
8
|
+
Home,
|
|
9
|
+
LayoutGrid,
|
|
10
|
+
MailCheck,
|
|
11
|
+
ShieldCheck,
|
|
12
|
+
Sparkles,
|
|
13
|
+
} from 'lucide-react'
|
|
14
|
+
import { cn } from '@/utilities/ui'
|
|
15
|
+
|
|
16
|
+
type InfoPill = {
|
|
17
|
+
id?: string | null
|
|
18
|
+
label?: string | null
|
|
19
|
+
icon?: 'mail' | 'download' | 'shield' | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type CtaLink = {
|
|
23
|
+
label?: string | null
|
|
24
|
+
href?: string | null
|
|
25
|
+
variant?: 'primary' | 'secondary' | null
|
|
26
|
+
icon?: 'home' | 'grid' | 'download' | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ThanksConfettiProps = {
|
|
30
|
+
sectionId?: string | null
|
|
31
|
+
customCSS?: string | null
|
|
32
|
+
headline?: string | null
|
|
33
|
+
description?: string | null
|
|
34
|
+
subDescription?: string | null
|
|
35
|
+
replayLabel?: string | null
|
|
36
|
+
brandLabel?: string | null
|
|
37
|
+
showBrandLabel?: boolean | null
|
|
38
|
+
showReplayButton?: boolean | null
|
|
39
|
+
autoPlayConfetti?: boolean | null
|
|
40
|
+
confettiDuration?: number | null
|
|
41
|
+
infoPills?: InfoPill[] | null
|
|
42
|
+
ctaLinks?: CtaLink[] | null
|
|
43
|
+
className?: string
|
|
44
|
+
disableInnerContainer?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ConfettiParticle = {
|
|
48
|
+
x: number
|
|
49
|
+
y: number
|
|
50
|
+
vx: number
|
|
51
|
+
vy: number
|
|
52
|
+
size: number
|
|
53
|
+
rotation: number
|
|
54
|
+
rotationSpeed: number
|
|
55
|
+
color: string
|
|
56
|
+
shape: 'circle' | 'square'
|
|
57
|
+
life: number
|
|
58
|
+
maxLife: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const CONFETTI_COLORS = ['#d6d3d1', '#a8a29e', '#10b981', '#6ee7b7', '#ffffff', '#f5f5f4']
|
|
62
|
+
|
|
63
|
+
const defaultInfoPills: Required<Pick<InfoPill, 'label' | 'icon'>>[] = [
|
|
64
|
+
{ icon: 'mail', label: 'Confirmation sent' },
|
|
65
|
+
{ icon: 'download', label: 'Files ready' },
|
|
66
|
+
{ icon: 'shield', label: 'License activated' },
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
const defaultCtaLinks: Required<Pick<CtaLink, 'label' | 'href' | 'variant' | 'icon'>>[] = [
|
|
70
|
+
{ label: 'Back to Home', href: '/', variant: 'primary', icon: 'home' },
|
|
71
|
+
{ label: 'Continue Browsing', href: '/components', variant: 'secondary', icon: 'grid' },
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
function PillIcon({ icon }: { icon?: InfoPill['icon'] }) {
|
|
75
|
+
if (icon === 'download') return <DownloadCloud className="h-3.5 w-3.5" />
|
|
76
|
+
if (icon === 'shield') return <ShieldCheck className="h-3.5 w-3.5" />
|
|
77
|
+
|
|
78
|
+
return <MailCheck className="h-3.5 w-3.5" />
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function CtaIcon({ icon }: { icon?: CtaLink['icon'] }) {
|
|
82
|
+
if (icon === 'grid') return <LayoutGrid className="h-4 w-4" />
|
|
83
|
+
if (icon === 'download') return <DownloadCloud className="h-4 w-4" />
|
|
84
|
+
|
|
85
|
+
return <Home className="h-4 w-4" />
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getSafeDuration(duration?: number | null) {
|
|
89
|
+
if (typeof duration !== 'number' || !Number.isFinite(duration)) return 3000
|
|
90
|
+
return Math.min(Math.max(duration, 1000), 8000)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createParticles({
|
|
94
|
+
amount,
|
|
95
|
+
originX,
|
|
96
|
+
originY,
|
|
97
|
+
angle,
|
|
98
|
+
spread,
|
|
99
|
+
velocity,
|
|
100
|
+
width,
|
|
101
|
+
height,
|
|
102
|
+
}: {
|
|
103
|
+
amount: number
|
|
104
|
+
originX: number
|
|
105
|
+
originY: number
|
|
106
|
+
angle?: number
|
|
107
|
+
spread: number
|
|
108
|
+
velocity: number
|
|
109
|
+
width: number
|
|
110
|
+
height: number
|
|
111
|
+
}): ConfettiParticle[] {
|
|
112
|
+
return Array.from({ length: amount }, () => {
|
|
113
|
+
const baseAngle = typeof angle === 'number' ? angle : Math.random() * 360
|
|
114
|
+
const randomAngle = (baseAngle - spread / 2 + Math.random() * spread) * (Math.PI / 180)
|
|
115
|
+
const speed = velocity * (0.45 + Math.random() * 0.75)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
x: originX * width,
|
|
119
|
+
y: originY * height,
|
|
120
|
+
vx: Math.cos(randomAngle) * speed,
|
|
121
|
+
vy: Math.sin(randomAngle) * speed,
|
|
122
|
+
size: 4 + Math.random() * 7,
|
|
123
|
+
rotation: Math.random() * Math.PI,
|
|
124
|
+
rotationSpeed: -0.22 + Math.random() * 0.44,
|
|
125
|
+
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)] ?? '#ffffff',
|
|
126
|
+
shape: Math.random() > 0.5 ? 'circle' : 'square',
|
|
127
|
+
life: 0,
|
|
128
|
+
maxLife: 90 + Math.random() * 70,
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function DDThanks({
|
|
134
|
+
sectionId,
|
|
135
|
+
headline = 'Thank you! 🎉',
|
|
136
|
+
description = 'Your order has been confirmed and is on its way.',
|
|
137
|
+
subDescription = "We've sent a confirmation email with your download links and license keys. Check your inbox — and maybe your spam folder too.",
|
|
138
|
+
replayLabel = 'Replay confetti',
|
|
139
|
+
brandLabel = 'DesignsDusted',
|
|
140
|
+
showBrandLabel = true,
|
|
141
|
+
showReplayButton = true,
|
|
142
|
+
autoPlayConfetti = true,
|
|
143
|
+
confettiDuration = 3000,
|
|
144
|
+
infoPills,
|
|
145
|
+
ctaLinks,
|
|
146
|
+
className,
|
|
147
|
+
}: ThanksConfettiProps) {
|
|
148
|
+
const hasRun = useRef(false)
|
|
149
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
|
150
|
+
const animationRef = useRef<number | null>(null)
|
|
151
|
+
const particlesRef = useRef<ConfettiParticle[]>([])
|
|
152
|
+
const [visible, setVisible] = useState(false)
|
|
153
|
+
|
|
154
|
+
const pills = useMemo(() => {
|
|
155
|
+
const source = infoPills?.length ? infoPills : defaultInfoPills
|
|
156
|
+
return source.filter((pill) => pill.label)
|
|
157
|
+
}, [infoPills])
|
|
158
|
+
|
|
159
|
+
const links = useMemo(() => {
|
|
160
|
+
const source = ctaLinks?.length ? ctaLinks : defaultCtaLinks
|
|
161
|
+
return source.filter((link) => link.label && link.href)
|
|
162
|
+
}, [ctaLinks])
|
|
163
|
+
|
|
164
|
+
const stopAnimation = useCallback(() => {
|
|
165
|
+
if (animationRef.current) {
|
|
166
|
+
cancelAnimationFrame(animationRef.current)
|
|
167
|
+
animationRef.current = null
|
|
168
|
+
}
|
|
169
|
+
}, [])
|
|
170
|
+
|
|
171
|
+
const fireConfetti = useCallback(() => {
|
|
172
|
+
const canvas = canvasRef.current
|
|
173
|
+
const ctx = canvas?.getContext('2d')
|
|
174
|
+
if (!canvas || !ctx) return
|
|
175
|
+
|
|
176
|
+
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
177
|
+
if (reducedMotion) return
|
|
178
|
+
|
|
179
|
+
const pixelRatio = Math.min(window.devicePixelRatio || 1, 2)
|
|
180
|
+
const width = window.innerWidth
|
|
181
|
+
const height = window.innerHeight
|
|
182
|
+
|
|
183
|
+
canvas.width = width * pixelRatio
|
|
184
|
+
canvas.height = height * pixelRatio
|
|
185
|
+
canvas.style.width = `${width}px`
|
|
186
|
+
canvas.style.height = `${height}px`
|
|
187
|
+
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0)
|
|
188
|
+
|
|
189
|
+
const duration = getSafeDuration(confettiDuration)
|
|
190
|
+
const end = performance.now() + duration
|
|
191
|
+
|
|
192
|
+
particlesRef.current = [
|
|
193
|
+
...particlesRef.current,
|
|
194
|
+
...createParticles({
|
|
195
|
+
amount: 120,
|
|
196
|
+
originX: 0.5,
|
|
197
|
+
originY: 0.5,
|
|
198
|
+
spread: 100,
|
|
199
|
+
velocity: 20,
|
|
200
|
+
width,
|
|
201
|
+
height,
|
|
202
|
+
}),
|
|
203
|
+
...createParticles({
|
|
204
|
+
amount: 60,
|
|
205
|
+
originX: 0,
|
|
206
|
+
originY: 0.6,
|
|
207
|
+
angle: -35,
|
|
208
|
+
spread: 70,
|
|
209
|
+
velocity: 18,
|
|
210
|
+
width,
|
|
211
|
+
height,
|
|
212
|
+
}),
|
|
213
|
+
...createParticles({
|
|
214
|
+
amount: 60,
|
|
215
|
+
originX: 1,
|
|
216
|
+
originY: 0.6,
|
|
217
|
+
angle: 215,
|
|
218
|
+
spread: 70,
|
|
219
|
+
velocity: 18,
|
|
220
|
+
width,
|
|
221
|
+
height,
|
|
222
|
+
}),
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
stopAnimation()
|
|
226
|
+
|
|
227
|
+
const frame = (now: number) => {
|
|
228
|
+
ctx.clearRect(0, 0, width, height)
|
|
229
|
+
|
|
230
|
+
if (now < end && Math.random() > 0.35) {
|
|
231
|
+
particlesRef.current.push(
|
|
232
|
+
...createParticles({
|
|
233
|
+
amount: 2,
|
|
234
|
+
originX: 0,
|
|
235
|
+
originY: 0.3,
|
|
236
|
+
angle: -25,
|
|
237
|
+
spread: 55,
|
|
238
|
+
velocity: 10,
|
|
239
|
+
width,
|
|
240
|
+
height,
|
|
241
|
+
}),
|
|
242
|
+
...createParticles({
|
|
243
|
+
amount: 2,
|
|
244
|
+
originX: 1,
|
|
245
|
+
originY: 0.3,
|
|
246
|
+
angle: 205,
|
|
247
|
+
spread: 55,
|
|
248
|
+
velocity: 10,
|
|
249
|
+
width,
|
|
250
|
+
height,
|
|
251
|
+
}),
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
particlesRef.current = particlesRef.current.filter((particle) => {
|
|
256
|
+
particle.life += 1
|
|
257
|
+
particle.x += particle.vx
|
|
258
|
+
particle.y += particle.vy
|
|
259
|
+
particle.vy += 0.28
|
|
260
|
+
particle.vx *= 0.992
|
|
261
|
+
particle.rotation += particle.rotationSpeed
|
|
262
|
+
|
|
263
|
+
const opacity = Math.max(0, 1 - particle.life / particle.maxLife)
|
|
264
|
+
if (opacity <= 0 || particle.y > height + 40) return false
|
|
265
|
+
|
|
266
|
+
ctx.save()
|
|
267
|
+
ctx.globalAlpha = opacity
|
|
268
|
+
ctx.translate(particle.x, particle.y)
|
|
269
|
+
ctx.rotate(particle.rotation)
|
|
270
|
+
ctx.fillStyle = particle.color
|
|
271
|
+
|
|
272
|
+
if (particle.shape === 'circle') {
|
|
273
|
+
ctx.beginPath()
|
|
274
|
+
ctx.arc(0, 0, particle.size * 0.5, 0, Math.PI * 2)
|
|
275
|
+
ctx.fill()
|
|
276
|
+
} else {
|
|
277
|
+
ctx.fillRect(-particle.size / 2, -particle.size / 2, particle.size, particle.size)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
ctx.restore()
|
|
281
|
+
return true
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
if (particlesRef.current.length || now < end) {
|
|
285
|
+
animationRef.current = requestAnimationFrame(frame)
|
|
286
|
+
} else {
|
|
287
|
+
ctx.clearRect(0, 0, width, height)
|
|
288
|
+
animationRef.current = null
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
animationRef.current = requestAnimationFrame(frame)
|
|
293
|
+
}, [confettiDuration, stopAnimation])
|
|
294
|
+
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
if (hasRun.current) return
|
|
297
|
+
hasRun.current = true
|
|
298
|
+
|
|
299
|
+
const timer = window.setTimeout(() => {
|
|
300
|
+
setVisible(true)
|
|
301
|
+
if (autoPlayConfetti !== false) fireConfetti()
|
|
302
|
+
}, 200)
|
|
303
|
+
|
|
304
|
+
return () => window.clearTimeout(timer)
|
|
305
|
+
}, [autoPlayConfetti, fireConfetti])
|
|
306
|
+
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
return () => {
|
|
309
|
+
stopAnimation()
|
|
310
|
+
particlesRef.current = []
|
|
311
|
+
}
|
|
312
|
+
}, [stopAnimation])
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<section
|
|
316
|
+
id={sectionId ?? undefined}
|
|
317
|
+
className={cn(
|
|
318
|
+
'thanks-confetti-section relative flex min-h-screen w-full flex-col items-center justify-center overflow-hidden bg-stone-transparent dark:bg-stone-950 px-4 py-24 text-center hue-rotate-300 invert-100 dark:invert-0',
|
|
319
|
+
className,
|
|
320
|
+
)}
|
|
321
|
+
>
|
|
322
|
+
<canvas
|
|
323
|
+
ref={canvasRef}
|
|
324
|
+
aria-hidden="true"
|
|
325
|
+
className="pointer-events-none fixed inset-0 z-60 h-screen w-screen"
|
|
326
|
+
/>
|
|
327
|
+
|
|
328
|
+
<div className="pointer-events-none absolute inset-0">
|
|
329
|
+
<div
|
|
330
|
+
className="absolute inset-0 opacity-20"
|
|
331
|
+
style={{
|
|
332
|
+
backgroundImage:
|
|
333
|
+
'radial-gradient(circle at 20% 50%, rgba(16,185,129,0.12) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255,255,255,0.05) 0%, transparent 50%)',
|
|
334
|
+
}}
|
|
335
|
+
/>
|
|
336
|
+
<div
|
|
337
|
+
className="absolute inset-0 opacity-5"
|
|
338
|
+
style={{
|
|
339
|
+
backgroundImage:
|
|
340
|
+
'linear-gradient(rgba(255,255,255,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.5) 1px, transparent 1px)',
|
|
341
|
+
backgroundSize: '60px 60px',
|
|
342
|
+
}}
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<div
|
|
347
|
+
className="relative z-10 flex w-full max-w-lg flex-col items-center px-2"
|
|
348
|
+
style={{
|
|
349
|
+
opacity: visible ? 1 : 0,
|
|
350
|
+
transform: visible ? 'translateY(0) scale(1)' : 'translateY(32px) scale(0.96)',
|
|
351
|
+
transition:
|
|
352
|
+
'opacity 0.7s cubic-bezier(0.34,1.56,0.64,1), transform 0.7s cubic-bezier(0.34,1.56,0.64,1)',
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
<div className="relative mb-8">
|
|
356
|
+
<div
|
|
357
|
+
className="flex h-24 w-24 items-center justify-center rounded-full border border-emerald-500/10"
|
|
358
|
+
style={{
|
|
359
|
+
background: 'linear-gradient(135deg, #10b981 0%, #6ee7b7 100%)',
|
|
360
|
+
boxShadow: '0 0 60px rgba(16,185,129,0.35), 0 0 120px rgba(16,185,129,0.15)',
|
|
361
|
+
opacity: visible ? 1 : 0,
|
|
362
|
+
transform: visible ? 'scale(1)' : 'scale(0.5)',
|
|
363
|
+
transition:
|
|
364
|
+
'opacity 0.5s ease 0.3s, transform 0.6s cubic-bezier(0.34,1.56,0.64,1) 0.3s',
|
|
365
|
+
}}
|
|
366
|
+
>
|
|
367
|
+
<Check className="h-11 w-11 stroke-[3px] text-emerald-500 dark:text-white" />
|
|
368
|
+
</div>
|
|
369
|
+
<div
|
|
370
|
+
className="absolute inset-0 rounded-full bg-emerald-500/20"
|
|
371
|
+
style={{
|
|
372
|
+
animation: visible
|
|
373
|
+
? 'thanks-confetti-ping 2s cubic-bezier(0,0,0.2,1) infinite 0.8s'
|
|
374
|
+
: 'none',
|
|
375
|
+
}}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div
|
|
380
|
+
style={{
|
|
381
|
+
opacity: visible ? 1 : 0,
|
|
382
|
+
transform: visible ? 'translateY(0)' : 'translateY(20px)',
|
|
383
|
+
transition: 'opacity 0.6s ease 0.5s, transform 0.6s ease 0.5s',
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
386
|
+
{headline && (
|
|
387
|
+
<h1 className="mb-3 text-4xl font-bold tracking-tight text-white md:text-5xl">
|
|
388
|
+
{headline}
|
|
389
|
+
</h1>
|
|
390
|
+
)}
|
|
391
|
+
|
|
392
|
+
{description && (
|
|
393
|
+
<p className="mb-2 text-lg leading-relaxed text-stone-400">{description}</p>
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
{subDescription && (
|
|
397
|
+
<p className="mb-10 text-sm leading-relaxed text-stone-500">{subDescription}</p>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
{pills.length > 0 && (
|
|
402
|
+
<div
|
|
403
|
+
className="mb-10 flex flex-wrap items-center justify-center gap-3"
|
|
404
|
+
style={{
|
|
405
|
+
opacity: visible ? 1 : 0,
|
|
406
|
+
transform: visible ? 'translateY(0)' : 'translateY(16px)',
|
|
407
|
+
transition: 'opacity 0.6s ease 0.7s, transform 0.6s ease 0.7s',
|
|
408
|
+
}}
|
|
409
|
+
>
|
|
410
|
+
{pills.map((item, index) => (
|
|
411
|
+
<div
|
|
412
|
+
key={
|
|
413
|
+
'id' in item ? (item.id ?? `${item.label}-${index}`) : `${item.label}-${index}`
|
|
414
|
+
}
|
|
415
|
+
className="flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-2"
|
|
416
|
+
>
|
|
417
|
+
<span className="flex h-4 w-4 items-center justify-center text-emerald-400">
|
|
418
|
+
<PillIcon icon={item.icon} />
|
|
419
|
+
</span>
|
|
420
|
+
<span className="whitespace-nowrap text-sm text-stone-400">{item.label}</span>
|
|
421
|
+
</div>
|
|
422
|
+
))}
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
|
|
426
|
+
{links.length > 0 && (
|
|
427
|
+
<div
|
|
428
|
+
className="flex w-full flex-col gap-3 sm:flex-row"
|
|
429
|
+
style={{
|
|
430
|
+
opacity: visible ? 1 : 0,
|
|
431
|
+
transform: visible ? 'translateY(0)' : 'translateY(16px)',
|
|
432
|
+
transition: 'opacity 0.6s ease 0.9s, transform 0.6s ease 0.9s',
|
|
433
|
+
}}
|
|
434
|
+
>
|
|
435
|
+
{links.map((link, index) => {
|
|
436
|
+
const isPrimary =
|
|
437
|
+
(link.variant ?? (index === 0 ? 'primary' : 'secondary')) === 'primary'
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<Link
|
|
441
|
+
key={`${link.href}-${index}`}
|
|
442
|
+
href={link.href ?? '#'}
|
|
443
|
+
className={cn(
|
|
444
|
+
'inline-flex flex-1 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg px-6 py-3.5 text-sm font-semibold transition-colors',
|
|
445
|
+
'backdrop-blur-lg rounded-2xl squircle px-5 py-2.5 text-sm font-medium transition-all duration-300 0 hover:shadow-2xl hover:-translate-y-0.5 hover:transition-all hover:duration-300',
|
|
446
|
+
isPrimary
|
|
447
|
+
? 'bg-white text-stone-900 hover:bg-stone-100'
|
|
448
|
+
: 'border border-white/15 bg-white/10 text-white hover:bg-white/20',
|
|
449
|
+
)}
|
|
450
|
+
>
|
|
451
|
+
<CtaIcon icon={link.icon} />
|
|
452
|
+
{link.label}
|
|
453
|
+
</Link>
|
|
454
|
+
)
|
|
455
|
+
})}
|
|
456
|
+
</div>
|
|
457
|
+
)}
|
|
458
|
+
|
|
459
|
+
{showReplayButton !== false && replayLabel && (
|
|
460
|
+
<div
|
|
461
|
+
className="mt-8"
|
|
462
|
+
style={{
|
|
463
|
+
opacity: visible ? 1 : 0,
|
|
464
|
+
transition: 'opacity 0.6s ease 1.1s',
|
|
465
|
+
}}
|
|
466
|
+
>
|
|
467
|
+
<button
|
|
468
|
+
type="button"
|
|
469
|
+
onClick={fireConfetti}
|
|
470
|
+
className="flex cursor-pointer items-center gap-1.5 text-xs text-stone-600 transition-colors hover:text-stone-400"
|
|
471
|
+
>
|
|
472
|
+
<Sparkles className="h-3 w-3" />
|
|
473
|
+
{replayLabel}
|
|
474
|
+
</button>
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
{showBrandLabel !== false && brandLabel && (
|
|
480
|
+
<div
|
|
481
|
+
className="absolute bottom-8 left-1/2 z-10 -translate-x-1/2 text-xs font-black uppercase tracking-[0.35em] text-stone-500"
|
|
482
|
+
style={{
|
|
483
|
+
opacity: visible ? 0.4 : 0,
|
|
484
|
+
transition: 'opacity 0.6s ease 1.2s',
|
|
485
|
+
}}
|
|
486
|
+
>
|
|
487
|
+
{brandLabel}
|
|
488
|
+
</div>
|
|
489
|
+
)}
|
|
490
|
+
|
|
491
|
+
<style>{`
|
|
492
|
+
@keyframes thanks-confetti-ping {
|
|
493
|
+
75%, 100% {
|
|
494
|
+
transform: scale(2);
|
|
495
|
+
opacity: 0;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
`}</style>
|
|
499
|
+
</section>
|
|
500
|
+
)
|
|
501
|
+
}
|