mirror of
https://github.com/domfelipe/portfolio.git
synced 2026-08-07 05:56:42 +00:00
- Hero com terminal animado e identidade de engenheiro de agentes de IA - 10 projetos curados (destaques: Mika e Agentes de IA com métricas) - Skills, contato, dark mode, SEO/OG por locale - Rotas /pt e /en com redirect por Accept-Language
99 lines
3 KiB
TypeScript
99 lines
3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import type { TermLine } from "@/i18n/dictionaries/en";
|
|
|
|
const TYPE_SPEED = 26;
|
|
const LINE_PAUSE = 320;
|
|
|
|
export default function Terminal({ title, lines }: { title: string; lines: TermLine[] }) {
|
|
const [step, setStep] = useState(0);
|
|
const [chars, setChars] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (step >= lines.length) return;
|
|
const line = lines[step];
|
|
if (line.kind === "cmd") {
|
|
if (chars < line.text.length) {
|
|
const id = setTimeout(() => setChars((c) => c + 1), TYPE_SPEED);
|
|
return () => clearTimeout(id);
|
|
}
|
|
const id = setTimeout(() => {
|
|
setStep((s) => s + 1);
|
|
setChars(0);
|
|
}, LINE_PAUSE);
|
|
return () => clearTimeout(id);
|
|
}
|
|
const id = setTimeout(() => setStep((s) => s + 1), LINE_PAUSE);
|
|
return () => clearTimeout(id);
|
|
}, [step, chars, lines]);
|
|
|
|
const done = step >= lines.length;
|
|
|
|
return (
|
|
<div className="overflow-hidden rounded-xl border border-[var(--term-line)] bg-[var(--term-bg)] shadow-2xl shadow-black/40">
|
|
<div className="flex items-center gap-2 border-b border-[var(--term-line)] px-4 py-3">
|
|
<span className="size-3 rounded-full bg-[#ff5f57]" />
|
|
<span className="size-3 rounded-full bg-[#febc2e]" />
|
|
<span className="size-3 rounded-full bg-[#28c840]" />
|
|
<span className="ml-3 font-mono text-xs text-[#7d8da1]">{title}</span>
|
|
</div>
|
|
<div className="min-h-[264px] p-5 font-mono text-[13px] leading-7">
|
|
{lines.slice(0, step).map((line, i) => (
|
|
<TermLineRow key={i} line={line} />
|
|
))}
|
|
{!done && lines[step] && (
|
|
<TermLineRow
|
|
line={{
|
|
...lines[step],
|
|
text: lines[step].text.slice(0, lines[step].kind === "cmd" ? chars : lines[step].text.length),
|
|
}}
|
|
cursor
|
|
/>
|
|
)}
|
|
{done && (
|
|
<p className="text-[#e9eef5]">
|
|
<Prompt />
|
|
<span className="ml-2 inline-block h-4 w-2 translate-y-[3px] animate-blink bg-[#f5a524]" />
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Prompt() {
|
|
return <span className="font-semibold text-[#f5a524]">$</span>;
|
|
}
|
|
|
|
function TermLineRow({ line, cursor }: { line: TermLine; cursor?: boolean }) {
|
|
const cursorEl = cursor && (
|
|
<span className="ml-0.5 inline-block h-4 w-2 translate-y-[3px] animate-blink bg-[#f5a524]" />
|
|
);
|
|
|
|
if (line.kind === "cmd") {
|
|
return (
|
|
<p className="text-[#e9eef5]">
|
|
<Prompt />
|
|
<span className="ml-2">{line.text}</span>
|
|
{cursorEl}
|
|
</p>
|
|
);
|
|
}
|
|
if (line.kind === "ok") {
|
|
return (
|
|
<p className="text-[#7d8da1]">
|
|
<span className="font-semibold text-[#3ecf8e]">✓</span>
|
|
<span className="ml-2">{line.text}</span>
|
|
{cursorEl}
|
|
</p>
|
|
);
|
|
}
|
|
return (
|
|
<p className="text-[#7d8da1]">
|
|
<span className="font-semibold text-[#5ec8f2]">→</span>
|
|
<span className="ml-2">{line.text}</span>
|
|
{cursorEl}
|
|
</p>
|
|
);
|
|
}
|