nextworks 0.2.0-alpha.2 → 0.2.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +280 -228
  2. package/dist/cli_manifests/blocks_manifest.json +193 -194
  3. package/dist/commands/blocks.d.ts +3 -0
  4. package/dist/commands/blocks.d.ts.map +1 -1
  5. package/dist/commands/blocks.js +128 -19
  6. package/dist/commands/blocks.js.map +1 -1
  7. package/dist/commands/doctor.d.ts +136 -0
  8. package/dist/commands/doctor.d.ts.map +1 -0
  9. package/dist/commands/doctor.js +696 -0
  10. package/dist/commands/doctor.js.map +1 -0
  11. package/dist/commands/remove-blocks.d.ts +4 -1
  12. package/dist/commands/remove-blocks.d.ts.map +1 -1
  13. package/dist/commands/remove-blocks.js +24 -6
  14. package/dist/commands/remove-blocks.js.map +1 -1
  15. package/dist/index.js +125 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +104 -194
  18. package/dist/kits/blocks/.nextworks/docs/BLOCKS_README.md +115 -75
  19. package/dist/kits/blocks/.nextworks/docs/THEME_GUIDE.md +269 -251
  20. package/dist/kits/blocks/app/templates/digitalagency/PresetThemeVars.tsx +80 -80
  21. package/dist/kits/blocks/app/templates/digitalagency/README.md +42 -36
  22. package/dist/kits/blocks/app/templates/digitalagency/components/About.tsx +71 -99
  23. package/dist/kits/blocks/app/templates/digitalagency/components/CTA.tsx +40 -74
  24. package/dist/kits/blocks/app/templates/digitalagency/components/Contact.tsx +123 -227
  25. package/dist/kits/blocks/app/templates/digitalagency/components/Footer.tsx +89 -89
  26. package/dist/kits/blocks/app/templates/digitalagency/components/Hero.tsx +83 -90
  27. package/dist/kits/blocks/app/templates/digitalagency/components/Navbar.tsx +121 -168
  28. package/dist/kits/blocks/app/templates/digitalagency/components/NetworkPattern.tsx +288 -297
  29. package/dist/kits/blocks/app/templates/digitalagency/components/Portfolio.tsx +157 -157
  30. package/dist/kits/blocks/app/templates/digitalagency/components/Pricing.tsx +114 -114
  31. package/dist/kits/blocks/app/templates/digitalagency/components/Process.tsx +59 -59
  32. package/dist/kits/blocks/app/templates/digitalagency/components/Services.tsx +55 -55
  33. package/dist/kits/blocks/app/templates/digitalagency/components/Team.tsx +28 -28
  34. package/dist/kits/blocks/app/templates/digitalagency/components/Testimonials.tsx +65 -65
  35. package/dist/kits/blocks/app/templates/digitalagency/page.tsx +38 -38
  36. package/dist/kits/blocks/app/templates/gallery/PresetThemeVars.tsx +130 -85
  37. package/dist/kits/blocks/app/templates/gallery/page.tsx +357 -303
  38. package/dist/kits/blocks/app/templates/productlaunch/PresetThemeVars.tsx +75 -74
  39. package/dist/kits/blocks/app/templates/productlaunch/README.md +62 -55
  40. package/dist/kits/blocks/app/templates/productlaunch/components/About.tsx +84 -178
  41. package/dist/kits/blocks/app/templates/productlaunch/components/CTA.tsx +50 -93
  42. package/dist/kits/blocks/app/templates/productlaunch/components/Contact.tsx +231 -231
  43. package/dist/kits/blocks/app/templates/productlaunch/components/FAQ.tsx +86 -93
  44. package/dist/kits/blocks/app/templates/productlaunch/components/Features.tsx +83 -84
  45. package/dist/kits/blocks/app/templates/productlaunch/components/Footer.tsx +132 -132
  46. package/dist/kits/blocks/app/templates/productlaunch/components/Hero.tsx +88 -89
  47. package/dist/kits/blocks/app/templates/productlaunch/components/Navbar.tsx +116 -162
  48. package/dist/kits/blocks/app/templates/productlaunch/components/Pricing.tsx +106 -106
  49. package/dist/kits/blocks/app/templates/productlaunch/components/ProcessTimeline.tsx +110 -110
  50. package/dist/kits/blocks/app/templates/productlaunch/components/ServicesGrid.tsx +68 -68
  51. package/dist/kits/blocks/app/templates/productlaunch/components/Team.tsx +104 -104
  52. package/dist/kits/blocks/app/templates/productlaunch/components/Testimonials.tsx +90 -89
  53. package/dist/kits/blocks/app/templates/productlaunch/components/TrustBadges.tsx +76 -76
  54. package/dist/kits/blocks/app/templates/productlaunch/page.tsx +43 -43
  55. package/dist/kits/blocks/app/templates/saasdashboard/PresetThemeVars.tsx +80 -80
  56. package/dist/kits/blocks/app/templates/saasdashboard/README.md +44 -38
  57. package/dist/kits/blocks/app/templates/saasdashboard/components/Contact.tsx +129 -176
  58. package/dist/kits/blocks/app/templates/saasdashboard/components/Dashboard.tsx +293 -293
  59. package/dist/kits/blocks/app/templates/saasdashboard/components/FAQ.tsx +55 -55
  60. package/dist/kits/blocks/app/templates/saasdashboard/components/Features.tsx +90 -91
  61. package/dist/kits/blocks/app/templates/saasdashboard/components/Footer.tsx +77 -77
  62. package/dist/kits/blocks/app/templates/saasdashboard/components/Hero.tsx +104 -105
  63. package/dist/kits/blocks/app/templates/saasdashboard/components/Hero_mask.tsx +126 -127
  64. package/dist/kits/blocks/app/templates/saasdashboard/components/Navbar.tsx +117 -159
  65. package/dist/kits/blocks/app/templates/saasdashboard/components/Pricing.tsx +90 -90
  66. package/dist/kits/blocks/app/templates/saasdashboard/components/SmoothScroll.tsx +96 -97
  67. package/dist/kits/blocks/app/templates/saasdashboard/components/Testimonials.tsx +72 -72
  68. package/dist/kits/blocks/app/templates/saasdashboard/components/TrustBadges.tsx +53 -53
  69. package/dist/kits/blocks/app/templates/saasdashboard/page.tsx +39 -39
  70. package/dist/kits/blocks/components/enhanced-theme-provider.tsx +195 -183
  71. package/dist/kits/blocks/components/providers/BlocksAppProviders.tsx +27 -27
  72. package/dist/kits/blocks/components/sections/About.tsx +291 -291
  73. package/dist/kits/blocks/components/sections/CTA.tsx +257 -258
  74. package/dist/kits/blocks/components/sections/CommandShowcase.tsx +517 -0
  75. package/dist/kits/blocks/components/sections/Contact.tsx +267 -267
  76. package/dist/kits/blocks/components/sections/FAQ.tsx +214 -225
  77. package/dist/kits/blocks/components/sections/FeaturedProjectShowcase.tsx +687 -0
  78. package/dist/kits/blocks/components/sections/Features.tsx +268 -270
  79. package/dist/kits/blocks/components/sections/Footer.tsx +302 -302
  80. package/dist/kits/blocks/components/sections/HeroMotion.tsx +308 -308
  81. package/dist/kits/blocks/components/sections/HeroOverlay.tsx +358 -358
  82. package/dist/kits/blocks/components/sections/HeroProductDemo.tsx +246 -0
  83. package/dist/kits/blocks/components/sections/HeroSplit.tsx +352 -352
  84. package/dist/kits/blocks/components/sections/HeroWithVideo.tsx +495 -0
  85. package/dist/kits/blocks/components/sections/Navbar.tsx +352 -353
  86. package/dist/kits/blocks/components/sections/PortfolioSimple.tsx +549 -550
  87. package/dist/kits/blocks/components/sections/Pricing.tsx +264 -264
  88. package/dist/kits/blocks/components/sections/ProcessTimeline.tsx +325 -325
  89. package/dist/kits/blocks/components/sections/ProjectDeepDive.tsx +805 -0
  90. package/dist/kits/blocks/components/sections/SelectedWorkRail.tsx +485 -0
  91. package/dist/kits/blocks/components/sections/ServicesGrid.tsx +210 -210
  92. package/dist/kits/blocks/components/sections/Team.tsx +309 -309
  93. package/dist/kits/blocks/components/sections/Testimonials.tsx +158 -158
  94. package/dist/kits/blocks/components/sections/TrustBadges.tsx +162 -162
  95. package/dist/kits/blocks/components/sections/product-demo/ApprovalInboxPanel.tsx +128 -0
  96. package/dist/kits/blocks/components/sections/product-demo/DemoStage.tsx +519 -0
  97. package/dist/kits/blocks/components/sections/product-demo/DemoWindow.tsx +140 -0
  98. package/dist/kits/blocks/components/sections/product-demo/KnowledgePanel.tsx +130 -0
  99. package/dist/kits/blocks/components/sections/product-demo/RunConsolePanel.tsx +430 -0
  100. package/dist/kits/blocks/components/sections/product-demo/TaskListPanel.tsx +302 -0
  101. package/dist/kits/blocks/components/sections/product-demo/WorkflowStudioPanel.tsx +744 -0
  102. package/dist/kits/blocks/components/sections/product-demo/types.ts +262 -0
  103. package/dist/kits/blocks/components/theme-provider.tsx +1 -34
  104. package/dist/kits/blocks/components/ui/alert-dialog.tsx +134 -134
  105. package/dist/kits/blocks/components/ui/brand-node.tsx +121 -121
  106. package/dist/kits/blocks/components/ui/button.tsx +122 -122
  107. package/dist/kits/blocks/components/ui/card.tsx +95 -95
  108. package/dist/kits/blocks/components/ui/checkbox.tsx +30 -30
  109. package/dist/kits/blocks/components/ui/cta-button.tsx +125 -125
  110. package/dist/kits/blocks/components/ui/dropdown-menu.tsx +201 -201
  111. package/dist/kits/blocks/components/ui/feature-card.tsx +91 -91
  112. package/dist/kits/blocks/components/ui/input.tsx +27 -27
  113. package/dist/kits/blocks/components/ui/label.tsx +29 -29
  114. package/dist/kits/blocks/components/ui/pricing-card.tsx +120 -120
  115. package/dist/kits/blocks/components/ui/select.tsx +25 -25
  116. package/dist/kits/blocks/components/ui/skeleton.tsx +13 -13
  117. package/dist/kits/blocks/components/ui/table.tsx +98 -98
  118. package/dist/kits/blocks/components/ui/testimonial-card.tsx +108 -108
  119. package/dist/kits/blocks/components/ui/textarea.tsx +26 -26
  120. package/dist/kits/blocks/components/ui/theme-selector.tsx +243 -247
  121. package/dist/kits/blocks/components/ui/theme-toggle.tsx +74 -74
  122. package/dist/kits/blocks/components/ui/toaster.tsx +7 -7
  123. package/dist/kits/blocks/lib/themes.ts +400 -400
  124. package/dist/kits/blocks/lib/utils.ts +6 -9
  125. package/dist/kits/blocks/package-deps.json +40 -28
  126. package/dist/kits/blocks/tsconfig.json +11 -0
  127. package/dist/kits/blocks/tsconfig.tsbuildinfo +1 -0
  128. package/dist/utils/file-operations.d.ts +3 -1
  129. package/dist/utils/file-operations.d.ts.map +1 -1
  130. package/dist/utils/file-operations.js +84 -12
  131. package/dist/utils/file-operations.js.map +1 -1
  132. package/dist/utils/installation-tracker.d.ts +2 -2
  133. package/dist/utils/installation-tracker.d.ts.map +1 -1
  134. package/dist/utils/installation-tracker.js +11 -11
  135. package/dist/utils/installation-tracker.js.map +1 -1
  136. package/dist/utils/package-manager.d.ts +6 -0
  137. package/dist/utils/package-manager.d.ts.map +1 -0
  138. package/dist/utils/package-manager.js +58 -0
  139. package/dist/utils/package-manager.js.map +1 -0
  140. package/dist/utils/yarn-pnp.d.ts +6 -0
  141. package/dist/utils/yarn-pnp.d.ts.map +1 -0
  142. package/dist/utils/yarn-pnp.js +39 -0
  143. package/dist/utils/yarn-pnp.js.map +1 -0
  144. package/package.json +5 -2
  145. package/dist/kits/blocks/notes/THEME_GUIDE.md +0 -29
