259 lines
7.7 KiB
TypeScript
259 lines
7.7 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { gsap } from "gsap";
|
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
import {
|
|
MessageSquare,
|
|
ExternalLink,
|
|
Copy,
|
|
Check,
|
|
Key,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
import { useReducedMotion } from "../hooks/useReducedMotion";
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
interface ContactItem {
|
|
label: string;
|
|
value: string;
|
|
href?: string;
|
|
isLink?: boolean;
|
|
unavailable?: boolean;
|
|
icon?: LucideIcon;
|
|
}
|
|
|
|
interface ContactGroup {
|
|
category: string;
|
|
items: ContactItem[];
|
|
}
|
|
|
|
const contacts: ContactGroup[] = [
|
|
{
|
|
category: "Messaging",
|
|
items: [
|
|
{
|
|
label: "Simplex",
|
|
value: "Open chat",
|
|
href: "https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2F3_XOiEnk2Ebup7DqgOwGM5kXlIZX-uJo%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAPJz5X-WHhJk_y1MLRxy7NTfspD-vKF0pmmi_GAJpExY%253D%26q%3Dc%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion",
|
|
isLink: true,
|
|
},
|
|
{
|
|
label: "Discord",
|
|
value:
|
|
"@sysrand [1408202642588438589] / @forgecadrape [1452113626365169958]",
|
|
},
|
|
{ label: "Telegram", value: "@forgecadrape (main) / @xorededx" },
|
|
{ label: "XMPP", value: "sw@anoxinon.me (main) / me0w@xmpp.jp" },
|
|
{
|
|
label: "Session",
|
|
value:
|
|
"056834db96cedde6012da9d5402c683c0eb260ed866da145a8f86e7f329bf77222",
|
|
},
|
|
{
|
|
label: "Tox",
|
|
value:
|
|
"5622A2D2218AF235E39E356792270413BBE7CAEEAA531A20EEF11BF2FD10576E5050F0A55970",
|
|
},
|
|
{ label: "Potato Chat", value: "@depoliticized" },
|
|
{
|
|
label: "Element",
|
|
value: "Open profile",
|
|
href: "https://matrix.to/#/@depoliticized:tchncs.de",
|
|
isLink: true,
|
|
},
|
|
{ label: "Briar", value: "temporary unavailable", unavailable: true },
|
|
],
|
|
},
|
|
{
|
|
category: "Email",
|
|
items: [
|
|
{
|
|
label: "Primary",
|
|
value: "contact@stealer.wtf (temporary unavailable)",
|
|
unavailable: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
category: "Git",
|
|
items: [
|
|
{
|
|
label: "serpent256",
|
|
value: "View profile",
|
|
href: "https://git.fingeri.ng/serpent256",
|
|
isLink: true,
|
|
},
|
|
{
|
|
label: "raped.cc",
|
|
value: "View profile (main)",
|
|
href: "https://git.fingeri.ng/raped.cc",
|
|
isLink: true,
|
|
},
|
|
{
|
|
label: "whiskers",
|
|
value: "View org",
|
|
href: "https://git.fingeri.ng/whiskers",
|
|
isLink: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
category: "PGP",
|
|
items: [
|
|
{
|
|
label: "Public Key",
|
|
value: "Download /pub-pgp.txt",
|
|
href: "./pub-pgp.txt",
|
|
isLink: true,
|
|
icon: Key,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
function CopyButton({ text }: { text: string }) {
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const handleCopy = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
} catch {
|
|
// Fallback
|
|
}
|
|
};
|
|
|
|
return (
|
|
<button
|
|
onClick={handleCopy}
|
|
className="p-1.5 rounded-md text-grey-400 hover:text-pink-brand hover:bg-pink-brand/10 transition-all"
|
|
aria-label="Copy to clipboard"
|
|
>
|
|
{copied ? (
|
|
<Check className="w-3.5 h-3.5" />
|
|
) : (
|
|
<Copy className="w-3.5 h-3.5" />
|
|
)}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export default function ContactSection() {
|
|
const sectionRef = useRef<HTMLElement>(null);
|
|
const reducedMotion = useReducedMotion();
|
|
|
|
useEffect(() => {
|
|
if (reducedMotion || !sectionRef.current) return;
|
|
|
|
const ctx = gsap.context(() => {
|
|
gsap.fromTo(
|
|
".contact-reveal",
|
|
{ y: 40, opacity: 0 },
|
|
{
|
|
y: 0,
|
|
opacity: 1,
|
|
duration: 0.8,
|
|
stagger: 0.1,
|
|
ease: "expo.out",
|
|
scrollTrigger: {
|
|
trigger: sectionRef.current,
|
|
start: "top 70%",
|
|
toggleActions: "play none none none",
|
|
},
|
|
},
|
|
);
|
|
}, sectionRef);
|
|
|
|
return () => ctx.revert();
|
|
}, [reducedMotion]);
|
|
|
|
return (
|
|
<section
|
|
id="contact"
|
|
ref={sectionRef}
|
|
className="relative py-32 md:py-40 px-6 overflow-hidden"
|
|
>
|
|
{!reducedMotion && (
|
|
<div className="absolute bottom-0 left-1/2 w-[800px] h-[400px] -translate-x-1/2 translate-y-1/2 rounded-full bg-pink-brand/3 blur-[120px] pointer-events-none" />
|
|
)}
|
|
|
|
<div className="max-w-6xl mx-auto relative z-10">
|
|
{/* Section label */}
|
|
<div className="contact-reveal flex items-center gap-4 mb-16">
|
|
<span className="text-pink-brand font-mono text-xs tracking-[0.3em] uppercase">
|
|
006
|
|
</span>
|
|
<div className="flex-1 h-px bg-pink-brand/20" />
|
|
<span className="text-grey-500 font-display text-xs tracking-[0.2em] uppercase">
|
|
Contact
|
|
</span>
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<div className="contact-reveal mb-16">
|
|
<h2 className="font-display text-4xl md:text-6xl lg:text-7xl font-light text-grey-900 mb-4">
|
|
Get in <span className="text-gradient">touch</span>
|
|
</h2>
|
|
<p className="text-grey-500 text-lg font-body max-w-xl">
|
|
Reach out through any of these channels. I prefer encrypted
|
|
messaging when possible.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Contact grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
{contacts.map((group) => (
|
|
<div key={group.category} className="contact-reveal">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<MessageSquare className="w-4 h-4 text-pink-brand" />
|
|
<h3 className="font-display text-sm font-medium text-grey-900 uppercase tracking-widest">
|
|
{group.category}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{group.items.map((item) => (
|
|
<div
|
|
key={item.label + item.value}
|
|
className="group flex items-center justify-between p-4 rounded-xl bg-skin-100/40 border border-pink-brand/5 hover:border-pink-brand/15 hover:bg-skin-100/70 transition-all duration-300"
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<span className="text-xs font-mono text-grey-400 block mb-0.5">
|
|
{item.label}
|
|
</span>
|
|
{item.isLink && item.href ? (
|
|
<a
|
|
href={item.href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1.5 text-sm text-grey-700 hover:text-pink-brand transition-colors font-body"
|
|
>
|
|
{item.icon && <item.icon className="w-3.5 h-3.5" />}
|
|
{item.value}
|
|
<ExternalLink className="w-3 h-3" />
|
|
</a>
|
|
) : item.unavailable ? (
|
|
<span className="text-sm italic text-red-400 font-body">
|
|
{item.value}
|
|
</span>
|
|
) : (
|
|
<span className="text-sm text-grey-700 font-body break-all">
|
|
{item.value}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{!item.isLink && !item.unavailable && (
|
|
<CopyButton text={item.value} />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|