Files
public-blog/components/portfolio/ContactTerminal.tsx

115 lines
5.0 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { contact } from "@/lib/portfolio";
type CopyTarget = "email" | "ssh";
type CopyStatus = "idle" | "copied" | "failed";
const rows = [
{ label: "EMAIL", value: contact.email, kind: "copy" as const, target: "email" as CopyTarget },
{ label: "GIT", value: contact.git, kind: "link" as const },
{ label: "LINKEDIN", value: contact.linkedIn, kind: "link" as const },
{ label: "SSH", value: contact.ssh, kind: "copy" as const, target: "ssh" as CopyTarget },
];
export function ContactTerminal() {
const [copyState, setCopyState] = useState<Record<CopyTarget, CopyStatus>>({
email: "idle",
ssh: "idle",
});
const resetTimers = useRef<Partial<Record<CopyTarget, ReturnType<typeof setTimeout>>>>({});
useEffect(() => {
const timers = resetTimers.current;
return () => {
Object.values(timers).forEach((timer) => {
if (timer) {
clearTimeout(timer);
}
});
};
}, []);
async function copyValue(target: CopyTarget, value: string) {
if (resetTimers.current[target]) {
clearTimeout(resetTimers.current[target]);
}
try {
await navigator.clipboard.writeText(value);
setCopyState((current) => ({ ...current, [target]: "copied" }));
} catch {
setCopyState((current) => ({ ...current, [target]: "failed" }));
}
resetTimers.current[target] = setTimeout(() => {
setCopyState((current) => ({ ...current, [target]: "idle" }));
}, 2000);
}
return (
<section id="contact" aria-labelledby="contact-title" className="mx-auto max-w-7xl px-4 py-20 text-blacksite-text sm:px-6 lg:px-8">
<div className="border border-blacksite-line bg-blacksite-surface shadow-[0_32px_120px_rgba(0,0,0,0.42)]">
<div className="flex items-center justify-between gap-4 border-b border-blacksite-line px-5 py-4 sm:px-7">
<p className="font-mono text-xs text-blacksite-muted">&gt; open contact.channel</p>
<span className="section-label">SECURE HANDOFF</span>
</div>
<div className="grid gap-0 lg:grid-cols-[0.8fr_1.2fr]">
<div className="border-b border-blacksite-line px-5 py-8 sm:px-7 lg:border-b-0 lg:border-r lg:px-10">
<p className="section-label mb-4">CONTACT TERMINAL</p>
<h2 id="contact-title" className="font-mono text-3xl font-semibold uppercase tracking-[-0.04em] text-blacksite-text sm:text-5xl">
Send signal.
</h2>
<p className="mt-5 max-w-md text-sm leading-7 text-blacksite-muted sm:text-base">
Reach the operator directly for systems work, hardened infrastructure, or unusually interesting problems.
</p>
</div>
<div className="divide-y divide-blacksite-line">
{rows.map((row) => (
<div key={row.label} className="grid gap-3 px-5 py-5 sm:px-7 md:grid-cols-[7rem_minmax(0,1fr)] lg:px-8">
<p className="section-label pt-1">{row.label}</p>
{row.kind === "copy" ? (
<button
type="button"
onClick={() => copyValue(row.target, row.value)}
className="group flex min-w-0 items-center justify-between gap-4 text-left font-mono text-sm text-blacksite-text transition-colors hover:underline focus-visible:outline-blacksite-text"
>
<span className="break-all">{row.value}</span>
<span className="shrink-0 text-[11px] uppercase tracking-[0.16em] text-blacksite-muted group-hover:text-blacksite-text">
{copyState[row.target] === "copied" ? "copied" : copyState[row.target] === "failed" ? "copy failed" : "copy"}
</span>
</button>
) : (
<a
href={row.value}
target="_blank"
rel="noreferrer"
className="group flex min-w-0 items-center justify-between gap-4 font-mono text-sm text-blacksite-text transition-colors hover:underline focus-visible:outline-blacksite-text"
>
<span className="break-all">{row.value}</span>
<span aria-hidden="true" className="shrink-0 text-blacksite-muted transition-transform group-hover:translate-x-0.5 group-hover:text-blacksite-text">
</span>
</a>
)}
</div>
))}
<div className="grid gap-3 bg-blacksite-surface2/45 px-5 py-5 sm:px-7 md:grid-cols-[7rem_minmax(0,1fr)] lg:px-8">
<p className="section-label">AVAILABILITY</p>
<p className="flex items-center gap-3 font-mono text-sm uppercase tracking-[0.14em] text-blacksite-text">
<span aria-hidden="true" className="size-2 rounded-full bg-signal-green shadow-[0_0_18px_rgba(182,255,0,0.5)]" />
Open to interesting problems
</p>
</div>
</div>
</div>
</div>
</section>
);
}