From 2a79b981d5f39d91f3b071e5c04bb6ecbf656bf5 Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Wed, 20 May 2026 01:23:54 +0500 Subject: [PATCH 01/14] feat: add Pro gating config with per-component free variant limits --- config/pro.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 config/pro.ts diff --git a/config/pro.ts b/config/pro.ts new file mode 100644 index 0000000..121a6d0 --- /dev/null +++ b/config/pro.ts @@ -0,0 +1,52 @@ +export const PRO_COMPONENTS: Record = { + button: { freeCount: 5 }, + badge: { freeCount: 5 }, + accordion: { freeCount: 5 }, + avatar: { freeCount: 5 }, + breadcrumb: { freeCount: 5 }, + checkbox: { freeCount: 5 }, + dialog: { freeCount: 5 }, + drawer: { freeCount: 5 }, + "dropdown-menu": { freeCount: 5 }, + input: { freeCount: 5 }, + "input-otp": { freeCount: 5 }, + popover: { freeCount: 5 }, + progress: { freeCount: 5 }, + "radio-group": { freeCount: 5 }, + select: { freeCount: 5 }, + separator: { freeCount: 5 }, + sheet: { freeCount: 5 }, + skeleton: { freeCount: 5 }, + slider: { freeCount: 5 }, + switch: { freeCount: 5 }, + tabs: { freeCount: 5 }, + textarea: { freeCount: 5 }, + toast: { freeCount: 5 }, + "toggle-group": { freeCount: 5 }, + tooltip: { freeCount: 5 }, + card: { freeCount: 3 }, + "date-picker": { freeCount: 2 }, + alert: { freeCount: 4 }, +}; + +export function isProComponent( + componentName: string, + variantIndex?: number, +): boolean { + if (!variantIndex !== undefined && variantIndex === undefined) { + return false; + } + + const config = PRO_COMPONENTS[componentName]; + if (!config) return false; + + if (variantIndex !== undefined) { + return variantIndex >= config.freeCount; + } + + return true; +} + +export function getFreeCount(componentName: string): number { + return PRO_COMPONENTS[componentName]?.freeCount ?? 5; +} From c5f4c2d00f999ab07de25a0eaddfb4eb96e9db67 Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Wed, 20 May 2026 01:23:59 +0500 Subject: [PATCH 02/14] feat: add ProLock component with blur overlay and purchase CTA --- components/bilalUi/pro/index.ts | 1 + components/bilalUi/pro/pro-lock.tsx | 50 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 components/bilalUi/pro/pro-lock.tsx diff --git a/components/bilalUi/pro/index.ts b/components/bilalUi/pro/index.ts index 1fd4028..0225402 100644 --- a/components/bilalUi/pro/index.ts +++ b/components/bilalUi/pro/index.ts @@ -3,3 +3,4 @@ export { ProBadge } from "./pro-badge"; export { ProCard } from "./pro-card"; export { ProFeatureCard } from "./pro-feature-card"; export { ProComingSoon } from "./pro-coming-soon"; +export { ProLock } from "./pro-lock"; diff --git a/components/bilalUi/pro/pro-lock.tsx b/components/bilalUi/pro/pro-lock.tsx new file mode 100644 index 0000000..481380e --- /dev/null +++ b/components/bilalUi/pro/pro-lock.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { Crown, Sparkles, LockKeyhole } from "lucide-react"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; + +interface ProLockProps { + label?: string; + className?: string; +} + +export function ProLock({ + label = "This is a Pro component", + className, +}: ProLockProps) { + return ( +
+
+ +
+ +
+
+ + Pro +
+

+ {label} +

+

+ Unlock all premium component variants, page blocks, and full templates + with a single $15 payment. +

+
+ + + + Unlock with Pro + +
+ ); +} From ed981f9cf8cc68bfe58f43316b8ad6d5661e27f4 Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Wed, 20 May 2026 01:24:04 +0500 Subject: [PATCH 03/14] feat: add pro prop to ComponentPreview for gating preview and code behind ProLock --- components/bilalUi/component-preview.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/components/bilalUi/component-preview.tsx b/components/bilalUi/component-preview.tsx index cee38ea..d548d52 100644 --- a/components/bilalUi/component-preview.tsx +++ b/components/bilalUi/component-preview.tsx @@ -17,6 +17,7 @@ import { AnimatePresence, motion } from "motion/react"; import { OpenInV0Button } from "./open-in-v0-button"; import { Spinner } from "@/components/ui/spinner"; import { CodeBlock } from "@/components/ui/code-block"; +import { ProLock } from "./pro/pro-lock"; function SuccessParticles({ buttonRef, @@ -93,6 +94,7 @@ interface ComponentPreviewProps { * npx shadcn@latest add {NEXT_PUBLIC_APP_URL}/registry/{registry} */ registry?: string; + pro?: boolean; } export function ComponentPreview({ @@ -103,6 +105,7 @@ export function ComponentPreview({ sourceName, installCommand, registry, + pro, }: ComponentPreviewProps) { const [isCopied, setIsCopied] = React.useState(false); const [isInstallCopied, setIsInstallCopied] = React.useState(false); @@ -324,17 +327,23 @@ export function ComponentPreview({ >
- {children} + {pro ? : children}
- + {pro ? ( +
+ +
+ ) : ( + + )}
From 24ca59416fa8880de7197d010a60f093ff127b32 Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Wed, 20 May 2026 01:24:15 +0500 Subject: [PATCH 04/14] feat: gate source and registry APIs to return locked placeholder for Pro component variants --- app/api/source/[name]/route.ts | 56 ++++++++++++++++++++++--- app/registry/[name]/route.ts | 76 +++++++++++++++++++++++++--------- 2 files changed, 108 insertions(+), 24 deletions(-) diff --git a/app/api/source/[name]/route.ts b/app/api/source/[name]/route.ts index a5cd274..0fc8185 100644 --- a/app/api/source/[name]/route.ts +++ b/app/api/source/[name]/route.ts @@ -1,7 +1,8 @@ -// File: app/api/source/[name]/route.ts import { NextResponse } from "next/server" import fs from "fs/promises" +import fsSync from "fs" import path from "path" +import { PRO_COMPONENTS } from "@/config/pro" async function findFilesRecursively( dir: string, @@ -36,25 +37,64 @@ function selectBestMatch(matches: string[], preferDemo: boolean): string | null return nonDemoMatches[0] || demoMatches[0] || sorted[0]; } +function getComponentGroupFromPath(filePath: string): string | null { + const parts = filePath.replace(/\\/g, "/").split("/"); + const bilalUiIndex = parts.indexOf("bilalUi"); + if (bilalUiIndex === -1 || bilalUiIndex + 1 >= parts.length) return null; + const group = parts[bilalUiIndex + 1]; + if (group === "demo" || group === "pricing" || group === "pro") return null; + return group; +} + +function isProFile(filePath: string): boolean { + const group = getComponentGroupFromPath(filePath); + if (!group) return false; + + const config = PRO_COMPONENTS[group]; + if (!config) return false; + + const filename = path.basename(filePath, path.extname(filePath)); + const groupDir = path.dirname(filePath); + const groupDirname = path.basename(groupDir); + + if (groupDirname !== group) return false; + + try { + const allFiles = fsSync.readdirSync(groupDir) + .filter(f => f.endsWith(".tsx")) + .sort(); + const index = allFiles.indexOf(`${filename}.tsx`); + if (index === -1) return false; + return index >= config.freeCount; + } catch { + return false; + } +} + +const PRO_PLACEHOLDER = `// This is a Pro component. +// Purchase the Lifetime plan ($15) to unlock the full source code. +// Go to /pricing for more details. + +export function ProComponent() { + return null; +} +`; + export async function GET( request: Request, { params }: { params: Promise<{ name: string }> } ) { const { name } = await params - // Strict Validation & Sanitization if (!name) { return new NextResponse("Component name required", { status: 400 }) } - // Sanitize input to prevent path traversal const safeName = path.basename(name) if (safeName !== name) { return new NextResponse("Invalid request path", { status: 400 }) } - // Validate component name format (alphanumeric + hyphens only) - // Allow .json extension if that's part of the route pattern (though usually stripped) const componentName = safeName.replace(".json", "") if (!/^[a-zA-Z0-9-]+$/.test(componentName)) { return new NextResponse("Invalid component name", { status: 400 }) @@ -72,6 +112,12 @@ export async function GET( return new NextResponse("Component not found", { status: 404 }) } + if (isProFile(filePath)) { + return new NextResponse(PRO_PLACEHOLDER, { + headers: { "Content-Type": "text/plain" }, + }); + } + const fileContent = await fs.readFile(filePath, "utf-8") return new NextResponse(fileContent, { headers: { "Content-Type": "text/plain" }, diff --git a/app/registry/[name]/route.ts b/app/registry/[name]/route.ts index 3d28d8c..6266aad 100644 --- a/app/registry/[name]/route.ts +++ b/app/registry/[name]/route.ts @@ -1,7 +1,8 @@ -// File: app/registry/[name]/route.ts import { NextResponse } from "next/server" import fs from "fs/promises" +import fsSync from "fs" import path from "path" +import { PRO_COMPONENTS } from "@/config/pro" const COMPONENTS_DIR_NAME = "bilalUi" const EXCLUDED_FROM_INDEX = new Set([ @@ -12,22 +13,65 @@ const EXCLUDED_FROM_INDEX = new Set([ "TestDemo.tsx", ]) -// Helper to extract component data from a file +function getComponentGroupFromPath(filePath: string): string | null { + const parts = filePath.replace(/\\/g, "/").split("/"); + const bilalUiIndex = parts.indexOf("bilalUi"); + if (bilalUiIndex === -1 || bilalUiIndex + 1 >= parts.length) return null; + const group = parts[bilalUiIndex + 1]; + if (group === "demo" || group === "pricing" || group === "pro") return null; + return group; +} + +function isProFile(filePath: string): boolean { + const group = getComponentGroupFromPath(filePath); + if (!group) return false; + + const config = PRO_COMPONENTS[group]; + if (!config) return false; + + const filename = path.basename(filePath, path.extname(filePath)); + const groupDir = path.dirname(filePath); + const groupDirname = path.basename(groupDir); + + if (groupDirname !== group) return false; + + try { + const allFiles = fsSync.readdirSync(groupDir) + .filter(f => f.endsWith(".tsx")) + .sort(); + const index = allFiles.indexOf(`${filename}.tsx`); + if (index === -1) return false; + return index >= config.freeCount; + } catch { + return false; + } +} + +const PRO_PLACEHOLDER_CONTENT = `// This is a Pro component. +// Purchase the Lifetime plan ($15) to unlock the full source code. +// Go to /pricing for more details. + +export function ProComponent() { + return null; +} +`; + async function getComponentData(filePath: string, componentsDir: string) { const content = await fs.readFile(filePath, "utf-8") const filename = path.basename(filePath, path.extname(filePath)) - - // Extract dependencies + + const isPro = isProFile(filePath); + const resolvedContent = isPro ? PRO_PLACEHOLDER_CONTENT : content; + const dependencies = new Set() const registryDependencies = new Set() - - // Regex for imports + const importRegex = /import\s+(?:[\w\s{},*]+)\s+from\s+['"]([^'"]+)['"]/g let match - - while ((match = importRegex.exec(content)) !== null) { + + while ((match = importRegex.exec(resolvedContent)) !== null) { const importPath = match[1] - + if (importPath.startsWith("@/components/ui/") || importPath.startsWith(`@/components/${COMPONENTS_DIR_NAME}/`)) { const depName = path.basename(importPath, path.extname(importPath)) registryDependencies.add(depName) @@ -46,18 +90,17 @@ async function getComponentData(filePath: string, componentsDir: string) { files: [ { path: relativePath, - content: content, + content: resolvedContent, type: "registry:component" } ] } } -// Recursively find all component files async function getAllComponentFiles(dir: string, fileList: string[] = []) { try { const entries = await fs.readdir(dir, { withFileTypes: true }) - + for (const entry of entries) { const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { @@ -82,7 +125,7 @@ async function findComponentFiles( ): Promise { try { const entries = await fs.readdir(dir, { withFileTypes: true }) - + for (const entry of entries) { if (entry.isDirectory()) { await findComponentFiles(path.join(dir, entry.name), filename, matches) @@ -115,13 +158,11 @@ export async function GET( { params }: { params: Promise<{ name: string }> } ) { const { name } = await params - - // Basic validation & Sanitization + if (!name || !name.endsWith(".json")) { return new NextResponse("Invalid request", { status: 400 }) } - // Sanitize input to prevent path traversal const safeName = path.basename(name) if (safeName !== name) { return new NextResponse("Invalid request path", { status: 400 }) @@ -130,12 +171,10 @@ export async function GET( const componentsDir = path.join(process.cwd(), "components", COMPONENTS_DIR_NAME) const componentName = safeName.replace(".json", "") - // Validate component name format (alphanumeric + hyphens only) if (!/^[a-zA-Z0-9-]+$/.test(componentName) && componentName !== "index" && componentName !== "registry") { return new NextResponse("Invalid component name", { status: 400 }) } - // Handle Index / Registry request if (componentName === "index" || componentName === "registry") { try { const allFiles = await getAllComponentFiles(componentsDir) @@ -147,7 +186,6 @@ export async function GET( } } - // Handle Individual Component request const matches = await findComponentFiles(componentsDir, `${componentName}.tsx`) const filePath = selectBestMatch(matches, componentName.endsWith("-demo")) From 9f56c3e5a8ac2fe267b535ef2bd2070bd9977aaf Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Wed, 20 May 2026 01:24:21 +0500 Subject: [PATCH 05/14] feat: mark component variants 6+ as Pro in Button and Badge MDX docs --- content/docs/components/badge.mdx | 29 ++++++++++++----------- content/docs/components/button.mdx | 37 ++++++++++++++++-------------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/content/docs/components/badge.mdx b/content/docs/components/badge.mdx index 24cea72..42a8a88 100644 --- a/content/docs/components/badge.mdx +++ b/content/docs/components/badge.mdx @@ -70,21 +70,21 @@ Token-style badges with removable actions. ### Badge With Icon Leading icon treatment for plan and state labels. - + ### Shortcut Badge Badges paired with keyboard shortcuts for quick actions. - + ### Badge Status Row One state shown in all four appearances. - + @@ -94,6 +94,7 @@ Alpha through deprecated release lifecycle examples. @@ -101,7 +102,7 @@ Alpha through deprecated release lifecycle examples. ### Changelog Badge Release note labels for updates, fixes, docs, and perf changes. - + @@ -111,6 +112,7 @@ Flags for runtime, lab, legacy, and headless availability. @@ -121,6 +123,7 @@ Circular count badges for inbox, alerts, and review totals. @@ -128,62 +131,62 @@ Circular count badges for inbox, alerts, and review totals. ### Plan Tiers Badge Pricing and plan emphasis with larger badge sizing. - + ### Tag Cloud Badge Mixed-size tags with removable actions. - + ### Status Pills Badge Rounded status pills including a live animated dot state. - + ### Special Types Badge WIP, lab, request, and featured metadata styles. - + ### Outline Row Badge Outline treatment across many supported badge variants. - + ### Ghost Row Badge Ghost appearance examples for lightweight metadata. - + ### Inline Text Badge Inline badge usage inside prose and UI copy. - + ### Disabled Badge Disabled states across multiple variants and appearances. - + ### Version Table Badge Structured package table with version and lifecycle badges. - + diff --git a/content/docs/components/button.mdx b/content/docs/components/button.mdx index 87aaed7..b3a307b 100644 --- a/content/docs/components/button.mdx +++ b/content/docs/components/button.mdx @@ -74,91 +74,91 @@ A vibrant gradient border button with multiple color schemes and animation effec ### Pulse Button Pulse ring emphasis for a primary action. - + Pulse ### Gradient Shift Button Animated gradient surface with hover emphasis. - + Gradient ### Slide Reveal Button Reveal effect on hover for text or icon swaps. - + Slide Reveal ### Bounce Button Subtle bounce animation for repeated calls to action. - + Bounce ### Shimmer Button Animated shimmer for loading or featured actions. - + Shimmer ### Rotate Button Micro-rotation feedback on hover. - + Rotate ### Glow Button Glowing outline on hover to draw attention. - + Glow ### Flip Button Perspective tilt for playful depth. - + Flip ### Fill Button Border-to-fill transition for secondary CTAs. - + Fill ### Float Button Slow floating motion for ambient emphasis. - + Float ### Neon Button Neon-like accent for dark surfaces. - + Neon ### Ink Spread Button Ink-like hover spread using pseudo-elements. - + Ink Spread ### Spring Button Springy scale response for tactile feedback. - + Spring @@ -168,6 +168,7 @@ Underline reveal for minimal text actions. Underline @@ -175,28 +176,28 @@ Underline reveal for minimal text actions. ### Ripple Button Click ripple feedback with motion. - + Ripple ### Expand Button Larger scale expansion on hover. - + Expand ### Glass Button Glassmorphism styling for translucent surfaces. - + Glass ### Slide Up Button Subtle upward motion on hover. - + Slide Up @@ -206,6 +207,7 @@ Disabled skeleton-style loading state. Loading @@ -216,6 +218,7 @@ Rotating gradient border treatment. Rotate Border From c5395ca337f41e7a26d9f61125643231128b08ac Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Wed, 20 May 2026 01:26:39 +0500 Subject: [PATCH 06/14] fix: replace raw code placeholder with proper ProPlaceholder UI component for locked variants --- app/api/source/[name]/route.ts | 37 +++++++++++++++++++++++++++++----- app/registry/[name]/route.ts | 37 +++++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/app/api/source/[name]/route.ts b/app/api/source/[name]/route.ts index 0fc8185..ca5a2a0 100644 --- a/app/api/source/[name]/route.ts +++ b/app/api/source/[name]/route.ts @@ -71,12 +71,39 @@ function isProFile(filePath: string): boolean { } } -const PRO_PLACEHOLDER = `// This is a Pro component. -// Purchase the Lifetime plan ($15) to unlock the full source code. -// Go to /pricing for more details. +const PRO_PLACEHOLDER = `"use client"; -export function ProComponent() { - return null; +import { Crown, LockKeyhole, Sparkles } from "lucide-react"; +import Link from "next/link"; + +interface ProPlaceholderProps { + children?: React.ReactNode; +} + +export function ProPlaceholder({ children }: ProPlaceholderProps) { + return ( +
+
+ +
+
+
+ + Pro Component +
+

+ Purchase the Lifetime plan ($15) to unlock this component. +

+
+ + + Unlock with Pro + +
+ ); } `; diff --git a/app/registry/[name]/route.ts b/app/registry/[name]/route.ts index 6266aad..7020e44 100644 --- a/app/registry/[name]/route.ts +++ b/app/registry/[name]/route.ts @@ -47,12 +47,39 @@ function isProFile(filePath: string): boolean { } } -const PRO_PLACEHOLDER_CONTENT = `// This is a Pro component. -// Purchase the Lifetime plan ($15) to unlock the full source code. -// Go to /pricing for more details. +const PRO_PLACEHOLDER_CONTENT = `"use client"; -export function ProComponent() { - return null; +import { Crown, LockKeyhole, Sparkles } from "lucide-react"; +import Link from "next/link"; + +interface ProPlaceholderProps { + children?: React.ReactNode; +} + +export function ProPlaceholder({ children }: ProPlaceholderProps) { + return ( +
+
+ +
+
+
+ + Pro Component +
+

+ Purchase the Lifetime plan ($15) to unlock this component. +

+
+ + + Unlock with Pro + +
+ ); } `; From 0aabd69f7b50e5261fdbc6d22fffa8cfca21b3fa Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Wed, 20 May 2026 01:29:40 +0500 Subject: [PATCH 07/14] fix: show live preview for Pro components, lock only code tab; simplify ProLock UI (remove gradients and shadows) --- app/api/source/[name]/route.ts | 8 ++++---- app/registry/[name]/route.ts | 8 ++++---- components/bilalUi/component-preview.tsx | 2 +- components/bilalUi/pro/pro-lock.tsx | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/api/source/[name]/route.ts b/app/api/source/[name]/route.ts index ca5a2a0..5e8ce38 100644 --- a/app/api/source/[name]/route.ts +++ b/app/api/source/[name]/route.ts @@ -82,9 +82,9 @@ interface ProPlaceholderProps { export function ProPlaceholder({ children }: ProPlaceholderProps) { return ( -
-
- +
+
+
@@ -97,7 +97,7 @@ export function ProPlaceholder({ children }: ProPlaceholderProps) {
Unlock with Pro diff --git a/app/registry/[name]/route.ts b/app/registry/[name]/route.ts index 7020e44..598d9e8 100644 --- a/app/registry/[name]/route.ts +++ b/app/registry/[name]/route.ts @@ -58,9 +58,9 @@ interface ProPlaceholderProps { export function ProPlaceholder({ children }: ProPlaceholderProps) { return ( -
-
- +
+
+
@@ -73,7 +73,7 @@ export function ProPlaceholder({ children }: ProPlaceholderProps) {
Unlock with Pro diff --git a/components/bilalUi/component-preview.tsx b/components/bilalUi/component-preview.tsx index d548d52..1f39ab2 100644 --- a/components/bilalUi/component-preview.tsx +++ b/components/bilalUi/component-preview.tsx @@ -327,7 +327,7 @@ export function ComponentPreview({ >
- {pro ? : children} + {children}
diff --git a/components/bilalUi/pro/pro-lock.tsx b/components/bilalUi/pro/pro-lock.tsx index 481380e..0cfaa16 100644 --- a/components/bilalUi/pro/pro-lock.tsx +++ b/components/bilalUi/pro/pro-lock.tsx @@ -16,12 +16,12 @@ export function ProLock({ return (
-
- +
+
@@ -40,7 +40,7 @@ export function ProLock({ Unlock with Pro From e986aec966c13e2e868af26737ee5fb45862d0a1 Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Wed, 20 May 2026 01:31:49 +0500 Subject: [PATCH 08/14] refactor: use shadcn Badge/Button components and Next Link in ProLock and ProPlaceholder --- app/api/source/[name]/route.ts | 19 ++++++++++--------- app/registry/[name]/route.ts | 19 ++++++++++--------- components/bilalUi/pro/pro-lock.tsx | 19 ++++++++++--------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/app/api/source/[name]/route.ts b/app/api/source/[name]/route.ts index 5e8ce38..1a292d5 100644 --- a/app/api/source/[name]/route.ts +++ b/app/api/source/[name]/route.ts @@ -75,6 +75,8 @@ const PRO_PLACEHOLDER = `"use client"; import { Crown, LockKeyhole, Sparkles } from "lucide-react"; import Link from "next/link"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; interface ProPlaceholderProps { children?: React.ReactNode; @@ -87,21 +89,20 @@ export function ProPlaceholder({ children }: ProPlaceholderProps) {
-
+ Pro Component -
+

Purchase the Lifetime plan ($15) to unlock this component.

- - - Unlock with Pro - +
); } diff --git a/app/registry/[name]/route.ts b/app/registry/[name]/route.ts index 598d9e8..25a3199 100644 --- a/app/registry/[name]/route.ts +++ b/app/registry/[name]/route.ts @@ -51,6 +51,8 @@ const PRO_PLACEHOLDER_CONTENT = `"use client"; import { Crown, LockKeyhole, Sparkles } from "lucide-react"; import Link from "next/link"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; interface ProPlaceholderProps { children?: React.ReactNode; @@ -63,21 +65,20 @@ export function ProPlaceholder({ children }: ProPlaceholderProps) {
-
+ Pro Component -
+

Purchase the Lifetime plan ($15) to unlock this component.

- - - Unlock with Pro - +
); } diff --git a/components/bilalUi/pro/pro-lock.tsx b/components/bilalUi/pro/pro-lock.tsx index 0cfaa16..ec02831 100644 --- a/components/bilalUi/pro/pro-lock.tsx +++ b/components/bilalUi/pro/pro-lock.tsx @@ -3,6 +3,8 @@ import { Crown, Sparkles, LockKeyhole } from "lucide-react"; import Link from "next/link"; import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; interface ProLockProps { label?: string; @@ -25,10 +27,10 @@ export function ProLock({
-
+ Pro -
+

{label}

@@ -38,13 +40,12 @@ export function ProLock({

- - - Unlock with Pro - +
); } From 160271cdb97359b44d38685241aff8e74ef93d50 Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Thu, 21 May 2026 14:16:29 +0500 Subject: [PATCH 09/14] feat: integrate LemonSqueezy payments for Pro component gating - Add LemonSqueezy SDK and env-based config (store, product, variant, webhook) - Create checkout API (GET redirect + POST JSON) to generate LemonSqueezy checkout sessions - Add license key verification API via LemonSqueezy validate endpoint - Implement webhook handler with signature verification - Build license dialog UI for entering and validating license keys - Add useProAccess hook for client-side license state (localStorage) - Update ProLock to show license key entry option and auto-hide when verified - Update pricing page/card: replace Coming Soon with live checkout button when LemonSqueezy is configured - Remove gradients, neon effects, and heavy shadows from pricing and pro components - Use existing Badge component (variant='pro', appearance='outline') instead of manual styling --- app/(root)/pricing/page.tsx | 10 +- app/api/debug/env/route.ts | 28 ++++ app/api/lemon/checkout/route.ts | 58 ++++++++ app/api/lemon/verify/route.ts | 18 +++ app/api/lemon/webhook/route.ts | 39 ++++++ components/bilalUi/pricing/pricing-card.tsx | 98 ++++++++------ components/bilalUi/pro/license-dialog.tsx | 143 ++++++++++++++++++++ components/bilalUi/pro/pro-lock.tsx | 90 ++++++++---- config/lemon.ts | 18 +++ hooks/use-pro-access.ts | 49 +++++++ lib/lemon.ts | 61 +++++++++ package-lock.json | 10 ++ package.json | 1 + 13 files changed, 546 insertions(+), 77 deletions(-) create mode 100644 app/api/debug/env/route.ts create mode 100644 app/api/lemon/checkout/route.ts create mode 100644 app/api/lemon/verify/route.ts create mode 100644 app/api/lemon/webhook/route.ts create mode 100644 components/bilalUi/pro/license-dialog.tsx create mode 100644 config/lemon.ts create mode 100644 hooks/use-pro-access.ts create mode 100644 lib/lemon.ts diff --git a/app/(root)/pricing/page.tsx b/app/(root)/pricing/page.tsx index 5f08702..9c531bc 100644 --- a/app/(root)/pricing/page.tsx +++ b/app/(root)/pricing/page.tsx @@ -6,6 +6,9 @@ import { PricingFAQ, PricingCTA, } from "@/components/bilalUi/pricing"; +import { isLemonConfigured } from "@/config/lemon"; + +const lemonReady = isLemonConfigured(); const plans = [ { @@ -44,7 +47,8 @@ const plans = [ cta: "Get Lifetime Access", href: "#", popular: true, - comingSoon: true, + comingSoon: !lemonReady, + checkout: lemonReady, }, ]; @@ -130,7 +134,7 @@ export default function PricingPage() {
{plans.map((plan) => ( - + ))}
@@ -148,7 +152,7 @@ export default function PricingPage() { diff --git a/app/api/debug/env/route.ts b/app/api/debug/env/route.ts new file mode 100644 index 0000000..e7636ff --- /dev/null +++ b/app/api/debug/env/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import { isLemonConfigured, getLemonConfig } from "@/config/lemon"; + +export async function GET() { + const configured = isLemonConfigured(); + let config: Record = {}; + + if (configured) { + const c = getLemonConfig(); + config = { + storeId: String(c.storeId), + productId: String(c.productId), + variantId: String(c.variantId), + apiKeyLength: String(c.apiKey.length), + }; + } + + return NextResponse.json({ + configured, + env: { + hasApiKey: !!process.env.LEMONSQUEEZY_API_KEY, + hasStoreId: !!process.env.LEMONSQUEEZY_STORE_ID, + hasProductId: !!process.env.LEMONSQUEEZY_PRODUCT_ID, + hasVariantId: !!process.env.LEMONSQUEEZY_VARIANT_ID, + }, + config, + }); +} diff --git a/app/api/lemon/checkout/route.ts b/app/api/lemon/checkout/route.ts new file mode 100644 index 0000000..0d06175 --- /dev/null +++ b/app/api/lemon/checkout/route.ts @@ -0,0 +1,58 @@ +import { NextResponse, NextRequest } from "next/server"; +import { createCheckoutUrl, setupLemon } from "@/lib/lemon"; +import { isLemonConfigured } from "@/config/lemon"; + +export async function GET() { + if (!isLemonConfigured()) { + return NextResponse.json( + { error: "Payment not configured" }, + { status: 503 }, + ); + } + + setupLemon(); + + const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + + const url = await createCheckoutUrl({ + redirectUrl: `${appUrl}/pricing?checkout=success`, + }); + + if (!url) { + return NextResponse.json( + { error: "Failed to create checkout" }, + { status: 500 }, + ); + } + + return NextResponse.redirect(url); +} + +export async function POST(req: Request) { + if (!isLemonConfigured()) { + return NextResponse.json( + { error: "Payment not configured" }, + { status: 503 }, + ); + } + + setupLemon(); + + const body = await req.json().catch(() => ({})); + const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; + + const url = await createCheckoutUrl({ + redirectUrl: `${appUrl}/pricing?checkout=success`, + email: body.email, + name: body.name, + }); + + if (!url) { + return NextResponse.json( + { error: "Failed to create checkout" }, + { status: 500 }, + ); + } + + return NextResponse.json({ url }); +} diff --git a/app/api/lemon/verify/route.ts b/app/api/lemon/verify/route.ts new file mode 100644 index 0000000..7aab42c --- /dev/null +++ b/app/api/lemon/verify/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { verifyLicenseKey } from "@/lib/lemon"; + +export async function POST(req: Request) { + const body = await req.json().catch(() => ({})); + const { licenseKey } = body; + + if (!licenseKey || typeof licenseKey !== "string") { + return NextResponse.json( + { valid: false, error: "License key is required" }, + { status: 400 }, + ); + } + + const result = await verifyLicenseKey(licenseKey.trim()); + + return NextResponse.json(result); +} diff --git a/app/api/lemon/webhook/route.ts b/app/api/lemon/webhook/route.ts new file mode 100644 index 0000000..416322c --- /dev/null +++ b/app/api/lemon/webhook/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { getLemonConfig } from "@/config/lemon"; + +export async function POST(req: Request) { + const { webhookSecret } = getLemonConfig(); + + const signature = req.headers.get("x-signature"); + const body = await req.text(); + + if (!signature) { + return NextResponse.json({ error: "Missing signature" }, { status: 401 }); + } + + const expected = await crypto.subtle + .digest("SHA-256", new TextEncoder().encode(body + webhookSecret)) + .then((h) => + Array.from(new Uint8Array(h)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""), + ); + + if (signature !== expected) { + return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); + } + + const event = JSON.parse(body); + const eventName = event.meta?.event_name; + + if (eventName === "order_created") { + const licenseKey = event.data?.attributes?.first_order_item?.license_key; + const email = event.data?.attributes?.user_email; + + if (licenseKey && email) { + console.log(`[LemonSqueezy] Order from ${email}, license: ${licenseKey}`); + } + } + + return NextResponse.json({ received: true }); +} diff --git a/components/bilalUi/pricing/pricing-card.tsx b/components/bilalUi/pricing/pricing-card.tsx index 0feed48..9997f2a 100644 --- a/components/bilalUi/pricing/pricing-card.tsx +++ b/components/bilalUi/pricing/pricing-card.tsx @@ -1,7 +1,8 @@ "use client"; +import { useState } from "react"; import { motion } from "motion/react"; -import { Check, ArrowRight, Crown, Sparkles } from "lucide-react"; +import { Check, ArrowRight, Crown, Sparkles, Loader2 } from "lucide-react"; import Link from "next/link"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; @@ -19,6 +20,7 @@ interface PricingCardProps { popular?: boolean; badge?: string; comingSoon?: boolean; + checkout?: boolean; className?: string; } @@ -33,6 +35,7 @@ export function PricingCard({ popular, badge, comingSoon, + checkout, className, }: PricingCardProps) { if (popular) { @@ -47,6 +50,7 @@ export function PricingCard({ href={href} badge={badge} comingSoon={comingSoon} + checkout={checkout} className={className} /> ); @@ -97,15 +101,13 @@ function FreeCard({ initial={{ opacity: 0, x: -30 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.5, ease: "easeOut" }} - whileHover={{ y: -4 }} + whileHover={{ y: -2 }} className={cn( - "relative h-full overflow-hidden rounded-2xl border border-zinc-200 bg-white p-8 transition-shadow duration-300 hover:shadow-lg dark:border-zinc-800 dark:bg-zinc-950", + "relative h-full overflow-hidden rounded-2xl border border-zinc-200 bg-white p-8 dark:border-zinc-800 dark:bg-zinc-950", className, )} > -
- -
+

{name} @@ -164,6 +166,7 @@ function PremiumCard({ href, badge: badgeLabel, comingSoon, + checkout, className, }: { name: string; @@ -175,38 +178,34 @@ function PremiumCard({ href: string; badge?: string; comingSoon?: boolean; + checkout?: boolean; className?: string; }) { + const [loading, setLoading] = useState(false); + + async function handleCheckout() { + setLoading(true); + try { + const res = await fetch("/api/lemon/checkout", { method: "POST" }); + const data = await res.json(); + if (data.url) { + window.location.href = data.url; + } + } catch { + setLoading(false); + } + } return ( - {/* Animated gradient orbs */} - - - - {/* Shimmer line */} - -
@@ -214,9 +213,9 @@ function PremiumCard({
-
+
-

+

{name}

@@ -245,7 +244,7 @@ function PremiumCard({ {isFeatureMore(feature) ? ( ) : ( - + )} {getFeatureLabel(feature)} {isFeatureSoon(feature) && ( @@ -256,20 +255,31 @@ function PremiumCard({
- - {comingSoon ? "Coming Soon" : cta} - {!comingSoon && } - + {checkout && !comingSoon ? ( + + ) : ( + + {comingSoon ? "Coming Soon" : cta} + {!comingSoon && } + + )}
diff --git a/components/bilalUi/pro/license-dialog.tsx b/components/bilalUi/pro/license-dialog.tsx new file mode 100644 index 0000000..cffb710 --- /dev/null +++ b/components/bilalUi/pro/license-dialog.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useState } from "react"; +import { Crown, Check, X, Loader2, KeyRound } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; + +interface LicenseDialogProps { + open: boolean; + onClose: () => void; + onVerify: (key: string) => Promise<{ success: boolean; error?: string }>; +} + +export function LicenseDialog({ + open, + onClose, + onVerify, +}: LicenseDialogProps) { + const [key, setKey] = useState(""); + const [status, setStatus] = useState< + "idle" | "loading" | "success" | "error" + >("idle"); + const [error, setError] = useState(""); + + if (!open) return null; + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!key.trim()) return; + + setStatus("loading"); + setError(""); + + const result = await onVerify(key.trim()); + + if (result.success) { + setStatus("success"); + setTimeout(() => { + onClose(); + setKey(""); + setStatus("idle"); + }, 1200); + } else { + setStatus("error"); + setError(result.error ?? "Invalid license key"); + } + } + + return ( +
+
+
+
+ + + Pro License + + +
+ +

+ Enter your license key +

+

+ Paste the license key you received after purchase to unlock all + Pro components. +

+ +
+
+ + { + setKey(e.target.value); + if (status === "error") setStatus("idle"); + }} + placeholder="XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX" + className="w-full rounded-xl border border-zinc-800 bg-zinc-900 py-2.5 pl-10 pr-3 text-sm text-white placeholder:text-zinc-600 focus:border-zinc-600 focus:outline-none focus:ring-1 focus:ring-zinc-600" + disabled={status === "loading" || status === "success"} + autoFocus + /> +
+ + {status === "error" && ( +

+ + {error} +

+ )} + + {status === "success" && ( +

+ + License verified! Unlocking Pro... +

+ )} + + +
+ +

+ Didn't get a key? Check your email receipt or{" "} + + visit LemonSqueezy orders + +

+
+
+
+ ); +} diff --git a/components/bilalUi/pro/pro-lock.tsx b/components/bilalUi/pro/pro-lock.tsx index ec02831..46b9d01 100644 --- a/components/bilalUi/pro/pro-lock.tsx +++ b/components/bilalUi/pro/pro-lock.tsx @@ -1,10 +1,12 @@ "use client"; -import { Crown, Sparkles, LockKeyhole } from "lucide-react"; +import { useState } from "react"; +import { Crown, LockKeyhole, Sparkles, KeyRound } from "lucide-react"; import Link from "next/link"; import { cn } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { useProAccess } from "@/hooks/use-pro-access"; +import { LicenseDialog } from "./license-dialog"; interface ProLockProps { label?: string; @@ -15,37 +17,65 @@ export function ProLock({ label = "This is a Pro component", className, }: ProLockProps) { + const { hasAccess, verifyAndStore } = useProAccess(); + const [showDialog, setShowDialog] = useState(false); + + if (hasAccess) { + return null; + } + return ( -
-
- -
+ <> +
+
+
+ +
+ + + + Pro + + +
+

{label}

+

+ Unlock all premium component variants, page blocks, and full + templates with a single $15 payment. +

+
-
- - - Pro - -

- {label} -

-

- Unlock all premium component variants, page blocks, and full templates - with a single $15 payment. -

+
+ + + Unlock with Pro + + +
+
- -
+ setShowDialog(false)} + onVerify={verifyAndStore} + /> + ); } diff --git a/config/lemon.ts b/config/lemon.ts new file mode 100644 index 0000000..5bc8007 --- /dev/null +++ b/config/lemon.ts @@ -0,0 +1,18 @@ +export function getLemonConfig() { + return { + apiKey: process.env.LEMONSQUEEZY_API_KEY!, + storeId: Number(process.env.LEMONSQUEEZY_STORE_ID!), + productId: Number(process.env.LEMONSQUEEZY_PRODUCT_ID!), + variantId: Number(process.env.LEMONSQUEEZY_VARIANT_ID!), + webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET!, + }; +} + +export function isLemonConfigured() { + return ( + !!process.env.LEMONSQUEEZY_API_KEY && + !!process.env.LEMONSQUEEZY_STORE_ID && + !!process.env.LEMONSQUEEZY_PRODUCT_ID && + !!process.env.LEMONSQUEEZY_VARIANT_ID + ); +} diff --git a/hooks/use-pro-access.ts b/hooks/use-pro-access.ts new file mode 100644 index 0000000..3c67b73 --- /dev/null +++ b/hooks/use-pro-access.ts @@ -0,0 +1,49 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +const LICENSE_KEY = "bilal-ui-license-key"; +const LICENSE_STATUS = "bilal-ui-license-status"; + +export function useProAccess() { + const [hasAccess, setHasAccess] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const stored = localStorage.getItem(LICENSE_STATUS); + setHasAccess(stored === "verified"); + setLoading(false); + }, []); + + const verifyAndStore = useCallback(async (key: string) => { + setLoading(true); + try { + const res = await fetch("/api/lemon/verify", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ licenseKey: key }), + }); + const data = await res.json(); + + if (data.valid) { + localStorage.setItem(LICENSE_KEY, key); + localStorage.setItem(LICENSE_STATUS, "verified"); + setHasAccess(true); + return { success: true }; + } + return { success: false, error: data.error ?? "Invalid license key" }; + } catch { + return { success: false, error: "Failed to verify license key" }; + } finally { + setLoading(false); + } + }, []); + + const clearAccess = useCallback(() => { + localStorage.removeItem(LICENSE_KEY); + localStorage.removeItem(LICENSE_STATUS); + setHasAccess(false); + }, []); + + return { hasAccess, loading, verifyAndStore, clearAccess }; +} diff --git a/lib/lemon.ts b/lib/lemon.ts new file mode 100644 index 0000000..49545ce --- /dev/null +++ b/lib/lemon.ts @@ -0,0 +1,61 @@ +import { lemonSqueezySetup, createCheckout } from "@lemonsqueezy/lemonsqueezy.js"; +import { getLemonConfig } from "@/config/lemon"; + +export function setupLemon() { + const { apiKey } = getLemonConfig(); + lemonSqueezySetup({ apiKey }); +} + +export async function createCheckoutUrl(opts?: { + redirectUrl?: string; + email?: string; + name?: string; +}): Promise { + setupLemon(); + const { storeId, variantId } = getLemonConfig(); + + const response = await createCheckout(storeId, variantId, { + productOptions: opts?.redirectUrl + ? { + redirectUrl: opts.redirectUrl, + enabledVariants: [variantId], + } + : { enabledVariants: [variantId] }, + checkoutData: { + email: opts?.email, + name: opts?.name, + }, + }); + + if (response.error) { + console.error("LemonSqueezy checkout error:", response.error); + return null; + } + + return response.data.data.attributes.url; +} + +export async function verifyLicenseKey( + licenseKey: string, +): Promise<{ valid: boolean; error?: string }> { + try { + const res = await fetch( + "https://api.lemonsqueezy.com/v1/licenses/validate", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ license_key: licenseKey }), + }, + ); + + const data = await res.json(); + + if (data.valid && data.license_key?.status === "active") { + return { valid: true }; + } + + return { valid: false, error: data.error ?? "License key is not valid" }; + } catch (err) { + return { valid: false, error: "Failed to verify license key" }; + } +} diff --git a/package-lock.json b/package-lock.json index 88e6efd..af76e9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@base-ui/react": "^1.2.0", "@hookform/resolvers": "^5.2.2", + "@lemonsqueezy/lemonsqueezy.js": "^4.0.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-aspect-ratio": "^1.1.8", @@ -1664,6 +1665,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lemonsqueezy/lemonsqueezy.js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@lemonsqueezy/lemonsqueezy.js/-/lemonsqueezy.js-4.0.0.tgz", + "integrity": "sha512-xcY1/lDrY7CpIF98WKiL1ElsfoVhddP7FT0fw7ssOzrFqQsr44HgolKrQZxd9SywsCPn12OTOUieqDIokI3mFg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/@mdx-js/mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", diff --git a/package.json b/package.json index c1ae297..734a18e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@base-ui/react": "^1.2.0", "@hookform/resolvers": "^5.2.2", + "@lemonsqueezy/lemonsqueezy.js": "^4.0.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-aspect-ratio": "^1.1.8", From b2c8ff30fa40c4809dd1960af624f651c62b2c68 Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Thu, 21 May 2026 14:21:01 +0500 Subject: [PATCH 10/14] feat: temporarily make all components free and add Coming Soon badge to pricing - Set PRO_FREE_MODE=true to disable pro gating for all components - Add 'Coming Soon' badge on the Lifetime pricing card --- app/(root)/pricing/page.tsx | 1 + config/pro.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/(root)/pricing/page.tsx b/app/(root)/pricing/page.tsx index 9c531bc..257f3c8 100644 --- a/app/(root)/pricing/page.tsx +++ b/app/(root)/pricing/page.tsx @@ -47,6 +47,7 @@ const plans = [ cta: "Get Lifetime Access", href: "#", popular: true, + badge: "Coming Soon", comingSoon: !lemonReady, checkout: lemonReady, }, diff --git a/config/pro.ts b/config/pro.ts index 121a6d0..c549a21 100644 --- a/config/pro.ts +++ b/config/pro.ts @@ -1,3 +1,5 @@ +export const PRO_FREE_MODE = true; + export const PRO_COMPONENTS: Record = { button: { freeCount: 5 }, badge: { freeCount: 5 }, @@ -33,6 +35,8 @@ export function isProComponent( componentName: string, variantIndex?: number, ): boolean { + if (PRO_FREE_MODE) return false; + if (!variantIndex !== undefined && variantIndex === undefined) { return false; } From efd76cc6f4c1f4d398c671c0a8cf02b00e922fde Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Thu, 21 May 2026 14:23:56 +0500 Subject: [PATCH 11/14] feat: disable Lifetime checkout with Coming Soon badge and colorize premium feature icons - Set comingSoon=true on Lifetime plan so button shows disabled 'Coming Soon' state - Colorize PremiumCard feature icons with a rotating palette (amber, emerald, sky, violet, pink, cyan, rose, indigo) - Remove checkout logic temporarily since plan is marked as coming soon --- app/(root)/pricing/page.tsx | 4 +- components/bilalUi/pricing/pricing-card.tsx | 51 +++++++++++++-------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/app/(root)/pricing/page.tsx b/app/(root)/pricing/page.tsx index 257f3c8..5e84a3f 100644 --- a/app/(root)/pricing/page.tsx +++ b/app/(root)/pricing/page.tsx @@ -48,8 +48,8 @@ const plans = [ href: "#", popular: true, badge: "Coming Soon", - comingSoon: !lemonReady, - checkout: lemonReady, + comingSoon: true, + checkout: false, }, ]; diff --git a/components/bilalUi/pricing/pricing-card.tsx b/components/bilalUi/pricing/pricing-card.tsx index 9997f2a..df507d5 100644 --- a/components/bilalUi/pricing/pricing-card.tsx +++ b/components/bilalUi/pricing/pricing-card.tsx @@ -233,25 +233,38 @@ function PremiumCard({
    - {features.map((feature, i) => ( - - {isFeatureMore(feature) ? ( - - ) : ( - - )} - {getFeatureLabel(feature)} - {isFeatureSoon(feature) && ( - Soon - )} - - ))} + {features.map((feature, i) => { + const colors = [ + "text-amber-500", + "text-emerald-500", + "text-sky-500", + "text-violet-500", + "text-pink-500", + "text-cyan-500", + "text-rose-500", + "text-indigo-500", + ]; + const iconColor = isFeatureMore(feature) ? "text-amber-500" : (colors[i % colors.length]); + return ( + + {isFeatureMore(feature) ? ( + + ) : ( + + )} + {getFeatureLabel(feature)} + {isFeatureSoon(feature) && ( + Soon + )} + + ); + })}
From 73d1cb9969b18e67a534e43f632783f8bac69d4b Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Thu, 21 May 2026 15:06:04 +0500 Subject: [PATCH 12/14] feat: add placeholder pages for six block categories Introduce dedicated documentation pages for Hero, Features, CTA, Dashboard, Pricing, and Testimonials block categories. Each page includes a title, description, and a coming-soon message for future block implementations. Update meta.json to register the new pages in the docs sidebar. --- content/docs/blocks/cta.mdx | 6 ++++++ content/docs/blocks/dashboard.mdx | 6 ++++++ content/docs/blocks/features.mdx | 6 ++++++ content/docs/blocks/hero.mdx | 6 ++++++ content/docs/blocks/meta.json | 8 +++++++- content/docs/blocks/pricing.mdx | 6 ++++++ content/docs/blocks/testimonials.mdx | 6 ++++++ 7 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 content/docs/blocks/cta.mdx create mode 100644 content/docs/blocks/dashboard.mdx create mode 100644 content/docs/blocks/features.mdx create mode 100644 content/docs/blocks/hero.mdx create mode 100644 content/docs/blocks/pricing.mdx create mode 100644 content/docs/blocks/testimonials.mdx diff --git a/content/docs/blocks/cta.mdx b/content/docs/blocks/cta.mdx new file mode 100644 index 0000000..26ba9aa --- /dev/null +++ b/content/docs/blocks/cta.mdx @@ -0,0 +1,6 @@ +--- +title: CTA +description: Call-to-action blocks for conversions and user engagement. +--- + +CTA blocks are coming soon. These will include centered CTAs, banner CTAs, and multi-action layouts. diff --git a/content/docs/blocks/dashboard.mdx b/content/docs/blocks/dashboard.mdx new file mode 100644 index 0000000..16a28bb --- /dev/null +++ b/content/docs/blocks/dashboard.mdx @@ -0,0 +1,6 @@ +--- +title: Dashboard +description: Dashboard shell and layout blocks for admin panels and apps. +--- + +Dashboard blocks are coming soon. These will include sidebar layouts, analytics panels, and data tables. diff --git a/content/docs/blocks/features.mdx b/content/docs/blocks/features.mdx new file mode 100644 index 0000000..b2b095f --- /dev/null +++ b/content/docs/blocks/features.mdx @@ -0,0 +1,6 @@ +--- +title: Features +description: Feature grid and showcase blocks for highlighting product capabilities. +--- + +Features blocks are coming soon. These will include icon grids, numbered lists, and feature comparison layouts. diff --git a/content/docs/blocks/hero.mdx b/content/docs/blocks/hero.mdx new file mode 100644 index 0000000..24abadc --- /dev/null +++ b/content/docs/blocks/hero.mdx @@ -0,0 +1,6 @@ +--- +title: Hero +description: Hero section blocks for landing pages and marketing sites. +--- + +Hero section blocks are coming soon. These will include centered heroes, SaaS heroes, and split layout options. diff --git a/content/docs/blocks/meta.json b/content/docs/blocks/meta.json index 5102013..57a4aa1 100644 --- a/content/docs/blocks/meta.json +++ b/content/docs/blocks/meta.json @@ -1,5 +1,11 @@ { "pages": [ - "introduction" + "introduction", + "hero", + "features", + "cta", + "dashboard", + "pricing", + "testimonials" ] } diff --git a/content/docs/blocks/pricing.mdx b/content/docs/blocks/pricing.mdx new file mode 100644 index 0000000..0ce502e --- /dev/null +++ b/content/docs/blocks/pricing.mdx @@ -0,0 +1,6 @@ +--- +title: Pricing +description: Pricing table and plan comparison blocks. +--- + +Pricing blocks are coming soon. These will include tiered pricing, comparison tables, and feature breakdowns. diff --git a/content/docs/blocks/testimonials.mdx b/content/docs/blocks/testimonials.mdx new file mode 100644 index 0000000..24b4ada --- /dev/null +++ b/content/docs/blocks/testimonials.mdx @@ -0,0 +1,6 @@ +--- +title: Testimonials +description: Testimonial and social proof blocks for building trust. +--- + +Testimonials blocks are coming soon. These will include card grids, carousels, and featured quote layouts. From 16343bcef937161f6ed73e96f41b77c7c58a5c0f Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Thu, 21 May 2026 15:06:21 +0500 Subject: [PATCH 13/14] refactor: simplify blocks introduction page Replace dynamic BlocksGrid component import with a clean static overview. Direct users to browse categories via the sidebar navigation. --- content/docs/blocks/introduction.mdx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/content/docs/blocks/introduction.mdx b/content/docs/blocks/introduction.mdx index 5f41bfb..1bf7cb7 100644 --- a/content/docs/blocks/introduction.mdx +++ b/content/docs/blocks/introduction.mdx @@ -3,11 +3,7 @@ title: Blocks description: Multi-component sections you can copy and ship as complete layouts. --- -import { Callout } from "@/components/ui/callout"; - Blocks are reusable section-level UI patterns made from multiple components. They are designed to help you ship complete pages faster. - - Block templates are being prepared and will roll out soon. - +Choose a category from the sidebar to browse available blocks. From 396f0b475799c61b2a980644e8ae7b35eda877d1 Mon Sep 17 00:00:00 2001 From: bilals2008 Date: Thu, 21 May 2026 15:06:45 +0500 Subject: [PATCH 14/14] feat: add block category links to sidebar navigation Introduce dedicated sidebar entries for each block category: Hero, Features, CTA, Dashboard, Pricing, and Testimonials. Each entry includes an icon, description, and links to its respective documentation page under /docs/blocks. --- config/navigation.ts | 56 +++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/config/navigation.ts b/config/navigation.ts index 72732b6..c33f9da 100644 --- a/config/navigation.ts +++ b/config/navigation.ts @@ -337,34 +337,52 @@ export const navigationSections: NavSection[] = [ items: [ { id: "blocks-introduction", - title: "Introduction", + title: "Overview", href: "/docs/blocks/introduction", - description: "Overview of reusable multi-component blocks", + description: "Browse all reusable multi-component blocks", icon: "Map", }, { - id: "blocks-hero-sections", - title: "Hero Sections", - href: "/docs/blocks/hero-sections", - description: "Ready-to-use hero block patterns", + id: "blocks-hero", + title: "Hero", + href: "/docs/blocks/hero", + description: "Hero section blocks for landing pages", icon: "PanelTop", - isComingSoon: true, }, { - id: "blocks-pricing-sections", - title: "Pricing Sections", - href: "/docs/blocks/pricing-sections", - description: "Composable pricing table and plan blocks", - icon: "ListFilter", - isComingSoon: true, + id: "blocks-features", + title: "Features", + href: "/docs/blocks/features", + description: "Feature grid and showcase blocks", + icon: "LayoutGrid", }, { - id: "blocks-dashboard-shells", - title: "Dashboard Shells", - href: "/docs/blocks/dashboard-shells", - description: "Starter dashboard structures and app shells", - icon: "RectangleEllipsis", - isComingSoon: true, + id: "blocks-cta", + title: "CTA", + href: "/docs/blocks/cta", + description: "Call-to-action blocks", + icon: "MousePointerClick", + }, + { + id: "blocks-dashboard", + title: "Dashboard", + href: "/docs/blocks/dashboard", + description: "Dashboard layout blocks", + icon: "LayoutDashboard", + }, + { + id: "blocks-pricing", + title: "Pricing", + href: "/docs/blocks/pricing", + description: "Pricing table blocks", + icon: "CreditCard", + }, + { + id: "blocks-testimonials", + title: "Testimonials", + href: "/docs/blocks/testimonials", + description: "Testimonial and social proof blocks", + icon: "MessageSquareQuote", }, ], },