added admin login to frontend + obscurification for contact details
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user