Files
narlblog/frontend/src/pages/contact.astro
T
2026-05-15 16:12:18 +02:00

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>