95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
|
|
interface TerminalBlockProps {
|
|
title?: string;
|
|
lines: string[];
|
|
className?: string;
|
|
typingEffect?: boolean;
|
|
}
|
|
|
|
export const TerminalBlock: React.FC<TerminalBlockProps> = ({
|
|
title = "bash",
|
|
lines,
|
|
className,
|
|
typingEffect = false,
|
|
}) => {
|
|
const [displayedLines, setDisplayedLines] = useState<string[]>(
|
|
typingEffect ? [] : lines,
|
|
);
|
|
const [currentLineIndex, setCurrentLineIndex] = useState(0);
|
|
const [currentCharIndex, setCurrentCharIndex] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (!typingEffect) return;
|
|
|
|
if (currentLineIndex < lines.length) {
|
|
const currentLineFull = lines[currentLineIndex];
|
|
|
|
if (currentCharIndex < currentLineFull.length) {
|
|
const timeout = setTimeout(
|
|
() => {
|
|
setDisplayedLines((prev) => {
|
|
const newLines = [...prev];
|
|
if (newLines[currentLineIndex] === undefined) {
|
|
newLines[currentLineIndex] = currentLineFull[currentCharIndex];
|
|
} else {
|
|
newLines[currentLineIndex] += currentLineFull[currentCharIndex];
|
|
}
|
|
return newLines;
|
|
});
|
|
setCurrentCharIndex((prev) => prev + 1);
|
|
},
|
|
15 + Math.random() * 20,
|
|
); // Random variance for realism
|
|
|
|
return () => clearTimeout(timeout);
|
|
} else {
|
|
// Line finished, wait a bit then move to next
|
|
const timeout = setTimeout(() => {
|
|
setCurrentLineIndex((prev) => prev + 1);
|
|
setCurrentCharIndex(0);
|
|
}, 300);
|
|
return () => clearTimeout(timeout);
|
|
}
|
|
}
|
|
}, [typingEffect, lines, currentLineIndex, currentCharIndex]);
|
|
|
|
return (
|
|
<div
|
|
className={`rounded-md overflow-hidden bg-[#1e1e1e] shadow-xl border border-gray-800 ${className}`}
|
|
>
|
|
<div className="flex items-center px-4 py-2 bg-[#252526] border-b border-gray-700">
|
|
<div className="flex space-x-2 mr-4">
|
|
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
|
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
|
|
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
|
</div>
|
|
<div className="text-xs text-gray-400 font-mono">{title}</div>
|
|
</div>
|
|
<div className="p-6 font-mono text-sm leading-relaxed min-h-[200px]">
|
|
{displayedLines.map((line, idx) => (
|
|
<div key={idx} className="text-gray-300 break-words">
|
|
{line.startsWith("$") ? (
|
|
<span className="text-green-400">{line}</span>
|
|
) : line.startsWith(">") ? (
|
|
<span className="text-blue-400 ml-4">{line}</span>
|
|
) : line.startsWith("#") ? (
|
|
<span className="text-gray-500 italic">{line}</span>
|
|
) : (
|
|
<span className="text-gray-300">{line}</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
{/* Cursor */}
|
|
{typingEffect && currentLineIndex < lines.length && (
|
|
<div className="animate-pulse inline-block w-2 h-4 bg-gray-500 align-middle ml-1"></div>
|
|
)}
|
|
{/* Static Cursor when done */}
|
|
{(!typingEffect || currentLineIndex >= lines.length) && (
|
|
<div className="animate-pulse mt-2 inline-block w-2 h-4 bg-gray-500 align-middle"></div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|