Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
10 changes: 10 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TemplateCard } from "@/components/TemplateCard";
import { CodeBlock } from "@/components/CodeBlock";

export default function Page() {
const jsonLd = {
Expand Down Expand Up @@ -27,6 +28,15 @@ export default function Page() {
Opinionated production-ready templates for mobile, cloud, and web developers
</p>
</div>
<div className="mx-auto max-w-md">
<CodeBlock>
<CodeBlock.Header>
<span>terminal</span>
<CodeBlock.CopyButton code="npx create-cur8d" />
</CodeBlock.Header>
<CodeBlock.Code code="npx create-cur8d" />
</CodeBlock>
</div>
</section>

{/* Templates Section */}
Expand Down
32 changes: 32 additions & 0 deletions src/components/CodeBlock/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button
isIconOnly
size="sm"
variant="ghost"
className={`code-block__copy-button ${className || ""}`}
onClick={copyToClipboard}
aria-label="Copy code"
>
{copied ? <Check className="text-success h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
);
}
49 changes: 49 additions & 0 deletions src/components/CodeBlock/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<CodeBlock>
<CodeBlock.Code code={code} />
</CodeBlock>
);

expect(screen.getByText(code)).toBeInTheDocument();
});

it("copies code to clipboard when copy button is clicked", async () => {
const code = "npx create-cur8d";
render(
<CodeBlock>
<CodeBlock.Header>
<CodeBlock.CopyButton code={code} />
</CodeBlock.Header>
<CodeBlock.Code code={code} />
</CodeBlock>
);

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();
});
});
});
48 changes: 48 additions & 0 deletions src/components/CodeBlock/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`code-block ${className || ""}`}>
{children}
</div>
);
}

interface CodeBlockHeaderProps {
children: React.ReactNode;
className?: string;
}

function CodeBlockHeader({ children, className }: CodeBlockHeaderProps) {
return (
<div className={`code-block__header ${className || ""}`}>
{children}
</div>
);
}

interface CodeBlockCodeProps {
code: string;
language?: string;
className?: string;
}

function CodeBlockCode({ code, language, className }: CodeBlockCodeProps) {
return (
<pre className={`code-block__code ${className || ""}`}>
<code className={language ? `language-${language}` : ""}>{code}</code>
</pre>
);
}

export const CodeBlock = Object.assign(CodeBlockRoot, {
Header: CodeBlockHeader,
Code: CodeBlockCode,
CopyButton: CodeBlockCopyButton,
});