457 lines
26 KiB
JavaScript
457 lines
26 KiB
JavaScript
"use strict";
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function shell({ title, content, compact = false, user = null }) {
|
|
const container = compact ? "max-w-md" : "max-w-5xl";
|
|
const authenticated = user?.authenticated;
|
|
const userId = user?.userId;
|
|
|
|
const authButtons = 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 shadow-sm" href="/login">Get Started</a>
|
|
</div>`;
|
|
|
|
const menuItems = `
|
|
<li><a href="/#how">How it works</a></li>
|
|
<li><a href="/#pricing">Pricing</a></li>
|
|
<li><a href="/app">Dashboard</a></li>
|
|
`;
|
|
|
|
return `<!doctype html>
|
|
<html lang="en" class="scroll-smooth">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>${escapeHtml(title)}</title>
|
|
<meta name="theme-color" content="#000000" />
|
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
<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>
|
|
<body class="min-h-screen bg-base-100 text-base-content antialiased flex flex-col">
|
|
<div class="fixed inset-0 -z-10 bg-base-100 selection:bg-primary selection:text-primary-content"></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="navbar-start">
|
|
<a href="/" class="btn btn-ghost text-xl font-bold tracking-tight px-0 hover:bg-transparent">XArtAudio</a>
|
|
</div>
|
|
<div class="navbar-center hidden md:flex">
|
|
<ul class="menu menu-horizontal px-1 gap-2 font-medium text-sm">
|
|
${menuItems}
|
|
</ul>
|
|
</div>
|
|
<div class="navbar-end gap-2">
|
|
<div class="hidden md:flex">
|
|
${authButtons}
|
|
</div>
|
|
<div class="dropdown dropdown-end md: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 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">
|
|
${menuItems}
|
|
<li class="mt-2 border-t border-base-content/10 pt-2"></li>
|
|
${authenticated
|
|
? `<li><span class="text-xs opacity-50">@${escapeHtml(userId)}</span></li>
|
|
<li><form method="POST" action="/auth/logout" class="w-full"><button class="w-full text-left">Log out</button></form></li>`
|
|
: `<li><a href="/login">Sign in</a></li>
|
|
<li><a href="/login" class="font-bold text-primary">Get Started</a></li>`
|
|
}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="flex-grow w-full ${container} mx-auto px-4 py-8 md:py-12">
|
|
${content}
|
|
</main>
|
|
|
|
<footer class="footer footer-center p-10 bg-base-200 text-base-content rounded-t-2xl mt-auto max-w-5xl mx-auto opacity-80">
|
|
<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>
|
|
if ("serviceWorker" in navigator) {
|
|
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function renderLandingPage({ authenticated, userId }) {
|
|
const primaryCta = authenticated
|
|
? `<a href="/app" class="btn btn-primary btn-lg font-bold shadow-lg hover:shadow-xl transition-all">Open Dashboard</a>`
|
|
: `<div class="flex flex-col sm:flex-row gap-4 items-center justify-center">
|
|
<a href="/login" class="btn btn-primary btn-lg font-bold shadow-lg hover:shadow-xl transition-all">Start for Free</a>
|
|
<a href="#how" class="btn btn-ghost btn-lg group">Learn more <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg></a>
|
|
</div>`;
|
|
|
|
return shell({
|
|
title: "XArtAudio | Turn X Articles into Audiobooks",
|
|
user: { authenticated, userId },
|
|
content: `
|
|
<section class="py-16 md:py-24 text-center max-w-3xl mx-auto">
|
|
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-base-200/50 border border-base-content/5 text-sm font-medium mb-8 text-base-content/70">
|
|
<span class="badge badge-primary badge-xs">New</span> Webhook-first automation
|
|
</div>
|
|
<h1 class="text-5xl md:text-7xl font-extrabold tracking-tight mb-6 bg-gradient-to-br from-base-content to-base-content/50 bg-clip-text text-transparent">
|
|
Listen to X Articles.
|
|
</h1>
|
|
<p class="text-xl md:text-2xl text-base-content/70 mb-10 leading-relaxed max-w-2xl mx-auto">
|
|
Mention our bot under any long-form X post. We'll convert it to audio and send you a link instantly.
|
|
</p>
|
|
${primaryCta}
|
|
<p class="mt-6 text-sm text-base-content/40 font-mono">No credit card required • ${escapeHtml(userId ? `Logged in as @${userId}` : "Public access valid")}</p>
|
|
</section>
|
|
|
|
<section id="how" class="py-16 border-t border-base-content/5">
|
|
<div class="text-center mb-16">
|
|
<h2 class="text-3xl font-bold mb-4">How it works</h2>
|
|
<p class="text-base-content/60 max-w-2xl mx-auto">Three simple steps to turn reading into listening.</p>
|
|
</div>
|
|
|
|
<div class="grid md:grid-cols-3 gap-8">
|
|
<div class="card bg-base-100 border border-base-content/10 hover:border-base-content/20 transition-colors">
|
|
<div class="card-body items-center text-center">
|
|
<div class="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" /></svg>
|
|
</div>
|
|
<h3 class="font-bold text-lg mb-2">1. Mention the bot</h3>
|
|
<p class="text-base-content/70">Reply to any long article on X with <code class="bg-base-200 px-1 py-0.5 rounded">@XArtAudio</code>.</p>
|
|
</div>
|
|
</div>
|
|
<div class="card bg-base-100 border border-base-content/10 hover:border-base-content/20 transition-colors">
|
|
<div class="card-body items-center text-center">
|
|
<div class="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" /></svg>
|
|
</div>
|
|
<h3 class="font-bold text-lg mb-2">2. Processing</h3>
|
|
<p class="text-base-content/70">Our webhook verifies the article and generates high-quality audio.</p>
|
|
</div>
|
|
</div>
|
|
<div class="card bg-base-100 border border-base-content/10 hover:border-base-content/20 transition-colors">
|
|
<div class="card-body items-center text-center">
|
|
<div class="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary mb-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
|
</div>
|
|
<h3 class="font-bold text-lg mb-2">3. Listen</h3>
|
|
<p class="text-base-content/70">Receive a link to your audiobook. Listen anywhere, anytime.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="pricing" class="py-16 border-t border-base-content/5">
|
|
<div class="text-center mb-16">
|
|
<h2 class="text-3xl font-bold mb-4">Simple Pricing</h2>
|
|
<p class="text-base-content/60">Pay only for what you convert. Credits never expire.</p>
|
|
</div>
|
|
|
|
<div class="card bg-base-200 border border-base-content/5 max-w-4xl mx-auto overflow-hidden">
|
|
<div class="grid md:grid-cols-2">
|
|
<div class="p-8 md:p-12">
|
|
<h3 class="text-2xl font-bold mb-4">Pay-as-you-go</h3>
|
|
<div class="text-5xl font-extrabold mb-6">$1 <span class="text-xl font-normal text-base-content/60">/ 25k chars</span></div>
|
|
<ul class="space-y-3 mb-8 text-left">
|
|
<li class="flex items-center gap-2"><svg class="w-5 h-5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> High-quality Neural Voices</li>
|
|
<li class="flex items-center gap-2"><svg class="w-5 h-5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> Permanent Storage</li>
|
|
<li class="flex items-center gap-2"><svg class="w-5 h-5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> Shareable Links</li>
|
|
</ul>
|
|
<a href="/login" class="btn btn-primary w-full shadow-md hover:shadow-lg">Get Started</a>
|
|
</div>
|
|
<div class="bg-base-300/50 p-8 md:p-12 flex flex-col justify-center">
|
|
<h4 class="font-bold mb-4">Credit Logic</h4>
|
|
<div class="space-y-4 text-sm">
|
|
<div class="flex justify-between border-b border-base-content/10 pb-2">
|
|
<span>Base (up to 25k chars)</span>
|
|
<span class="font-mono font-bold">1 credit</span>
|
|
</div>
|
|
<div class="flex justify-between border-b border-base-content/10 pb-2">
|
|
<span>Additional 10k chars</span>
|
|
<span class="font-mono font-bold">+1 credit</span>
|
|
</div>
|
|
<div class="flex justify-between text-base-content/60">
|
|
<span>Maximum article size</span>
|
|
<span class="font-mono">120k chars</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
`,
|
|
});
|
|
}
|
|
|
|
function renderLoginPage({ returnTo = "/app", error = null }) {
|
|
const errorBlock = error ? `<div role="alert" class="alert alert-error mb-6 text-sm py-2 px-4 rounded-lg flex items-center gap-2"><svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg><span>${escapeHtml(error)}</span></div>` : "";
|
|
|
|
return shell({
|
|
title: "Sign in | XArtAudio",
|
|
user: null,
|
|
content: `
|
|
<div class="flex flex-col items-center justify-center min-h-[60vh]">
|
|
<div class="w-full max-w-sm">
|
|
<div class="text-center mb-8">
|
|
<h1 class="text-3xl font-bold mb-2">Welcome back</h1>
|
|
<p class="text-base-content/60">Sign in to your account to continue</p>
|
|
</div>
|
|
|
|
<div class="card bg-base-100 border border-base-content/10 shadow-sm">
|
|
<div class="card-body p-6">
|
|
${errorBlock}
|
|
<form method="POST" action="/auth/x" class="mb-6">
|
|
<input type="hidden" name="returnTo" value="${escapeHtml(returnTo)}" />
|
|
<button class="btn btn-outline w-full">Continue with X</button>
|
|
</form>
|
|
|
|
<div class="divider text-xs text-base-content/40 my-6">Email</div>
|
|
|
|
<form method="POST" action="/auth/email/sign-in" class="space-y-3">
|
|
<input type="hidden" name="returnTo" value="${escapeHtml(returnTo)}" />
|
|
<label class="form-control">
|
|
<span class="label-text font-medium mb-1">Email</span>
|
|
<input name="email" type="email" required class="input input-bordered w-full" placeholder="you@domain.com" />
|
|
</label>
|
|
<label class="form-control">
|
|
<span class="label-text font-medium mb-1">Password</span>
|
|
<input name="password" type="password" required minlength="8" maxlength="128" class="input input-bordered w-full" placeholder="••••••••" />
|
|
</label>
|
|
<button class="btn btn-primary w-full shadow-sm hover:shadow">Sign in</button>
|
|
</form>
|
|
|
|
<div class="divider text-xs text-base-content/40 my-6">Create account</div>
|
|
|
|
<form method="POST" action="/auth/email/sign-up" class="space-y-3">
|
|
<input type="hidden" name="returnTo" value="${escapeHtml(returnTo)}" />
|
|
<label class="form-control">
|
|
<span class="label-text font-medium mb-1">Name</span>
|
|
<input name="name" required minlength="2" maxlength="80" class="input input-bordered w-full" placeholder="Matiss" />
|
|
</label>
|
|
<label class="form-control">
|
|
<span class="label-text font-medium mb-1">Email</span>
|
|
<input name="email" type="email" required class="input input-bordered w-full" placeholder="you@domain.com" />
|
|
</label>
|
|
<label class="form-control">
|
|
<span class="label-text font-medium mb-1">Password</span>
|
|
<input name="password" type="password" required minlength="8" maxlength="128" class="input input-bordered w-full" placeholder="••••••••" />
|
|
</label>
|
|
<button class="btn btn-ghost border border-base-content/20 w-full">Create account</button>
|
|
</form>
|
|
|
|
<div class="text-center text-xs text-base-content/50 mt-2">
|
|
X sign-in requires X_OAUTH_CLIENT_ID and X_OAUTH_CLIENT_SECRET.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
});
|
|
}
|
|
|
|
function renderAppPage({ userId, summary, jobs, flash = null, showDeveloperActions = true }) {
|
|
const flashMarkup = flash ? `<div role="alert" class="alert alert-info mb-6 flex items-center gap-2 text-sm shadow-sm"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg><span>${escapeHtml(flash)}</span></div>` : "";
|
|
|
|
const jobsMarkup = jobs.length === 0
|
|
? `<div class="text-center py-16 bg-base-100 border border-base-content/10 rounded-xl">
|
|
<div class="w-16 h-16 bg-base-200 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-base-content/30" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
|
|
</div>
|
|
<h3 class="font-bold text-lg mb-1">No audiobooks yet</h3>
|
|
<p class="text-sm text-base-content/60">Use the simulation form below to generate your first audiobook.</p>
|
|
</div>`
|
|
: `<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
${jobs.map((job) => `
|
|
<a class="card bg-base-100 shadow-sm hover:shadow-md hover:border-primary/20 transition-all border border-base-content/10 group h-full" href="/audio/${job.assetId}">
|
|
<div class="card-body p-5">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<div class="badge badge-sm badge-ghost font-mono text-xs opacity-70">${escapeHtml(job.status)}</div>
|
|
<span class="text-xs font-mono opacity-50 text-right">${job.creditsCharged} cr</span>
|
|
</div>
|
|
<h3 class="font-bold text-lg leading-tight group-hover:text-primary transition-colors line-clamp-2" title="${escapeHtml(job.article.title)}">${escapeHtml(job.article.title)}</h3>
|
|
</div>
|
|
</a>`).join("")}
|
|
</div>`;
|
|
|
|
const developerActionsMarkup = showDeveloperActions
|
|
? `<div class="space-y-6">
|
|
<div class="card bg-base-100 shadow-sm border border-base-content/10 sticky top-24">
|
|
<div class="card-body p-5">
|
|
<h2 class="card-title text-sm font-bold uppercase tracking-wider opacity-60 mb-2">Developer Actions</h2>
|
|
|
|
<form method="POST" action="/app/actions/topup" class="mb-4">
|
|
<label class="label text-xs font-medium pl-0">Top Up Credits</label>
|
|
<div class="join w-full">
|
|
<input name="amount" type="number" min="1" max="500" value="10" class="input input-bordered input-sm join-item w-full" />
|
|
<button class="btn btn-primary btn-sm join-item">Add</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="divider my-2"></div>
|
|
|
|
<form method="POST" action="/app/actions/simulate-mention" class="flex flex-col gap-3">
|
|
<label class="label text-xs font-medium pl-0">Simulate Mention</label>
|
|
<input name="title" required class="input input-bordered input-sm w-full" placeholder="Article Title" />
|
|
<textarea name="body" required rows="3" class="textarea textarea-bordered textarea-sm w-full" placeholder="Paste article text..."></textarea>
|
|
<button class="btn btn-accent btn-sm w-full">Generate</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>`
|
|
: "";
|
|
|
|
return shell({
|
|
title: "Dashboard | XArtAudio",
|
|
user: { authenticated: true, userId },
|
|
content: `
|
|
<div class="flex flex-col md:flex-row justify-between items-end mb-8 gap-4 border-b border-base-content/5 pb-6">
|
|
<div>
|
|
<h1 class="text-3xl font-bold mb-1">Dashboard</h1>
|
|
<p class="text-base-content/60">Manage your credits and audiobooks</p>
|
|
</div>
|
|
</div>
|
|
|
|
${flashMarkup}
|
|
|
|
<div class="stats shadow-sm border border-base-content/10 w-full bg-base-100 mb-8 rounded-xl overflow-hidden divide-x divide-base-content/5">
|
|
<div class="stat place-items-center py-6">
|
|
<div class="stat-title text-sm uppercase tracking-wider opacity-60">Available Credits</div>
|
|
<div class="stat-value text-primary mt-1 text-4xl">${summary.balance}</div>
|
|
<div class="stat-desc mt-2">For generating audio</div>
|
|
</div>
|
|
<div class="stat place-items-center py-6">
|
|
<div class="stat-title text-sm uppercase tracking-wider opacity-60">Audiobooks</div>
|
|
<div class="stat-value text-base-content mt-1 text-4xl">${summary.totalJobs}</div>
|
|
<div class="stat-desc mt-2">Generated so far</div>
|
|
</div>
|
|
<div class="stat place-items-center py-6">
|
|
<div class="stat-title text-sm uppercase tracking-wider opacity-60">Total Spent</div>
|
|
<div class="stat-value text-secondary mt-1 text-4xl">${summary.totalCreditsSpent}</div>
|
|
<div class="stat-desc mt-2">Lifetime credits</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid gap-8 ${showDeveloperActions ? "md:grid-cols-3" : ""} mb-12">
|
|
<div class="${showDeveloperActions ? "md:col-span-2" : ""} space-y-8">
|
|
<section>
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-bold flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-base-content/50" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
|
|
Recent Audiobooks
|
|
</h2>
|
|
</div>
|
|
${jobsMarkup}
|
|
</section>
|
|
</div>
|
|
${developerActionsMarkup}
|
|
</div>
|
|
`,
|
|
});
|
|
}
|
|
|
|
function renderAudioPage({ audio, accessDecision, userId, playbackUrl = null }) {
|
|
if (!audio) {
|
|
return shell({
|
|
title: "Audio not found",
|
|
compact: true,
|
|
user: { authenticated: Boolean(userId), userId },
|
|
content: `<div class="max-w-md mx-auto mt-12 text-center"><div role="alert" class="alert alert-error mb-4"><span>Audio not found.</span></div><a href="/app" class="btn btn-ghost">Back to Dashboard</a></div>`,
|
|
});
|
|
}
|
|
|
|
const action = accessDecision.allowed
|
|
? `<div role="alert" class="alert alert-success shadow-sm">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
|
<span><span class="font-bold">Unlocked!</span> Permanent access granted.</span>
|
|
</div>`
|
|
: accessDecision.reason === "auth_required"
|
|
? `<div class="card bg-base-200 border border-base-content/10">
|
|
<div class="card-body items-center text-center p-6">
|
|
<h3 class="font-bold text-lg mb-2">Sign in to listen</h3>
|
|
<p class="text-sm mb-4 text-base-content/70">You need an account to unlock this audiobook.</p>
|
|
<a href="/login?returnTo=/audio/${audio.id}" class="btn btn-primary w-full max-w-xs">Sign in to unlock</a>
|
|
</div>
|
|
</div>`
|
|
: `<div class="card bg-base-200 border border-base-content/10">
|
|
<div class="card-body items-center text-center p-6">
|
|
<h3 class="font-bold text-lg mb-2">Unlock Audiobook</h3>
|
|
<p class="text-sm mb-4 text-base-content/70">Unlock this audiobook permanently for <span class="font-bold text-base-content">${accessDecision.creditsRequired} credits</span>.</p>
|
|
<form method="POST" action="/audio/${audio.id}/unlock" class="w-full max-w-xs">
|
|
<button class="btn btn-primary w-full">Pay & Listen</button>
|
|
</form>
|
|
</div>
|
|
</div>`;
|
|
|
|
return shell({
|
|
title: `${escapeHtml(audio.articleTitle)} | XArtAudio`,
|
|
compact: true,
|
|
user: { authenticated: Boolean(userId), userId },
|
|
content: `
|
|
<div class="mb-6">
|
|
<a href="/app" class="btn btn-ghost btn-sm gap-2 pl-0 hover:bg-transparent text-base-content/60 hover:text-base-content">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
|
|
Back to Dashboard
|
|
</a>
|
|
</div>
|
|
|
|
<div class="card w-full shadow-xl bg-base-100 border border-base-content/10 overflow-hidden">
|
|
<div class="card-body p-6 md:p-8">
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<span class="badge badge-accent">Audiobook</span>
|
|
<span class="text-xs font-mono text-base-content/50 uppercase tracking-widest">${escapeHtml(audio.id.substring(0, 8))}</span>
|
|
</div>
|
|
|
|
<h1 class="text-2xl md:text-3xl font-extrabold mb-4 leading-tight">${escapeHtml(audio.articleTitle)}</h1>
|
|
|
|
<div class="flex items-center gap-4 text-sm text-base-content/60 mb-8 border-b border-base-content/10 pb-6">
|
|
<span class="flex items-center gap-1">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
|
${audio.durationSec}s duration
|
|
</span>
|
|
</div>
|
|
|
|
${action}
|
|
|
|
${accessDecision.allowed
|
|
? `<div class="mt-8 pt-6 border-t border-base-content/10">
|
|
<h3 class="font-bold mb-3 text-xs uppercase tracking-widest opacity-60">Direct Stream URL</h3>
|
|
<div class="mockup-code bg-base-300 text-base-content p-4 text-xs overflow-x-auto">
|
|
<pre><code>${escapeHtml(playbackUrl || `stream://${audio.storageKey}`)}</code></pre>
|
|
</div>
|
|
</div>`
|
|
: ""}
|
|
</div>
|
|
</div>
|
|
`,
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
shell,
|
|
renderLandingPage,
|
|
renderLoginPage,
|
|
renderAppPage,
|
|
renderAudioPage,
|
|
};
|