feat: enhance Markdown component with syntax highlighting and improve code snippet display
CI / typecheck + build (push) Canceled after 0s
CI / deploy to VPS (push) Canceled after 0s

This commit is contained in:
asepharyana
2026-08-21 15:17:09 +07:00
parent 87ce3ebbc6
commit d62c5d33a4
6 changed files with 265 additions and 156 deletions
+76 -36
View File
@@ -4,20 +4,35 @@ import React, { useState } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug"; import rehypeSlug from "rehype-slug";
import rehypeHighlight from "rehype-highlight";
function CodeBlock({ children, className, ...props }: React.HTMLAttributes<HTMLElement>) { function extractText(node: React.ReactNode): string {
if (node === null || node === undefined || typeof node === "boolean") {
return "";
}
if (typeof node === "string" || typeof node === "number") {
return String(node);
}
if (Array.isArray(node)) {
return node.map(extractText).join("");
}
if (React.isValidElement(node)) {
return extractText((node.props as { children?: React.ReactNode })?.children);
}
return "";
}
function PreBlock({ children, ...props }: React.HTMLAttributes<HTMLPreElement>) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const textContent = String(children).replace(/\n$/, ""); const textContent = extractText(children).replace(/\n$/, "");
const match = /language-(\w+)/.exec(className || "");
const language = match ? match[1] : "";
// If inline code let language = "text";
if (!className && typeof children === "string" && !children.includes("\n")) { if (React.isValidElement(children)) {
return ( const className = (children.props as { className?: string })?.className || "";
<code className={className} {...props}> const match = /language-(\w+)/.exec(className);
{children} if (match) {
</code> language = match[1];
); }
} }
function handleCopy() { function handleCopy() {
@@ -27,37 +42,46 @@ function CodeBlock({ children, className, ...props }: React.HTMLAttributes<HTMLE
} }
return ( return (
<div className="relative group my-4 rounded-lg overflow-hidden border border-[var(--border-color)] bg-[var(--code-block-bg)]"> <div className="relative group my-5 rounded-xl overflow-hidden border border-[var(--border-code)] bg-[var(--code-block-bg)] shadow-xs">
{/* Code block header bar */} {/* Code block header bar */}
<div className="flex items-center justify-between px-3.5 py-1.5 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] text-[11px] text-[var(--text-muted)] font-mono"> <div className="flex items-center justify-between px-4 py-2 bg-[var(--code-header-bg)] border-b border-[var(--border-code)] text-xs font-mono select-none">
<span>{language || "text"}</span> <div className="flex items-center gap-2.5">
<div className="flex items-center gap-1.5 opacity-75 group-hover:opacity-100 transition-opacity">
<span className="w-2.5 h-2.5 rounded-full bg-rose-400/80 dark:bg-rose-500/60 inline-block" />
<span className="w-2.5 h-2.5 rounded-full bg-amber-400/80 dark:bg-amber-500/60 inline-block" />
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400/80 dark:bg-emerald-500/60 inline-block" />
</div>
<span className="text-[11px] font-medium uppercase tracking-wider text-[var(--text-muted)] group-hover:text-[var(--text-secondary)] transition-colors">
{language}
</span>
</div>
<button <button
onClick={handleCopy} onClick={handleCopy}
type="button" type="button"
className="flex items-center gap-1 text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-elevated-hover)] hover:bg-[var(--border-subtle)] px-2 py-0.5 rounded border border-[var(--border-color)] transition-all" aria-label="Copy code to clipboard"
className="flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--code-button-bg)] hover:bg-[var(--code-button-hover-bg)] px-2.5 py-1 rounded-md border border-[var(--border-code)] transition-all font-sans shadow-xs cursor-pointer"
> >
{copied ? ( {copied ? (
<> <>
<svg className="w-3 h-3 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4 L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
<span className="text-emerald-400 font-sans">Copied!</span> <span className="text-emerald-500 font-medium">Copied!</span>
</> </>
) : ( ) : (
<> <>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg> </svg>
<span className="font-sans">Copy</span> <span>Copy</span>
</> </>
)} )}
</button> </button>
</div> </div>
<pre className="!m-0 !bg-transparent p-4 overflow-x-auto text-sm text-[var(--pre-text)] font-mono leading-relaxed"> <pre className="!m-0 !bg-transparent p-4 overflow-x-auto text-[13.5px] text-[var(--pre-text)] font-mono leading-relaxed" {...props}>
<code className={className} {...props}> {children}
{children}
</code>
</pre> </pre>
</div> </div>
); );
@@ -67,31 +91,47 @@ export default function Markdown({ source }: { source: string }) {
return ( return (
<div <div
className="prose max-w-none className="prose max-w-none
prose-headings:text-[var(--text-primary)] prose-headings:font-medium prose-headings:tracking-tight prose-headings:text-[var(--text-primary)] prose-headings:font-semibold prose-headings:tracking-tight
prose-h1:text-2xl prose-h1:mb-6 prose-h1:mt-8 prose-h1:pb-2 prose-h1:border-b prose-h1:border-[var(--border-color)] prose-h1:text-2xl sm:prose-h1:text-3xl prose-h1:mb-6 prose-h1:mt-8 prose-h1:pb-3 prose-h1:border-b prose-h1:border-[var(--border-color)]
prose-h2:text-xl prose-h2:mb-4 prose-h2:mt-8 prose-h2:text-xl sm:prose-h2:text-2xl prose-h2:mb-4 prose-h2:mt-8 prose-h2:pb-2 prose-h2:border-b prose-h2:border-[var(--border-subtle)]
prose-h3:text-base prose-h3:mb-3 prose-h3:mt-6 prose-h3:text-lg prose-h3:mb-3 prose-h3:mt-6
prose-p:text-[var(--text-secondary)] prose-p:leading-relaxed prose-p:mb-4 prose-p:text-[var(--text-secondary)] prose-p:leading-relaxed prose-p:mb-4
prose-a:text-[var(--accent)] prose-a:no-underline hover:prose-a:underline hover:prose-a:text-[var(--accent-hover)] prose-a:text-[var(--accent)] prose-a:font-medium prose-a:no-underline hover:prose-a:underline hover:prose-a:text-[var(--accent-hover)]
prose-ul:text-[var(--text-secondary)] prose-ul:my-3 prose-li:my-1 prose-ul:text-[var(--text-secondary)] prose-ul:my-4 prose-li:my-1.5
prose-ol:text-[var(--text-secondary)] prose-ol:my-3 prose-ol:text-[var(--text-secondary)] prose-ol:my-4
prose-strong:text-[var(--text-primary)] prose-strong:font-semibold prose-strong:text-[var(--text-primary)] prose-strong:font-semibold
prose-hr:border-[var(--border-color)] prose-hr:my-8" prose-hr:border-[var(--border-color)] prose-hr:my-8"
> >
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeSlug]} rehypePlugins={[rehypeSlug, rehypeHighlight]}
components={{ components={{
code: CodeBlock as any, pre: PreBlock as any,
pre: ({ children }) => <>{children}</>, code: ({ children, className, ...props }: React.HTMLAttributes<HTMLElement>) => {
if (className?.includes("hljs") || className?.includes("language-")) {
return (
<code className={className} {...props}>
{children}
</code>
);
}
return (
<code
className="px-1.5 py-0.5 rounded-md bg-[var(--code-inline-bg)] text-[var(--code-inline-text)] border border-[var(--code-inline-border)] font-mono text-[0.875em] font-medium"
{...props}
>
{children}
</code>
);
},
table: ({ node, ...props }) => ( table: ({ node, ...props }) => (
<div className="overflow-x-auto my-6 border border-[var(--border-color)] rounded-lg"> <div className="overflow-x-auto my-6 border border-[var(--border-color)] rounded-xl shadow-xs">
<table className="w-full border-collapse" {...props} /> <table className="w-full border-collapse" {...props} />
</div> </div>
), ),
blockquote: ({ children }) => { blockquote: ({ children }) => {
return ( return (
<blockquote className="border-l-4 border-[var(--brand)] bg-[var(--bg-elevated)]/60 pl-4 py-2 my-4 rounded-r text-[var(--text-muted)] not-italic"> <blockquote className="border-l-4 border-[var(--brand)] bg-[var(--brand)]/5 dark:bg-[var(--brand)]/10 pl-4 py-2.5 my-5 rounded-r-lg text-[var(--text-secondary)] not-italic">
{children} {children}
</blockquote> </blockquote>
); );
+25 -36
View File
@@ -1,11 +1,13 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Markdown from "@/components/Markdown";
const CONFIGS = [ const CONFIGS = [
{ {
id: "claude", id: "claude",
label: "Claude Desktop", label: "Claude Desktop",
lang: "json",
filename: "~/Library/Application Support/Claude/claude_desktop_config.json", filename: "~/Library/Application Support/Claude/claude_desktop_config.json",
content: `{ content: `{
"mcpServers": { "mcpServers": {
@@ -23,6 +25,7 @@ const CONFIGS = [
{ {
id: "http", id: "http",
label: "Remote HTTP (SSE)", label: "Remote HTTP (SSE)",
lang: "json",
filename: "Streamable HTTP Endpoint", filename: "Streamable HTTP Endpoint",
content: `{ content: `{
"mcpServers": { "mcpServers": {
@@ -38,6 +41,7 @@ const CONFIGS = [
{ {
id: "cli", id: "cli",
label: "CLI Client", label: "CLI Client",
lang: "bash",
filename: "Terminal One-Shot / REPL", filename: "Terminal One-Shot / REPL",
content: `# One-shot question content: `# One-shot question
bun run apps/mcp-client/src/ask.ts "How does ret2win stack alignment work?" bun run apps/mcp-client/src/ask.ts "How does ret2win stack alignment work?"
@@ -49,18 +53,15 @@ bun run apps/mcp-client/src/index.ts`,
export default function McpConfigSnippet() { export default function McpConfigSnippet() {
const [activeTab, setActiveTab] = useState("claude"); const [activeTab, setActiveTab] = useState("claude");
const [copied, setCopied] = useState(false);
const current = CONFIGS.find((c) => c.id === activeTab) || CONFIGS[0]; const current = CONFIGS.find((c) => c.id === activeTab) || CONFIGS[0];
function handleCopy() { const markdownSnippet = `\`\`\`${current.lang}
navigator.clipboard.writeText(current.content); ${current.content}
setCopied(true); \`\`\``;
setTimeout(() => setCopied(false), 2000);
}
return ( return (
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl overflow-hidden shadow-md"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl overflow-hidden shadow-sm">
{/* Header bar with tabs */} {/* Header bar with tabs */}
<div className="flex items-center justify-between px-4 py-2.5 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] flex-wrap gap-2"> <div className="flex items-center justify-between px-4 py-2.5 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] flex-wrap gap-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
@@ -68,9 +69,9 @@ export default function McpConfigSnippet() {
<button <button
key={c.id} key={c.id}
onClick={() => setActiveTab(c.id)} onClick={() => setActiveTab(c.id)}
className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${ className={`px-3 py-1.5 text-xs rounded-lg font-medium transition-all cursor-pointer ${
activeTab === c.id activeTab === c.id
? "bg-[var(--brand)] text-white shadow-sm" ? "bg-[var(--brand)] text-white shadow-xs"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]"
}`} }`}
> >
@@ -79,37 +80,25 @@ export default function McpConfigSnippet() {
))} ))}
</div> </div>
<button <div className="text-[11px] text-[var(--text-dim)] font-mono hidden sm:block">
onClick={handleCopy} {current.filename}
type="button" </div>
className="flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated-hover)] px-2.5 py-1 rounded border border-[var(--border-color)] transition-all shadow-sm"
>
{copied ? (
<>
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-emerald-500 font-medium">Copied!</span>
</>
) : (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span>Copy Config</span>
</>
)}
</button>
</div> </div>
{/* Code preview */} {/* Code preview via Markdown syntax highlighter */}
<pre className="p-4 text-xs font-mono text-[var(--pre-text)] bg-[var(--code-block-bg)] overflow-x-auto leading-relaxed"> <div className="p-4 [&>.prose>.group]:my-0 [&>.prose]:max-w-none">
<code>{current.content}</code> <Markdown source={markdownSnippet} />
</pre> </div>
<div className="px-4 py-2 bg-[var(--bg-elevated)] border-t border-[var(--border-color)] text-[11px] text-[var(--text-dim)]"> <div className="px-4 py-2 bg-[var(--bg-elevated)] border-t border-[var(--border-color)] text-[11px] text-[var(--text-dim)] flex items-center justify-between flex-wrap gap-2">
Config location: <span className="font-mono text-[var(--text-muted)]">{current.filename}</span> <span>
Config target: <span className="font-mono text-[var(--text-secondary)]">{current.filename}</span>
</span>
<span className="text-[10px] text-[var(--text-muted)]">
Format: <span className="uppercase font-mono font-semibold">{current.lang}</span>
</span>
</div> </div>
</div> </div>
); );
} }
+150 -15
View File
@@ -18,19 +18,41 @@
--text-muted: #64748b; --text-muted: #64748b;
--text-dim: #94a3b8; --text-dim: #94a3b8;
--brand: #5e6ad2; --brand: #4f46e5;
--brand-hover: #6a75e0; /* lighter for hover in light mode */ --brand-hover: #4338ca;
--accent: #4f46e5; --accent: #4f46e5;
--accent-hover: #4338ca; --accent-hover: #4338ca;
--nav-bg: rgba(255, 255, 255, 0.88); --nav-bg: rgba(255, 255, 255, 0.88);
/* Code & Syntax variables (Light Mode) */
--code-bg: #f1f5f9; --code-bg: #f1f5f9;
--code-block-bg: #0f172a; --code-block-bg: #f8fafc;
--pre-text: #f8fafc; --code-header-bg: #f1f5f9;
--border-code: #e2e8f0;
--code-button-bg: #ffffff;
--code-button-hover-bg: #e2e8f0;
--pre-text: #1e293b;
--code-inline-bg: #f1f5f9;
--code-inline-text: #0f172a;
--code-inline-border: #e2e8f0;
--table-zebra: rgba(241, 245, 249, 0.6); --table-zebra: rgba(241, 245, 249, 0.6);
--scrollbar-thumb: #cbd5e1; --scrollbar-thumb: #cbd5e1;
--scrollbar-thumb-hover: #94a3b8; --scrollbar-thumb-hover: #94a3b8;
--shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05); --shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05);
/* Syntax Highlighting (Light Mode) */
--hl-keyword: #cf222e;
--hl-string: #0a3069;
--hl-title: #8250df;
--hl-attr: #953800;
--hl-number: #0550ae;
--hl-comment: #6e7781;
--hl-builtin: #0969da;
--hl-operator: #57606a;
--hl-variable: #953800;
--hl-meta: #6e7781;
} }
/* Dark mode overrides (html.dark) */ /* Dark mode overrides (html.dark) */
@@ -51,18 +73,40 @@ html.dark {
--text-dim: #62666d; --text-dim: #62666d;
--brand: #5e6ad2; --brand: #5e6ad2;
--brand-hover: #7170ff; /* lighter for hover in dark mode */ --brand-hover: #7170ff;
--accent: #7170ff; --accent: #7170ff;
--accent-hover: #828fff; --accent-hover: #828fff;
--nav-bg: rgba(8, 9, 10, 0.85); --nav-bg: rgba(8, 9, 10, 0.85);
/* Code & Syntax variables (Dark Mode) */
--code-bg: #141517; --code-bg: #141517;
--code-block-bg: #0c0d0e; --code-block-bg: #0d1117;
--pre-text: #e2e4e7; --code-header-bg: #161b22;
--border-code: #30363d;
--code-button-bg: #21262d;
--code-button-hover-bg: #30363d;
--pre-text: #e6edf3;
--code-inline-bg: #161b22;
--code-inline-text: #e6edf3;
--code-inline-border: #30363d;
--table-zebra: rgba(255, 255, 255, 0.02); --table-zebra: rgba(255, 255, 255, 0.02);
--scrollbar-thumb: #23252a; --scrollbar-thumb: #23252a;
--scrollbar-thumb-hover: #383b42; --scrollbar-thumb-hover: #383b42;
--shadow-card: none; --shadow-card: none;
/* Syntax Highlighting (Dark Mode) */
--hl-keyword: #ff7b72;
--hl-string: #a5d6ff;
--hl-title: #d2a8ff;
--hl-attr: #ffa657;
--hl-number: #79c0ff;
--hl-comment: #8b949e;
--hl-builtin: #7ee787;
--hl-operator: #8b949e;
--hl-variable: #ffa657;
--hl-meta: #8b949e;
} }
@theme inline { @theme inline {
@@ -75,6 +119,7 @@ html.dark {
--color-elevated: var(--bg-elevated); --color-elevated: var(--bg-elevated);
--color-elevated-hover: var(--bg-elevated-hover); --color-elevated-hover: var(--bg-elevated-hover);
--color-border: var(--border-color); --color-border: var(--border-color);
--border-code: var(--border-code);
--color-border-subtle: var(--border-subtle); --color-border-subtle: var(--border-subtle);
--color-text-primary: var(--text-primary); --color-text-primary: var(--text-primary);
@@ -189,24 +234,26 @@ body {
.prose pre { .prose pre {
background: var(--code-block-bg) !important; background: var(--code-block-bg) !important;
border: 1px solid var(--border-color); color: var(--pre-text) !important;
border-radius: 0.625rem; border: 1px solid var(--border-code);
border-radius: 0.75rem;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
overflow-x: auto; overflow-x: auto;
position: relative; position: relative;
font-family: var(--font-mono);
} }
.prose code { .prose code {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 0.85em; font-size: 0.875em;
} }
.prose :not(pre) > code { .prose :not(pre) > code {
background: var(--code-bg); background: var(--code-inline-bg);
color: var(--text-primary); color: var(--code-inline-text);
border: 1px solid var(--border-color); border: 1px solid var(--code-inline-border);
padding: 0.15rem 0.35rem; padding: 0.15rem 0.375rem;
border-radius: 0.25rem; border-radius: 0.375rem;
font-weight: 500; font-weight: 500;
} }
@@ -230,6 +277,94 @@ body {
margin: 2.5rem 0; margin: 2.5rem 0;
} }
/* Syntax Highlighting Tokens (Highlight.js) */
.hljs {
color: var(--pre-text);
background: transparent;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst,
.hljs-doctag {
color: var(--hl-keyword);
font-weight: 600;
}
.hljs-string,
.hljs-regexp,
.hljs-link {
color: var(--hl-string);
}
.hljs-title,
.hljs-title.function_,
.hljs-title.class_,
.hljs-section {
color: var(--hl-title);
font-weight: 600;
}
.hljs-attr,
.hljs-attribute,
.hljs-variable,
.hljs-template-variable,
.hljs-type {
color: var(--hl-attr);
}
.hljs-number,
.hljs-literal {
color: var(--hl-number);
}
.hljs-comment,
.hljs-quote {
color: var(--hl-comment);
font-style: italic;
}
.hljs-built_in,
.hljs-symbol,
.hljs-bullet {
color: var(--hl-builtin);
}
.hljs-meta,
.hljs-meta .hljs-keyword,
.hljs-meta .hljs-string {
color: var(--hl-meta);
}
.hljs-params {
color: var(--pre-text);
}
.hljs-punctuation,
.hljs-operator {
color: var(--hl-operator);
}
.hljs-addition {
color: #1a7f37;
background-color: rgba(46, 160, 67, 0.15);
border-radius: 2px;
}
.hljs-deletion {
color: #cf222e;
background-color: rgba(248, 81, 73, 0.15);
border-radius: 2px;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
/* Animations */ /* Animations */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); } from { opacity: 0; transform: translateY(4px); }
+1
View File
@@ -18,6 +18,7 @@
"react": "19.2.8", "react": "19.2.8",
"react-dom": "19.2.8", "react-dom": "19.2.8",
"react-markdown": "^9.0.0", "react-markdown": "^9.0.0",
"rehype-highlight": "^7.0.2",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1" "remark-gfm": "^4.0.1"
}, },
-69
View File
@@ -1,69 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert h-5 w-[100px]"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the{" "}
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
page.tsx
</code>{" "}
file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert h-[14px] w-4"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={14}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
}
+13
View File
File diff suppressed because one or more lines are too long