Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Failing after 3m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
181 lines
5.2 KiB
TypeScript
181 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useRef } from "react";
|
|
import * as Prism from "prismjs";
|
|
import "prismjs/components/prism-python";
|
|
import "prismjs/components/prism-typescript";
|
|
import "prismjs/components/prism-javascript";
|
|
import "prismjs/components/prism-jsx";
|
|
import "prismjs/components/prism-tsx";
|
|
import "prismjs/components/prism-docker";
|
|
import "prismjs/components/prism-yaml";
|
|
import "prismjs/components/prism-json";
|
|
import "prismjs/components/prism-markup";
|
|
import "prismjs/components/prism-css";
|
|
import "prismjs/components/prism-sql";
|
|
import "prismjs/components/prism-bash";
|
|
import "prismjs/components/prism-markdown";
|
|
|
|
import { cn } from "../utils/cn";
|
|
import { CodeWindow } from "./Effects/CodeWindow";
|
|
|
|
interface FileExampleProps {
|
|
filename: string;
|
|
content: string;
|
|
language: string;
|
|
description?: string;
|
|
tags?: string[];
|
|
id: string;
|
|
}
|
|
|
|
const prismLanguageMap: Record<string, string> = {
|
|
py: "python",
|
|
ts: "typescript",
|
|
tsx: "tsx",
|
|
js: "javascript",
|
|
jsx: "jsx",
|
|
dockerfile: "docker",
|
|
docker: "docker",
|
|
yml: "yaml",
|
|
yaml: "yaml",
|
|
json: "json",
|
|
html: "markup",
|
|
css: "css",
|
|
sql: "sql",
|
|
sh: "bash",
|
|
bash: "bash",
|
|
md: "markdown",
|
|
};
|
|
|
|
export const FileExample: React.FC<FileExampleProps> = ({
|
|
filename,
|
|
content,
|
|
language,
|
|
}) => {
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const [isCopied, setIsCopied] = useState(false);
|
|
|
|
const fileExtension = filename.split(".").pop() || language;
|
|
const prismLanguage = prismLanguageMap[fileExtension] || "markup";
|
|
|
|
const highlightedCode = Prism.highlight(
|
|
content,
|
|
Prism.languages[prismLanguage] || Prism.languages.markup,
|
|
prismLanguage,
|
|
);
|
|
|
|
const handleCopy = async (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
try {
|
|
await navigator.clipboard.writeText(content);
|
|
setIsCopied(true);
|
|
setTimeout(() => setIsCopied(false), 900);
|
|
} catch (err) {
|
|
console.error("Failed to copy:", err);
|
|
}
|
|
};
|
|
|
|
const handleDownload = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
const blob = new Blob([content], { type: "text/plain" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const actions = (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className={`h-7 w-7 inline-flex items-center justify-center rounded-full border border-slate-200 bg-white text-slate-400 hover:text-slate-900 hover:border-slate-400 transition-all ${isCopied ? "text-green-500 border-green-200" : ""}`}
|
|
onClick={handleCopy}
|
|
title="Copy to clipboard"
|
|
>
|
|
<svg
|
|
className="w-3 h-3"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<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>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="h-7 w-7 inline-flex items-center justify-center rounded-full border border-slate-200 bg-white text-slate-400 hover:text-slate-900 hover:border-slate-400 transition-all"
|
|
onClick={handleDownload}
|
|
title="Download file"
|
|
>
|
|
<svg
|
|
className="w-3 h-3"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<div className="my-8 group/file">
|
|
<CodeWindow
|
|
title={filename}
|
|
actions={actions}
|
|
className={cn(
|
|
"transition-all duration-300",
|
|
isExpanded
|
|
? "max-w-4xl"
|
|
: "max-w-[600px] cursor-pointer hover:border-slate-200",
|
|
)}
|
|
minHeight={isExpanded ? "auto" : "80px"}
|
|
>
|
|
<div
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
className={cn(
|
|
"relative",
|
|
!isExpanded && "max-h-[80px] overflow-hidden",
|
|
)}
|
|
>
|
|
<pre
|
|
className="m-0 p-0 overflow-x-auto text-[13px] leading-[1.65] font-mono text-slate-800"
|
|
style={{
|
|
fontFamily:
|
|
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
}}
|
|
>
|
|
<code
|
|
className={`language-${prismLanguage}`}
|
|
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
|
></code>
|
|
</pre>
|
|
|
|
{!isExpanded && (
|
|
<div className="absolute inset-0 bg-gradient-to-t from-white/80 to-transparent flex items-end justify-center pb-2">
|
|
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
|
Click to expand
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CodeWindow>
|
|
</div>
|
|
);
|
|
};
|