blocks-dusted 0.1.0
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/LICENSE +21 -0
- package/README.md +68 -0
- package/bin/index.js +8 -0
- package/package.json +40 -0
- package/src/cli.js +79 -0
- package/src/commands/add.js +327 -0
- package/src/commands/doctor.js +21 -0
- package/src/commands/list.js +18 -0
- package/src/installers/checkRequirements.js +101 -0
- package/src/installers/copyTemplateFiles.js +79 -0
- package/src/installers/installDependencies.js +69 -0
- package/src/installers/patchPayloadBlocksArray.js +122 -0
- package/src/installers/patchRenderBlocks.js +110 -0
- package/src/installers/runGenerators.js +3 -0
- package/src/installers/writeInstalledBlockReadme.js +117 -0
- package/src/registry/listTemplates.js +17 -0
- package/src/registry/loadTemplateManifest.js +20 -0
- package/src/registry/validateTemplateManifest.js +69 -0
- package/src/utils/logger.js +8 -0
- package/src/utils/packageManager.js +42 -0
- package/src/utils/paths.js +7 -0
- package/src/utils/project.js +122 -0
- package/src/utils/strings.js +7 -0
- package/templates/blocks/DD-BookCall/Component.tsx +573 -0
- package/templates/blocks/DD-BookCall/config.ts +239 -0
- package/templates/blocks/DD-BookCall/manifest.json +112 -0
- package/templates/blocks/DD-CardTemplate01/Component.tsx +144 -0
- package/templates/blocks/DD-CardTemplate01/config.ts +106 -0
- package/templates/blocks/DD-CardTemplate01/manifest.json +84 -0
- package/templates/blocks/DD-Carousel-A/Component.tsx +266 -0
- package/templates/blocks/DD-Carousel-A/config.ts +163 -0
- package/templates/blocks/DD-Carousel-A/manifest.json +86 -0
- package/templates/blocks/DD-Carousel-B/Component.tsx +432 -0
- package/templates/blocks/DD-Carousel-B/config.ts +176 -0
- package/templates/blocks/DD-Carousel-B/manifest.json +86 -0
- package/templates/blocks/DD-ComparisonTable/Component.tsx +272 -0
- package/templates/blocks/DD-ComparisonTable/config.ts +105 -0
- package/templates/blocks/DD-ComparisonTable/manifest.json +78 -0
- package/templates/blocks/DD-Contact/Component.tsx +439 -0
- package/templates/blocks/DD-Contact/config.ts +217 -0
- package/templates/blocks/DD-Contact/manifest.json +107 -0
- package/templates/blocks/DD-FeatCat/Component.tsx +361 -0
- package/templates/blocks/DD-FeatCat/config.ts +204 -0
- package/templates/blocks/DD-FeatCat/manifest.json +87 -0
- package/templates/blocks/DD-FeatStrip/Component.tsx +615 -0
- package/templates/blocks/DD-FeatStrip/config.ts +134 -0
- package/templates/blocks/DD-FeatStrip/manifest.json +82 -0
- package/templates/blocks/DD-Hero/Component.tsx +317 -0
- package/templates/blocks/DD-Hero/config.ts +267 -0
- package/templates/blocks/DD-Hero/manifest.json +92 -0
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +995 -0
- package/templates/blocks/DD-HorizontalScroll/config.ts +231 -0
- package/templates/blocks/DD-HorizontalScroll/manifest.json +85 -0
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +571 -0
- package/templates/blocks/DD-MasonaryMedia/config.ts +390 -0
- package/templates/blocks/DD-MasonaryMedia/manifest.json +101 -0
- package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -0
- package/templates/blocks/DD-Pricing/Component.tsx +380 -0
- package/templates/blocks/DD-Pricing/config.ts +215 -0
- package/templates/blocks/DD-Pricing/manifest.json +78 -0
- package/templates/blocks/DD-Process/Component.tsx +155 -0
- package/templates/blocks/DD-Process/config.ts +150 -0
- package/templates/blocks/DD-Process/manifest.json +82 -0
- package/templates/blocks/DD-Section/Component.tsx +327 -0
- package/templates/blocks/DD-Section/config.ts +214 -0
- package/templates/blocks/DD-Section/manifest.json +85 -0
- package/templates/blocks/DD-Services/Component.tsx +188 -0
- package/templates/blocks/DD-Services/config.ts +149 -0
- package/templates/blocks/DD-Services/manifest.json +95 -0
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +317 -0
- package/templates/blocks/DD-ShowcaseGrid/config.ts +197 -0
- package/templates/blocks/DD-ShowcaseGrid/manifest.json +80 -0
- package/templates/blocks/DD-Slider-A/Component.tsx +252 -0
- package/templates/blocks/DD-Slider-A/config.ts +118 -0
- package/templates/blocks/DD-Slider-A/manifest.json +86 -0
- package/templates/blocks/DD-StackCards/Component.tsx +496 -0
- package/templates/blocks/DD-StackCards/config.ts +399 -0
- package/templates/blocks/DD-StackCards/manifest.json +87 -0
- package/templates/blocks/DD-Team/Component.tsx +265 -0
- package/templates/blocks/DD-Team/config.ts +173 -0
- package/templates/blocks/DD-Team/manifest.json +82 -0
- package/templates/blocks/DD-TechStack/Component.tsx +189 -0
- package/templates/blocks/DD-TechStack/config.ts +153 -0
- package/templates/blocks/DD-TechStack/manifest.json +118 -0
- package/templates/blocks/DD-Testimonials/Component.tsx +147 -0
- package/templates/blocks/DD-Testimonials/config.ts +120 -0
- package/templates/blocks/DD-Testimonials/manifest.json +87 -0
- package/templates/blocks/DD-Work/Component.tsx +328 -0
- package/templates/blocks/DD-Work/config.ts +165 -0
- package/templates/blocks/DD-Work/manifest.json +87 -0
- package/templates/blocks/DD-Work-B/Component.tsx +320 -0
- package/templates/blocks/DD-Work-B/config.ts +165 -0
- package/templates/blocks/DD-Work-B/manifest.json +75 -0
- package/templates/shared/components/ui/anims/fade.tsx +83 -0
- package/templates/shared/components/ui/button.tsx +233 -0
- package/templates/shared/components/ui/input.tsx +41 -0
- package/templates/shared/fields/lucide-icon-picker/Cell.tsx +24 -0
- package/templates/shared/fields/lucide-icon-picker/README.md +160 -0
- package/templates/shared/fields/lucide-icon-picker/field.ts +40 -0
- package/templates/shared/fields/lucide-icon-picker/index.tsx +203 -0
- package/templates/shared/utilities/canUseDOM.ts +1 -0
- package/templates/shared/utilities/getURL.ts +31 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import * as LucideIcons from "lucide-react";
|
|
6
|
+
export type DDTechStackProps = Record<string, any>;
|
|
7
|
+
|
|
8
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
9
|
+
type MediaObj = { url?: string | null; alt?: string | null };
|
|
10
|
+
type TechStackItem = {
|
|
11
|
+
image?: string | MediaObj | null;
|
|
12
|
+
icon?: string | null;
|
|
13
|
+
label?: string | null;
|
|
14
|
+
showLabel?: boolean | null;
|
|
15
|
+
};
|
|
16
|
+
type TechStackRow = {
|
|
17
|
+
duration?: number | null;
|
|
18
|
+
direction?: "left" | "right" | null;
|
|
19
|
+
items?: TechStackItem[] | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getMediaUrl(
|
|
23
|
+
media: string | MediaObj | null | undefined,
|
|
24
|
+
): string | null {
|
|
25
|
+
if (!media) return null;
|
|
26
|
+
if (typeof media === "string") return media;
|
|
27
|
+
return media.url ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getMediaAlt(
|
|
31
|
+
media: string | MediaObj | null | undefined,
|
|
32
|
+
): string | null {
|
|
33
|
+
if (!media || typeof media === "string") return null;
|
|
34
|
+
return (media as MediaObj).alt ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Dynamic Lucide Icon ──────────────────────────────────────────────────────
|
|
38
|
+
function isLocalImageSrc(src?: string | null): src is string {
|
|
39
|
+
return typeof src === "string" && src.startsWith("/api/media/file/");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function DynamicIcon({
|
|
43
|
+
name,
|
|
44
|
+
size = 36,
|
|
45
|
+
...props
|
|
46
|
+
}: { name?: string | null } & Omit<LucideIcons.LucideProps, "name">) {
|
|
47
|
+
if (!name) return null;
|
|
48
|
+
const Icon = (
|
|
49
|
+
LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>
|
|
50
|
+
)[name];
|
|
51
|
+
if (!Icon) return null;
|
|
52
|
+
return <Icon size={size} {...props} />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Tech Item Card ───────────────────────────────────────────────────────────
|
|
56
|
+
function TechItem({
|
|
57
|
+
imageUrl,
|
|
58
|
+
imageAlt,
|
|
59
|
+
icon,
|
|
60
|
+
label,
|
|
61
|
+
showLabel,
|
|
62
|
+
}: {
|
|
63
|
+
imageUrl?: string | null;
|
|
64
|
+
imageAlt?: string | null;
|
|
65
|
+
icon?: string | null;
|
|
66
|
+
label?: string | null;
|
|
67
|
+
showLabel?: boolean | null;
|
|
68
|
+
}) {
|
|
69
|
+
const safeImageUrl = isLocalImageSrc(imageUrl) ? imageUrl : null;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="relative group/tech text-center shrink-0">
|
|
73
|
+
<div className="w-20 h-20 bg-linear-to-b from-[#1a1a1f] to-[#0f0f12] rounded-2xl p-4 transition-all duration-300 hover:scale-105 hover:shadow-[0_0_30px_rgba(140,138,132,0.15)] flex items-center justify-center border border-[#2A2A30] invert-[1] dark:invert-[0]">
|
|
74
|
+
{safeImageUrl ? (
|
|
75
|
+
<Image
|
|
76
|
+
src={safeImageUrl}
|
|
77
|
+
alt={imageAlt ?? label ?? "Tech logo"}
|
|
78
|
+
width={40}
|
|
79
|
+
height={40}
|
|
80
|
+
loading="lazy"
|
|
81
|
+
className="object-contain w-10 h-10"
|
|
82
|
+
/>
|
|
83
|
+
) : (
|
|
84
|
+
<DynamicIcon className="text-[#9A9790]" name={icon} size={36} />
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
{showLabel !== false && label && (
|
|
88
|
+
<span className="text-sm whitespace-nowrap text-[#9A9790] mt-2 block">
|
|
89
|
+
{label}
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
97
|
+
export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
98
|
+
eyebrow,
|
|
99
|
+
heading,
|
|
100
|
+
techRows,
|
|
101
|
+
sectionId,
|
|
102
|
+
customCSS,
|
|
103
|
+
}) => {
|
|
104
|
+
const rows = (techRows ?? []) as TechStackRow[];
|
|
105
|
+
if (!rows.length) return null;
|
|
106
|
+
|
|
107
|
+
const rowMaskStyle: React.CSSProperties = {
|
|
108
|
+
maskImage:
|
|
109
|
+
"linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%)",
|
|
110
|
+
WebkitMaskImage:
|
|
111
|
+
"linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%)",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<section
|
|
116
|
+
id={sectionId ?? undefined}
|
|
117
|
+
className="relative w-full bg-transparent py-12 px-4 md:px-6 max-w-6xl mx-auto"
|
|
118
|
+
>
|
|
119
|
+
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
120
|
+
|
|
121
|
+
{(eyebrow || heading) && (
|
|
122
|
+
<div className="mb-12">
|
|
123
|
+
{eyebrow && (
|
|
124
|
+
<p className="text-sm uppercase tracking-widest text-[#9A9790] mb-2">
|
|
125
|
+
{eyebrow}
|
|
126
|
+
</p>
|
|
127
|
+
)}
|
|
128
|
+
{heading && (
|
|
129
|
+
<h2
|
|
130
|
+
className="text-3xl font-bold tracking-tighter sm:text-4xl text-stone-900 dark:text-stone-50"
|
|
131
|
+
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
|
|
132
|
+
>
|
|
133
|
+
{heading}
|
|
134
|
+
</h2>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
<div className="relative">
|
|
140
|
+
{rows.map((row, rowIndex) => {
|
|
141
|
+
const duration = row.duration ?? 40;
|
|
142
|
+
const direction = row.direction ?? "left";
|
|
143
|
+
const items = row.items ?? [];
|
|
144
|
+
|
|
145
|
+
if (items.length === 0) return null;
|
|
146
|
+
|
|
147
|
+
// Quadruple items for a seamless infinite loop
|
|
148
|
+
const duplicated = [...items, ...items, ...items, ...items];
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div
|
|
152
|
+
key={rowIndex}
|
|
153
|
+
className="overflow-x-hidden overflow-y-visible py-6 relative"
|
|
154
|
+
style={rowMaskStyle}
|
|
155
|
+
>
|
|
156
|
+
<div
|
|
157
|
+
className="flex shrink-0 justify-around gap-8 flex-row animate-marquee"
|
|
158
|
+
style={{
|
|
159
|
+
animationDuration: `${duration}s`,
|
|
160
|
+
animationDirection:
|
|
161
|
+
direction === "right" ? "reverse" : "normal",
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{duplicated.map((item, idx) => {
|
|
165
|
+
const imgUrl = getMediaUrl(
|
|
166
|
+
item.image as string | MediaObj | null,
|
|
167
|
+
);
|
|
168
|
+
const imgAlt = getMediaAlt(
|
|
169
|
+
item.image as string | MediaObj | null,
|
|
170
|
+
);
|
|
171
|
+
return (
|
|
172
|
+
<TechItem
|
|
173
|
+
key={idx}
|
|
174
|
+
imageUrl={imgUrl}
|
|
175
|
+
imageAlt={imgAlt}
|
|
176
|
+
icon={item.icon}
|
|
177
|
+
label={item.label}
|
|
178
|
+
showLabel={item.showLabel}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
})}
|
|
186
|
+
</div>
|
|
187
|
+
</section>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Block } from 'payload'
|
|
2
|
+
import { iconPicker } from '@/fields/lucide-icon-picker/field'
|
|
3
|
+
|
|
4
|
+
export const DDTechStack: Block = {
|
|
5
|
+
slug: 'DD-TechStack',
|
|
6
|
+
interfaceName: 'DDTechStack',
|
|
7
|
+
labels: {
|
|
8
|
+
singular: 'Tech Stack',
|
|
9
|
+
plural: 'Tech Stacks'
|
|
10
|
+
},
|
|
11
|
+
fields: [
|
|
12
|
+
{
|
|
13
|
+
type: 'tabs',
|
|
14
|
+
tabs: [
|
|
15
|
+
{
|
|
16
|
+
label: 'Header',
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: 'eyebrow',
|
|
20
|
+
type: 'text',
|
|
21
|
+
label: 'Eyebrow Label',
|
|
22
|
+
admin: { description: 'Small text above the heading (e.g. "Our Stack")' }
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'heading',
|
|
26
|
+
type: 'text',
|
|
27
|
+
label: 'Heading',
|
|
28
|
+
defaultValue: 'Technology Stack',
|
|
29
|
+
admin: { description: 'Main section heading. Leave blank to hide.' }
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: 'Rows',
|
|
35
|
+
fields: [
|
|
36
|
+
{
|
|
37
|
+
name: 'techRows',
|
|
38
|
+
type: 'array',
|
|
39
|
+
label: 'Tech Rows',
|
|
40
|
+
minRows: 1,
|
|
41
|
+
maxRows: 3,
|
|
42
|
+
admin: {
|
|
43
|
+
description: 'Up to 3 independently scrolling rows of tech stack items.'
|
|
44
|
+
},
|
|
45
|
+
fields: [
|
|
46
|
+
{
|
|
47
|
+
type: 'row',
|
|
48
|
+
fields: [
|
|
49
|
+
{
|
|
50
|
+
name: 'direction',
|
|
51
|
+
type: 'select',
|
|
52
|
+
label: 'Scroll Direction',
|
|
53
|
+
defaultValue: 'left',
|
|
54
|
+
options: [
|
|
55
|
+
{ label: '← Left', value: 'left' },
|
|
56
|
+
{ label: '→ Right', value: 'right' }
|
|
57
|
+
],
|
|
58
|
+
admin: { width: '50%' }
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'duration',
|
|
62
|
+
type: 'number',
|
|
63
|
+
label: 'Duration (seconds)',
|
|
64
|
+
defaultValue: 40,
|
|
65
|
+
min: 5,
|
|
66
|
+
max: 180,
|
|
67
|
+
admin: {
|
|
68
|
+
description: 'Scroll cycle time — lower = faster.',
|
|
69
|
+
width: '50%'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'items',
|
|
76
|
+
type: 'array',
|
|
77
|
+
label: 'Tech Items',
|
|
78
|
+
minRows: 1,
|
|
79
|
+
admin: {
|
|
80
|
+
description:
|
|
81
|
+
'Each item shows a logo image or Lucide icon with an optional label. Upload takes priority over icon.'
|
|
82
|
+
},
|
|
83
|
+
fields: [
|
|
84
|
+
{
|
|
85
|
+
name: 'image',
|
|
86
|
+
type: 'upload',
|
|
87
|
+
label: 'Custom Logo',
|
|
88
|
+
relationTo: 'media',
|
|
89
|
+
admin: {
|
|
90
|
+
description:
|
|
91
|
+
'Upload SVG, PNG, JPG, WebP, or GIF. Takes priority over the Lucide icon.'
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: 'row',
|
|
96
|
+
fields: [
|
|
97
|
+
iconPicker({
|
|
98
|
+
name: 'icon',
|
|
99
|
+
required: false,
|
|
100
|
+
description: 'Fallback if no logo is uploaded (e.g. Code2, Database)',
|
|
101
|
+
overrides: (field) => ({
|
|
102
|
+
...field,
|
|
103
|
+
label: 'Lucide Icon (fallback)',
|
|
104
|
+
admin: { ...field.admin, width: '40%' }
|
|
105
|
+
})
|
|
106
|
+
}),
|
|
107
|
+
{
|
|
108
|
+
name: 'label',
|
|
109
|
+
type: 'text',
|
|
110
|
+
label: 'Label',
|
|
111
|
+
admin: {
|
|
112
|
+
description: 'e.g. "React", "TypeScript"',
|
|
113
|
+
width: '40%'
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'showLabel',
|
|
118
|
+
type: 'checkbox',
|
|
119
|
+
label: 'Show Label',
|
|
120
|
+
defaultValue: true,
|
|
121
|
+
admin: { width: '20%' }
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
label: 'Settings',
|
|
133
|
+
fields: [
|
|
134
|
+
{
|
|
135
|
+
name: 'sectionId',
|
|
136
|
+
type: 'text',
|
|
137
|
+
label: 'Section ID',
|
|
138
|
+
admin: {
|
|
139
|
+
description: 'Unique ID for anchor linking (e.g. "tech-stack")'
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'customCSS',
|
|
144
|
+
type: 'code',
|
|
145
|
+
label: 'Custom CSS',
|
|
146
|
+
admin: { language: 'css' }
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "block",
|
|
3
|
+
"name": "DD-TechStack",
|
|
4
|
+
"label": "Studio Tech Stack",
|
|
5
|
+
"sourceName": "StudioTechStack",
|
|
6
|
+
"portableName": "DD-TechStack",
|
|
7
|
+
"slug": "DD-TechStack",
|
|
8
|
+
"version": "0.0.1",
|
|
9
|
+
"source": {
|
|
10
|
+
"repo": "Hadizainal/DesignsDustedv3",
|
|
11
|
+
"originalPath": "src/blocks/StudioTechStack",
|
|
12
|
+
"originalSlug": "studioTechStack",
|
|
13
|
+
"originalConfigExport": "StudioTechStack",
|
|
14
|
+
"originalComponentExport": "StudioTechStack",
|
|
15
|
+
"originalInterfaceName": "StudioTechStack"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
{
|
|
19
|
+
"from": "Component.tsx",
|
|
20
|
+
"to": "src/blocks/DD-TechStack/Component.tsx"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"from": "config.ts",
|
|
24
|
+
"to": "src/blocks/DD-TechStack/config.ts"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"dependencies": [
|
|
28
|
+
{
|
|
29
|
+
"name": "@radix-ui/react-slot"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "@tanstack/react-virtual"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "class-variance-authority"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "fuse.js"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "lucide-react"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"sharedFiles": [
|
|
45
|
+
{
|
|
46
|
+
"from": "templates/shared/fields/lucide-icon-picker",
|
|
47
|
+
"to": "src/fields/lucide-icon-picker",
|
|
48
|
+
"required": true,
|
|
49
|
+
"safeToCopy": true
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"from": "templates/shared/components/ui/input.tsx",
|
|
53
|
+
"to": "src/components/ui/input.tsx",
|
|
54
|
+
"required": true,
|
|
55
|
+
"safeToCopy": true
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"from": "templates/shared/components/ui/button.tsx",
|
|
59
|
+
"to": "src/components/ui/button.tsx",
|
|
60
|
+
"required": true,
|
|
61
|
+
"safeToCopy": true
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"requiredProjectFiles": [
|
|
65
|
+
{
|
|
66
|
+
"path": "src/utilities/ui.ts",
|
|
67
|
+
"reason": "Shared Input/Button components use the cn utility from @/utilities/ui."
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"optionalProjectFiles": [],
|
|
71
|
+
"collectionRegistration": {
|
|
72
|
+
"enabled": true,
|
|
73
|
+
"collections": [
|
|
74
|
+
"Pages",
|
|
75
|
+
"Posts"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"renderBlocksRegistration": {
|
|
79
|
+
"enabled": true
|
|
80
|
+
},
|
|
81
|
+
"renderBlocksBehavior": {
|
|
82
|
+
"wrapperless": false
|
|
83
|
+
},
|
|
84
|
+
"pageRouteIntegration": {
|
|
85
|
+
"required": false,
|
|
86
|
+
"manualSteps": []
|
|
87
|
+
},
|
|
88
|
+
"manual": [
|
|
89
|
+
"Confirm the target Payload project has compatible @payloadcms/ui admin packages for the lucide icon picker field.",
|
|
90
|
+
"Register DDTechStack in Pages/Posts blocks arrays if automatic registration was skipped.",
|
|
91
|
+
"Register DD-TechStack in RenderBlocks if automatic registration was skipped.",
|
|
92
|
+
"Run payload generate:types after schema changes.",
|
|
93
|
+
"Run payload generate:importmap if admin/component imports changed."
|
|
94
|
+
],
|
|
95
|
+
"manualSteps": [
|
|
96
|
+
"Confirm the target Payload project has compatible @payloadcms/ui admin packages for the lucide icon picker field.",
|
|
97
|
+
"Register DDTechStack in Pages/Posts blocks arrays if automatic registration was skipped.",
|
|
98
|
+
"Register DD-TechStack in RenderBlocks if automatic registration was skipped.",
|
|
99
|
+
"Run payload generate:types after schema changes.",
|
|
100
|
+
"Run payload generate:importmap if admin/component imports changed."
|
|
101
|
+
],
|
|
102
|
+
"patchTargets": {
|
|
103
|
+
"blockConfigRegistration": {
|
|
104
|
+
"description": "Installer can add import { DDTechStack } from '@/blocks/DD-TechStack/config' and add DDTechStack to clear Pages/Posts blocks arrays."
|
|
105
|
+
},
|
|
106
|
+
"renderBlocksRegistration": {
|
|
107
|
+
"description": "Installer can register DD-TechStack in src/blocks/**/RenderBlocks.tsx when the blockComponents map is clear."
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"notes": [
|
|
111
|
+
"Converted from DesignsDustedv3 StudioTechStack.",
|
|
112
|
+
"Generated Payload type imports are replaced with portable fallback types when needed.",
|
|
113
|
+
"Installer must not overwrite existing target files.",
|
|
114
|
+
"Includes shared lucide-icon-picker field files because config imports @/fields/lucide-icon-picker/field."
|
|
115
|
+
],
|
|
116
|
+
"exportName": "DDTechStack",
|
|
117
|
+
"interfaceName": "DDTechStack"
|
|
118
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { Star } from "lucide-react";
|
|
6
|
+
import { motion, type Variants } from "framer-motion";
|
|
7
|
+
export type DDTestimonialsProps = Record<string, any>;
|
|
8
|
+
type Media = { url?: string | null };
|
|
9
|
+
type Testimonial = {
|
|
10
|
+
id?: string | null;
|
|
11
|
+
title?: string | null;
|
|
12
|
+
content?: unknown;
|
|
13
|
+
designation?: unknown;
|
|
14
|
+
companyLogo?: Media | string | null;
|
|
15
|
+
fallbackAvatarUrl?: string | null;
|
|
16
|
+
rating?: number | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function isLocalImageSrc(src?: string | null): src is string {
|
|
20
|
+
return typeof src === "string" && src.startsWith("/api/media/file/");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const containerVariants: Variants = {
|
|
24
|
+
hidden: {},
|
|
25
|
+
show: { transition: { staggerChildren: 0.12, delayChildren: 0.05 } },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const cardVariants: Variants = {
|
|
29
|
+
hidden: { opacity: 0, y: 24 },
|
|
30
|
+
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const DDTestimonials: React.FC<DDTestimonialsProps> = ({
|
|
34
|
+
eyebrow,
|
|
35
|
+
headline,
|
|
36
|
+
testimonials,
|
|
37
|
+
sectionId,
|
|
38
|
+
customCSS,
|
|
39
|
+
}) => {
|
|
40
|
+
const populated = ((testimonials ?? []) as unknown[]).filter(
|
|
41
|
+
(t): t is Testimonial => typeof t === "object" && t !== null,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
47
|
+
|
|
48
|
+
<div id={sectionId ?? "testimonials"} className="py-24">
|
|
49
|
+
<div className="max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6">
|
|
50
|
+
{/* Header */}
|
|
51
|
+
<div className="text-center max-w-xl mx-auto mb-14">
|
|
52
|
+
{eyebrow && (
|
|
53
|
+
<p className="text-[11.5px] font-semibold text-stone-400 tracking-widest uppercase mb-3 top-caption">
|
|
54
|
+
{eyebrow}
|
|
55
|
+
</p>
|
|
56
|
+
)}
|
|
57
|
+
<h2 className="text-3xl font-semibold text-stone-900 dark:text-stone-50 leading-tight">
|
|
58
|
+
{headline}
|
|
59
|
+
</h2>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Testimonials */}
|
|
63
|
+
{populated.length > 0 && (
|
|
64
|
+
<motion.div
|
|
65
|
+
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
|
66
|
+
variants={containerVariants}
|
|
67
|
+
initial="hidden"
|
|
68
|
+
whileInView="show"
|
|
69
|
+
viewport={{ once: false, amount: 0.15 }}
|
|
70
|
+
>
|
|
71
|
+
{populated.map((t, i) => {
|
|
72
|
+
const avatarUrl =
|
|
73
|
+
typeof t.companyLogo === "object" &&
|
|
74
|
+
t.companyLogo !== null &&
|
|
75
|
+
"url" in t.companyLogo
|
|
76
|
+
? t.companyLogo.url
|
|
77
|
+
: (t.fallbackAvatarUrl ?? null);
|
|
78
|
+
const safeAvatarUrl = isLocalImageSrc(avatarUrl)
|
|
79
|
+
? avatarUrl
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
const stars = Math.min(5, Math.max(1, t.rating ?? 5));
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<motion.div
|
|
86
|
+
key={t.id ?? i}
|
|
87
|
+
className="rounded-2xl bg-stone-100 dark:bg-stone-900 border border-stone-100 dark:border-stone-800 p-8 flex flex-col"
|
|
88
|
+
variants={cardVariants}
|
|
89
|
+
style={{ cornerShape: "squircle" } as React.CSSProperties}
|
|
90
|
+
>
|
|
91
|
+
{/* Stars */}
|
|
92
|
+
<div className="flex items-center gap-1 mb-5">
|
|
93
|
+
{Array.from({ length: stars }).map((_, si) => (
|
|
94
|
+
<Star
|
|
95
|
+
key={si}
|
|
96
|
+
size={14}
|
|
97
|
+
className="text-amber-400"
|
|
98
|
+
fill="currentColor"
|
|
99
|
+
/>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Quote */}
|
|
104
|
+
{t.content ? (
|
|
105
|
+
<div className="text-sm text-stone-400 leading-relaxed flex-1 mb-6">
|
|
106
|
+
{/* TODO: Reconnect this project's RichText renderer for testimonial content. */}
|
|
107
|
+
<span>
|
|
108
|
+
Rich text content is configured for this testimonial.
|
|
109
|
+
</span>
|
|
110
|
+
</div>
|
|
111
|
+
) : null}
|
|
112
|
+
|
|
113
|
+
{/* Author / Company */}
|
|
114
|
+
<div className="flex items-center gap-3 pt-5 border-t border-stone-800">
|
|
115
|
+
{safeAvatarUrl && (
|
|
116
|
+
<div className="w-10 h-10 rounded-full overflow-hidden shrink-0 relative">
|
|
117
|
+
<Image
|
|
118
|
+
src={safeAvatarUrl}
|
|
119
|
+
alt={t.title ?? ""}
|
|
120
|
+
fill
|
|
121
|
+
sizes="40px"
|
|
122
|
+
className="object-cover object-top"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
<div>
|
|
127
|
+
<div className="text-sm font-semibold text-stone-800 dark:text-stone-50">
|
|
128
|
+
{t.title}
|
|
129
|
+
</div>
|
|
130
|
+
{t.designation ? (
|
|
131
|
+
<div className="text-xs text-stone-300">
|
|
132
|
+
{/* TODO: Reconnect RichText for designation if needed. */}
|
|
133
|
+
<span>Designation content configured.</span>
|
|
134
|
+
</div>
|
|
135
|
+
) : null}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</motion.div>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</motion.div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
};
|