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.
- package/README.md +967 -3
- package/package.json +2 -2
- package/src/commands/add.js +143 -45
- package/src/commands/list.js +2 -2
- package/src/installers/checkRequirements.js +1 -0
- package/src/installers/copyTemplateFiles.js +23 -2
- package/src/installers/patchPayloadConfigCollections.js +92 -0
- package/src/installers/writeInstalledBlockReadme.js +17 -4
- package/src/registry/listTemplates.js +16 -7
- package/src/registry/loadTemplateManifest.js +20 -11
- package/src/registry/validateTemplateManifest.js +6 -5
- package/src/utils/paths.js +7 -0
- package/templates/blocks/DD-BookCall/Component.tsx +535 -573
- package/templates/blocks/DD-CardTemplate01/Component.tsx +45 -49
- package/templates/blocks/DD-Carousel-A/Component.tsx +249 -266
- package/templates/blocks/DD-Carousel-B/Component.tsx +403 -432
- package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -272
- package/templates/blocks/DD-Contact/Component.tsx +434 -439
- package/templates/blocks/DD-Contact/config.ts +8 -3
- package/templates/blocks/DD-FeatCat/Component.tsx +302 -361
- package/templates/blocks/DD-FeatCat/config.ts +201 -204
- package/templates/blocks/DD-FeatStrip/Component.tsx +171 -201
- package/templates/blocks/DD-Hero/Component.tsx +297 -317
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +244 -303
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +202 -228
- package/templates/blocks/DD-MasonaryMedia/config.ts +13 -14
- package/templates/blocks/DD-Pricing/Component.tsx +372 -380
- package/templates/blocks/DD-Process/Component.tsx +149 -155
- package/templates/blocks/DD-Section/Component.tsx +266 -327
- package/templates/blocks/DD-Services/Component.tsx +176 -188
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -317
- package/templates/blocks/DD-Slider-A/Component.tsx +237 -252
- package/templates/blocks/DD-StackCards/Component.tsx +137 -160
- package/templates/blocks/DD-Team/Component.tsx +247 -265
- package/templates/blocks/DD-TechStack/Component.tsx +57 -72
- package/templates/blocks/DD-Testimonials/Component.tsx +147 -147
- package/templates/blocks/DD-Testimonials/config.ts +66 -66
- package/templates/blocks/DD-Work/Component.tsx +315 -328
- package/templates/blocks/DD-Work-B/Component.tsx +294 -320
- package/templates/collections/Testimonials/README.md +11 -0
- package/templates/collections/Testimonials/Testimonials.ts +161 -0
- package/templates/collections/Testimonials/manifest.json +55 -0
- package/templates/components/RichText/README.md +13 -0
- package/templates/components/RichText/converter/componentConverter/blocks.tsx +43 -0
- package/templates/components/RichText/converter/componentConverter/types.ts +11 -0
- package/templates/components/RichText/converter/index.tsx +18 -0
- package/templates/components/RichText/converter/internalLinks.tsx +45 -0
- package/templates/components/RichText/converter/textConverter.tsx +27 -0
- package/templates/components/RichText/index.tsx +35 -0
- package/templates/components/RichText/manifest.json +80 -0
|
@@ -1,55 +1,49 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
2
|
|
|
3
|
-
import React from
|
|
4
|
-
import Image from
|
|
5
|
-
import * as LucideIcons from
|
|
6
|
-
export type DDTechStackProps = Record<string, any
|
|
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
7
|
|
|
8
8
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
9
|
-
type MediaObj = { url?: string | null; alt?: string | null }
|
|
9
|
+
type MediaObj = { url?: string | null; alt?: string | null }
|
|
10
10
|
type TechStackItem = {
|
|
11
|
-
image?: string | MediaObj | null
|
|
12
|
-
icon?: string | null
|
|
13
|
-
label?: string | null
|
|
14
|
-
showLabel?: boolean | null
|
|
15
|
-
}
|
|
11
|
+
image?: string | MediaObj | null
|
|
12
|
+
icon?: string | null
|
|
13
|
+
label?: string | null
|
|
14
|
+
showLabel?: boolean | null
|
|
15
|
+
}
|
|
16
16
|
type TechStackRow = {
|
|
17
|
-
duration?: number | null
|
|
18
|
-
direction?:
|
|
19
|
-
items?: TechStackItem[] | null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getMediaUrl(
|
|
23
|
-
media
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (typeof media === "string") return media;
|
|
27
|
-
return media.url ?? null;
|
|
17
|
+
duration?: number | null
|
|
18
|
+
direction?: 'left' | 'right' | null
|
|
19
|
+
items?: TechStackItem[] | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getMediaUrl(media: string | MediaObj | null | undefined): string | null {
|
|
23
|
+
if (!media) return null
|
|
24
|
+
if (typeof media === 'string') return media
|
|
25
|
+
return media.url ?? null
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
function getMediaAlt(
|
|
31
|
-
media
|
|
32
|
-
)
|
|
33
|
-
if (!media || typeof media === "string") return null;
|
|
34
|
-
return (media as MediaObj).alt ?? null;
|
|
28
|
+
function getMediaAlt(media: string | MediaObj | null | undefined): string | null {
|
|
29
|
+
if (!media || typeof media === 'string') return null
|
|
30
|
+
return (media as MediaObj).alt ?? null
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
// ─── Dynamic Lucide Icon ──────────────────────────────────────────────────────
|
|
38
34
|
function isLocalImageSrc(src?: string | null): src is string {
|
|
39
|
-
return typeof src ===
|
|
35
|
+
return typeof src === 'string' && src.startsWith('/api/media/file/')
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
function DynamicIcon({
|
|
43
39
|
name,
|
|
44
40
|
size = 36,
|
|
45
41
|
...props
|
|
46
|
-
}: { name?: string | null } & Omit<LucideIcons.LucideProps,
|
|
47
|
-
if (!name) return null
|
|
48
|
-
const Icon = (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!Icon) return null;
|
|
52
|
-
return <Icon size={size} {...props} />;
|
|
42
|
+
}: { name?: string | null } & Omit<LucideIcons.LucideProps, 'name'>) {
|
|
43
|
+
if (!name) return null
|
|
44
|
+
const Icon = (LucideIcons as unknown as Record<string, React.FC<LucideIcons.LucideProps>>)[name]
|
|
45
|
+
if (!Icon) return null
|
|
46
|
+
return <Icon size={size} {...props} />
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
// ─── Tech Item Card ───────────────────────────────────────────────────────────
|
|
@@ -60,13 +54,13 @@ function TechItem({
|
|
|
60
54
|
label,
|
|
61
55
|
showLabel,
|
|
62
56
|
}: {
|
|
63
|
-
imageUrl?: string | null
|
|
64
|
-
imageAlt?: string | null
|
|
65
|
-
icon?: string | null
|
|
66
|
-
label?: string | null
|
|
67
|
-
showLabel?: boolean | null
|
|
57
|
+
imageUrl?: string | null
|
|
58
|
+
imageAlt?: string | null
|
|
59
|
+
icon?: string | null
|
|
60
|
+
label?: string | null
|
|
61
|
+
showLabel?: boolean | null
|
|
68
62
|
}) {
|
|
69
|
-
const safeImageUrl = isLocalImageSrc(imageUrl) ? imageUrl : null
|
|
63
|
+
const safeImageUrl = isLocalImageSrc(imageUrl) ? imageUrl : null
|
|
70
64
|
|
|
71
65
|
return (
|
|
72
66
|
<div className="relative group/tech text-center shrink-0">
|
|
@@ -74,7 +68,7 @@ function TechItem({
|
|
|
74
68
|
{safeImageUrl ? (
|
|
75
69
|
<Image
|
|
76
70
|
src={safeImageUrl}
|
|
77
|
-
alt={imageAlt ?? label ??
|
|
71
|
+
alt={imageAlt ?? label ?? 'Tech logo'}
|
|
78
72
|
width={40}
|
|
79
73
|
height={40}
|
|
80
74
|
loading="lazy"
|
|
@@ -85,12 +79,10 @@ function TechItem({
|
|
|
85
79
|
)}
|
|
86
80
|
</div>
|
|
87
81
|
{showLabel !== false && label && (
|
|
88
|
-
<span className="text-sm whitespace-nowrap text-[#9A9790] mt-2 block">
|
|
89
|
-
{label}
|
|
90
|
-
</span>
|
|
82
|
+
<span className="text-sm whitespace-nowrap text-[#9A9790] mt-2 block">{label}</span>
|
|
91
83
|
)}
|
|
92
84
|
</div>
|
|
93
|
-
)
|
|
85
|
+
)
|
|
94
86
|
}
|
|
95
87
|
|
|
96
88
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
@@ -101,15 +93,14 @@ export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
|
101
93
|
sectionId,
|
|
102
94
|
customCSS,
|
|
103
95
|
}) => {
|
|
104
|
-
const rows = (techRows ?? []) as TechStackRow[]
|
|
105
|
-
if (!rows.length) return null
|
|
96
|
+
const rows = (techRows ?? []) as TechStackRow[]
|
|
97
|
+
if (!rows.length) return null
|
|
106
98
|
|
|
107
99
|
const rowMaskStyle: React.CSSProperties = {
|
|
108
|
-
maskImage:
|
|
109
|
-
"linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%)",
|
|
100
|
+
maskImage: 'linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%)',
|
|
110
101
|
WebkitMaskImage:
|
|
111
|
-
|
|
112
|
-
}
|
|
102
|
+
'linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%)',
|
|
103
|
+
}
|
|
113
104
|
|
|
114
105
|
return (
|
|
115
106
|
<section
|
|
@@ -120,10 +111,9 @@ export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
|
120
111
|
|
|
121
112
|
{(eyebrow || heading) && (
|
|
122
113
|
<div className="mb-12">
|
|
114
|
+
#BlockName-DDTechStack
|
|
123
115
|
{eyebrow && (
|
|
124
|
-
<p className="text-sm uppercase tracking-widest text-[#9A9790] mb-2">
|
|
125
|
-
{eyebrow}
|
|
126
|
-
</p>
|
|
116
|
+
<p className="text-sm uppercase tracking-widest text-[#9A9790] mb-2">{eyebrow}</p>
|
|
127
117
|
)}
|
|
128
118
|
{heading && (
|
|
129
119
|
<h2
|
|
@@ -138,14 +128,14 @@ export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
|
138
128
|
|
|
139
129
|
<div className="relative">
|
|
140
130
|
{rows.map((row, rowIndex) => {
|
|
141
|
-
const duration = row.duration ?? 40
|
|
142
|
-
const direction = row.direction ??
|
|
143
|
-
const items = row.items ?? []
|
|
131
|
+
const duration = row.duration ?? 40
|
|
132
|
+
const direction = row.direction ?? 'left'
|
|
133
|
+
const items = row.items ?? []
|
|
144
134
|
|
|
145
|
-
if (items.length === 0) return null
|
|
135
|
+
if (items.length === 0) return null
|
|
146
136
|
|
|
147
137
|
// Quadruple items for a seamless infinite loop
|
|
148
|
-
const duplicated = [...items, ...items, ...items, ...items]
|
|
138
|
+
const duplicated = [...items, ...items, ...items, ...items]
|
|
149
139
|
|
|
150
140
|
return (
|
|
151
141
|
<div
|
|
@@ -157,17 +147,12 @@ export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
|
157
147
|
className="flex shrink-0 justify-around gap-8 flex-row animate-marquee"
|
|
158
148
|
style={{
|
|
159
149
|
animationDuration: `${duration}s`,
|
|
160
|
-
animationDirection:
|
|
161
|
-
direction === "right" ? "reverse" : "normal",
|
|
150
|
+
animationDirection: direction === 'right' ? 'reverse' : 'normal',
|
|
162
151
|
}}
|
|
163
152
|
>
|
|
164
153
|
{duplicated.map((item, idx) => {
|
|
165
|
-
const imgUrl = getMediaUrl(
|
|
166
|
-
|
|
167
|
-
);
|
|
168
|
-
const imgAlt = getMediaAlt(
|
|
169
|
-
item.image as string | MediaObj | null,
|
|
170
|
-
);
|
|
154
|
+
const imgUrl = getMediaUrl(item.image as string | MediaObj | null)
|
|
155
|
+
const imgAlt = getMediaAlt(item.image as string | MediaObj | null)
|
|
171
156
|
return (
|
|
172
157
|
<TechItem
|
|
173
158
|
key={idx}
|
|
@@ -177,13 +162,13 @@ export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
|
177
162
|
label={item.label}
|
|
178
163
|
showLabel={item.showLabel}
|
|
179
164
|
/>
|
|
180
|
-
)
|
|
165
|
+
)
|
|
181
166
|
})}
|
|
182
167
|
</div>
|
|
183
168
|
</div>
|
|
184
|
-
)
|
|
169
|
+
)
|
|
185
170
|
})}
|
|
186
171
|
</div>
|
|
187
172
|
</section>
|
|
188
|
-
)
|
|
189
|
-
}
|
|
173
|
+
)
|
|
174
|
+
}
|
|
@@ -1,147 +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
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
t.companyLogo
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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>
|
|
134
|
-
</div>
|
|
135
|
-
) : null}
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
</motion.div>
|
|
139
|
-
);
|
|
140
|
-
})}
|
|
141
|
-
</motion.div>
|
|
142
|
-
)}
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
</>
|
|
146
|
-
);
|
|
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 */}#BlockName-DDTestimonials
|
|
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
|
+
{/* Testimonials */}
|
|
62
|
+
{populated.length > 0 && (
|
|
63
|
+
<motion.div
|
|
64
|
+
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
|
65
|
+
variants={containerVariants}
|
|
66
|
+
initial="hidden"
|
|
67
|
+
whileInView="show"
|
|
68
|
+
viewport={{ once: false, amount: 0.15 }}
|
|
69
|
+
>
|
|
70
|
+
{populated.map((t, i) => {
|
|
71
|
+
const avatarUrl =
|
|
72
|
+
typeof t.companyLogo === "object" &&
|
|
73
|
+
t.companyLogo !== null &&
|
|
74
|
+
"url" in t.companyLogo
|
|
75
|
+
? t.companyLogo.url
|
|
76
|
+
: (t.fallbackAvatarUrl ?? null);
|
|
77
|
+
const safeAvatarUrl = isLocalImageSrc(avatarUrl)
|
|
78
|
+
? avatarUrl
|
|
79
|
+
: null;
|
|
80
|
+
|
|
81
|
+
const stars = Math.min(5, Math.max(1, t.rating ?? 5));
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<motion.div
|
|
85
|
+
key={t.id ?? i}
|
|
86
|
+
className="rounded-2xl bg-stone-100 dark:bg-stone-900 border border-stone-100 dark:border-stone-800 p-8 flex flex-col"
|
|
87
|
+
variants={cardVariants}
|
|
88
|
+
style={{ cornerShape: "squircle" } as React.CSSProperties}
|
|
89
|
+
>
|
|
90
|
+
{/* Stars */}
|
|
91
|
+
<div className="flex items-center gap-1 mb-5">
|
|
92
|
+
{Array.from({ length: stars }).map((_, si) => (
|
|
93
|
+
<Star
|
|
94
|
+
key={si}
|
|
95
|
+
size={14}
|
|
96
|
+
className="text-amber-400"
|
|
97
|
+
fill="currentColor"
|
|
98
|
+
/>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Quote */}
|
|
103
|
+
{t.content ? (
|
|
104
|
+
<div className="text-sm text-stone-400 leading-relaxed flex-1 mb-6">
|
|
105
|
+
{/* TODO: Reconnect this project's RichText renderer for testimonial content. */}
|
|
106
|
+
<span>
|
|
107
|
+
Reconnect this project's RichText renderer for
|
|
108
|
+
testimonial content.
|
|
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>Reconnect RichText for designation</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
|
+
};
|