added admin login to frontend + obscurification for contact details

This commit is contained in:
2026-05-14 17:21:34 +02:00
parent 0102c89d81
commit 244dc076cb
13 changed files with 722 additions and 44 deletions
+78 -41
View File
@@ -1,5 +1,6 @@
---
import Layout from '../layouts/Layout.astro';
import ContactForm from '../components/react/ContactForm';
interface ContactLink {
kind: string;
@@ -32,18 +33,6 @@ try {
const links: ContactLink[] = siteConfig.contact_links ?? [];
const intro = siteConfig.contact_intro ?? '';
function hrefFor(link: ContactLink): string {
const v = link.value.trim();
if (link.kind === 'email') {
return v.startsWith('mailto:') ? v : `mailto:${v}`;
}
return v;
}
function isExternal(link: ContactLink): boolean {
return link.kind !== 'email';
}
const KIND_LABEL: Record<string, string> = {
email: 'Email',
mastodon: 'Mastodon',
@@ -52,6 +41,19 @@ const KIND_LABEL: Record<string, string> = {
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.">
@@ -74,43 +76,78 @@ const KIND_LABEL: Record<string, string> = {
</div>
)}
{links.length === 0 && !error && (
<div class="glass p-10 text-center">
<p class="font-display italic text-[var(--subtext0)] text-lg">
No contact channels listed yet.
</p>
</div>
)}
{links.length > 0 && (
<ul class="space-y-3">
{links.map((link) => (
<li>
<a
href={hrefFor(link)}
{...(isExternal(link) ? { target: '_blank', rel: 'noopener noreferrer me' } : {})}
class="group flex items-baseline justify-between gap-4 px-5 py-4 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-xs uppercase tracking-[0.25em] text-[var(--subtext0)] mb-1">
{KIND_LABEL[link.kind] ?? link.kind}
<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-display italic text-xl md:text-2xl text-[var(--text)] group-hover:text-[var(--mauve)] transition-colors truncate">
{link.label}
<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>
</div>
<div class="font-mono text-xs text-[var(--subtext0)] hidden md:block truncate max-w-[40%]" title={link.value}>
{link.value}
</div>
</a>
</li>
))}
</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:load />
</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>