203 lines
7.0 KiB
Plaintext
203 lines
7.0 KiB
Plaintext
---
|
|
import '../styles/global.css';
|
|
import '@fontsource-variable/fraunces';
|
|
import '@fontsource-variable/eb-garamond';
|
|
import '@fontsource-variable/jetbrains-mono';
|
|
import '@fontsource/vt323';
|
|
import '@fontsource/space-mono';
|
|
import '@fontsource/space-mono/700.css';
|
|
import CyberFx from '../components/CyberFx.astro';
|
|
import Search from '../components/react/Search';
|
|
import LogoutButton from '../components/react/LogoutButton';
|
|
import EditableText from '../components/react/EditableText';
|
|
import { getSiteMode, copy } from '../lib/siteMode';
|
|
|
|
interface Props {
|
|
title: string;
|
|
wide?: boolean;
|
|
description?: string;
|
|
image?: string;
|
|
type?: 'website' | 'article';
|
|
/** Strips the top-bar controls and footer nav — used by the login page
|
|
* so the sign-in screen is a focused, chrome-free stage. */
|
|
minimal?: boolean;
|
|
}
|
|
|
|
const { title, wide = false, description, image, type = 'website', minimal = false } = Astro.props;
|
|
|
|
const siteMode = getSiteMode();
|
|
const c = copy(siteMode);
|
|
|
|
const API_URL = process.env.PUBLIC_API_URL || 'http://backend:3000';
|
|
const isAdmin = Astro.cookies.get('admin_session')?.value === '1';
|
|
|
|
let siteConfig: {
|
|
title: string;
|
|
subtitle: string;
|
|
footer: string;
|
|
favicon: string;
|
|
theme: string;
|
|
custom_css: string;
|
|
contact_links?: { kind: string; label: string; value: string }[];
|
|
} = {
|
|
title: "Ela's Atelier",
|
|
subtitle: "Works on paper, canvas, and elsewhere",
|
|
footer: "Hand-arranged with care",
|
|
favicon: "/favicon.svg",
|
|
theme: "salon",
|
|
custom_css: "",
|
|
contact_links: []
|
|
};
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/api/config`);
|
|
if (res.ok) {
|
|
siteConfig = await res.json();
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to fetch config:", e);
|
|
}
|
|
|
|
const fullTitle = `${title} · ${siteConfig.title}`;
|
|
const year = new Date().getFullYear();
|
|
const hasContact = (siteConfig.contact_links?.length ?? 0) > 0;
|
|
---
|
|
|
|
<!doctype html>
|
|
<html lang="en" class={`mode-${siteMode}`}>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link rel="icon" href={siteConfig.favicon} />
|
|
<meta name="generator" content={Astro.generator} />
|
|
<title>{fullTitle}</title>
|
|
{description && <meta name="description" content={description} />}
|
|
<meta property="og:title" content={fullTitle} />
|
|
{description && <meta property="og:description" content={description} />}
|
|
<meta property="og:type" content={type} />
|
|
<meta property="og:site_name" content={siteConfig.title} />
|
|
<meta property="og:url" content={Astro.url.href} />
|
|
{image && <meta property="og:image" content={image} />}
|
|
<meta name="twitter:card" content={image ? 'summary_large_image' : 'summary'} />
|
|
<meta name="twitter:title" content={fullTitle} />
|
|
{description && <meta name="twitter:description" content={description} />}
|
|
{image && <meta name="twitter:image" content={image} />}
|
|
<link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/feed.xml" />
|
|
{siteConfig.custom_css && <style set:html={siteConfig.custom_css} />}
|
|
<script is:inline define:vars={{ siteTheme: siteConfig.theme }}>
|
|
// Theme is owner-controlled (site config). Drop any legacy
|
|
// per-visitor override so everyone sees the configured theme.
|
|
try { localStorage.removeItem('user-theme'); } catch (e) {}
|
|
document.documentElement.classList.add(siteTheme || 'salon');
|
|
</script>
|
|
<script is:inline>
|
|
// When a page is restored from the back/forward cache (e.g. after
|
|
// saving in the editor and hitting Back), the SSR'd post grid is
|
|
// stale. Force a fresh fetch so newly edited tags / titles show up.
|
|
window.addEventListener('pageshow', function (e) {
|
|
if (e.persisted) window.location.reload();
|
|
});
|
|
</script>
|
|
<slot name="head" />
|
|
</head>
|
|
<body class="text-text">
|
|
<div class="salon-atmosphere" aria-hidden="true"></div>
|
|
|
|
<header class="border-b border-[var(--surface2)]/60">
|
|
<div class={`max-w-6xl mx-auto px-6 md:px-10 py-5 md:py-7 flex flex-col gap-4 md:flex-row md:items-end md:gap-6 ${minimal ? 'justify-center text-center' : 'md:justify-between'}`}>
|
|
<a href="/" class="nameplate group" aria-label="Home">
|
|
{isAdmin ? (
|
|
<EditableText
|
|
client:visible
|
|
initial={siteConfig.title}
|
|
fieldKey="title"
|
|
isAdmin
|
|
ariaLabel="site title"
|
|
className="nameplate-title"
|
|
/>
|
|
) : (
|
|
<span class="nameplate-title group-hover:text-[var(--mauve)] transition-colors">{siteConfig.title}</span>
|
|
)}
|
|
{isAdmin ? (
|
|
<EditableText
|
|
client:visible
|
|
initial={siteConfig.subtitle}
|
|
fieldKey="subtitle"
|
|
isAdmin
|
|
ariaLabel="site subtitle"
|
|
className="nameplate-subtitle"
|
|
/>
|
|
) : (
|
|
<span class="nameplate-subtitle">{siteConfig.subtitle}</span>
|
|
)}
|
|
</a>
|
|
|
|
{!minimal && (
|
|
<nav class="topbar-cluster w-full md:w-auto" aria-label="Site controls">
|
|
{hasContact && (
|
|
<a href="/contact" class="topbar-control">Contact</a>
|
|
)}
|
|
<Search client:idle mode={siteMode} />
|
|
<span class="topbar-divider" aria-hidden="true"></span>
|
|
{isAdmin ? (
|
|
<LogoutButton client:idle />
|
|
) : (
|
|
<a
|
|
href="/admin/login"
|
|
class="topbar-control tc-collapse-sm"
|
|
aria-label="Admin login"
|
|
title="Admin login"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/></svg>
|
|
<span class="tc-label">Admin</span>
|
|
</a>
|
|
)}
|
|
</nav>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
<main class={`mx-auto px-6 md:px-10 py-10 md:py-16 ${wide ? 'max-w-[95vw]' : 'max-w-6xl'}`}>
|
|
<slot />
|
|
</main>
|
|
|
|
<footer class={`max-w-6xl mx-auto px-6 md:px-10 text-center border-t border-[var(--surface2)]/40 ${minimal ? 'py-8 mt-8' : 'py-12 md:py-16 mt-12'}`}>
|
|
{!minimal && (
|
|
<>
|
|
<div class="section-rule mb-6">
|
|
<span class="ornament">✦</span>
|
|
<span class="font-display italic text-[var(--subtext0)] text-sm">{c.footerEnd}</span>
|
|
<span class="ornament">✦</span>
|
|
</div>
|
|
<p class="font-display italic text-base text-[var(--subtext1)] mb-2">
|
|
{isAdmin ? (
|
|
<EditableText
|
|
client:visible
|
|
initial={siteConfig.footer}
|
|
fieldKey="footer"
|
|
isAdmin
|
|
ariaLabel="footer text"
|
|
className="inline"
|
|
/>
|
|
) : siteConfig.footer}
|
|
</p>
|
|
<div class="text-xs text-[var(--subtext0)] tracking-[0.2em] uppercase mb-3 flex items-center justify-center gap-3 flex-wrap">
|
|
<a href="/feed.xml" class="hover:text-[var(--mauve)] transition-colors">RSS Feed</a>
|
|
{hasContact && (
|
|
<>
|
|
<span class="text-[var(--overlay0)]" aria-hidden="true">·</span>
|
|
<a href="/contact" class="hover:text-[var(--mauve)] transition-colors">Contact</a>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
<div class="site-copyright text-[var(--overlay0)] text-xs italic">
|
|
© {year} · {siteConfig.title}
|
|
</div>
|
|
</footer>
|
|
|
|
<CyberFx />
|
|
</body>
|
|
</html>
|