154 lines
4.4 KiB
Plaintext
154 lines
4.4 KiB
Plaintext
---
|
|
import Layout from '../layouts/Layout.astro';
|
|
import ContactForm from '../components/react/ContactForm';
|
|
|
|
interface ContactLink {
|
|
kind: string;
|
|
label: string;
|
|
value: string;
|
|
}
|
|
|
|
interface SiteConfig {
|
|
contact_intro?: string;
|
|
contact_links?: ContactLink[];
|
|
}
|
|
|
|
const API_URL = process.env.PUBLIC_API_URL || 'http://localhost:3000';
|
|
|
|
let siteConfig: SiteConfig = {};
|
|
let error = '';
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/api/config`);
|
|
if (res.ok) {
|
|
siteConfig = await res.json();
|
|
} else {
|
|
error = 'Failed to load contact details.';
|
|
}
|
|
} catch (e) {
|
|
error = `Could not connect to backend: ${e instanceof Error ? e.message : String(e)}`;
|
|
console.error(error);
|
|
}
|
|
|
|
const links: ContactLink[] = siteConfig.contact_links ?? [];
|
|
const intro = siteConfig.contact_intro ?? '';
|
|
|
|
const KIND_LABEL: Record<string, string> = {
|
|
email: 'Email',
|
|
mastodon: 'Mastodon',
|
|
bluesky: 'Bluesky',
|
|
github: 'GitHub',
|
|
instagram: 'Instagram',
|
|
url: 'Link',
|
|
};
|
|
|
|
function b64(s: string): string {
|
|
return Buffer.from(s, 'utf-8').toString('base64');
|
|
}
|
|
|
|
function obfuscateEmail(addr: string): { user: string; host: string; display: string; u64: string; h64: string } | null {
|
|
const at = addr.indexOf('@');
|
|
if (at === -1) return null;
|
|
const user = addr.slice(0, at);
|
|
const host = addr.slice(at + 1);
|
|
const display = `${user} [at] ${host.replace(/\./g, ' [dot] ')}`;
|
|
return { user, host, display, u64: b64(user), h64: b64(host) };
|
|
}
|
|
---
|
|
|
|
<Layout title="Contact" description="Get in touch.">
|
|
<section class="max-w-2xl mx-auto">
|
|
<div class="mb-10 md:mb-14">
|
|
<div class="font-display italic text-[var(--subtext0)] text-xs tracking-[0.3em] uppercase mb-4">Correspondence</div>
|
|
<h1 class="font-display italic font-semibold text-[var(--text)] text-5xl md:text-6xl leading-[1.08] tracking-tight mb-6">
|
|
Get in touch
|
|
</h1>
|
|
{intro && (
|
|
<p class="font-sans text-lg text-[var(--subtext1)] leading-relaxed whitespace-pre-line">
|
|
{intro}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<div class="glass p-6 text-center mb-8 border-[var(--red)]/40">
|
|
<p class="font-display italic text-[var(--red)]">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{links.length > 0 && (
|
|
<ul class="space-y-3 mb-12">
|
|
{links.map((link) => {
|
|
const isEmail = link.kind === 'email';
|
|
const obf = isEmail ? obfuscateEmail(link.value.trim()) : null;
|
|
return (
|
|
<li>
|
|
<a
|
|
href={obf ? '#' : link.value}
|
|
{...(!isEmail ? { target: '_blank', rel: 'noopener noreferrer me' } : {})}
|
|
{...(obf ? { 'data-mail': '', 'data-mail-u': obf.u64, 'data-mail-h': obf.h64 } : {})}
|
|
class="group grid md:grid-cols-[1fr_minmax(0,auto)] gap-2 md:gap-6 px-5 md:px-6 py-4 md:items-baseline border border-[var(--surface2)]/60 hover:border-[var(--mauve)]/60 transition-colors"
|
|
style="border-radius: 1px"
|
|
>
|
|
<div class="min-w-0">
|
|
<div class="font-display italic text-[11px] uppercase tracking-[0.18em] text-[var(--subtext0)] mb-1 pr-1">
|
|
{KIND_LABEL[link.kind] ?? link.kind}
|
|
</div>
|
|
<div class="font-display italic text-xl md:text-2xl text-[var(--text)] group-hover:text-[var(--mauve)] transition-colors break-words">
|
|
{link.label}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="font-mono text-xs text-[var(--subtext0)] md:text-right break-words min-w-0"
|
|
title={obf ? obf.display : link.value}
|
|
data-mail-text={obf ? '' : undefined}
|
|
>
|
|
{obf ? obf.display : link.value}
|
|
</div>
|
|
</a>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
|
|
<div class="mt-8">
|
|
<h2 class="font-display italic text-2xl md:text-3xl text-[var(--text)] border-l-2 border-[var(--mauve)] pl-4 mb-6">
|
|
Or send a note directly
|
|
</h2>
|
|
<ContactForm client:idle />
|
|
</div>
|
|
|
|
<div class="section-rule mt-16">
|
|
<span class="ornament">✦</span>
|
|
</div>
|
|
</section>
|
|
</Layout>
|
|
|
|
<script>
|
|
function hydrateMail() {
|
|
document.querySelectorAll<HTMLAnchorElement>('a[data-mail]').forEach((a) => {
|
|
const u = a.dataset.mailU;
|
|
const h = a.dataset.mailH;
|
|
if (!u || !h) return;
|
|
try {
|
|
const user = atob(u);
|
|
const host = atob(h);
|
|
const addr = `${user}@${host}`;
|
|
a.href = `mailto:${addr}`;
|
|
a.querySelectorAll<HTMLElement>('[data-mail-text]').forEach((el) => {
|
|
el.textContent = addr;
|
|
el.setAttribute('title', addr);
|
|
});
|
|
} catch {
|
|
// leave obfuscated form
|
|
}
|
|
});
|
|
}
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', hydrateMail);
|
|
} else {
|
|
hydrateMail();
|
|
}
|
|
</script>
|