"use client"; import { onContactTopic } from "./contactTopic"; import * as React from "react"; import { useContent } from "@/src/components/ContentProvider"; import { CONTACT_EMAIL_PARTS } from "@/src/content/site"; import { assembleEmail, mailtoHref, spellOutEmail } from "@/src/domain/email"; const TYPE_MS = 9; const MONO = "font-mono text-[11px] uppercase tracking-[0.2em]"; /** The text typed out so far. Jumps to the full text when motion is reduced. */ const useTyped = (text: string): string => { const [length, setLength] = React.useState(text.length); React.useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setLength(text.length); return; } setLength(0); const timer = window.setInterval(() => { setLength((current) => { if (current >= text.length) { window.clearInterval(timer); return current; } return Math.min(text.length, current + 3); }); }, TYPE_MS); return () => window.clearInterval(timer); }, [text]); return text.slice(0, length); }; /** Blanks the visitor fills in are set apart from the prepared wording. */ const withBlanks = (text: string): React.ReactNode => text.split(/(\[[^\]]*\]?)/).map((part, index) => part.startsWith("[") ? ( {part} ) : ( part ), ); /** * ContactComposer: pick what you need and the email writes itself. One press * opens it in the visitor's own mail app, ready to finish and send. The address * is assembled in the browser, so it never appears in the page source. */ export const ContactComposer: React.FC = () => { const { contact } = useContent(); const topics = contact.topics; const [topicId, setTopicId] = React.useState(topics[0]?.id ?? ""); const [address, setAddress] = React.useState(null); const [copied, setCopied] = React.useState(false); React.useEffect(() => { setAddress(assembleEmail(CONTACT_EMAIL_PARTS)); }, []); // A button elsewhere on the page can open the message with its topic chosen. React.useEffect( () => onContactTopic((id) => { if (topics.some((item) => item.id === id)) setTopicId(id); }), [topics], ); const topic = topics.find((item) => item.id === topicId) ?? topics[0]; const typed = useTyped(topic?.draft.body ?? ""); if (!topic) return null; const done = typed.length === topic.draft.body.length; const copy = async () => { if (!address) return; try { await navigator.clipboard.writeText(address); setCopied(true); window.setTimeout(() => setCopied(false), 1800); } catch { // Clipboard access can be refused; the address stays visible to copy by hand. } }; return (
{contact.draft}

{contact.need}

{topics.map((item) => { const selected = item.id === topic.id; return ( ); })}
{contact.to}
{address ?? spellOutEmail(CONTACT_EMAIL_PARTS)}
{contact.subject}
{topic.draft.subject}
{withBlanks(typed)}
{contact.open}
); };