@@ -1,550 +1,549 @@
1
- "use client";
2
-
3
- import React, { useState } from "react";
4
- import { motion } from "motion/react";
5
- import { cn } from "@/lib/utils";
6
- import { CTAButton } from "@/components/ui/cta-button";
7
- import { ExternalLink, TrendingUp } from "lucide-react";
8
- import Image from "next/image";
9
-
10
- // TypeScript Interfaces
11
- export interface PortfolioProject {
12
- id: number;
13
- title: string;
14
- category: string;
15
- industry: string;
16
- result: string;
17
- description: string;
18
- image?: string;
19
- tags: string[];
20
- color: string;
21
- url?: string;
22
- }
23
-
24
- /**
25
- * Props for the PortfolioSimple section component.
26
- *
27
- * @remarks
28
- * - Styling: slot-style className overrides are merged after defaults via cn().
29
- * - Motion: entrance animations respect enableMotion; prefers-reduced-motion is supported.
30
- *
31
- * @public
32
- */
33
- interface PortfolioSimpleProps {
34
- /** Optional id on root section */
35
- id?: string;
36
- /** Root className merged into section slot */
37
- className?: string;
38
-
39
- // Content
40
- projects?: PortfolioProject[];
41
- /**
42
- * List of filter labels. Filtering matches project.category exactly.
43
- * The first item is selected by default; a value of "All Projects" disables filtering.
44
- */
45
- filters?: string[];
46
- /** When false, disables entrance animations and hover transitions. */
47
- enableMotion?: boolean;
48
-
49
- // Section Content
50
- sectionTitle?: string;
51
- sectionSubtitle?: string;
52
- ctaTitle?: string;
53
- ctaDescription?: string;
54
-
55
- /** Label for the first call-to-action button */
56
- cta1Label?: string;
57
- /** URL or anchor target for the first CTA button */
58
- cta1Href?: string;
59
- /** Label for the second call-to-action button */
60
- cta2Label?: string;
61
- /** URL or anchor target for the second CTA button */
62
- cta2Href?: string;
63
-
64
- /** Styling configuration objects */
65
- section?: {
66
- className?: string;
67
- };
68
- container?: {
69
- className?: string;
70
- };
71
- header?: {
72
- className?: string;
73
- };
74
- title?: {
75
- className?: string;
76
- };
77
- subtitle?: {
78
- className?: string;
79
- };
80
- filterContainer?: {
81
- className?: string;
82
- };
83
- filterButton?: {
84
- className?: string;
85
- };
86
- activeFilterButton?: {
87
- className?: string;
88
- };
89
- grid?: {
90
- className?: string;
91
- };
92
- projectCard?: {
93
- className?: string;
94
- };
95
- imageContainer?: {
96
- className?: string;
97
- };
98
- projectInfo?: {
99
- className?: string;
100
- };
101
- projectTitle?: {
102
- className?: string;
103
- };
104
- projectDescription?: {
105
- className?: string;
106
- };
107
- tagsContainer?: {
108
- className?: string;
109
- };
110
- tagStyle?: {
111
- className?: string;
112
- };
113
- result?: {
114
- className?: string;
115
- };
116
- ctaSection?: {
117
- className?: string;
118
- };
119
- ctaTitleStyle?: {
120
- className?: string;
121
- };
122
- ctaDescriptionStyle?: {
123
- className?: string;
124
- };
125
- ctaButtons?: {
126
- className?: string;
127
- };
128
- cta1Button?: {
129
- className?: string;
130
- };
131
- cta2Button?: {
132
- className?: string;
133
- };
134
-
135
- // Callbacks
136
- /** Called when a project image or card is clicked. */
137
- onProjectClick?: (project: PortfolioProject) => void;
138
- onPrimaryCtaClick?: () => void;
139
- onSecondaryCtaClick?: () => void;
140
-
141
- /** ARIA label for the portfolio section */
142
- ariaLabel?: string;
143
- }
144
-
145
- // Simple project image component
146
- interface ProjectImageProps {
147
- project: PortfolioProject;
148
- onClick?: () => void;
149
- imageContainer?: {
150
- className?: string;
151
- };
152
- }
153
-
154
- const ProjectImage: React.FC<ProjectImageProps> = ({
155
- project,
156
- onClick,
157
- imageContainer = {
158
- className:
159
- "relative rounded-lg overflow-hidden bg-muted shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-xl cursor-pointer aspect-[16/10]",
160
- },
161
- }) => {
162
- const colorClasses = {
163
- blue: "bg-blue-100 dark:bg-blue-900",
164
- green: "bg-green-100 dark:bg-green-900",
165
- purple: "bg-purple-100 dark:bg-purple-900",
166
- red: "bg-red-100 dark:bg-red-900",
167
- yellow: "bg-yellow-100 dark:bg-yellow-900",
168
- indigo: "bg-indigo-100 dark:bg-indigo-900",
169
- } as const;
170
-
171
- const fallbackBg =
172
- colorClasses[project.color as keyof typeof colorClasses] ||
173
- colorClasses.blue;
174
-
175
- return (
176
- <div className={imageContainer.className} onClick={onClick}>
177
- {project.image ? (
178
- <Image
179
- src={project.image}
180
- alt={project.title}
181
- fill
182
- className="object-cover"
183
- sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
184
- />
185
- ) : (
186
- <div
187
- className={`flex h-full w-full items-center justify-center ${fallbackBg}`}
188
- >
189
- <div className="p-8 text-center">
190
- <div className="bg-background/20 dark:bg-foreground/20 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full">
191
- <ExternalLink className="h-8 w-8 text-[var(--card-muted-fg)]" />
192
- </div>
193
- <h3 className="mb-2 text-lg font-semibold text-[var(--card-title-fg)]">
194
- {project.title}
195
- </h3>
196
- <p className="text-sm text-[var(--card-muted-fg)]">
197
- {project.category}
198
- </p>
199
- </div>
200
- </div>
201
- )}
202
- </div>
203
- );
204
- };
205
-
206
- const defaultProjects = [
207
- {
208
- id: 1,
209
- title: "TechStartup Growth Campaign",
210
- category: "Digital Marketing",
211
- industry: "Technology",
212
- result: "+300% Leads",
213
- description:
214
- "Comprehensive digital marketing campaign that increased qualified leads by 300% through targeted SEO, social media marketing, and conversion optimization strategies.",
215
- tags: ["SEO", "Social Media", "PPC", "Analytics"],
216
- color: "blue",
217
- url: "#",
218
- },
219
- {
220
- id: 2,
221
- title: "E-Commerce Brand Launch",
222
- category: "Brand Marketing",
223
- industry: "Retail",
224
- result: "+250% Revenue",
225
- description:
226
- "Complete brand launch and digital marketing strategy for a new e-commerce platform, resulting in 250% revenue growth within the first 6 months.",
227
- tags: ["Brand Strategy", "Content Marketing", "Influencer", "Email"],
228
- color: "green",
229
- url: "#",
230
- },
231
- {
232
- id: 3,
233
- title: "Healthcare Practice Expansion",
234
- category: "Local Marketing",
235
- industry: "Healthcare",
236
- result: "+180% Patients",
237
- description:
238
- "Local SEO and digital marketing campaign that helped a healthcare practice expand to three new locations and increase patient acquisition by 180%.",
239
- tags: ["Local SEO", "Google Ads", "Reputation", "Content"],
240
- color: "purple",
241
- url: "#",
242
- },
243
- {
244
- id: 4,
245
- title: "SaaS Product Launch",
246
- category: "B2B Marketing",
247
- industry: "Software",
248
- result: "+400% Signups",
249
- description:
250
- "Strategic B2B marketing campaign for a SaaS product launch, achieving 400% increase in trial signups through targeted content marketing and account-based marketing.",
251
- tags: ["B2B Strategy", "Content", "LinkedIn Ads", "Webinars"],
252
- color: "indigo",
253
- url: "#",
254
- },
255
- {
256
- id: 5,
257
- title: "Restaurant Chain Rebrand",
258
- category: "Brand Marketing",
259
- industry: "Food & Beverage",
260
- result: "+220% Foot Traffic",
261
- description:
262
- "Complete rebrand and digital marketing transformation for a restaurant chain, resulting in 220% increase in foot traffic and 150% growth in online orders.",
263
- tags: ["Rebranding", "Social Media", "Local SEO", "Delivery"],
264
- color: "red",
265
- url: "#",
266
- },
267
- {
268
- id: 6,
269
- title: "Fitness App User Acquisition",
270
- category: "Mobile Marketing",
271
- industry: "Health & Fitness",
272
- result: "+500% Downloads",
273
- description:
274
- "Mobile-first marketing campaign that achieved 500% increase in app downloads through app store optimization, influencer partnerships, and targeted social media advertising.",
275
- tags: ["ASO", "Influencer", "Social Ads", "Retention"],
276
- color: "yellow",
277
- url: "#",
278
- },
279
- ];
280
-
281
- const defaultFilters = [
282
- "All Projects",
283
- "Digital Marketing",
284
- "Brand Marketing",
285
- "B2B Marketing",
286
- "Local Marketing",
287
- "Mobile Marketing",
288
- ];
289
-
290
- /**
291
- * Responsive portfolio/gallery grid with simple category filtering and CTA block.
292
- *
293
- * @remarks
294
- * - Filtering uses exact category match; include "All Projects" to disable filtering.
295
- * - Accessibility: Renders a semantic <section> with aria-label; ensure images have informative titles or alt text.
296
- *
297
- * @example
298
- * <PortfolioSimple sectionTitle="Our Work" />
299
- */
300
- export function PortfolioSimple({
301
- id,
302
- className,
303
- projects = defaultProjects,
304
- filters = defaultFilters,
305
- enableMotion = true,
306
- sectionTitle = "Our Recent Work",
307
- sectionSubtitle = "Take a look at some of our successful projects that have helped businesses transform their digital presence and achieve remarkable growth.",
308
- ctaTitle = "Ready to See Your Project Here?",
309
- ctaDescription = "Let's discuss how we can help transform your business with a custom digital solution.",
310
- cta1Label = "Get Free Quote",
311
- cta1Href = "#contact",
312
- cta2Label,
313
- cta2Href,
314
- section = {
315
- className: "py-16 md:py-24 bg-background",
316
- },
317
- container = {
318
- className: "max-w-7xl mx-auto px-6",
319
- },
320
- header = {
321
- className: "space-y-6 text-center mb-12",
322
- },
323
- title = {
324
- className:
325
- "text-3xl md:text-4xl font-bold font-poppins text-foreground text-[var(--heading-fg)]",
326
- },
327
- subtitle = {
328
- className:
329
- "text-xl font-inter text-muted-foreground max-w-2xl mx-auto leading-relaxed text-[var(--subheading-fg)]",
330
- },
331
- filterContainer = {
332
- className: "flex gap-4 flex-wrap justify-center mt-8",
333
- },
334
- filterButton = {
335
- className:
336
- "px-6 py-2 rounded-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md border-[var(--badge-border)] text-[var(--badge-fg)] bg-[var(--badge-bg)]",
337
- },
338
- activeFilterButton = {
339
- className:
340
- "px-6 py-2 rounded-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md bg-[var(--badge-active-bg)] text-[var(--badge-active-fg)] border-[var(--badge-active-border)]",
341
- },
342
- grid = {
343
- className:
344
- "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10 mt-12",
345
- },
346
- projectCard = {
347
- className: "group cursor-pointer transition-all duration-300",
348
- },
349
- imageContainer = {
350
- className:
351
- "relative rounded-lg overflow-hidden bg-muted shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-xl cursor-pointer aspect-[16/10]",
352
- },
353
- projectInfo = {
354
- className: "space-y-4 px-2 mt-4",
355
- },
356
- projectTitle = {
357
- className:
358
- "text-xl font-bold font-poppins transition-colors duration-200 text-[var(--card-title-fg)] group-hover:text-[var(--card-title-hover-fg)]",
359
- },
360
- projectDescription = {
361
- className: "text-base font-inter text-[var(--card-muted-fg)]",
362
- },
363
- tagsContainer = {
364
- className: "flex gap-2 flex-wrap",
365
- },
366
- tagStyle = {
367
- className:
368
- "px-2 py-1 text-xs border border-border rounded-full text-muted-foreground border-[var(--badge-border)] text-[var(--badge-fg)] bg-[var(--badge-bg)]",
369
- },
370
- result = {
371
- className:
372
- "font-bold text-[var(--metric-fg)] text-sm whitespace-nowrap ml-4",
373
- },
374
- ctaSection = {
375
- className: "space-y-6 text-center mt-16 pt-12 border-t border-border",
376
- },
377
- ctaTitleStyle = {
378
- className: "text-xl font-bold text-foreground text-[var(--heading-fg)]",
379
- },
380
- ctaDescriptionStyle = {
381
- className:
382
- "text-muted-foreground max-w-md mx-auto text-[var(--subheading-fg)]",
383
- },
384
- ctaButtons = {
385
- className: "flex gap-4 justify-center",
386
- },
387
- cta1Button = {
388
- className:
389
- "bg-primary hover:bg-primary/90 dark:bg-primary dark:hover:bg-primary/90 text-primary-foreground font-medium shadow-md hover:shadow-lg transition-all duration-200 hover:-translate-y-0.5 border-[var(--btn-border)] focus-visible:ring-[var(--btn-ring)]",
390
- },
391
- cta2Button = {
392
- className:
393
- "border-primary text-primary hover:bg-primary/10 font-medium shadow-md hover:shadow-lg transition-all duration-200 hover:-translate-y-0.5 border-[var(--btn-border)] focus-visible:ring-[var(--btn-ring)]",
394
- },
395
- onProjectClick,
396
- onPrimaryCtaClick,
397
- onSecondaryCtaClick,
398
- ariaLabel = "Portfolio showcase section",
399
- }: PortfolioSimpleProps) {
400
- const [activeFilter, setActiveFilter] = useState(
401
- filters[0] || "All Projects",
402
- );
403
-
404
- const filteredProjects = projects.filter(
405
- (project) =>
406
- activeFilter === "All Projects" || project.category === activeFilter,
407
- );
408
-
409
- return (
410
- <section
411
- id={id}
412
- className={cn(section.className, className)}
413
- aria-label={ariaLabel}
414
- >
415
- <div className={container.className}>
416
- {/* Section Header */}
417
- <div className={header.className}>
418
- <h2 className={title.className}>{sectionTitle}</h2>
419
- <p className={subtitle.className}>{sectionSubtitle}</p>
420
-
421
- {/* Filter Buttons */}
422
- <div className={filterContainer.className}>
423
- {filters.map((filter) => (
424
- <button
425
- key={filter}
426
- className={
427
- activeFilter === filter
428
- ? activeFilterButton.className
429
- : filterButton.className
430
- }
431
- onClick={() => setActiveFilter(filter)}
432
- >
433
- {filter}
434
- </button>
435
- ))}
436
- </div>
437
- </div>
438
-
439
- {/* Portfolio Grid */}
440
- <div className={grid.className}>
441
- {filteredProjects.map((project, index) => (
442
- <motion.div
443
- key={project.id}
444
- initial={
445
- enableMotion ? { opacity: 0, y: 12 } : { opacity: 1, y: 0 }
446
- }
447
- whileInView={
448
- enableMotion ? { opacity: 1, y: 0 } : { opacity: 1, y: 0 }
449
- }
450
- viewport={
451
- enableMotion
452
- ? { once: true, amount: 0.2 }
453
- : { once: true, amount: 0 }
454
- }
455
- transition={
456
- enableMotion
457
- ? {
458
- type: "spring",
459
- stiffness: 125,
460
- damping: 50,
461
- mass: 1,
462
- delay: 0.1 + index * 0.05,
463
- }
464
- : { type: "tween", duration: 0 }
465
- }
466
- className="motion-reduce:transform-none motion-reduce:transition-none"
467
- >
468
- <div
469
- className={cn(
470
- projectCard.className,
471
- enableMotion
472
- ? "transition-all duration-200 hover:-translate-y-1"
473
- : "cursor-default transition-none hover:!translate-y-0 hover:shadow-none",
474
- )}
475
- >
476
- <div className="space-y-4">
477
- {/* Project Image */}
478
- <ProjectImage
479
- project={project}
480
- onClick={() => onProjectClick?.(project)}
481
- imageContainer={imageContainer}
482
- />
483
-
484
- {/* Project Info */}
485
- <div className={projectInfo.className}>
486
- <div className="flex w-full items-start justify-between">
487
- <div className="flex-1 space-y-2">
488
- <h3 className={projectTitle.className}>
489
- {project.title}
490
- </h3>
491
- <p className={projectDescription.className}>
492
- {project.description}
493
- </p>
494
- </div>
495
- </div>
496
-
497
- {/* Tags & Result */}
498
- <div className="flex w-full items-center justify-between">
499
- <div className={tagsContainer.className}>
500
- {project.tags.map((tag, index) => (
501
- <span key={index} className={tagStyle.className}>
502
- {tag}
503
- </span>
504
- ))}
505
- </div>
506
- <div className="flex items-center gap-2">
507
- <TrendingUp className="h-4 w-4 text-[var(--metric-fg)]" />
508
- <span className={result.className}>
509
- {project.result}
510
- </span>
511
- </div>
512
- </div>
513
- </div>
514
- </div>
515
- </div>
516
- </motion.div>
517
- ))}
518
- </div>
519
-
520
- {/* CTA Section */}
521
- <div className={ctaSection.className}>
522
- <h3 className={ctaTitleStyle.className}>{ctaTitle}</h3>
523
- <p className={ctaDescriptionStyle.className}>{ctaDescription}</p>
524
- <div className={ctaButtons.className}>
525
- <CTAButton
526
- ctaButtonLabel={cta1Label}
527
- ctaButtonHref={cta1Href}
528
- button={{
529
- className: cta1Button.className,
530
- }}
531
- onClick={onPrimaryCtaClick}
532
- />
533
- {cta2Label && (
534
- <CTAButton
535
- ctaButtonLabel={cta2Label}
536
- ctaButtonHref={cta2Href}
537
- button={{
538
- variant: "outline",
539
- size: "lg",
540
- className: cta2Button.className,
541
- }}
542
- onClick={onSecondaryCtaClick}
543
- />
544
- )}
545
- </div>
546
- </div>
547
- </div>
548
- </section>
549
- );
550
- }
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { motion } from "motion/react";
5
+ import { cn } from "@/lib/utils";
6
+ import { CTAButton } from "@/components/ui/cta-button";
7
+ import { ExternalLink, TrendingUp } from "lucide-react";
8
+ import Image from "next/image";
9
+
10
+ export interface PortfolioProject {
11
+ id: number;
12
+ title: string;
13
+ category: string;
14
+ industry: string;
15
+ result: string;
16
+ description: string;
17
+ image?: string;
18
+ tags: string[];
19
+ color: string;
20
+ url?: string;
21
+ }
22
+
23
+ /**
24
+ * Props for the PortfolioSimple section component.
25
+ *
26
+ * @remarks
27
+ * - Styling: slot-style className overrides are merged after defaults via cn().
28
+ * - Motion: entrance animations respect enableMotion; prefers-reduced-motion is supported.
29
+ *
30
+ * @public
31
+ */
32
+ interface PortfolioSimpleProps {
33
+ /** Optional id on root section */
34
+ id?: string;
35
+ /** Root className merged into section slot */
36
+ className?: string;
37
+
38
+ // Content
39
+ projects?: PortfolioProject[];
40
+ /**
41
+ * List of filter labels. Filtering matches project.category exactly.
42
+ * The first item is selected by default; a value of "All Projects" disables filtering.
43
+ */
44
+ filters?: string[];
45
+ /** When false, disables entrance animations and hover transitions. */
46
+ enableMotion?: boolean;
47
+
48
+ // Section Content
49
+ sectionTitle?: string;
50
+ sectionSubtitle?: string;
51
+ ctaTitle?: string;
52
+ ctaDescription?: string;
53
+
54
+ /** Label for the first call-to-action button */
55
+ cta1Label?: string;
56
+ /** URL or anchor target for the first CTA button */
57
+ cta1Href?: string;
58
+ /** Label for the second call-to-action button */
59
+ cta2Label?: string;
60
+ /** URL or anchor target for the second CTA button */
61
+ cta2Href?: string;
62
+
63
+ /** Styling configuration objects */
64
+ section?: {
65
+ className?: string;
66
+ };
67
+ container?: {
68
+ className?: string;
69
+ };
70
+ header?: {
71
+ className?: string;
72
+ };
73
+ title?: {
74
+ className?: string;
75
+ };
76
+ subtitle?: {
77
+ className?: string;
78
+ };
79
+ filterContainer?: {
80
+ className?: string;
81
+ };
82
+ filterButton?: {
83
+ className?: string;
84
+ };
85
+ activeFilterButton?: {
86
+ className?: string;
87
+ };
88
+ grid?: {
89
+ className?: string;
90
+ };
91
+ projectCard?: {
92
+ className?: string;
93
+ };
94
+ imageContainer?: {
95
+ className?: string;
96
+ };
97
+ projectInfo?: {
98
+ className?: string;
99
+ };
100
+ projectTitle?: {
101
+ className?: string;
102
+ };
103
+ projectDescription?: {
104
+ className?: string;
105
+ };
106
+ tagsContainer?: {
107
+ className?: string;
108
+ };
109
+ tagStyle?: {
110
+ className?: string;
111
+ };
112
+ result?: {
113
+ className?: string;
114
+ };
115
+ ctaSection?: {
116
+ className?: string;
117
+ };
118
+ ctaTitleStyle?: {
119
+ className?: string;
120
+ };
121
+ ctaDescriptionStyle?: {
122
+ className?: string;
123
+ };
124
+ ctaButtons?: {
125
+ className?: string;
126
+ };
127
+ cta1Button?: {
128
+ className?: string;
129
+ };
130
+ cta2Button?: {
131
+ className?: string;
132
+ };
133
+
134
+ // Callbacks
135
+ /** Called when a project image or card is clicked. */
136
+ onProjectClick?: (project: PortfolioProject) => void;
137
+ onPrimaryCtaClick?: () => void;
138
+ onSecondaryCtaClick?: () => void;
139
+
140
+ /** ARIA label for the portfolio section */
141
+ ariaLabel?: string;
142
+ }
143
+
144
+ // Simple project image component
145
+ interface ProjectImageProps {
146
+ project: PortfolioProject;
147
+ onClick?: () => void;
148
+ imageContainer?: {
149
+ className?: string;
150
+ };
151
+ }
152
+
153
+ const ProjectImage: React.FC<ProjectImageProps> = ({
154
+ project,
155
+ onClick,
156
+ imageContainer = {
157
+ className:
158
+ "relative rounded-lg overflow-hidden bg-muted shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-xl cursor-pointer aspect-[16/10]",
159
+ },
160
+ }) => {
161
+ const colorClasses = {
162
+ blue: "bg-blue-100 dark:bg-blue-900",
163
+ green: "bg-green-100 dark:bg-green-900",
164
+ purple: "bg-purple-100 dark:bg-purple-900",
165
+ red: "bg-red-100 dark:bg-red-900",
166
+ yellow: "bg-yellow-100 dark:bg-yellow-900",
167
+ indigo: "bg-indigo-100 dark:bg-indigo-900",
168
+ } as const;
169
+
170
+ const fallbackBg =
171
+ colorClasses[project.color as keyof typeof colorClasses] ||
172
+ colorClasses.blue;
173
+
174
+ return (
175
+ <div className={imageContainer.className} onClick={onClick}>
176
+ {project.image ? (
177
+ <Image
178
+ src={project.image}
179
+ alt={project.title}
180
+ fill
181
+ className="object-cover"
182
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
183
+ />
184
+ ) : (
185
+ <div
186
+ className={`flex h-full w-full items-center justify-center ${fallbackBg}`}
187
+ >
188
+ <div className="p-8 text-center">
189
+ <div className="bg-background/20 dark:bg-foreground/20 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full">
190
+ <ExternalLink className="h-8 w-8 text-[var(--card-muted-fg)]" />
191
+ </div>
192
+ <h3 className="mb-2 text-lg font-semibold text-[var(--card-title-fg)]">
193
+ {project.title}
194
+ </h3>
195
+ <p className="text-sm text-[var(--card-muted-fg)]">
196
+ {project.category}
197
+ </p>
198
+ </div>
199
+ </div>
200
+ )}
201
+ </div>
202
+ );
203
+ };
204
+
205
+ const defaultProjects = [
206
+ {
207
+ id: 1,
208
+ title: "TechStartup Growth Campaign",
209
+ category: "Digital Marketing",
210
+ industry: "Technology",
211
+ result: "+300% Leads",
212
+ description:
213
+ "Comprehensive digital marketing campaign that increased qualified leads by 300% through targeted SEO, social media marketing, and conversion optimization strategies.",
214
+ tags: ["SEO", "Social Media", "PPC", "Analytics"],
215
+ color: "blue",
216
+ url: "#",
217
+ },
218
+ {
219
+ id: 2,
220
+ title: "E-Commerce Brand Launch",
221
+ category: "Brand Marketing",
222
+ industry: "Retail",
223
+ result: "+250% Revenue",
224
+ description:
225
+ "Complete brand launch and digital marketing strategy for a new e-commerce platform, resulting in 250% revenue growth within the first 6 months.",
226
+ tags: ["Brand Strategy", "Content Marketing", "Influencer", "Email"],
227
+ color: "green",
228
+ url: "#",
229
+ },
230
+ {
231
+ id: 3,
232
+ title: "Healthcare Practice Expansion",
233
+ category: "Local Marketing",
234
+ industry: "Healthcare",
235
+ result: "+180% Patients",
236
+ description:
237
+ "Local SEO and digital marketing campaign that helped a healthcare practice expand to three new locations and increase patient acquisition by 180%.",
238
+ tags: ["Local SEO", "Google Ads", "Reputation", "Content"],
239
+ color: "purple",
240
+ url: "#",
241
+ },
242
+ {
243
+ id: 4,
244
+ title: "SaaS Product Launch",
245
+ category: "B2B Marketing",
246
+ industry: "Software",
247
+ result: "+400% Signups",
248
+ description:
249
+ "Strategic B2B marketing campaign for a SaaS product launch, achieving 400% increase in trial signups through targeted content marketing and account-based marketing.",
250
+ tags: ["B2B Strategy", "Content", "LinkedIn Ads", "Webinars"],
251
+ color: "indigo",
252
+ url: "#",
253
+ },
254
+ {
255
+ id: 5,
256
+ title: "Restaurant Chain Rebrand",
257
+ category: "Brand Marketing",
258
+ industry: "Food & Beverage",
259
+ result: "+220% Foot Traffic",
260
+ description:
261
+ "Complete rebrand and digital marketing transformation for a restaurant chain, resulting in 220% increase in foot traffic and 150% growth in online orders.",
262
+ tags: ["Rebranding", "Social Media", "Local SEO", "Delivery"],
263
+ color: "red",
264
+ url: "#",
265
+ },
266
+ {
267
+ id: 6,
268
+ title: "Fitness App User Acquisition",
269
+ category: "Mobile Marketing",
270
+ industry: "Health & Fitness",
271
+ result: "+500% Downloads",
272
+ description:
273
+ "Mobile-first marketing campaign that achieved 500% increase in app downloads through app store optimization, influencer partnerships, and targeted social media advertising.",
274
+ tags: ["ASO", "Influencer", "Social Ads", "Retention"],
275
+ color: "yellow",
276
+ url: "#",
277
+ },
278
+ ];
279
+
280
+ const defaultFilters = [
281
+ "All Projects",
282
+ "Digital Marketing",
283
+ "Brand Marketing",
284
+ "B2B Marketing",
285
+ "Local Marketing",
286
+ "Mobile Marketing",
287
+ ];
288
+
289
+ /**
290
+ * Responsive portfolio/gallery grid with simple category filtering and CTA block.
291
+ *
292
+ * @remarks
293
+ * - Filtering uses exact category match; include "All Projects" to disable filtering.
294
+ * - Accessibility: Renders a semantic <section> with aria-label; ensure images have informative titles or alt text.
295
+ *
296
+ * @example
297
+ * <PortfolioSimple sectionTitle="Our Work" />
298
+ */
299
+ export function PortfolioSimple({
300
+ id,
301
+ className,
302
+ projects = defaultProjects,
303
+ filters = defaultFilters,
304
+ enableMotion = true,
305
+ sectionTitle = "Our Recent Work",
306
+ sectionSubtitle = "Take a look at some of our successful projects that have helped businesses transform their digital presence and achieve remarkable growth.",
307
+ ctaTitle = "Ready to See Your Project Here?",
308
+ ctaDescription = "Let's discuss how we can help transform your business with a custom digital solution.",
309
+ cta1Label = "Get Free Quote",
310
+ cta1Href = "#contact",
311
+ cta2Label,
312
+ cta2Href,
313
+ section = {
314
+ className: "py-16 md:py-24 bg-background",
315
+ },
316
+ container = {
317
+ className: "max-w-7xl mx-auto px-6",
318
+ },
319
+ header = {
320
+ className: "space-y-6 text-center mb-12",
321
+ },
322
+ title = {
323
+ className:
324
+ "text-3xl md:text-4xl font-bold font-poppins text-foreground text-[var(--heading-fg)]",
325
+ },
326
+ subtitle = {
327
+ className:
328
+ "text-xl font-inter text-muted-foreground max-w-2xl mx-auto leading-relaxed text-[var(--subheading-fg)]",
329
+ },
330
+ filterContainer = {
331
+ className: "flex gap-4 flex-wrap justify-center mt-8",
332
+ },
333
+ filterButton = {
334
+ className:
335
+ "px-6 py-2 rounded-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md border-[var(--badge-border)] text-[var(--badge-fg)] bg-[var(--badge-bg)]",
336
+ },
337
+ activeFilterButton = {
338
+ className:
339
+ "px-6 py-2 rounded-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md bg-[var(--badge-active-bg)] text-[var(--badge-active-fg)] border-[var(--badge-active-border)]",
340
+ },
341
+ grid = {
342
+ className:
343
+ "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10 mt-12",
344
+ },
345
+ projectCard = {
346
+ className: "group cursor-pointer transition-all duration-300",
347
+ },
348
+ imageContainer = {
349
+ className:
350
+ "relative rounded-lg overflow-hidden bg-muted shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-xl cursor-pointer aspect-[16/10]",
351
+ },
352
+ projectInfo = {
353
+ className: "space-y-4 px-2 mt-4",
354
+ },
355
+ projectTitle = {
356
+ className:
357
+ "text-xl font-bold font-poppins transition-colors duration-200 text-[var(--card-title-fg)] group-hover:text-[var(--card-title-hover-fg)]",
358
+ },
359
+ projectDescription = {
360
+ className: "text-base font-inter text-[var(--card-muted-fg)]",
361
+ },
362
+ tagsContainer = {
363
+ className: "flex gap-2 flex-wrap",
364
+ },
365
+ tagStyle = {
366
+ className:
367
+ "px-2 py-1 text-xs border border-border rounded-full text-muted-foreground border-[var(--badge-border)] text-[var(--badge-fg)] bg-[var(--badge-bg)]",
368
+ },
369
+ result = {
370
+ className:
371
+ "font-bold text-[var(--metric-fg)] text-sm whitespace-nowrap ml-4",
372
+ },
373
+ ctaSection = {
374
+ className: "space-y-6 text-center mt-16 pt-12 border-t border-border",
375
+ },
376
+ ctaTitleStyle = {
377
+ className: "text-xl font-bold text-foreground text-[var(--heading-fg)]",
378
+ },
379
+ ctaDescriptionStyle = {
380
+ className:
381
+ "text-muted-foreground max-w-md mx-auto text-[var(--subheading-fg)]",
382
+ },
383
+ ctaButtons = {
384
+ className: "flex gap-4 justify-center",
385
+ },
386
+ cta1Button = {
387
+ className:
388
+ "bg-primary hover:bg-primary/90 dark:bg-primary dark:hover:bg-primary/90 text-primary-foreground font-medium shadow-md hover:shadow-lg transition-all duration-200 hover:-translate-y-0.5 border-[var(--btn-border)] focus-visible:ring-[var(--btn-ring)]",
389
+ },
390
+ cta2Button = {
391
+ className:
392
+ "border-primary text-primary hover:bg-primary/10 font-medium shadow-md hover:shadow-lg transition-all duration-200 hover:-translate-y-0.5 border-[var(--btn-border)] focus-visible:ring-[var(--btn-ring)]",
393
+ },
394
+ onProjectClick,
395
+ onPrimaryCtaClick,
396
+ onSecondaryCtaClick,
397
+ ariaLabel = "Portfolio showcase section",
398
+ }: PortfolioSimpleProps) {
399
+ const [activeFilter, setActiveFilter] = useState(
400
+ filters[0] || "All Projects",
401
+ );
402
+
403
+ const filteredProjects = projects.filter(
404
+ (project) =>
405
+ activeFilter === "All Projects" || project.category === activeFilter,
406
+ );
407
+
408
+ return (
409
+ <section
410
+ id={id}
411
+ className={cn(section.className, className)}
412
+ aria-label={ariaLabel}
413
+ >
414
+ <div className={container.className}>
415
+ {/* Section Header */}
416
+ <div className={header.className}>
417
+ <h2 className={title.className}>{sectionTitle}</h2>
418
+ <p className={subtitle.className}>{sectionSubtitle}</p>
419
+
420
+ {/* Filter Buttons */}
421
+ <div className={filterContainer.className}>
422
+ {filters.map((filter) => (
423
+ <button
424
+ key={filter}
425
+ className={
426
+ activeFilter === filter
427
+ ? activeFilterButton.className
428
+ : filterButton.className
429
+ }
430
+ onClick={() => setActiveFilter(filter)}
431
+ >
432
+ {filter}
433
+ </button>
434
+ ))}
435
+ </div>
436
+ </div>
437
+
438
+ {/* Portfolio Grid */}
439
+ <div className={grid.className}>
440
+ {filteredProjects.map((project, index) => (
441
+ <motion.div
442
+ key={project.id}
443
+ initial={
444
+ enableMotion ? { opacity: 0, y: 12 } : { opacity: 1, y: 0 }
445
+ }
446
+ whileInView={
447
+ enableMotion ? { opacity: 1, y: 0 } : { opacity: 1, y: 0 }
448
+ }
449
+ viewport={
450
+ enableMotion
451
+ ? { once: true, amount: 0.2 }
452
+ : { once: true, amount: 0 }
453
+ }
454
+ transition={
455
+ enableMotion
456
+ ? {
457
+ type: "spring",
458
+ stiffness: 125,
459
+ damping: 50,
460
+ mass: 1,
461
+ delay: 0.1 + index * 0.05,
462
+ }
463
+ : { type: "tween", duration: 0 }
464
+ }
465
+ className="motion-reduce:transform-none motion-reduce:transition-none"
466
+ >
467
+ <div
468
+ className={cn(
469
+ projectCard.className,
470
+ enableMotion
471
+ ? "transition-all duration-200 hover:-translate-y-1"
472
+ : "cursor-default transition-none hover:!translate-y-0 hover:shadow-none",
473
+ )}
474
+ >
475
+ <div className="space-y-4">
476
+ {/* Project Image */}
477
+ <ProjectImage
478
+ project={project}
479
+ onClick={() => onProjectClick?.(project)}
480
+ imageContainer={imageContainer}
481
+ />
482
+
483
+ {/* Project Info */}
484
+ <div className={projectInfo.className}>
485
+ <div className="flex w-full items-start justify-between">
486
+ <div className="flex-1 space-y-2">
487
+ <h3 className={projectTitle.className}>
488
+ {project.title}
489
+ </h3>
490
+ <p className={projectDescription.className}>
491
+ {project.description}
492
+ </p>
493
+ </div>
494
+ </div>
495
+
496
+ {/* Tags & Result */}
497
+ <div className="flex w-full items-center justify-between">
498
+ <div className={tagsContainer.className}>
499
+ {project.tags.map((tag, index) => (
500
+ <span key={index} className={tagStyle.className}>
501
+ {tag}
502
+ </span>
503
+ ))}
504
+ </div>
505
+ <div className="flex items-center gap-2">
506
+ <TrendingUp className="h-4 w-4 text-[var(--metric-fg)]" />
507
+ <span className={result.className}>
508
+ {project.result}
509
+ </span>
510
+ </div>
511
+ </div>
512
+ </div>
513
+ </div>
514
+ </div>
515
+ </motion.div>
516
+ ))}
517
+ </div>
518
+
519
+ {/* CTA Section */}
520
+ <div className={ctaSection.className}>
521
+ <h3 className={ctaTitleStyle.className}>{ctaTitle}</h3>
522
+ <p className={ctaDescriptionStyle.className}>{ctaDescription}</p>
523
+ <div className={ctaButtons.className}>
524
+ <CTAButton
525
+ ctaButtonLabel={cta1Label}
526
+ ctaButtonHref={cta1Href}
527
+ button={{
528
+ className: cta1Button.className,
529
+ }}
530
+ onClick={onPrimaryCtaClick}
531
+ />
532
+ {cta2Label && (
533
+ <CTAButton
534
+ ctaButtonLabel={cta2Label}
535
+ ctaButtonHref={cta2Href}
536
+ button={{
537
+ variant: "outline",
538
+ size: "lg",
539
+ className: cta2Button.className,
540
+ }}
541
+ onClick={onSecondaryCtaClick}
542
+ />
543
+ )}
544
+ </div>
545
+ </div>
546
+ </div>
547
+ </section>
548
+ );
549
+ }