diff --git a/src/app/globals.css b/src/app/globals.css index 9602afe..3065800 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -99,4 +99,20 @@ [cmdk-empty=""] { color: hsl(var(--muted-foreground)) !important; } + + .code-block { + @apply relative flex flex-col rounded-xl border border-border bg-secondary/50 overflow-hidden; + } + + .code-block__header { + @apply flex items-center justify-between px-4 py-2 border-b border-border bg-secondary/30 text-xs font-medium text-muted-foreground; + } + + .code-block__code { + @apply p-4 overflow-x-auto font-mono text-sm leading-relaxed; + } + + .code-block__copy-button { + @apply hover:bg-secondary/80 transition-colors; + } } diff --git a/src/app/page.tsx b/src/app/page.tsx index 854363e..6293fa4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,5 @@ import { TemplateCard } from "@/components/TemplateCard"; +import { CodeBlock } from "@/components/CodeBlock"; export default function Page() { const jsonLd = { @@ -27,6 +28,15 @@ export default function Page() { Opinionated production-ready templates for mobile, cloud, and web developers

+
+ + + terminal + + + + +
{/* Templates Section */} diff --git a/src/components/CodeBlock/CopyButton.tsx b/src/components/CodeBlock/CopyButton.tsx new file mode 100644 index 0000000..e02376c --- /dev/null +++ b/src/components/CodeBlock/CopyButton.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@heroui/react"; +import { Copy, Check } from "lucide-react"; + +export function CodeBlockCopyButton({ code, className }: { code: string; className?: string }) { + const [copied, setCopied] = useState(false); + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy: ", err); + } + }; + + return ( + + ); +} diff --git a/src/components/CodeBlock/__tests__/index.test.tsx b/src/components/CodeBlock/__tests__/index.test.tsx new file mode 100644 index 0000000..6689a0b --- /dev/null +++ b/src/components/CodeBlock/__tests__/index.test.tsx @@ -0,0 +1,49 @@ +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { CodeBlock } from "../index"; +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Mock navigator.clipboard +Object.assign(navigator, { + clipboard: { + writeText: vi.fn().mockImplementation(() => Promise.resolve()), + }, +}); + +describe("CodeBlock", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders the code correctly", () => { + const code = "npx create-cur8d"; + render( + + + + ); + + expect(screen.getByText(code)).toBeInTheDocument(); + }); + + it("copies code to clipboard when copy button is clicked", async () => { + const code = "npx create-cur8d"; + render( + + + + + + + ); + + const copyButton = screen.getByLabelText("Copy code"); + fireEvent.click(copyButton); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(code); + + // Check if it changes to check icon (success) + await waitFor(() => { + expect(screen.queryByLabelText("Copy code")?.querySelector(".text-success")).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/CodeBlock/index.tsx b/src/components/CodeBlock/index.tsx new file mode 100644 index 0000000..e9dd1b8 --- /dev/null +++ b/src/components/CodeBlock/index.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { CodeBlockCopyButton } from "./CopyButton"; + +interface CodeBlockProps { + children: React.ReactNode; + className?: string; +} + +function CodeBlockRoot({ children, className }: CodeBlockProps) { + return ( +
+ {children} +
+ ); +} + +interface CodeBlockHeaderProps { + children: React.ReactNode; + className?: string; +} + +function CodeBlockHeader({ children, className }: CodeBlockHeaderProps) { + return ( +
+ {children} +
+ ); +} + +interface CodeBlockCodeProps { + code: string; + language?: string; + className?: string; +} + +function CodeBlockCode({ code, language, className }: CodeBlockCodeProps) { + return ( +
+      {code}
+    
+ ); +} + +export const CodeBlock = Object.assign(CodeBlockRoot, { + Header: CodeBlockHeader, + Code: CodeBlockCode, + CopyButton: CodeBlockCopyButton, +});