added contact page
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
|
||||
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 ?? '';
|
||||
|
||||
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',
|
||||
bluesky: 'Bluesky',
|
||||
github: 'GitHub',
|
||||
instagram: 'Instagram',
|
||||
url: 'Link',
|
||||
};
|
||||
---
|
||||
|
||||
<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-[0.95] 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 && !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}
|
||||
</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>
|
||||
</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>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<div class="section-rule mt-16">
|
||||
<span class="ornament">✦</span>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
Reference in New Issue
Block a user