Arag / static /import-book.html
AuthorBot
Fix book URL import to extract full ISBN metadata and survive repeat fetches.
c687a0b
Raw
History Blame Contribute Delete
35.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Import Book from URL β€” AuthorBot</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0b12;
--surface: #111420;
--surface-2: #181b28;
--border: rgba(255,255,255,0.07);
--border-active: rgba(99,102,241,0.6);
--text: #e2e8f0;
--text-muted: #64748b;
--text-secondary: #94a3b8;
--accent: #6366f1;
--accent-dark: #4f46e5;
--accent-glow: rgba(99,102,241,0.2);
--success: #10b981;
--success-bg: rgba(16,185,129,0.1);
--error: #f87171;
--error-bg: rgba(248,113,113,0.1);
--warning: #fbbf24;
--warning-bg: rgba(251,191,36,0.1);
--radius: 14px;
--radius-sm: 8px;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
/* ── Modal Shell ──────────────────────────────── */
.modal {
width: 100%;
max-width: 640px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 40px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04) inset;
animation: slideUp 0.35s cubic-bezier(0.22,1,0.36,1);
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(24px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ── Header ──────────────────────────────────── */
.header {
padding: 28px 32px 24px;
background: linear-gradient(135deg, #111624 0%, #0f1220 100%);
border-bottom: 1px solid var(--border);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -60px; right: -60px;
width: 200px; height: 200px;
background: var(--accent-glow);
border-radius: 50%;
filter: blur(60px);
pointer-events: none;
}
.header-top {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 8px;
}
.header-icon {
width: 44px; height: 44px;
background: linear-gradient(135deg, var(--accent), #818cf8);
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 20px;
flex-shrink: 0;
box-shadow: 0 8px 24px rgba(99,102,241,0.35);
}
.header h1 {
font-size: 1.25rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.header p {
color: var(--text-secondary);
font-size: 0.875rem;
margin-top: 4px;
}
/* ── Platform pills ──────────────────────────── */
.platforms {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 16px;
}
.platform-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: 100px;
background: rgba(255,255,255,0.05);
border: 1px solid var(--border);
font-size: 11px;
color: var(--text-secondary);
white-space: nowrap;
}
.platform-pill .dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent);
opacity: 0.8;
}
/* ── Body ────────────────────────────────────── */
.body {
padding: 28px 32px;
}
/* ── URL Input section ───────────────────────── */
.url-section label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 8px;
letter-spacing: 0.02em;
}
.input-row {
display: flex;
gap: 10px;
}
.input-wrap {
flex: 1;
position: relative;
}
.input-wrap svg {
position: absolute;
left: 14px; top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
pointer-events: none;
}
input[type="url"], input[type="text"], textarea {
width: 100%;
padding: 12px 14px 12px 40px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--surface-2);
color: var(--text);
font-family: inherit;
font-size: 0.9rem;
transition: border-color 0.2s, box-shadow 0.2s;
outline: none;
}
input[type="text"].no-icon, textarea {
padding-left: 14px;
}
input[type="url"]:focus, input[type="text"]:focus, textarea:focus {
border-color: var(--border-active);
box-shadow: 0 0 0 3px var(--accent-glow);
}
textarea {
resize: vertical;
min-height: 88px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
padding: 12px 20px;
border-radius: var(--radius-sm);
border: none;
font-family: inherit;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent), #818cf8);
color: white;
box-shadow: 0 4px 16px rgba(99,102,241,0.35);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(99,102,241,0.5);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-ghost {
background: var(--surface-2);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.btn-ghost:hover { background: rgba(255,255,255,0.07); color: var(--text); }
.btn-success {
background: linear-gradient(135deg, #059669, #10b981);
color: white;
box-shadow: 0 4px 16px rgba(16,185,129,0.3);
}
.btn-success:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(16,185,129,0.45);
}
/* ── Loader ──────────────────────────────────── */
.loader {
display: none;
flex-direction: column;
align-items: center;
padding: 32px;
gap: 16px;
}
.spinner {
width: 40px; height: 40px;
border: 3px solid rgba(99,102,241,0.2);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loader p { color: var(--text-secondary); font-size: 0.875rem; }
/* ── Alert bar ───────────────────────────────── */
.alert {
display: none;
align-items: flex-start;
gap: 10px;
padding: 14px 16px;
border-radius: var(--radius-sm);
font-size: 0.875rem;
margin-bottom: 20px;
}
.alert.error { background: var(--error-bg); border: 1px solid rgba(248,113,113,0.25); color: var(--error); }
.alert.warning { background: var(--warning-bg); border: 1px solid rgba(251,191,36,0.25); color: var(--warning); }
.alert.success { background: var(--success-bg); border: 1px solid rgba(16,185,129,0.25); color: var(--success); }
/* ── Preview card ────────────────────────────── */
.preview {
display: none;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 24px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.preview-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
background: rgba(255,255,255,0.02);
}
.platform-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 100px;
background: var(--accent-glow);
border: 1px solid rgba(99,102,241,0.3);
color: #a5b4fc;
font-size: 12px;
font-weight: 600;
}
.confidence-bar-wrap {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
}
.confidence-bar {
width: 80px;
height: 6px;
border-radius: 3px;
background: rgba(255,255,255,0.1);
overflow: hidden;
}
.confidence-fill {
height: 100%;
border-radius: 3px;
background: linear-gradient(90deg, var(--accent), #10b981);
transition: width 0.5s ease;
}
.confidence-label {
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
}
.preview-book {
display: flex;
gap: 20px;
padding: 20px;
}
.preview-cover {
width: 80px;
min-width: 80px;
height: 112px;
border-radius: 8px;
background: var(--surface);
border: 1px solid var(--border);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
flex-shrink: 0;
}
.preview-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-info { flex: 1; min-width: 0; }
.preview-info h2 { font-size: 1.05rem; font-weight: 700; margin-bottom: 4px; line-height: 1.3; }
.preview-info .book-author { color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 10px; }
.preview-info .book-desc {
color: var(--text-secondary);
font-size: 0.8rem;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.preview-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.meta-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 100px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
font-size: 11px;
color: var(--text-secondary);
}
.meta-chip.highlight {
background: rgba(99,102,241,0.08);
border-color: rgba(99,102,241,0.25);
color: #a5b4fc;
}
/* ── Author bio panel ────────────────────────── */
.author-bio-panel {
margin: 0 20px 20px;
padding: 14px 16px;
border-radius: var(--radius-sm);
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
animation: fadeIn 0.3s ease;
}
.author-bio-label {
font-size: 11px;
font-weight: 600;
color: var(--accent);
letter-spacing: 0.05em;
text-transform: uppercase;
margin-bottom: 8px;
}
.author-bio-text {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.65;
max-height: 120px;
overflow-y: auto;
}
.rating-stars {
display: inline-flex;
align-items: center;
gap: 2px;
font-size: 12px;
color: #fbbf24;
}
/* ── Edit form ───────────────────────────────── */
.edit-form {
display: none;
padding: 0 20px 20px;
border-top: 1px solid var(--border);
}
.edit-toggle {
display: flex;
align-items: center;
gap: 6px;
padding: 12px 20px;
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
transition: color 0.2s;
user-select: none;
}
.edit-toggle:hover { color: var(--text); }
.edit-toggle svg { transition: transform 0.2s; }
.edit-toggle.open svg { transform: rotate(180deg); }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 16px;
}
.form-field { display: flex; flex-direction: column; gap: 6px; }
.form-field.full { grid-column: 1 / -1; }
.form-field label {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
letter-spacing: 0.02em;
}
/* ── Action row ──────────────────────────────── */
.actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
/* ── Success state ───────────────────────────── */
.success-state {
display: none;
flex-direction: column;
align-items: center;
text-align: center;
padding: 40px 32px;
gap: 16px;
animation: fadeIn 0.4s ease;
}
.success-icon {
width: 72px; height: 72px;
background: var(--success-bg);
border: 2px solid rgba(16,185,129,0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
}
.success-state h2 { font-size: 1.4rem; font-weight: 700; }
.success-state p { color: var(--text-secondary); font-size: 0.875rem; max-width: 380px; line-height: 1.6; }
.success-book-title {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 12px 20px;
font-weight: 600;
font-size: 1rem;
max-width: 400px;
width: 100%;
}
.divider {
height: 1px;
background: var(--border);
margin: 20px 0;
}
.step-hint {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-radius: var(--radius-sm);
background: rgba(99,102,241,0.08);
border: 1px solid rgba(99,102,241,0.2);
text-align: left;
font-size: 0.8rem;
color: #a5b4fc;
max-width: 440px;
width: 100%;
}
@media (max-width: 540px) {
body { padding: 12px; align-items: flex-start; }
.header, .body { padding-left: 20px; padding-right: 20px; }
.input-row { flex-direction: column; }
.form-grid { grid-template-columns: 1fr; }
.preview-book { flex-direction: column; }
.preview-cover { width: 60px; min-width: 60px; height: 84px; }
.actions { flex-direction: column-reverse; }
.btn { width: 100%; }
}
</style>
</head>
<body>
<div class="modal" id="importModal">
<!-- Header -->
<div class="header">
<div class="header-top">
<div class="header-icon">πŸ“š</div>
<div>
<h1>Import Book from URL</h1>
<p>Paste any book link β€” we'll fetch the details automatically</p>
</div>
</div>
<div class="platforms" id="platformPills">
<!-- injected by JS -->
</div>
</div>
<!-- Body -->
<div class="body">
<!-- Alert -->
<div class="alert" id="alert" role="alert">
<span id="alertIcon"></span>
<span id="alertMsg"></span>
</div>
<!-- Step 1: URL input -->
<div id="stepUrl">
<div class="url-section">
<label for="bookUrl">Book URL</label>
<div class="input-row">
<div class="input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/>
</svg>
<input
type="url"
id="bookUrl"
placeholder="https://www.amazon.com/dp/B09... or goodreads.com/book/show/..."
autocomplete="off"
spellcheck="false"
>
</div>
<button class="btn btn-primary" id="fetchBtn" onclick="fetchPreview()">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
</svg>
Fetch
</button>
</div>
</div>
</div>
<!-- Loader -->
<div class="loader" id="loader">
<div class="spinner"></div>
<p id="loaderMsg">Fetching book details…</p>
</div>
<!-- Step 2: Preview + edit -->
<div id="stepPreview" style="display:none">
<!-- Preview card -->
<div class="preview" id="previewCard">
<div class="preview-header">
<span class="platform-badge" id="platformBadge">πŸ”— Platform</span>
<div class="confidence-bar-wrap">
<div class="confidence-bar">
<div class="confidence-fill" id="confidenceFill" style="width:0%"></div>
</div>
<span class="confidence-label" id="confidenceLabel">0% match</span>
</div>
</div>
<div class="preview-book">
<div class="preview-cover" id="previewCover">πŸ“–</div>
<div class="preview-info">
<h2 id="previewTitle">β€”</h2>
<p class="book-author" id="previewAuthor"></p>
<p class="book-desc" id="previewDesc"></p>
<div class="preview-meta" id="previewMeta"></div>
</div>
</div>
<!-- Author Bio Panel (shown when about_author is available) -->
<div class="author-bio-panel" id="authorBioPanel" style="display:none">
<div class="author-bio-label">✍️ About the Author</div>
<div class="author-bio-text" id="authorBioText"></div>
</div>
<!-- Edit toggle -->
<div class="edit-toggle" id="editToggle" onclick="toggleEdit()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Edit details
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-left:auto">
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
<div class="edit-form" id="editForm">
<div class="form-grid">
<div class="form-field full">
<label>Title</label>
<input type="text" class="no-icon" id="editTitle" placeholder="Book title">
</div>
<div class="form-field">
<label>Author</label>
<input type="text" class="no-icon" id="editAuthor" placeholder="Author name">
</div>
<div class="form-field">
<label>Genre</label>
<input type="text" class="no-icon" id="editGenre" placeholder="e.g. Fiction">
</div>
<div class="form-field">
<label>ISBN</label>
<input type="text" class="no-icon" id="editISBN" placeholder="ISBN-13 or ISBN-10">
</div>
<div class="form-field">
<label>Cover Image URL <span style="color:var(--text-muted)">(optional)</span></label>
<input type="text" class="no-icon" id="editCoverUrl" placeholder="https://...">
</div>
<div class="form-field full">
<label>Description / Synopsis</label>
<textarea class="no-icon" id="editDesc" placeholder="Short description or back-cover copy…"></textarea>
</div>
<div class="form-field full">
<label>About the Author</label>
<textarea class="no-icon" id="editAboutAuthor" placeholder="Author biography…" style="min-height:80px"></textarea>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="actions">
<button class="btn btn-ghost" onclick="resetForm()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
Back
</button>
<button class="btn btn-success" id="importBtn" onclick="confirmImport()">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="20 6 9 17 4 12"/>
</svg>
Add to My Books
</button>
</div>
</div>
<!-- Step 3: Success -->
<div class="success-state" id="stepSuccess">
<div class="success-icon">βœ…</div>
<h2>Book Added!</h2>
<p id="successMsg">Your book has been imported and is ready. Upload a PDF or EPUB to enable AI-powered chat.</p>
<div class="success-book-title" id="successTitle"></div>
<div class="step-hint">
πŸ’‘ <span>Now go to <strong>Books β†’ Upload PDF/EPUB</strong> to enable readers to ask questions about this book.</span>
</div>
<div style="display:flex;gap:10px;margin-top:8px">
<button class="btn btn-ghost" onclick="resetForm()">Import another</button>
<button class="btn btn-primary" onclick="goToBooks()">Go to Books</button>
</div>
</div>
</div>
</div>
<script>
// ── Config ──────────────────────────────────────────────────────────────
const API = window.location.origin + '/api';
const TOKEN = localStorage.getItem('access_token') || '';
const SLUG = localStorage.getItem('author_slug') || '';
const PLATFORMS = [
{ name: 'Amazon', icon: 'πŸ›’' },
{ name: 'Barnes & Noble', icon: 'πŸ“•' },
{ name: 'Goodreads', icon: '⭐' },
{ name: 'Google Books', icon: 'πŸ”' },
{ name: 'Apple Books', icon: '🍎' },
{ name: 'Kobo', icon: 'πŸ“–' },
{ name: 'ThriftBooks', icon: 'πŸ’Έ' },
{ name: 'AbeBooks', icon: 'πŸ›οΈ' },
{ name: 'WorldCat', icon: '🌍' },
{ name: 'Open Library', icon: 'πŸ›' },
];
// ── Render platform pills ──────────────────────────────────────────────
const pillsEl = document.getElementById('platformPills');
PLATFORMS.forEach(p => {
pillsEl.insertAdjacentHTML('beforeend',
`<span class="platform-pill"><span class="dot"></span>${p.icon} ${p.name}</span>`
);
});
// ── State ───────────────────────────────────────────────────────────────
let _currentMeta = null;
// ── Helpers ─────────────────────────────────────────────────────────────
function show(id) { document.getElementById(id).style.display = ''; }
function hide(id) { document.getElementById(id).style.display = 'none'; }
function flex(id) { document.getElementById(id).style.display = 'flex'; }
function block(id) { document.getElementById(id).style.display = 'block'; }
function showAlert(type, msg) {
const el = document.getElementById('alert');
const icons = { error: '❌', warning: '⚠️', success: 'βœ…' };
document.getElementById('alertIcon').textContent = icons[type] || '';
document.getElementById('alertMsg').textContent = msg;
el.className = `alert ${type}`;
el.style.display = 'flex';
}
function hideAlert() {
document.getElementById('alert').style.display = 'none';
}
function setLoading(msg) {
hide('stepUrl');
hide('stepPreview');
hide('stepSuccess');
hideAlert();
document.getElementById('loaderMsg').textContent = msg || 'Fetching book details…';
flex('loader');
}
function stopLoading() { hide('loader'); }
// ── Step 1 β†’ Step 2: Fetch preview ─────────────────────────────────────
async function fetchPreview() {
const url = document.getElementById('bookUrl').value.trim();
if (!url) { showAlert('error', 'Please paste a book URL first.'); return; }
if (!url.startsWith('http')) { showAlert('error', 'Please enter a full URL starting with https://'); return; }
setLoading('Detecting platform and fetching metadata…');
try {
const res = await fetch(`${API}/admin/${SLUG}/books/import-url/preview`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + TOKEN,
},
body: JSON.stringify({ url, force_refresh: true }),
});
const data = await res.json();
stopLoading();
if (!res.ok) {
show('stepUrl');
showAlert('error', data.detail || 'Failed to fetch book details.');
return;
}
_currentMeta = data;
renderPreview(data);
} catch (e) {
stopLoading();
show('stepUrl');
showAlert('error', 'Network error: ' + e.message);
}
}
// ── Render preview card ──────────────────────────────────────────────────
function renderPreview(data) {
// Platform badge
const platformIcons = {
amazon: 'πŸ›’', barnes_noble: 'πŸ“•', goodreads: '⭐',
google_books: 'πŸ”', apple_books: '🍎', kobo: 'πŸ“–',
thriftbooks: 'πŸ’Έ', abebooks: 'πŸ›οΈ', worldcat: '🌍', open_library: 'πŸ›',
};
const icon = platformIcons[data.platform] || 'πŸ”—';
document.getElementById('platformBadge').textContent = `${icon} ${data.platform_display || data.platform}`;
// Confidence
const pct = Math.round((data.confidence || 0) * 100);
document.getElementById('confidenceFill').style.width = pct + '%';
document.getElementById('confidenceLabel').textContent = pct + '% matched';
// Cover
const coverEl = document.getElementById('previewCover');
if (data.cover_url) {
coverEl.innerHTML = `<img src="${esc(data.cover_url)}" alt="cover" onerror="this.parentElement.textContent='πŸ“–'">`;
} else {
coverEl.textContent = 'πŸ“–';
}
// Text
document.getElementById('previewTitle').textContent = data.title || '(Title not found)';
document.getElementById('previewAuthor').textContent = data.author ? 'by ' + data.author : '';
document.getElementById('previewDesc').textContent = data.description || '';
// Meta chips β€” comprehensive set of all scraped fields
const chips = [];
if (data.rating) {
const stars = parseFloat(data.rating);
const filled = Math.round(stars);
const starStr = 'β˜…'.repeat(filled) + 'β˜†'.repeat(5 - filled);
const countStr = data.rating_count ? ` (${data.rating_count})` : '';
chips.push({ text: `<span class="rating-stars">${starStr}</span> ${data.rating}${countStr}`, highlight: true });
}
if (data.genre) chips.push({ text: `🏷️ ${data.genre}` });
if (data.page_count) chips.push({ text: `πŸ“„ ${data.page_count} pages` });
if (data.language && data.language.toLowerCase() !== 'en')
chips.push({ text: `🌐 ${data.language}` });
if (data.publisher) chips.push({ text: `🏒 ${data.publisher}` });
if (data.publish_year) chips.push({ text: `πŸ“… ${data.publish_year}` });
if (data.isbn) chips.push({ text: `πŸ”’ ISBN ${data.isbn}` });
document.getElementById('previewMeta').innerHTML =
chips.map(c => `<span class="meta-chip${c.highlight ? ' highlight':''}">${ c.text }</span>`).join('');
// Pre-fill edit fields
document.getElementById('editTitle').value = data.title || '';
document.getElementById('editAuthor').value = data.author || '';
document.getElementById('editGenre').value = data.genre || '';
document.getElementById('editISBN').value = data.isbn || '';
document.getElementById('editCoverUrl').value = data.cover_url || '';
document.getElementById('editDesc').value = data.description || '';
document.getElementById('editAboutAuthor').value = data.about_author || '';
// Alert handling
if (data.error && !data.title) {
showAlert('error', data.error + ' β€” please fill in the details manually.');
} else if (data.error || (data.warnings && data.warnings.length)) {
const msg = data.error || data.warnings.join('; ');
showAlert('warning', '⚠️ Partial data β€” some fields enriched via Google Books/Open Library: ' + msg);
} else {
hideAlert();
}
// Show author bio panel (outside preview-book div, inside .preview card)
const bioPanel = document.getElementById('authorBioPanel');
if (data.about_author) {
bioPanel.style.display = 'block';
document.getElementById('authorBioText').textContent = data.about_author;
} else {
bioPanel.style.display = 'none';
}
// Show preview
block('previewCard');
show('stepPreview');
show('stepUrl');
}
// ── Edit toggle ───────────────────────────────────────────────────────────
function toggleEdit() {
const toggle = document.getElementById('editToggle');
const form = document.getElementById('editForm');
const open = toggle.classList.toggle('open');
form.style.display = open ? 'block' : 'none';
}
// ── Step 2 β†’ Step 3: Confirm import ───────────────────────────────────────
async function confirmImport() {
if (!_currentMeta) return;
const title = (document.getElementById('editTitle').value.trim()
|| _currentMeta.title || '').trim();
if (!title) {
showAlert('error', 'Please enter a title before importing.');
return;
}
document.getElementById('importBtn').disabled = true;
setLoading('Saving book to your library…');
try {
const body = {
url: _currentMeta.source_url,
title: document.getElementById('editTitle').value.trim(),
author: document.getElementById('editAuthor').value.trim(),
description: document.getElementById('editDesc').value.trim(),
cover_url: document.getElementById('editCoverUrl').value.trim(),
isbn: document.getElementById('editISBN').value.trim(),
genre: document.getElementById('editGenre').value.trim(),
};
const res = await fetch(`${API}/admin/${SLUG}/books/import-url`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + TOKEN,
},
body: JSON.stringify(body),
});
const data = await res.json();
stopLoading();
document.getElementById('importBtn').disabled = false;
if (!res.ok) {
show('stepPreview');
showAlert('error', data.detail || 'Import failed.');
return;
}
// Success!
const coverUrl = document.getElementById('editCoverUrl').value.trim()
|| _currentMeta?.cover_url || '';
showSuccess(data, coverUrl);
} catch (e) {
stopLoading();
document.getElementById('importBtn').disabled = false;
show('stepPreview');
showAlert('error', 'Network error: ' + e.message);
}
}
// ── Step 3: Success ─────────────────────────────────────────────────────
function showSuccess(data, coverUrl) {
document.getElementById('successTitle').textContent = data.title || 'Book imported';
document.getElementById('successMsg').textContent =
`Imported from ${data.platform || 'external source'}. ` +
(data.next_steps || 'Upload a PDF/EPUB to enable AI chat.');
flex('stepSuccess');
}
function goToBooks() {
// Navigate to the books section of the admin SPA
const adminUrl = window.location.origin + '/admin';
if (window.opener) {
window.opener.postMessage({ type: 'navigate', section: 'books' }, '*');
window.close();
} else {
window.location.href = adminUrl + '#books';
}
}
// ── Reset ────────────────────────────────────────────────────────────────
function resetForm() {
_currentMeta = null;
document.getElementById('bookUrl').value = '';
document.getElementById('importBtn').disabled = false;
hide('stepPreview');
hide('stepSuccess');
hide('loader');
hideAlert();
show('stepUrl');
// Close edit form
document.getElementById('editForm').style.display = 'none';
document.getElementById('editToggle').classList.remove('open');
}
// ── Keyboard shortcut ─────────────────────────────────────────────────────
document.getElementById('bookUrl').addEventListener('keydown', e => {
if (e.key === 'Enter') fetchPreview();
});
// ── Security: escape for display ─────────────────────────────────────────
function esc(str) {
return String(str)
.replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Auth guard ────────────────────────────────────────────────────────────
if (!TOKEN || !SLUG) {
document.querySelector('.body').innerHTML =
`<div style="text-align:center;padding:40px;color:var(--text-secondary)">
<div style="font-size:40px;margin-bottom:12px">πŸ”’</div>
<p style="margin-bottom:16px">You need to be signed in to import books.</p>
<a href="/static/admin.html" style="color:var(--accent);text-decoration:none;font-weight:600">← Sign In</a>
</div>`;
}
</script>
</body>
</html>