mirror of
https://github.com/domfelipe/portfolio.git
synced 2026-08-07 10:16:42 +00:00
feat: portfolio bilíngue (PT/EN) — Next.js 16 + Tailwind v4
- 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
This commit is contained in:
commit
be348f70f6
32 changed files with 8631 additions and 0 deletions
99
src/components/Terminal.tsx
Normal file
99
src/components/Terminal.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue