diff --git a/apps/www/app/(blog)/blog/page.tsx b/apps/www/app/(blog)/blog/page.tsx index e82c806dd..6fceb88e0 100644 --- a/apps/www/app/(blog)/blog/page.tsx +++ b/apps/www/app/(blog)/blog/page.tsx @@ -30,8 +30,8 @@ export default async function Page({ const selectedTag = params?.tag ?? ""; const posts = blogSource.getPages().sort((a, b) => { - const dateA = new Date(a.data?.publishedOn || 0).getTime(); - const dateB = new Date(b.data?.publishedOn || 0).getTime(); + const dateA = new Date(a.data?.publishedOn).getTime(); + const dateB = new Date(b.data?.publishedOn).getTime(); return dateB - dateA; }); diff --git a/apps/www/app/(docs)/blocks/layout.tsx b/apps/www/app/(docs)/blocks/layout.tsx new file mode 100644 index 000000000..4295a9527 --- /dev/null +++ b/apps/www/app/(docs)/blocks/layout.tsx @@ -0,0 +1,17 @@ +import { DocsSidebar } from "@/components/docs-sidebar"; +import { SidebarProvider } from "@/components/ui/sidebar"; + +export default function DocsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ + +
{children}
+
+
+ ); +} diff --git a/apps/www/app/(docs)/blocks/page.tsx b/apps/www/app/(docs)/blocks/page.tsx new file mode 100644 index 000000000..3a78c4f38 --- /dev/null +++ b/apps/www/app/(docs)/blocks/page.tsx @@ -0,0 +1,53 @@ +import Link from "next/link"; + +import { BlockDisplay } from "@/components/block-display"; +import { Button } from "@/components/ui/button"; + +export const dynamic = "force-static"; +export const revalidate = false; + +const FEATURED_BLOCKS = [ + "animated-feature-card-1", + "animated-feature-card-2", + "animated-feature-card-3", + "animated-feature-card-4", + "animated-feature-card-5", + "animated-feature-card-6", + "animated-feature-card-7", + "animated-feature-card-8", + "animated-feature-card-9", + "animated-feature-card-10", + "animated-feature-card-11", + + "feature-1", + "footer-1", + "faq-3", + "header-1", + "header-2", + "header-3", + "hero-1", + "hero-2", + // "hero-3", + // "dashboard-01", + // "sidebar-07", + // "sidebar-03", + // "login-03", + // "login-04", +]; + +export default async function BlocksPage() { + return ( +
+ {FEATURED_BLOCKS.map((name) => ( + + ))} +
+
+ +
+
+
+ ); +} diff --git a/apps/www/components/block-display.tsx b/apps/www/components/block-display.tsx new file mode 100644 index 000000000..5bc6d2086 --- /dev/null +++ b/apps/www/components/block-display.tsx @@ -0,0 +1,69 @@ +import * as React from "react"; +import { registryItemFileSchema } from "shadcn/schema"; +import { z } from "zod"; + +import { BlockViewer } from "@/components/block-viewer"; +import { ComponentPreview } from "@/components/component-preview"; +import { highlightCode } from "@/lib/highlight-code"; +import { + createFileTreeForRegistryItemFiles, + getRegistryItem, +} from "@/lib/registry"; +import { cn } from "@/lib/utils"; + +export async function BlockDisplay({ name }: { name: string }) { + // const item = await getCachedRegistryItem(name); + + // if (!item?.files) { + // return null; + // } + + // const [tree, highlightedFiles] = await Promise.all([ + // getCachedFileTree(item.files), + // getCachedHighlightedFiles(item.files), + // ]); + + return ( + + .p-6]:p-0", + // item.meta?.containerClassName, + )} + /> + + ); +} + +const getCachedRegistryItem = React.cache(async (name: string) => { + return await getRegistryItem(name); +}); + +const getCachedFileTree = React.cache( + async (files: Array<{ path: string; target?: string }>) => { + if (!files) { + return null; + } + + return await createFileTreeForRegistryItemFiles(files); + }, +); + +const getCachedHighlightedFiles = React.cache( + async (files: z.infer[]) => { + return await Promise.all( + files.map(async (file) => ({ + ...file, + highlightedContent: await highlightCode(file.content ?? ""), + })), + ); + }, +); diff --git a/apps/www/components/block-viewer.tsx b/apps/www/components/block-viewer.tsx new file mode 100644 index 000000000..01543d017 --- /dev/null +++ b/apps/www/components/block-viewer.tsx @@ -0,0 +1,462 @@ +"use client"; + +import { + Check, + ChevronRight, + Clipboard, + File, + Folder, + Monitor, + RotateCw, + Smartphone, + Tablet, + Terminal, +} from "lucide-react"; +import * as React from "react"; +import { ImperativePanelHandle } from "react-resizable-panels"; +import { registryItemFileSchema, registryItemSchema } from "shadcn/schema"; +import { z } from "zod"; + +import { getIconForLanguageExtension } from "@/components/icons"; +import { OpenInV0Button } from "@/components/open-in-v0-button"; +import { Button } from "@/components/ui/button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "@/components/ui/resizable"; +import { Separator } from "@/components/ui/separator"; +import { + Sidebar, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarProvider, +} from "@/components/ui/sidebar"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; +import { trackEvent } from "@/lib/events"; +import { createFileTreeForRegistryItemFiles, FileTree } from "@/lib/registry"; +import { cn } from "@/lib/utils"; + +type BlockViewerContext = { + name: string; + + item: z.infer; + view: "code" | "preview"; + setView: (view: "code" | "preview") => void; + activeFile: string | null; + setActiveFile: (file: string) => void; + resizablePanelRef: React.RefObject | null; + tree: ReturnType | null; + highlightedFiles: + | (z.infer & { + highlightedContent: string; + })[] + | null; + iframeKey?: number; + setIframeKey?: React.Dispatch>; +}; + +const BlockViewerContext = React.createContext(null); + +function useBlockViewer() { + const context = React.useContext(BlockViewerContext); + if (!context) { + throw new Error( + "useBlockViewer must be used within a BlockViewerProvider.", + ); + } + return context; +} + +function BlockViewerProvider({ + item, + tree, + name, + highlightedFiles, + children, +}: Pick & { + children: React.ReactNode; +}) { + const [view, setView] = React.useState("preview"); + const [activeFile, setActiveFile] = React.useState< + BlockViewerContext["activeFile"] + >(highlightedFiles?.[0].target ?? null); + const resizablePanelRef = React.useRef(null); + const [iframeKey, setIframeKey] = React.useState(0); + + return ( + +
+ {children} +
+
+ ); +} + +function BlockViewerToolbar() { + const { setView, view, item, resizablePanelRef, setIframeKey, name } = + useBlockViewer(); + const { copyToClipboard, isCopied } = useCopyToClipboard(); + + return ( +
+ setView(value as "preview" | "code")} + > + + Preview + Code + + + + + {/* {item.description?.replace(/\.$/, "")} */} + +
+
+ { + setView("preview"); + if (resizablePanelRef?.current) { + resizablePanelRef.current.resize(parseInt(value)); + } + }} + className="gap-1 *:data-[slot=toggle-group-item]:!size-6 *:data-[slot=toggle-group-item]:!rounded-sm" + > + + + + + + + + + + + {/* + */} + + +
+ + + + +
+
+ ); +} + +function BlockViewerIframe({ className }: { className?: string }) { + const { iframeKey, name } = useBlockViewer(); + + return ( +