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,120 @@
|
|
|
1
|
+
import type { Block } from "payload";
|
|
2
|
+
|
|
3
|
+
export const DDTestimonials: Block = {
|
|
4
|
+
slug: "DD-Testimonials",
|
|
5
|
+
interfaceName: "DDTestimonials",
|
|
6
|
+
labels: {
|
|
7
|
+
singular: "Studio Testimonials",
|
|
8
|
+
plural: "Studio Testimonials Sections",
|
|
9
|
+
},
|
|
10
|
+
fields: [
|
|
11
|
+
{
|
|
12
|
+
type: "tabs",
|
|
13
|
+
tabs: [
|
|
14
|
+
{
|
|
15
|
+
label: "Header",
|
|
16
|
+
fields: [
|
|
17
|
+
{
|
|
18
|
+
name: "eyebrow",
|
|
19
|
+
type: "text",
|
|
20
|
+
label: "Eyebrow Label",
|
|
21
|
+
defaultValue: "Client Stories",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "headline",
|
|
25
|
+
type: "text",
|
|
26
|
+
label: "Headline",
|
|
27
|
+
required: true,
|
|
28
|
+
defaultValue: "Don't take our word for it",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: "Testimonials",
|
|
34
|
+
fields: [
|
|
35
|
+
// Reconnect option:
|
|
36
|
+
// If the target project has a Testimonials collection, replace the standalone
|
|
37
|
+
// array below with this relationship field.
|
|
38
|
+
// {
|
|
39
|
+
// name: 'testimonials',
|
|
40
|
+
// type: 'relationship',
|
|
41
|
+
// relationTo: 'testimonials',
|
|
42
|
+
// hasMany: true,
|
|
43
|
+
// label: 'Select Testimonials',
|
|
44
|
+
// admin: {
|
|
45
|
+
// description:
|
|
46
|
+
// 'Pick from the Testimonials collection. Fields used: quote, author, role, avatar, fallbackAvatarUrl, rating.'
|
|
47
|
+
// }
|
|
48
|
+
// },
|
|
49
|
+
{
|
|
50
|
+
name: "testimonials",
|
|
51
|
+
type: "array",
|
|
52
|
+
label: "Testimonials",
|
|
53
|
+
fields: [
|
|
54
|
+
{
|
|
55
|
+
name: "title",
|
|
56
|
+
type: "text",
|
|
57
|
+
label: "Author",
|
|
58
|
+
required: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "content",
|
|
62
|
+
type: "richText",
|
|
63
|
+
label: "Quote",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "designation",
|
|
67
|
+
type: "richText",
|
|
68
|
+
label: "Designation",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "companyLogo",
|
|
72
|
+
type: "upload",
|
|
73
|
+
relationTo: "media",
|
|
74
|
+
label: "Avatar / Logo",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "fallbackAvatarUrl",
|
|
78
|
+
type: "text",
|
|
79
|
+
label: "Fallback Avatar URL",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "rating",
|
|
83
|
+
type: "number",
|
|
84
|
+
label: "Rating",
|
|
85
|
+
defaultValue: 5,
|
|
86
|
+
min: 1,
|
|
87
|
+
max: 5,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
defaultValue: [
|
|
91
|
+
{
|
|
92
|
+
title: "DesignsDusted Client",
|
|
93
|
+
fallbackAvatarUrl: "",
|
|
94
|
+
rating: 5,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: "Settings",
|
|
102
|
+
fields: [
|
|
103
|
+
{
|
|
104
|
+
name: "sectionId",
|
|
105
|
+
type: "text",
|
|
106
|
+
label: "Section ID",
|
|
107
|
+
defaultValue: "testimonials",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "customCSS",
|
|
111
|
+
type: "code",
|
|
112
|
+
label: "Custom CSS",
|
|
113
|
+
admin: { language: "css" },
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "block",
|
|
3
|
+
"name": "DD-Testimonials",
|
|
4
|
+
"label": "Studio Testimonials Block",
|
|
5
|
+
"sourceName": "StudioTestimonialsBlock",
|
|
6
|
+
"portableName": "DD-Testimonials",
|
|
7
|
+
"slug": "DD-Testimonials",
|
|
8
|
+
"version": "0.0.1",
|
|
9
|
+
"source": {
|
|
10
|
+
"repo": "Hadizainal/DesignsDustedv3",
|
|
11
|
+
"originalPath": "src/blocks/StudioTestimonialsBlock",
|
|
12
|
+
"originalSlug": "studioTestimonialsBlock",
|
|
13
|
+
"originalConfigExport": "StudioTestimonialsBlock",
|
|
14
|
+
"originalComponentExport": "StudioTestimonialsBlock",
|
|
15
|
+
"originalInterfaceName": "StudioTestimonialsBlock"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
{
|
|
19
|
+
"from": "Component.tsx",
|
|
20
|
+
"to": "src/blocks/DD-Testimonials/Component.tsx"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"from": "config.ts",
|
|
24
|
+
"to": "src/blocks/DD-Testimonials/config.ts"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"dependencies": [
|
|
28
|
+
{
|
|
29
|
+
"name": "framer-motion"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "lucide-react"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"sharedFiles": [],
|
|
36
|
+
"requiredProjectFiles": [],
|
|
37
|
+
"optionalProjectFiles": [],
|
|
38
|
+
"collectionRegistration": {
|
|
39
|
+
"enabled": true,
|
|
40
|
+
"collections": [
|
|
41
|
+
"Pages",
|
|
42
|
+
"Posts"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"renderBlocksRegistration": {
|
|
46
|
+
"enabled": true
|
|
47
|
+
},
|
|
48
|
+
"renderBlocksBehavior": {
|
|
49
|
+
"wrapperless": false
|
|
50
|
+
},
|
|
51
|
+
"pageRouteIntegration": {
|
|
52
|
+
"required": false,
|
|
53
|
+
"manualSteps": []
|
|
54
|
+
},
|
|
55
|
+
"manual": [
|
|
56
|
+
"RichText rendering was replaced with fallback markup; reconnect the target project RichText renderer manually if needed.",
|
|
57
|
+
"If your project has a Testimonials collection, replace the standalone testimonials array with the commented relationship field and regenerate Payload types.",
|
|
58
|
+
"Register DDTestimonials in Pages/Posts blocks arrays if automatic registration was skipped.",
|
|
59
|
+
"Register DD-Testimonials in RenderBlocks if automatic registration was skipped.",
|
|
60
|
+
"Run payload generate:types after schema changes.",
|
|
61
|
+
"Run payload generate:importmap if admin/component imports changed."
|
|
62
|
+
],
|
|
63
|
+
"manualSteps": [
|
|
64
|
+
"RichText rendering was replaced with fallback markup; reconnect the target project RichText renderer manually if needed.",
|
|
65
|
+
"If your project has a Testimonials collection, replace the standalone testimonials array with the commented relationship field and regenerate Payload types.",
|
|
66
|
+
"Register DDTestimonials in Pages/Posts blocks arrays if automatic registration was skipped.",
|
|
67
|
+
"Register DD-Testimonials in RenderBlocks if automatic registration was skipped.",
|
|
68
|
+
"Run payload generate:types after schema changes.",
|
|
69
|
+
"Run payload generate:importmap if admin/component imports changed."
|
|
70
|
+
],
|
|
71
|
+
"patchTargets": {
|
|
72
|
+
"blockConfigRegistration": {
|
|
73
|
+
"description": "Installer can add import { DDTestimonials } from '@/blocks/DD-Testimonials/config' and add DDTestimonials to clear Pages/Posts blocks arrays."
|
|
74
|
+
},
|
|
75
|
+
"renderBlocksRegistration": {
|
|
76
|
+
"description": "Installer can register DD-Testimonials in src/blocks/**/RenderBlocks.tsx when the blockComponents map is clear."
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"notes": [
|
|
80
|
+
"Converted from DesignsDustedv3 StudioTestimonialsBlock.",
|
|
81
|
+
"Generated Payload type imports are replaced with portable fallback types when needed.",
|
|
82
|
+
"Installer must not overwrite existing target files.",
|
|
83
|
+
"Project-specific RichText setup is intentionally not bundled."
|
|
84
|
+
],
|
|
85
|
+
"exportName": "DDTestimonials",
|
|
86
|
+
"interfaceName": "DDTestimonials"
|
|
87
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/* eslint-disable @next/next/no-img-element */
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useRef, useState } from "react";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
import { BarChart2, ExternalLink } from "lucide-react";
|
|
7
|
+
import { motion, type Variants } from "framer-motion";
|
|
8
|
+
|
|
9
|
+
import { cn } from "@/utilities/ui";
|
|
10
|
+
export type DDWorkProps = Record<string, any>;
|
|
11
|
+
type Media = { url?: string | null; alt?: string | null; [key: string]: any };
|
|
12
|
+
type CategoryItem = { id?: string | null; label?: string | null };
|
|
13
|
+
type TagItem = { id?: string | null; tag?: string | null };
|
|
14
|
+
type ProjectItem = {
|
|
15
|
+
id?: string | null;
|
|
16
|
+
title?: string | null;
|
|
17
|
+
category?: string | null;
|
|
18
|
+
description?: string | null;
|
|
19
|
+
highlight?: string | null;
|
|
20
|
+
year?: string | null;
|
|
21
|
+
image?: Media | string | null;
|
|
22
|
+
imageFallbackUrl?: string | null;
|
|
23
|
+
tags?: TagItem[] | null;
|
|
24
|
+
link?: string | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function isLocalImageSrc(src?: string | null): src is string {
|
|
28
|
+
return typeof src === "string" && src.startsWith("/api/media/file/");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── SpotlightCard ────────────────────────────────────────────────────────────
|
|
32
|
+
interface SpotlightCardProps {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
className?: string;
|
|
35
|
+
spotlightColor?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function SpotlightCard({
|
|
39
|
+
children,
|
|
40
|
+
className,
|
|
41
|
+
spotlightColor = "rgba(214,211,209,0.08)",
|
|
42
|
+
}: SpotlightCardProps) {
|
|
43
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
|
|
45
|
+
|
|
46
|
+
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
47
|
+
if (!cardRef.current) return;
|
|
48
|
+
|
|
49
|
+
const rect = cardRef.current.getBoundingClientRect();
|
|
50
|
+
|
|
51
|
+
setPos({
|
|
52
|
+
x: e.clientX - rect.left,
|
|
53
|
+
y: e.clientY - rect.top,
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleMouseLeave = () => setPos(null);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
ref={cardRef}
|
|
62
|
+
className={cn("relative overflow-hidden", className)}
|
|
63
|
+
onMouseMove={handleMouseMove}
|
|
64
|
+
onMouseLeave={handleMouseLeave}
|
|
65
|
+
>
|
|
66
|
+
{pos && (
|
|
67
|
+
<div
|
|
68
|
+
className="pointer-events-none absolute inset-0 z-0 transition-opacity duration-300"
|
|
69
|
+
style={{
|
|
70
|
+
background: `radial-gradient(400px circle at ${pos.x}px ${pos.y}px, ${spotlightColor}, transparent 60%)`,
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<div className="relative z-10">{children}</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Animation variants ───────────────────────────────────────────────────────
|
|
81
|
+
const containerVariants: Variants = {
|
|
82
|
+
hidden: {},
|
|
83
|
+
show: {
|
|
84
|
+
transition: {
|
|
85
|
+
staggerChildren: 0.1,
|
|
86
|
+
delayChildren: 0.05,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const itemVariants: Variants = {
|
|
92
|
+
hidden: {
|
|
93
|
+
opacity: 0,
|
|
94
|
+
y: 24,
|
|
95
|
+
},
|
|
96
|
+
show: {
|
|
97
|
+
opacity: 1,
|
|
98
|
+
y: 0,
|
|
99
|
+
transition: {
|
|
100
|
+
duration: 0.45,
|
|
101
|
+
ease: "easeOut",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
107
|
+
export const DDWork: React.FC<DDWorkProps> = ({
|
|
108
|
+
eyebrow,
|
|
109
|
+
headlineMain,
|
|
110
|
+
headlineAccent,
|
|
111
|
+
categories,
|
|
112
|
+
projects,
|
|
113
|
+
sectionId,
|
|
114
|
+
customCSS,
|
|
115
|
+
}) => {
|
|
116
|
+
const categoryItems = (categories ?? []) as CategoryItem[];
|
|
117
|
+
const projectItems = (projects ?? []) as ProjectItem[];
|
|
118
|
+
const allLabel = categoryItems[0]?.label ?? "All";
|
|
119
|
+
const [activeCategory, setActiveCategory] = useState(allLabel);
|
|
120
|
+
|
|
121
|
+
const filtered =
|
|
122
|
+
activeCategory === allLabel
|
|
123
|
+
? projectItems
|
|
124
|
+
: projectItems.filter((p) => p.category === activeCategory);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<>
|
|
128
|
+
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
129
|
+
|
|
130
|
+
<div id={sectionId ?? "work"} className="py-24">
|
|
131
|
+
<div className="mx-auto max-w-6xl lg:max-w-7xl px-6">
|
|
132
|
+
{/* Header */}
|
|
133
|
+
<motion.div
|
|
134
|
+
variants={containerVariants}
|
|
135
|
+
initial="hidden"
|
|
136
|
+
whileInView="show"
|
|
137
|
+
viewport={{ once: false, amount: 0.2 }}
|
|
138
|
+
className="mb-12 flex flex-col gap-6 overflow-hidden md:flex-row md:items-end md:justify-between px-0 md:px-4"
|
|
139
|
+
>
|
|
140
|
+
<motion.div variants={itemVariants}>
|
|
141
|
+
{eyebrow && (
|
|
142
|
+
<p className="top-caption mb-3 text-sm font-semibold uppercase tracking-widest text-stone-500 dark:text-stone-400">
|
|
143
|
+
{eyebrow}
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
<h2 className="text-4xl font-bold leading-tight text-stone-900 dark:text-stone-50 md:text-5xl">
|
|
148
|
+
{headlineMain}
|
|
149
|
+
{headlineAccent && (
|
|
150
|
+
<>
|
|
151
|
+
<br />
|
|
152
|
+
<span className="text-stone-600 dark:text-stone-400">
|
|
153
|
+
{headlineAccent}
|
|
154
|
+
</span>
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</h2>
|
|
158
|
+
</motion.div>
|
|
159
|
+
|
|
160
|
+
{/* Filter tabs */}
|
|
161
|
+
{categoryItems.length > 1 && (
|
|
162
|
+
<motion.div
|
|
163
|
+
variants={itemVariants}
|
|
164
|
+
className="flex flex-wrap gap-2 py-4"
|
|
165
|
+
>
|
|
166
|
+
{categoryItems.map((cat, i) => {
|
|
167
|
+
const categoryLabel = cat.label ?? "";
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<button
|
|
171
|
+
key={cat.id ?? i}
|
|
172
|
+
onClick={() => setActiveCategory(categoryLabel)}
|
|
173
|
+
className={cn(
|
|
174
|
+
"cursor-pointer whitespace-nowrap rounded-full px-4 py-1.5 text-xs font-medium transition-all duration-300",
|
|
175
|
+
activeCategory === categoryLabel
|
|
176
|
+
? "border-indigo-500 bg-indigo-800 text-stone-200 shadow-lg shadow-blue-500/20"
|
|
177
|
+
: "border-stone-300 dark:border-stone-700 bg-stone-50/80 dark:bg-stone-900/80 text-stone-600 dark:text-stone-400 hover:border-stone-400 dark:hover:border-stone-500 hover:bg-stone-100 dark:hover:bg-stone-800 hover:text-stone-900 dark:hover:text-stone-50",
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
{categoryLabel}
|
|
181
|
+
</button>
|
|
182
|
+
);
|
|
183
|
+
})}
|
|
184
|
+
</motion.div>
|
|
185
|
+
)}
|
|
186
|
+
</motion.div>
|
|
187
|
+
|
|
188
|
+
{/* Projects Grid */}
|
|
189
|
+
<motion.div
|
|
190
|
+
variants={containerVariants}
|
|
191
|
+
initial="hidden"
|
|
192
|
+
whileInView="show"
|
|
193
|
+
viewport={{ once: false, amount: 0.1 }}
|
|
194
|
+
className="grid grid-cols-1 gap-5 py-4 md:grid-cols-2 lg:grid-cols-3"
|
|
195
|
+
>
|
|
196
|
+
{filtered.map((project, i) => {
|
|
197
|
+
const projectTitle = project.title ?? "";
|
|
198
|
+
const projectDescription = project.description ?? "";
|
|
199
|
+
const projectCategory = project.category ?? "";
|
|
200
|
+
const projectYear = project.year ?? "";
|
|
201
|
+
const projectHighlight = project.highlight ?? "";
|
|
202
|
+
const projectLink = project.link ?? "";
|
|
203
|
+
const imgSrc =
|
|
204
|
+
project.image && typeof project.image === "object"
|
|
205
|
+
? ((project.image as Media).url ??
|
|
206
|
+
project.imageFallbackUrl ??
|
|
207
|
+
undefined)
|
|
208
|
+
: (project.imageFallbackUrl ?? undefined);
|
|
209
|
+
const safeImgSrc = isLocalImageSrc(imgSrc) ? imgSrc : null;
|
|
210
|
+
|
|
211
|
+
const CardInner = (
|
|
212
|
+
<SpotlightCard
|
|
213
|
+
spotlightColor="rgba(214,211,209,0.08)"
|
|
214
|
+
className="group h-full overflow-hidden rounded-3xl border border-stone-200 dark:border-stone-800 bg-stone-50 dark:bg-stone-900 shadow-md outline-none transition-colors duration-300 hover:border-indigo-500 hover:shadow-2xl hover:outline-4 hover:outline-stone-200/10 dark:hover:outline-stone-200/10"
|
|
215
|
+
style={{ cornerShape: "squircle" } as React.CSSProperties}
|
|
216
|
+
>
|
|
217
|
+
{/* Image */}
|
|
218
|
+
<div className="relative h-52 w-full overflow-hidden rounded-t-2xl">
|
|
219
|
+
{safeImgSrc ? (
|
|
220
|
+
<Image
|
|
221
|
+
src={safeImgSrc}
|
|
222
|
+
alt={projectTitle}
|
|
223
|
+
fill
|
|
224
|
+
className="studio-card-image corner-squircle scale-105 rounded-[2.5rem] object-cover object-top transition-transform duration-500 group-hover:scale-110"
|
|
225
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
226
|
+
priority={false}
|
|
227
|
+
style={
|
|
228
|
+
{ cornerShape: "squircle" } as React.CSSProperties
|
|
229
|
+
}
|
|
230
|
+
/>
|
|
231
|
+
) : (
|
|
232
|
+
<div className="h-full w-full bg-stone-200 dark:bg-stone-800" />
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-black/20 to-transparent dark:from-stone-950/80 dark:via-stone-950/20 dark:to-transparent" />
|
|
236
|
+
|
|
237
|
+
{projectCategory && (
|
|
238
|
+
<span className="absolute top-2 left-2 z-20 border bg-lime-500/50 border-lime-500 text-stone-50 text-xs font-semibold px-2.5 py-1 rounded-full">
|
|
239
|
+
{projectCategory}
|
|
240
|
+
</span>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{projectYear && (
|
|
244
|
+
<span className="absolute top-2 right-2 z-20 border bg-stone-900/70 border-stone-700 text-stone-300 text-xs font-mono px-2.5 py-1 rounded-full">
|
|
245
|
+
{projectYear}
|
|
246
|
+
</span>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Content */}
|
|
251
|
+
<div className="card-content relative z-5 mx-2 mb-2 -mt-6 rounded-2xl bg-stone-50/90 dark:bg-stone-950/50 p-4 backdrop-blur-md drop-shadow-md transition-all duration-300 group-hover:-translate-y-1 group-hover:brightness-110 group-hover:shadow-2xl hover:duration-300 corner-squircle">
|
|
252
|
+
<div className="mb-4 flex items-start justify-between gap-4">
|
|
253
|
+
<div>
|
|
254
|
+
<h3 className="mb-2 text-lg font-bold text-stone-900 dark:text-stone-50">
|
|
255
|
+
{projectTitle}
|
|
256
|
+
</h3>
|
|
257
|
+
|
|
258
|
+
{projectDescription && (
|
|
259
|
+
<p className="text-md mb-5 leading-relaxed text-stone-800 dark:text-stone-400">
|
|
260
|
+
{projectDescription}
|
|
261
|
+
</p>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{projectLink && (
|
|
266
|
+
<ExternalLink className="mt-1 h-4 w-4 shrink-0 text-stone-400 dark:text-stone-400 transition-colors duration-300 group-hover:text-indigo-800" />
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{/* Highlight */}
|
|
271
|
+
{projectHighlight && (
|
|
272
|
+
<div className="mb-4 flex items-center gap-2 rounded-lg border border-stone-200 dark:border-stone-800 bg-stone-100/60 group-hover:bg-stone-300 group-hover:shadow-xl dark:bg-stone-800/60 p-2.5">
|
|
273
|
+
<BarChart2
|
|
274
|
+
size={14}
|
|
275
|
+
className="shrink-0 text-indigo-800 dark:text-indigo-400"
|
|
276
|
+
/>
|
|
277
|
+
<span className="text-xs font-medium text-indigo-800 dark:text-indigo-400">
|
|
278
|
+
{projectHighlight}
|
|
279
|
+
</span>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{/* Tags */}
|
|
284
|
+
{project.tags && project.tags.length > 0 && (
|
|
285
|
+
<div className="flex flex-wrap gap-1.5">
|
|
286
|
+
{project.tags.map((t, ti) => (
|
|
287
|
+
<span
|
|
288
|
+
key={t.id ?? ti}
|
|
289
|
+
className="rounded-full border border-stone-200 dark:border-indigo-700 bg-stone-100 dark:bg-indigo-900/50 px-2 py-0.5 text-[11.5px] text-stone-700 dark:text-stone-300 transition-all duration-300"
|
|
290
|
+
>
|
|
291
|
+
{t.tag}
|
|
292
|
+
</span>
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
</SpotlightCard>
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<motion.div
|
|
302
|
+
key={project.id ?? i}
|
|
303
|
+
variants={itemVariants}
|
|
304
|
+
className="h-full"
|
|
305
|
+
>
|
|
306
|
+
{projectLink ? (
|
|
307
|
+
<a
|
|
308
|
+
href={projectLink}
|
|
309
|
+
target="_blank"
|
|
310
|
+
rel="noopener noreferrer"
|
|
311
|
+
className="block h-full"
|
|
312
|
+
>
|
|
313
|
+
{CardInner}
|
|
314
|
+
</a>
|
|
315
|
+
) : (
|
|
316
|
+
CardInner
|
|
317
|
+
)}
|
|
318
|
+
</motion.div>
|
|
319
|
+
);
|
|
320
|
+
})}
|
|
321
|
+
</motion.div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export default DDWork;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { Block } from 'payload'
|
|
2
|
+
|
|
3
|
+
export const DDWork: Block = {
|
|
4
|
+
slug: 'DD-Work',
|
|
5
|
+
interfaceName: 'DDWork',
|
|
6
|
+
labels: {
|
|
7
|
+
singular: 'Studio Work',
|
|
8
|
+
plural: 'Studio Work Sections'
|
|
9
|
+
},
|
|
10
|
+
fields: [
|
|
11
|
+
{
|
|
12
|
+
type: 'tabs',
|
|
13
|
+
tabs: [
|
|
14
|
+
{
|
|
15
|
+
label: 'Header',
|
|
16
|
+
fields: [
|
|
17
|
+
{
|
|
18
|
+
name: 'eyebrow',
|
|
19
|
+
type: 'text',
|
|
20
|
+
label: 'Eyebrow Label',
|
|
21
|
+
defaultValue: 'Selected Work'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'headlineMain',
|
|
25
|
+
type: 'text',
|
|
26
|
+
label: 'Headline (Main)',
|
|
27
|
+
defaultValue: "Projects we're",
|
|
28
|
+
required: false
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'headlineAccent',
|
|
32
|
+
type: 'text',
|
|
33
|
+
label: 'Headline (Accent)',
|
|
34
|
+
defaultValue: 'proud of',
|
|
35
|
+
required: false
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: 'Projects',
|
|
41
|
+
fields: [
|
|
42
|
+
{
|
|
43
|
+
name: 'categories',
|
|
44
|
+
type: 'array',
|
|
45
|
+
label: 'Filter Categories',
|
|
46
|
+
admin: {
|
|
47
|
+
description: 'First item is the "show all" tab. e.g. All, Web App, Design System'
|
|
48
|
+
},
|
|
49
|
+
fields: [
|
|
50
|
+
{
|
|
51
|
+
name: 'label',
|
|
52
|
+
type: 'text',
|
|
53
|
+
label: 'Category Label',
|
|
54
|
+
required: true
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
defaultValue: [
|
|
58
|
+
{ label: 'All' },
|
|
59
|
+
{ label: 'Web App' },
|
|
60
|
+
{ label: 'Design System' },
|
|
61
|
+
{ label: 'CMS' },
|
|
62
|
+
{ label: 'Consulting' }
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'projects',
|
|
67
|
+
type: 'array',
|
|
68
|
+
label: 'Projects',
|
|
69
|
+
minRows: 1,
|
|
70
|
+
fields: [
|
|
71
|
+
{
|
|
72
|
+
name: 'title',
|
|
73
|
+
type: 'text',
|
|
74
|
+
label: 'Project Title',
|
|
75
|
+
required: true
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'category',
|
|
79
|
+
type: 'text',
|
|
80
|
+
label: 'Category',
|
|
81
|
+
required: true,
|
|
82
|
+
admin: {
|
|
83
|
+
description: 'Must match one of the filter categories above.'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'description',
|
|
88
|
+
type: 'textarea',
|
|
89
|
+
label: 'Description',
|
|
90
|
+
required: true
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'highlight',
|
|
94
|
+
type: 'text',
|
|
95
|
+
label: 'Highlight Stat',
|
|
96
|
+
admin: {
|
|
97
|
+
description: 'e.g. +40% conversion rate, 200+ components'
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'year',
|
|
102
|
+
type: 'text',
|
|
103
|
+
label: 'Year'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'image',
|
|
107
|
+
type: 'upload',
|
|
108
|
+
relationTo: 'media',
|
|
109
|
+
label: 'Project Image'
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'imageFallbackUrl',
|
|
113
|
+
type: 'text',
|
|
114
|
+
label: 'Image Fallback URL',
|
|
115
|
+
admin: {
|
|
116
|
+
placeholder: '/assets/placeholder/01.png',
|
|
117
|
+
description: 'Used when no image is uploaded.'
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'tags',
|
|
122
|
+
type: 'array',
|
|
123
|
+
label: 'Tags',
|
|
124
|
+
fields: [
|
|
125
|
+
{
|
|
126
|
+
name: 'tag',
|
|
127
|
+
type: 'text',
|
|
128
|
+
label: 'Tag',
|
|
129
|
+
required: true
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'link',
|
|
135
|
+
type: 'text',
|
|
136
|
+
label: 'Project Link (optional)',
|
|
137
|
+
admin: {
|
|
138
|
+
description: 'URL to the live project or case study.'
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
label: 'Settings',
|
|
147
|
+
fields: [
|
|
148
|
+
{
|
|
149
|
+
name: 'sectionId',
|
|
150
|
+
type: 'text',
|
|
151
|
+
label: 'Section ID',
|
|
152
|
+
defaultValue: 'work'
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'customCSS',
|
|
156
|
+
type: 'code',
|
|
157
|
+
label: 'Custom CSS',
|
|
158
|
+
admin: { language: 'css' }
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|