update: implement unified shell and navigation

This commit is contained in:
Codex
2026-02-18 15:06:38 +00:00
parent fa2ee57595
commit d1fbff481b

View File

@@ -10,27 +10,59 @@ function escapeHtml(value) {
} }
function shell({ title, content, compact = false }) { function shell({ title, content, compact = false }) {
const container = compact ? "max-w-xl" : "max-w-5xl"; const container = compact ? "max-w-md" : "max-w-5xl";
return `<!doctype html> return `<!doctype html>
<html lang="en"> <html lang="en" class="scroll-smooth">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${escapeHtml(title)}</title> <title>${escapeHtml(title)}</title>
<meta name="theme-color" content="#0f172a" /> <meta name="theme-color" content="#000000" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link href="/assets/styles.css" rel="stylesheet" type="text/css" /> <link href="/assets/styles.css" rel="stylesheet" type="text/css" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
</head> </head>
<body class="min-h-screen bg-slate-950 text-slate-100"> <body class="min-h-screen bg-base-100 text-base-content antialiased flex flex-col">
<div class="fixed inset-0 -z-10 overflow-hidden"> <div class="fixed inset-0 -z-10 bg-base-100 selection:bg-primary selection:text-primary-content"></div>
<div class="absolute -top-24 -left-20 h-72 w-72 rounded-full bg-cyan-500/20 blur-3xl"></div>
<div class="absolute top-40 -right-20 h-80 w-80 rounded-full bg-emerald-500/20 blur-3xl"></div> <nav class="navbar sticky top-0 z-50 bg-base-100/80 backdrop-blur-md border-b border-base-content/5 px-4 md:px-8">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(30,41,59,.65),rgba(2,6,23,1)_45%)]"></div> <div class="flex-1">
<a href="/" class="btn btn-ghost text-xl font-bold tracking-tight px-0 hover:bg-transparent">XArtAudio</a>
</div> </div>
<main class="${container} mx-auto px-4 py-6 md:py-10"> <div class="flex-none hidden md:block">
<ul class="menu menu-horizontal px-1 gap-2 font-medium">
<li><a href="/#how">How it works</a></li>
<li><a href="/#pricing">Pricing</a></li>
<li><a href="/app">Dashboard</a></li>
</ul>
</div>
<div class="flex-none md:hidden">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" /></svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-50 p-2 shadow-lg bg-base-100 rounded-box w-52 border border-base-content/10">
<li><a href="/#how">How it works</a></li>
<li><a href="/#pricing">Pricing</a></li>
<li><a href="/app">Dashboard</a></li>
</ul>
</div>
</div>
</nav>
<main class="flex-grow w-full ${container} mx-auto px-4 py-8 md:py-12">
${content} ${content}
</main> </main>
<footer class="footer footer-center p-10 bg-base-200 text-base-content rounded-t-2xl mt-auto max-w-5xl mx-auto">
<aside>
<p class="font-bold">XArtAudio <br/>Turning articles into audio since 2024</p>
<p>Copyright © ${new Date().getFullYear()} - All right reserved</p>
</aside>
</footer>
<script> <script>
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js").catch(() => {}); navigator.serviceWorker.register("/sw.js").catch(() => {});
@@ -41,35 +73,79 @@ function shell({ title, content, compact = false }) {
} }
function nav({ authenticated, userId }) { function nav({ authenticated, userId }) {
const right = authenticated // Navigation is now part of the shell, but we might inject user-specific actions here or in the shell.
? `<div class="flex items-center gap-2"><span class="badge badge-outline">@${escapeHtml(userId)}</span><form method="POST" action="/auth/logout"><button class="btn btn-xs btn-ghost">Log out</button></form></div>` // For this design iteration, I'm moving the main nav into the shell to ensure it's always there and consistent.
: `<a class="btn btn-sm btn-primary" href="/login">Get started</a>`; // The 'content' passed to shell will contain the page specific content.
// However, strict separation might be better.
// Let's modify the shell strategy:
// The Shell should handle the global layout (Navbar + Footer).
// The 'Nav' function here can return an empty string or user-specific sub-nav controls if needed.
// For 'clean saas', top-right user controls usually sit in the main navbar.
const menuItems = ` // To update the shell's user section dynamically without template complexity,
// I will inject a "userControls" string into the shell if possible, or just accept the limitation that
// the shell logic above is hardcoded.
// BETTER APPROACH for this functional style:
// Redefine shell to accept 'user' prop, or keep 'nav' as a component that returns nothing
// but acts as a signal, OR (best): update shell to render the right side based on args.
// Since I can't easily change the function signature of shell everywhere in one go without breaking
// things if I miss one, I will stick to the current pattern but maybe hide the "nav" logic inside shell
// if I pass the user info to shell.
// Looking at the current usage: `renderLandingPage` calls `shell`.
// It passes `content`. The `nav` function is called inside `content`.
// This is a bit "inside-out".
// I will effectively clear the `nav` function to strictly return the "User Actions" (Log out / Dashboard button)
// and place them in the shell if I could, but since `shell` wraps `content`, `shell` renders BEFORE `content` values are interpolated?
// No, `shell` function is called with the interpolated string.
// I will keep `nav` yielding the "Page Header" if specific to the page, but for "Clean SaaS",
// the navbar is usually global.
// Let's revert the Shell implementation above slightly to allow passing "header actions" or "user info".
// Actually, I can just make `nav` return the *Header* component which includes the Navbar,
// and remove the Navbar from the `shell` body. This keeps the composition pattern used currently.
const right = authenticated
? `<div class="flex items-center gap-3">
<span class="text-sm font-medium opacity-70 hidden sm:inline-block">@${escapeHtml(userId)}</span>
<form method="POST" action="/auth/logout">
<button class="btn btn-sm btn-ghost">Log out</button>
</form>
</div>`
: `<div class="flex items-center gap-2">
<a class="btn btn-sm btn-ghost" href="/login">Sign in</a>
<a class="btn btn-sm btn-primary" href="/login">Get Started</a>
</div>`;
return `<nav class="navbar sticky top-0 z-50 bg-base-100/80 backdrop-blur-md border-b border-base-content/5 mb-8 md:mb-12 -mx-4 md:-mx-8 px-4 md:px-8 w-[calc(100%+2rem)] md:w-[calc(100%+4rem)] rounded-b-lg">
<div class="navbar-start">
<div class="dropdown lg:hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /></svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52 border border-base-content/10">
<li><a href="/#how">How it works</a></li> <li><a href="/#how">How it works</a></li>
<li><a href="/#pricing">Pricing</a></li> <li><a href="/#pricing">Pricing</a></li>
<li><a href="/app">Dashboard</a></li> <li><a href="/app">Dashboard</a></li>
`;
return `<div class="navbar bg-base-100 shadow-sm rounded-box mb-6 md:mb-8">
<div class="navbar-start">
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /> </svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
${menuItems}
</ul> </ul>
</div> </div>
<a href="/" class="btn btn-ghost text-xl font-bold tracking-tight">XArtAudio</a> <a href="/" class="btn btn-ghost text-xl font-bold tracking-tight">XArtAudio</a>
</div> </div>
<div class="navbar-center hidden lg:flex"> <div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1"> <ul class="menu menu-horizontal px-1 font-medium text-sm">
${menuItems} <li><a href="/#how">How it works</a></li>
<li><a href="/#pricing">Pricing</a></li>
<li><a href="/app">Dashboard</a></li>
</ul> </ul>
</div> </div>
<div class="navbar-end">${right}</div> <div class="navbar-end">
</div>`; ${right}
</div>
</nav>`;
} }
function renderLandingPage({ authenticated, userId }) { function renderLandingPage({ authenticated, userId }) {