Arag / static /admin.html
AuthorBot
Fix book URL import to extract full ISBN metadata and survive repeat fetches.
c687a0b
Raw
History Blame Contribute Delete
59.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AuthorBot β€” Admin Dashboard</title>
<meta name="description" content="Author admin panel β€” manage books, imports, and settings.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<script src="/static/auth-client.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0b0d14;
--surface: #111420;
--surface2: #181c2a;
--surface3: #1e2236;
--border: rgba(255,255,255,0.07);
--border-act:rgba(99,102,241,0.5);
--accent: #6366f1;
--accent-h: #818cf8;
--accent-glow:rgba(99,102,241,0.15);
--accent-soft:rgba(99,102,241,0.08);
--success: #10b981;
--success-bg:rgba(16,185,129,0.08);
--error: #f87171;
--error-bg: rgba(248,113,113,0.08);
--warning: #fbbf24;
--warning-bg:rgba(251,191,36,0.08);
--text: #f1f5f9;
--text2: #94a3b8;
--text3: #64748b;
--radius: 16px;
--radius-sm: 10px;
--radius-xs: 6px;
--sidebar-w: 248px;
--shadow: 0 4px 24px rgba(0,0,0,0.4);
--tr: 0.18s cubic-bezier(.4,0,.2,1);
}
body { font-family:'Inter',-apple-system,sans-serif; background:var(--bg); color:var(--text); min-height:100vh; -webkit-font-smoothing:antialiased; }
::selection { background:var(--accent-glow); color:var(--text); }
::-webkit-scrollbar { width:5px; } ::-webkit-scrollbar-track { background:transparent; } ::-webkit-scrollbar-thumb { background:var(--surface3); border-radius:4px; }
@keyframes fadeUp { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
@keyframes scaleIn { from { opacity:0; transform:scale(.96); } to { opacity:1; transform:scale(1); } }
@keyframes spin { to { transform:rotate(360deg); } }
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:.45} }
@keyframes slideRight { from{opacity:0;transform:translateX(-12px)} to{opacity:1;transform:translateX(0)} }
/* ── Auth Screen ─────────────────────────────────────── */
#auth-screen {
min-height:100vh; display:flex; align-items:center; justify-content:center;
background:radial-gradient(ellipse at 30% 30%, rgba(99,102,241,.15) 0%, transparent 60%),
radial-gradient(ellipse at 70% 80%, rgba(99,102,241,.08) 0%, transparent 50%), var(--bg);
}
.auth-card {
background:var(--surface); border:1px solid var(--border); border-radius:24px;
padding:48px 44px; width:100%; max-width:440px; animation:scaleIn .4s ease;
box-shadow:0 24px 80px rgba(0,0,0,.6);
}
.auth-logo { font-size:1.6rem; font-weight:800; letter-spacing:-.02em; margin-bottom:6px; }
.auth-logo span { background:linear-gradient(135deg,var(--accent),var(--accent-h)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.auth-sub { color:var(--text2); font-size:.875rem; margin-bottom:36px; line-height:1.6; }
.field { margin-bottom:20px; }
.field label { display:block; font-size:.75rem; font-weight:600; color:var(--text2); margin-bottom:8px; letter-spacing:.04em; text-transform:uppercase; }
.field input {
width:100%; padding:12px 16px; border-radius:var(--radius-sm); border:1.5px solid var(--border);
background:var(--surface2); color:var(--text); font-size:.9rem; font-family:inherit;
transition:border-color var(--tr),box-shadow var(--tr); outline:none;
}
.field input:focus { border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-glow); }
.field input::placeholder { color:var(--text3); }
.btn-auth {
width:100%; padding:13px; border:none; border-radius:var(--radius-sm); font-size:.9rem; font-weight:700;
background:linear-gradient(135deg,var(--accent),var(--accent-h)); color:#fff; cursor:pointer;
box-shadow:0 4px 20px rgba(99,102,241,.4); transition:all var(--tr); font-family:inherit;
}
.btn-auth:hover:not(:disabled) { transform:translateY(-1px); box-shadow:0 8px 28px rgba(99,102,241,.55); }
.btn-auth:disabled { opacity:.55; cursor:not-allowed; }
.auth-msg { margin-top:14px; font-size:.8rem; min-height:18px; text-align:center; }
/* ── App shell ───────────────────────────────────────── */
#app { display:none; min-height:100vh; }
/* ── Sidebar ─────────────────────────────────────────── */
.sidebar {
position:fixed; top:0; left:0; bottom:0; width:var(--sidebar-w);
background:var(--surface); border-right:1px solid var(--border);
display:flex; flex-direction:column; padding:24px 12px; z-index:100;
}
.sidebar-brand { padding:0 12px; margin-bottom:32px; }
.sidebar-brand .name { font-size:1.15rem; font-weight:800; letter-spacing:-.02em; }
.sidebar-brand .name span { background:linear-gradient(135deg,var(--accent),var(--accent-h)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.sidebar-brand .role { font-size:.7rem; font-weight:600; color:var(--text3); letter-spacing:.06em; text-transform:uppercase; margin-top:2px; }
.nav-section { font-size:.65rem; font-weight:700; color:var(--text3); letter-spacing:.08em; text-transform:uppercase; padding:0 12px; margin-bottom:6px; margin-top:20px; }
.nav-item {
display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:var(--radius-sm);
color:var(--text2); font-size:.85rem; font-weight:500; cursor:pointer; border:none;
background:none; width:100%; text-align:left; transition:all var(--tr); margin-bottom:2px;
}
.nav-item .ni { font-size:1rem; width:22px; text-align:center; }
.nav-item:hover { background:var(--accent-soft); color:var(--text); }
.nav-item.active { background:var(--accent-soft); color:var(--accent-h); font-weight:600; }
.nav-item.active .ni { filter:drop-shadow(0 0 6px var(--accent)); }
.sidebar-footer { margin-top:auto; padding-top:16px; border-top:1px solid var(--border); }
.user-chip {
display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:var(--radius-sm);
background:var(--surface2); margin-bottom:8px;
}
.user-avatar { width:32px; height:32px; border-radius:50%; background:linear-gradient(135deg,var(--accent),var(--accent-h)); display:flex; align-items:center; justify-content:center; font-size:.85rem; font-weight:700; color:#fff; flex-shrink:0; }
.user-name { font-size:.8rem; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.user-email { font-size:.7rem; color:var(--text3); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* ── Main content ────────────────────────────────────── */
.main { margin-left:var(--sidebar-w); padding:36px 40px; }
.page { display:none; animation:fadeUp .3s ease; }
.page.active { display:block; }
.page-header { margin-bottom:28px; }
.page-title { font-size:1.5rem; font-weight:800; letter-spacing:-.02em; margin-bottom:4px; }
.page-sub { color:var(--text2); font-size:.875rem; }
/* ── Toast ───────────────────────────────────────────── */
#toasts { position:fixed; top:20px; right:20px; z-index:9999; display:flex; flex-direction:column; gap:8px; }
.toast {
background:var(--surface2); border:1px solid var(--border); border-radius:var(--radius-sm);
padding:13px 18px; display:flex; align-items:center; gap:10px; font-size:.8rem; font-weight:500;
box-shadow:var(--shadow); animation:slideRight .25s ease; min-width:260px; max-width:380px;
}
.toast.success { border-left:3px solid var(--success); }
.toast.error { border-left:3px solid var(--error); }
.toast.warning { border-left:3px solid var(--warning); }
.toast-close { margin-left:auto; cursor:pointer; background:none; border:none; color:var(--text3); font-size:1.1rem; padding:0; }
/* ── Book import page ────────────────────────────────── */
.import-layout { display:grid; grid-template-columns:1fr 380px; gap:28px; align-items:start; }
@media(max-width:960px) { .import-layout { grid-template-columns:1fr; } }
/* Tab switcher */
.tab-row { display:flex; gap:6px; margin-bottom:28px; background:var(--surface2); border-radius:var(--radius-sm); padding:5px; width:fit-content; }
.tab-btn {
padding:9px 20px; border:none; border-radius:8px; font-size:.85rem; font-weight:600;
cursor:pointer; background:none; color:var(--text2); transition:all var(--tr); font-family:inherit;
}
.tab-btn.active { background:var(--accent); color:#fff; box-shadow:0 2px 12px rgba(99,102,241,.4); }
.tab-btn .tab-badge { font-size:.65rem; background:var(--success); color:#fff; padding:1px 6px; border-radius:20px; margin-left:6px; }
/* Primary: URL import */
.import-card {
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
overflow:hidden; box-shadow:var(--shadow);
}
.import-card-header {
padding:20px 24px 0;
display:flex; align-items:center; gap:12px;
}
.import-icon { font-size:1.5rem; }
.import-card-header h2 { font-size:1rem; font-weight:700; }
.import-card-header p { font-size:.78rem; color:var(--text2); }
/* Platform pills */
.platforms-strip { display:flex; flex-wrap:wrap; gap:7px; padding:16px 24px; }
.platform-pill {
display:inline-flex; align-items:center; gap:5px; padding:4px 11px;
border-radius:20px; font-size:.72rem; font-weight:600;
background:var(--surface2); border:1px solid var(--border); color:var(--text2);
cursor:default; transition:all var(--tr);
}
.platform-pill.detected { background:var(--accent-soft); border-color:var(--accent); color:var(--accent-h); }
.platform-pill .dot { width:5px; height:5px; border-radius:50%; background:var(--text3); }
.platform-pill.detected .dot { background:var(--accent); box-shadow:0 0 5px var(--accent); }
/* URL input row */
.url-input-wrap { padding:0 24px 20px; }
.url-row { display:flex; gap:10px; }
.url-field {
flex:1; display:flex; align-items:center; gap:10px; padding:12px 16px;
background:var(--surface2); border:1.5px solid var(--border); border-radius:var(--radius-sm);
transition:border-color var(--tr), box-shadow var(--tr);
}
.url-field:focus-within { border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-glow); }
.url-field svg { color:var(--text3); flex-shrink:0; }
.url-field input { flex:1; background:none; border:none; outline:none; color:var(--text); font-size:.875rem; font-family:inherit; }
.url-field input::placeholder { color:var(--text3); }
/* Buttons */
.btn { display:inline-flex; align-items:center; justify-content:center; gap:7px; padding:12px 20px; border:none; border-radius:var(--radius-sm); font-size:.85rem; font-weight:600; cursor:pointer; font-family:inherit; transition:all var(--tr); white-space:nowrap; }
.btn-primary { background:linear-gradient(135deg,var(--accent),var(--accent-h)); color:#fff; box-shadow:0 4px 16px rgba(99,102,241,.35); }
.btn-primary:hover:not(:disabled) { transform:translateY(-1px); box-shadow:0 8px 24px rgba(99,102,241,.5); }
.btn-primary:disabled { opacity:.5; cursor:not-allowed; transform:none; }
.btn-ghost { background:var(--surface2); color:var(--text2); border:1px solid var(--border); }
.btn-ghost:hover { background:var(--surface3); color:var(--text); }
.btn-success { background:linear-gradient(135deg,#059669,#10b981); color:#fff; box-shadow:0 4px 16px rgba(16,185,129,.3); }
.btn-success:hover:not(:disabled) { transform:translateY(-1px); box-shadow:0 8px 24px rgba(16,185,129,.45); }
.btn-danger { background:var(--error-bg); color:var(--error); border:1px solid rgba(248,113,113,.25); }
.btn-danger:hover { background:var(--error); color:#fff; }
.btn-sm { padding:7px 14px; font-size:.78rem; }
/* Loader */
.fetch-loader { display:none; align-items:center; gap:14px; padding:20px 24px; }
.spinner { width:22px; height:22px; border:2.5px solid rgba(99,102,241,.2); border-top-color:var(--accent); border-radius:50%; animation:spin .7s linear infinite; flex-shrink:0; }
.fetch-loader p { color:var(--text2); font-size:.875rem; }
.fetch-loader .step-dots span { animation:pulse 1.2s infinite; }
.fetch-loader .step-dots span:nth-child(2) { animation-delay:.2s; }
.fetch-loader .step-dots span:nth-child(3) { animation-delay:.4s; }
/* Alert strip */
.alert-strip { display:none; align-items:flex-start; gap:10px; padding:12px 16px; font-size:.8rem; border-radius:var(--radius-sm); margin:0 24px 16px; }
.alert-strip.error { background:var(--error-bg); border:1px solid rgba(248,113,113,.25); color:var(--error); }
.alert-strip.warning { background:var(--warning-bg); border:1px solid rgba(251,191,36,.25); color:var(--warning); }
/* Preview card */
.preview-section { display:none; padding:0 24px 20px; }
.preview-book-card {
background:var(--surface2); border:1px solid var(--border); border-radius:var(--radius-sm); overflow:hidden;
}
.preview-book-top { display:flex; align-items:center; gap:8px; padding:12px 16px; background:rgba(255,255,255,.02); border-bottom:1px solid var(--border); }
.platform-badge { display:inline-flex; align-items:center; gap:5px; padding:3px 10px; border-radius:20px; background:var(--accent-soft); border:1px solid rgba(99,102,241,.25); color:var(--accent-h); font-size:.7rem; font-weight:600; }
.conf-bar-wrap { margin-left:auto; display:flex; align-items:center; gap:8px; }
.conf-bar { width:70px; height:5px; border-radius:3px; background:rgba(255,255,255,.07); overflow:hidden; }
.conf-fill { height:100%; border-radius:3px; background:linear-gradient(90deg,var(--accent),var(--success)); transition:width .5s ease; }
.conf-label { font-size:.68rem; color:var(--text3); }
.preview-body { display:flex; gap:16px; padding:16px; }
.preview-cover {
width:72px; min-width:72px; height:100px; border-radius:8px; border:1px solid var(--border);
background:var(--surface); display:flex; align-items:center; justify-content:center; font-size:24px; overflow:hidden; flex-shrink:0;
}
.preview-cover img { width:100%; height:100%; object-fit:cover; }
.preview-info { flex:1; min-width:0; }
.preview-info h3 { font-size:1rem; font-weight:700; line-height:1.3; margin-bottom:3px; }
.preview-info .p-author { font-size:.8rem; color:var(--text2); margin-bottom:8px; }
.preview-info .p-desc { font-size:.75rem; color:var(--text2); line-height:1.6; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; margin-bottom:10px; }
.chips { display:flex; flex-wrap:wrap; gap:5px; }
.chip { display:inline-flex; align-items:center; gap:3px; padding:2px 9px; border-radius:20px; font-size:.68rem; background:rgba(255,255,255,.04); border:1px solid var(--border); color:var(--text2); }
.chip.accent { background:var(--accent-soft); border-color:rgba(99,102,241,.2); color:var(--accent-h); }
/* Author bio panel */
.bio-panel { margin:0 16px 14px; padding:12px 14px; border-radius:8px; background:rgba(99,102,241,.05); border:1px solid var(--border); border-left:3px solid var(--accent); }
.bio-panel .bio-label { font-size:.65rem; font-weight:700; color:var(--accent); text-transform:uppercase; letter-spacing:.05em; margin-bottom:6px; }
.bio-panel .bio-text { font-size:.75rem; color:var(--text2); line-height:1.65; max-height:100px; overflow-y:auto; }
.rating-stars { color:#fbbf24; font-size:.8rem; }
/* Edit form toggle */
.edit-toggle-row { display:flex; align-items:center; gap:6px; padding:10px 16px; cursor:pointer; font-size:.78rem; color:var(--text3); border-top:1px solid var(--border); transition:color var(--tr); }
.edit-toggle-row:hover { color:var(--text); }
.edit-toggle-row .chevron { margin-left:auto; transition:transform var(--tr); }
.edit-toggle-row.open .chevron { transform:rotate(180deg); }
.edit-form-wrap { display:none; padding:0 16px 16px; }
.form-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:12px; }
.form-field { display:flex; flex-direction:column; gap:5px; }
.form-field.full { grid-column:1/-1; }
.form-field label { font-size:.7rem; font-weight:600; color:var(--text3); letter-spacing:.03em; }
.form-field input, .form-field textarea, .form-field select {
padding:9px 12px; border-radius:var(--radius-xs); border:1px solid var(--border); background:var(--surface2); color:var(--text); font-size:.8rem; font-family:inherit; outline:none; transition:border-color var(--tr),box-shadow var(--tr);
}
.form-field input:focus, .form-field textarea:focus, .form-field select:focus { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent-glow); }
.form-field textarea { resize:vertical; min-height:72px; }
/* Action row */
.action-row { display:flex; gap:8px; justify-content:flex-end; padding:0 24px 20px; }
/* Success screen */
.success-screen { display:none; flex-direction:column; align-items:center; text-align:center; padding:40px 24px; gap:14px; animation:fadeUp .35s ease; }
.success-icon { width:68px; height:68px; border-radius:50%; background:var(--success-bg); border:2px solid rgba(16,185,129,.3); display:flex; align-items:center; justify-content:center; font-size:32px; }
.success-screen h2 { font-size:1.3rem; font-weight:700; }
.success-screen p { color:var(--text2); font-size:.85rem; max-width:340px; line-height:1.6; }
.success-book { background:var(--surface2); border:1px solid var(--border); border-radius:var(--radius-sm); padding:10px 20px; font-weight:600; font-size:.95rem; width:100%; max-width:360px; }
.hint-box { display:flex; align-items:center; gap:10px; padding:12px 16px; background:var(--accent-soft); border:1px solid rgba(99,102,241,.2); border-radius:var(--radius-sm); font-size:.78rem; color:var(--accent-h); max-width:380px; width:100%; }
/* ── Secondary: Manual upload ────────────────────────── */
.manual-section { display:none; padding:0 24px 24px; }
.manual-section.active { display:block; }
.manual-form { display:flex; flex-direction:column; gap:14px; }
/* ── Right sidebar: Books list ───────────────────────── */
.books-sidebar { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; }
.books-sidebar-header { padding:16px 20px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; }
.books-sidebar-header h3 { font-size:.9rem; font-weight:700; }
.books-list { max-height:520px; overflow-y:auto; }
.book-row { display:flex; align-items:center; gap:12px; padding:12px 16px; border-bottom:1px solid var(--border); transition:background var(--tr); }
.book-row:hover { background:var(--surface2); }
.book-row:last-child { border-bottom:none; }
.book-thumb { width:36px; height:50px; border-radius:4px; background:var(--surface2); border:1px solid var(--border); display:flex; align-items:center; justify-content:center; font-size:16px; overflow:hidden; flex-shrink:0; }
.book-thumb img { width:100%; height:100%; object-fit:cover; }
.book-meta { flex:1; min-width:0; }
.book-meta .book-title { font-size:.8rem; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.book-meta .book-author-sm { font-size:.7rem; color:var(--text2); margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.book-meta .book-status { display:inline-flex; align-items:center; gap:3px; margin-top:4px; font-size:.65rem; font-weight:600; padding:1px 7px; border-radius:10px; }
.st-ready { background:var(--success-bg); color:var(--success); }
.st-import { background:var(--accent-soft); color:var(--accent-h); }
.st-proc { background:var(--warning-bg); color:var(--warning); }
.st-err { background:var(--error-bg); color:var(--error); }
/* Empty / loading state */
.empty-list { padding:40px 20px; text-align:center; }
.empty-list .ei { font-size:32px; opacity:.4; margin-bottom:10px; }
.empty-list p { font-size:.8rem; color:var(--text3); }
/* Upload area */
.drop-zone {
border:2px dashed var(--border); border-radius:var(--radius-sm); padding:32px 20px;
text-align:center; cursor:pointer; transition:all var(--tr);
}
.drop-zone:hover, .drop-zone.dragover { border-color:var(--accent); background:var(--accent-soft); }
.drop-zone .dz-icon { font-size:2rem; margin-bottom:10px; opacity:.7; }
.drop-zone p { font-size:.8rem; color:var(--text2); line-height:1.6; }
.drop-zone input[type=file] { display:none; }
/* Progress bar */
.upload-progress { display:none; margin-top:12px; }
.progress-bar { height:4px; background:rgba(255,255,255,.08); border-radius:2px; overflow:hidden; }
.progress-fill { height:100%; background:linear-gradient(90deg,var(--accent),var(--accent-h)); border-radius:2px; transition:width .3s ease; }
.progress-label { font-size:.75rem; color:var(--text2); margin-top:6px; text-align:center; }
@media(max-width:768px) { .sidebar{display:none;} .main{margin-left:0;padding:20px;} }
</style>
</head>
<body>
<div id="toasts"></div>
<!-- ── Auth Screen ─────────────────────────────────────────────── -->
<div id="auth-screen">
<div class="auth-card">
<div class="auth-logo">✍️ <span>AuthorBot</span></div>
<p class="auth-sub">Sign in to manage your books, imports, and settings.</p>
<div class="field">
<label>Email</label>
<input type="email" id="a-email" placeholder="you@example.com" autocomplete="email">
</div>
<div class="field">
<label>Password</label>
<input type="password" id="a-pass" placeholder="β€’β€’β€’β€’β€’β€’β€’β€’" autocomplete="current-password">
</div>
<button class="btn-auth" id="a-login-btn">Sign In</button>
<p class="auth-msg" id="a-login-msg" style="color:var(--error)"></p>
</div>
</div>
<!-- ── App Shell ───────────────────────────────────────────────── -->
<div id="app">
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-brand">
<div class="name">✍️ <span>AuthorBot</span></div>
<div class="role">Admin Panel</div>
</div>
<div class="nav-section">Library</div>
<button class="nav-item active" data-page="import" id="nav-import">
<span class="ni">πŸ“₯</span> Add Book
</button>
<button class="nav-item" data-page="books" id="nav-books">
<span class="ni">πŸ“š</span> My Books
</button>
<div class="nav-section">Settings</div>
<button class="nav-item" data-page="settings" id="nav-settings">
<span class="ni">βš™οΈ</span> Settings
</button>
<div class="sidebar-footer">
<div class="user-chip">
<div class="user-avatar" id="user-avatar">A</div>
<div>
<div class="user-name" id="user-name">Author</div>
<div class="user-email" id="user-email">β€”</div>
</div>
</div>
<button class="nav-item" id="logout-btn" style="color:var(--error)">
<span class="ni">πŸšͺ</span> Sign Out
</button>
</div>
</nav>
<!-- Main content -->
<main class="main">
<!-- ── PAGE: Add Book ──────────────────────────────────────── -->
<div class="page active" id="page-import">
<div class="page-header">
<h1 class="page-title">Add a Book</h1>
<p class="page-sub">Import from any major bookstore, or upload a PDF/EPUB manually</p>
</div>
<!-- Tab switcher -->
<div class="tab-row">
<button class="tab-btn active" id="tab-url" onclick="switchTab('url')">
πŸ”— Import by URL <span class="tab-badge">Recommended</span>
</button>
<button class="tab-btn" id="tab-manual" onclick="switchTab('manual')">
πŸ“„ Manual Upload
</button>
</div>
<div class="import-layout">
<!-- Left: Import card -->
<div>
<!-- ─ URL IMPORT ─ -->
<div class="import-card" id="url-tab-content">
<div class="import-card-header">
<div class="import-icon">πŸ”—</div>
<div>
<h2>Import from Bookstore URL</h2>
<p>Paste a link β€” we auto-fetch title, author, cover, description, genre & more</p>
</div>
</div>
<!-- Platform pills -->
<div class="platforms-strip" id="platform-pills">
<!-- injected by JS -->
</div>
<!-- URL input -->
<div class="url-input-wrap">
<div class="alert-strip" id="fetch-alert"></div>
<div class="url-row">
<div class="url-field">
<svg width="15" height="15" 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="book-url" placeholder="https://www.amazon.com/dp/... or goodreads.com/book/..." autocomplete="off" spellcheck="false">
</div>
<button class="btn btn-primary" id="fetch-btn" onclick="fetchPreview()">
<svg width="14" height="14" 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>
<!-- Loader -->
<div class="fetch-loader" id="fetch-loader">
<div class="spinner"></div>
<div>
<p id="loader-msg">Detecting platform and fetching metadata…</p>
<div class="step-dots" style="margin-top:3px;color:var(--accent);font-size:1rem;">
<span>Β·</span><span>Β·</span><span>Β·</span>
</div>
</div>
</div>
<!-- Preview section -->
<div class="preview-section" id="preview-section">
<div class="preview-book-card">
<!-- Header row -->
<div class="preview-book-top">
<span class="platform-badge" id="p-platform">πŸ”— Platform</span>
<div class="conf-bar-wrap">
<div class="conf-bar"><div class="conf-fill" id="p-conf-fill" style="width:0%"></div></div>
<span class="conf-label" id="p-conf-label">0% match</span>
</div>
</div>
<!-- Book body -->
<div class="preview-body">
<div class="preview-cover" id="p-cover">πŸ“–</div>
<div class="preview-info">
<h3 id="p-title">β€”</h3>
<p class="p-author" id="p-author"></p>
<p class="p-desc" id="p-desc"></p>
<div class="chips" id="p-chips"></div>
</div>
</div>
<!-- Author bio -->
<div class="bio-panel" id="bio-panel" style="display:none">
<div class="bio-label">✍️ About the Author</div>
<div class="bio-text" id="bio-text"></div>
</div>
<!-- Edit toggle -->
<div class="edit-toggle-row" id="edit-toggle" onclick="toggleEdit()">
<svg width="12" height="12" 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-4z"/></svg>
Edit / correct details
<svg class="chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div class="edit-form-wrap" id="edit-form">
<div class="form-grid">
<div class="form-field full"><label>Title</label><input type="text" id="e-title" placeholder="Book title"></div>
<div class="form-field"><label>Author</label><input type="text" id="e-author" placeholder="Author name"></div>
<div class="form-field"><label>Genre</label><input type="text" id="e-genre" placeholder="e.g. Fiction"></div>
<div class="form-field"><label>ISBN</label><input type="text" id="e-isbn" placeholder="ISBN-13"></div>
<div class="form-field"><label>Cover URL</label><input type="text" id="e-cover" placeholder="https://..."></div>
<div class="form-field"><label>Publisher</label><input type="text" id="e-publisher" placeholder="Publisher name"></div>
<div class="form-field full"><label>Description / Synopsis</label><textarea id="e-desc"></textarea></div>
<div class="form-field full"><label>About the Author</label><textarea id="e-bio"></textarea></div>
</div>
</div>
</div>
</div>
<!-- Action row -->
<div class="action-row" id="action-row" style="display:none">
<button class="btn btn-ghost" onclick="resetImport()">← Back</button>
<button class="btn btn-success" id="import-btn" onclick="confirmImport()">
<svg width="14" height="14" 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>
<!-- Success screen -->
<div class="success-screen" id="success-screen">
<div class="success-icon">βœ…</div>
<h2>Book Added!</h2>
<p id="success-msg">Your book has been imported. Upload a PDF or EPUB to enable AI-powered chat.</p>
<div class="success-book" id="success-title"></div>
<div class="hint-box">
πŸ’‘ <span>Go to <strong>My Books</strong> and upload a PDF or EPUB to enable reader Q&A.</span>
</div>
<div style="display:flex;gap:8px;margin-top:4px">
<button class="btn btn-ghost btn-sm" onclick="resetImport()">Import Another</button>
<button class="btn btn-primary btn-sm" onclick="nav('books')">Go to My Books</button>
</div>
</div>
</div>
<!-- ─ MANUAL UPLOAD ─ -->
<div class="import-card" id="manual-tab-content" style="display:none; padding:24px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;">
<div style="font-size:1.5rem">πŸ“„</div>
<div><h2 style="font-size:1rem;font-weight:700">Manual Book Entry</h2><p style="font-size:.78rem;color:var(--text2)">Fill in the details and upload your PDF or EPUB</p></div>
</div>
<div class="manual-form">
<div class="form-grid">
<div class="form-field full"><label>Book Title *</label><input type="text" id="m-title" placeholder="Enter book title"></div>
<div class="form-field"><label>Author Name</label><input type="text" id="m-author" placeholder="Author"></div>
<div class="form-field"><label>Genre</label><input type="text" id="m-genre" placeholder="e.g. Thriller"></div>
<div class="form-field full"><label>Description</label><textarea id="m-desc" placeholder="Brief description or back-cover copy…"></textarea></div>
</div>
<div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input').click()">
<input type="file" id="file-input" accept=".pdf,.epub" onchange="handleFileSelect(this)">
<div class="dz-icon">πŸ“</div>
<p><strong>Click to choose</strong> or drag &amp; drop<br><span style="font-size:.7rem;color:var(--text3)">PDF or EPUB Β· Max 50 MB</span></p>
<p id="file-selected" style="margin-top:8px;color:var(--accent-h);font-size:.8rem;font-weight:600"></p>
</div>
<div class="upload-progress" id="upload-progress">
<div class="progress-bar"><div class="progress-fill" id="progress-fill" style="width:0%"></div></div>
<p class="progress-label" id="progress-label">Uploading…</p>
</div>
<button class="btn btn-primary" id="manual-upload-btn" onclick="manualUpload()" style="width:100%">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/></svg>
Upload Book
</button>
</div>
</div>
</div>
<!-- Right: Books list -->
<div class="books-sidebar">
<div class="books-sidebar-header">
<h3>πŸ“š My Books</h3>
<button class="btn btn-ghost btn-sm" onclick="loadBooksPanel()">πŸ”„</button>
</div>
<div class="books-list" id="books-panel-list">
<div class="empty-list"><div class="ei">πŸ“š</div><p>Loading books…</p></div>
</div>
</div>
</div>
</div>
<!-- ── PAGE: My Books ──────────────────────────────────────── -->
<div class="page" id="page-books">
<div class="page-header" style="display:flex;align-items:center;justify-content:space-between;">
<div>
<h1 class="page-title">My Books</h1>
<p class="page-sub">All books in your catalog</p>
</div>
<button class="btn btn-primary" onclick="nav('import')">+ Add Book</button>
</div>
<div id="books-full-list">
<div class="empty-list"><div class="ei">πŸ“š</div><p>Loading…</p></div>
</div>
</div>
<!-- ── PAGE: Settings ─────────────────────────────────────── -->
<div class="page" id="page-settings">
<div class="page-header">
<h1 class="page-title">Settings</h1>
<p class="page-sub">Account and chatbot configuration</p>
</div>
<div style="max-width:560px">
<div class="import-card" style="padding:24px;margin-bottom:20px;">
<h3 style="font-weight:700;margin-bottom:4px">Account</h3>
<p style="font-size:.8rem;color:var(--text2);margin-bottom:16px">Your profile details</p>
<div class="form-grid">
<div class="form-field"><label>Full Name</label><input type="text" id="s-name" placeholder="Your name"></div>
<div class="form-field"><label>Email</label><input type="email" id="s-email" readonly></div>
</div>
<button class="btn btn-primary btn-sm" style="margin-top:12px" onclick="saveSettings()">Save</button>
</div>
<div class="import-card" style="padding:24px;">
<h3 style="font-weight:700;margin-bottom:4px">Chat Widget</h3>
<p style="font-size:.8rem;color:var(--text2);margin-bottom:12px">Your embed snippet</p>
<pre id="embed-snippet" style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:14px;font-size:.72rem;overflow:auto;color:var(--accent-h);white-space:pre-wrap;word-break:break-all">Loading…</pre>
<button class="btn btn-ghost btn-sm" style="margin-top:10px" onclick="copyEmbed()">πŸ“‹ Copy snippet</button>
</div>
</div>
</div>
</main>
</div>
<script>
/* ──────────────────────────────────────────────────────
Config + AuthClient
─────────────────────────────────────────────────────── */
const API = window.location.origin + '/api';
const auth = new AuthorBotAuthClient({
apiBase: API,
accessKey: 'access_token',
refreshKey: 'refresh_token',
slugKey: 'author_slug',
});
let _slug = '';
let _currentMeta = null;
let _allBooks = [];
const PLATFORMS = [
{ id:'amazon', name:'Amazon', icon:'πŸ›’' },
{ id:'barnes_noble', name:'Barnes & Noble', icon:'πŸ“•' },
{ id:'goodreads', name:'Goodreads', icon:'⭐' },
{ id:'google_books', name:'Google Books', icon:'πŸ”' },
{ id:'apple_books', name:'Apple Books', icon:'🍎' },
{ id:'kobo', name:'Kobo', icon:'πŸ“–' },
{ id:'thriftbooks', name:'ThriftBooks', icon:'πŸ’Έ' },
{ id:'abebooks', name:'AbeBooks', icon:'πŸ›οΈ' },
{ id:'worldcat', name:'WorldCat', icon:'🌍' },
{ id:'open_library', name:'Open Library', icon:'πŸ›' },
];
/* ── Toast ───────────────────────────────────────────── */
function toast(msg, type='info') {
const c = document.getElementById('toasts');
const t = document.createElement('div');
t.className = `toast ${type}`;
const icons = { success:'βœ…', error:'❌', warning:'⚠️', info:'ℹ️' };
t.innerHTML = `<span>${icons[type]||'ℹ️'}</span><span>${esc(msg)}</span><button class="toast-close" onclick="this.parentElement.remove()">Γ—</button>`;
c.appendChild(t);
setTimeout(() => { t.style.opacity='0'; t.style.transition='opacity .3s'; setTimeout(()=>t.remove(),300); }, 4500);
}
/* ── Routing / nav ───────────────────────────────────── */
function nav(page) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const pg = document.getElementById('page-' + page);
if (pg) pg.classList.add('active');
const nb = document.getElementById('nav-' + page);
if (nb) nb.classList.add('active');
if (page === 'books') loadFullBooks();
if (page === 'settings') loadSettings();
if (page === 'import') loadBooksPanel();
}
document.querySelectorAll('.nav-item[data-page]').forEach(btn => {
btn.onclick = () => nav(btn.dataset.page);
});
/* ── Session ─────────────────────────────────────────── */
auth.restoreSession(function(u) { return !!u.id; }).then(ok => {
if (ok) {
initApp();
}
});
document.getElementById('a-login-btn').onclick = async () => {
const msg = document.getElementById('a-login-msg');
const btn = document.getElementById('a-login-btn');
msg.textContent = '';
btn.disabled = true; btn.textContent = 'Signing in…';
try {
const data = await auth.login(
document.getElementById('a-email').value.trim(),
document.getElementById('a-pass').value
);
initApp();
} catch(e) { msg.textContent = e.message; }
finally { btn.disabled = false; btn.textContent = 'Sign In'; }
};
['a-email','a-pass'].forEach(id =>
document.getElementById(id).addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('a-login-btn').click();
})
);
document.getElementById('logout-btn').onclick = () => {
auth.logout().finally(() => location.reload());
};
function initApp() {
document.getElementById('auth-screen').style.display = 'none';
document.getElementById('app').style.display = 'block';
_slug = auth.getAuthorSlug() || localStorage.getItem('author_slug') || '';
// Load user profile
auth.get('/auth/me').then(u => {
document.getElementById('user-name').textContent = u.full_name || u.email || 'Author';
document.getElementById('user-email').textContent = u.email || '';
document.getElementById('user-avatar').textContent = (u.full_name || u.email || 'A')[0].toUpperCase();
if (!_slug && u.slug) {
_slug = u.slug;
localStorage.setItem('author_slug', _slug);
}
document.getElementById('s-name').value = u.full_name || '';
document.getElementById('s-email').value = u.email || '';
updateEmbedSnippet();
}).catch(()=>{});
buildPlatformPills();
loadBooksPanel();
nav('import');
}
/* ── Platform pills ──────────────────────────────────── */
function buildPlatformPills() {
const c = document.getElementById('platform-pills');
PLATFORMS.forEach(p => {
c.insertAdjacentHTML('beforeend',
`<span class="platform-pill" data-pid="${p.id}"><span class="dot"></span>${p.icon} ${p.name}</span>`
);
});
// Detect platform live as user types
document.getElementById('book-url').addEventListener('input', detectPlatformLive);
}
function detectPlatformLive() {
const url = document.getElementById('book-url').value.toLowerCase();
const patterns = { amazon:/amazon\.(com|co\.|ca|com\.)/, barnes_noble:/barnesandnoble\.com/, goodreads:/goodreads\.com/, google_books:/(books\.google\.|play\.google\.com\/store\/books)/, apple_books:/books\.apple\.com/, kobo:/kobo\.com/, thriftbooks:/thriftbooks\.com/, abebooks:/abebooks\./, worldcat:/worldcat\.org/, open_library:/openlibrary\.org/ };
document.querySelectorAll('.platform-pill').forEach(pill => {
const pid = pill.dataset.pid;
const match = patterns[pid] && patterns[pid].test(url);
pill.classList.toggle('detected', match);
});
}
/* ── Tab switcher ────────────────────────────────────── */
function switchTab(tab) {
document.getElementById('tab-url').classList.toggle('active', tab==='url');
document.getElementById('tab-manual').classList.toggle('active', tab==='manual');
document.getElementById('url-tab-content').style.display = tab==='url' ? '' : 'none';
document.getElementById('manual-tab-content').style.display= tab==='manual' ? '' : 'none';
}
/* ── FETCH preview ───────────────────────────────────── */
async function fetchPreview() {
const url = document.getElementById('book-url').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; }
setFetchState('loading', 'Detecting platform…');
try {
const steps = ['Detecting platform…', 'Querying book APIs…', 'Enriching metadata…', 'Fetching author bio…'];
let si = 0;
const stepInt = setInterval(() => {
si++;
const msg = steps[si % steps.length];
const el = document.getElementById('loader-msg');
if (el) el.textContent = msg;
}, 1400);
const res = await auth.request('POST', `/admin/${_slug}/books/import-url/preview`, { url, force_refresh: true });
clearInterval(stepInt);
const data = await res.json();
if (!res.ok) { setFetchState('input'); showAlert('error', data.detail || 'Failed to fetch book details.'); return; }
_currentMeta = data;
renderPreview(data);
} catch(e) {
setFetchState('input');
showAlert('error', 'Network error: ' + e.message);
}
}
function setFetchState(state, msg) {
const loader = document.getElementById('fetch-loader');
const preview= document.getElementById('preview-section');
const actions= document.getElementById('action-row');
const success= document.getElementById('success-screen');
const urlWrap= document.getElementById('url-input-wrap') || document.querySelector('.url-input-wrap');
loader.style.display = state==='loading' ? 'flex' : 'none';
preview.style.display = state==='preview' ? 'block' : 'none';
actions.style.display = state==='preview' ? 'flex' : 'none';
success.style.display = state==='success' ? 'flex' : 'none';
if (msg && state==='loading') document.getElementById('loader-msg').textContent = msg;
hideAlert();
}
/* ── Render preview card ─────────────────────────────── */
function renderPreview(data) {
const plat = PLATFORMS.find(p => p.id === data.platform) || { icon:'πŸ”—', name: data.platform_display || data.platform };
document.getElementById('p-platform').textContent = `${plat.icon} ${plat.name || data.platform_display}`;
const pct = Math.round((data.confidence||0)*100);
document.getElementById('p-conf-fill').style.width = pct + '%';
document.getElementById('p-conf-label').textContent = pct + '% matched';
// Cover
const cov = document.getElementById('p-cover');
if (data.cover_url) {
cov.innerHTML = `<img src="${esc(data.cover_url)}" alt="cover" onerror="this.parentElement.textContent='πŸ“–'">`;
} else { cov.textContent = 'πŸ“–'; }
document.getElementById('p-title').textContent = data.title || '(Title not found)';
document.getElementById('p-author').textContent = data.author ? 'by ' + data.author : '';
document.getElementById('p-desc').textContent = data.description || '';
// Chips
const chips = [];
if (data.rating) {
const stars = parseFloat(data.rating);
const filled = Math.round(stars);
chips.push(`<span class="chip accent"><span class="rating-stars">${'β˜…'.repeat(filled)}${'β˜†'.repeat(5-filled)}</span> ${data.rating}${data.rating_count ? ` (${data.rating_count})` : ''}</span>`);
}
if (data.genre) chips.push(`<span class="chip">🏷️ ${esc(data.genre.split(',')[0].trim())}</span>`);
if (data.page_count) chips.push(`<span class="chip">πŸ“„ ${data.page_count} pages</span>`);
if (data.language && data.language.toLowerCase() !== 'en') chips.push(`<span class="chip">🌐 ${esc(data.language)}</span>`);
if (data.publisher) chips.push(`<span class="chip">🏒 ${esc(data.publisher)}</span>`);
if (data.publish_year) chips.push(`<span class="chip">πŸ“… ${esc(data.publish_year)}</span>`);
if (data.isbn) chips.push(`<span class="chip">πŸ”’ ${esc(data.isbn)}</span>`);
document.getElementById('p-chips').innerHTML = chips.join('');
// Author bio
const bioPanel = document.getElementById('bio-panel');
if (data.about_author) {
bioPanel.style.display = 'block';
document.getElementById('bio-text').textContent = data.about_author;
} else { bioPanel.style.display = 'none'; }
// Pre-fill edit fields
document.getElementById('e-title').value = data.title || '';
document.getElementById('e-author').value = data.author || '';
document.getElementById('e-genre').value = data.genre || '';
document.getElementById('e-isbn').value = data.isbn || '';
document.getElementById('e-cover').value = data.cover_url || '';
document.getElementById('e-publisher').value = data.publisher || '';
document.getElementById('e-desc').value = data.description || '';
document.getElementById('e-bio').value = data.about_author || '';
// Alert
if (data.error && !data.title) {
showAlert('error', data.error + ' β€” please fill in details manually.');
} else if (data.error || (data.warnings && data.warnings.length)) {
const m = data.error || data.warnings.join('; ');
showAlert('warning', '⚠️ Partial data enriched via Google Books/Open Library: ' + m);
} else { hideAlert(); }
setFetchState('preview');
}
/* ── Edit toggle ─────────────────────────────────────── */
function toggleEdit() {
const toggle = document.getElementById('edit-toggle');
const form = document.getElementById('edit-form');
const open = toggle.classList.toggle('open');
form.style.display = open ? 'block' : 'none';
}
/* ── Confirm import ──────────────────────────────────── */
async function confirmImport() {
if (!_currentMeta) return;
const title = (document.getElementById('e-title').value.trim() || _currentMeta.title || '').trim();
if (!title) { showAlert('error', 'Please enter a title.'); return; }
const btn = document.getElementById('import-btn');
btn.disabled = true; btn.textContent = 'Saving…';
try {
const body = {
url: _currentMeta.source_url,
title,
author: document.getElementById('e-author').value.trim(),
description: document.getElementById('e-desc').value.trim(),
cover_url: document.getElementById('e-cover').value.trim(),
isbn: document.getElementById('e-isbn').value.trim(),
genre: document.getElementById('e-genre').value.trim(),
};
const res = await auth.request('POST', `/admin/${_slug}/books/import-url`, body);
const data = await res.json();
if (!res.ok) { showAlert('error', data.detail || 'Import failed.'); return; }
document.getElementById('success-title').textContent = data.title || title;
document.getElementById('success-msg').textContent = `Imported from ${data.platform || 'external source'}. ${data.next_steps||'Upload a PDF/EPUB to enable AI chat.'}`;
setFetchState('success');
loadBooksPanel();
toast('Book added!', 'success');
} catch(e) {
showAlert('error', 'Error: ' + e.message);
} finally { btn.disabled = false; btn.textContent = 'Add to My Books'; }
}
/* ── Reset / back ────────────────────────────────────── */
function resetImport() {
_currentMeta = null;
document.getElementById('book-url').value = '';
document.getElementById('edit-form').style.display = 'none';
document.getElementById('edit-toggle').classList.remove('open');
setFetchState('input');
hideAlert();
detectPlatformLive();
}
/* ── Alert helpers ───────────────────────────────────── */
function showAlert(type, msg) {
const el = document.getElementById('fetch-alert');
el.className = 'alert-strip ' + type;
el.textContent = msg;
el.style.display = 'flex';
}
function hideAlert() { document.getElementById('fetch-alert').style.display = 'none'; }
/* ── Manual upload ───────────────────────────────────── */
let _selectedFile = null;
function handleFileSelect(input) {
_selectedFile = input.files[0];
document.getElementById('file-selected').textContent = _selectedFile ? _selectedFile.name : '';
}
const dz = document.getElementById('drop-zone');
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('dragover'); });
dz.addEventListener('dragleave', () => dz.classList.remove('dragover'));
dz.addEventListener('drop', e => {
e.preventDefault(); dz.classList.remove('dragover');
_selectedFile = e.dataTransfer.files[0];
document.getElementById('file-selected').textContent = _selectedFile ? _selectedFile.name : '';
});
async function manualUpload() {
const title = document.getElementById('m-title').value.trim();
if (!title) { toast('Please enter a book title.', 'error'); return; }
if (!_selectedFile) { toast('Please select a PDF or EPUB file.', 'error'); return; }
const btn = document.getElementById('manual-upload-btn');
btn.disabled = true; btn.textContent = 'Uploading…';
document.getElementById('upload-progress').style.display = 'block';
document.getElementById('progress-fill').style.width = '15%';
document.getElementById('progress-label').textContent = 'Uploading…';
try {
const fd = new FormData();
fd.append('file', _selectedFile);
fd.append('title', title);
fd.append('book_author', document.getElementById('m-author').value.trim());
fd.append('genre', document.getElementById('m-genre').value.trim());
fd.append('description', document.getElementById('m-desc').value.trim());
document.getElementById('progress-fill').style.width = '60%';
const res = await auth.upload(`/admin/${_slug}/books/upload`, fd);
const data = await res.json();
document.getElementById('progress-fill').style.width = '100%';
if (!res.ok) throw new Error(data.detail || 'Upload failed.');
document.getElementById('progress-label').textContent = 'Done!';
toast(`"${title}" uploaded β€” processing…`, 'success');
setTimeout(() => {
document.getElementById('upload-progress').style.display = 'none';
document.getElementById('progress-fill').style.width = '0%';
}, 1500);
loadBooksPanel();
loadFullBooks();
} catch(e) {
toast('Upload failed: ' + e.message, 'error');
document.getElementById('progress-fill').style.width = '0%';
} finally {
btn.disabled = false; btn.textContent = 'Upload Book';
}
}
/* ── Books panel (sidebar) ───────────────────────────── */
async function loadBooksPanel() {
const c = document.getElementById('books-panel-list');
c.innerHTML = '<div class="empty-list"><div class="ei">πŸ“š</div><p>Loading…</p></div>';
try {
const data = await auth.get(`/admin/${_slug}/books?limit=50`);
_allBooks = data.books || data || [];
if (!_allBooks.length) {
c.innerHTML = '<div class="empty-list"><div class="ei">πŸ“š</div><p>No books yet. Import one above!</p></div>';
return;
}
c.innerHTML = _allBooks.map(b => bookRowHTML(b)).join('');
} catch(e) {
c.innerHTML = `<div class="empty-list"><p style="color:var(--error)">${esc(e.message)}</p></div>`;
}
}
function bookRowHTML(b) {
const statusMap = {
ready: ['st-ready', 'βœ… Ready'],
url_imported:['st-import', 'πŸ”— URL imported'],
processing: ['st-proc', 'βš™οΈ Processing'],
created: ['st-proc', '⏳ Created'],
error: ['st-err', '❌ Error'],
};
const [sc, sl] = statusMap[b.status] || ['st-proc', b.status];
const thumb = b.cover_url
? `<img src="${esc(b.cover_url)}" alt="" onerror="this.parentElement.textContent='πŸ“–'">`
: 'πŸ“–';
return `<div class="book-row">
<div class="book-thumb">${thumb}</div>
<div class="book-meta">
<div class="book-title">${esc(b.title)}</div>
<div class="book-author-sm">${esc(b.book_author || b.author_name || 'β€”')}</div>
<span class="book-status ${sc}">${sl}</span>
</div>
</div>`;
}
/* ── Full books page ─────────────────────────────────── */
async function loadFullBooks() {
const c = document.getElementById('books-full-list');
c.innerHTML = '<div class="empty-list"><div class="ei">πŸ“š</div><p>Loading…</p></div>';
try {
const data = await auth.get(`/admin/${_slug}/books?limit=100`);
const books = data.books || data || [];
if (!books.length) {
c.innerHTML = '<div class="empty-list"><div class="ei">πŸ“š</div><p>No books yet. Go to <strong>Add Book</strong> to import one!</p></div>';
return;
}
c.innerHTML = `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:16px">
${books.map(b => fullBookCard(b)).join('')}
</div>`;
} catch(e) {
c.innerHTML = `<div class="empty-list"><p style="color:var(--error)">${esc(e.message)}</p></div>`;
}
}
function fullBookCard(b) {
const statusMap = { ready:['st-ready','βœ… Ready'], url_imported:['st-import','πŸ”— Imported'], processing:['st-proc','βš™οΈ Processing'], error:['st-err','❌ Error'] };
const [sc,sl] = statusMap[b.status] || ['st-proc',b.status];
const cover = b.cover_url
? `<img src="${esc(b.cover_url)}" style="width:100%;height:100%;object-fit:cover" alt="" onerror="this.style.display='none'">`
: 'πŸ“–';
return `<div class="import-card" style="padding:0;overflow:hidden;">
<div style="height:160px;background:var(--surface2);display:flex;align-items:center;justify-content:center;font-size:3rem;overflow:hidden">${cover}</div>
<div style="padding:16px">
<div style="font-weight:700;font-size:.9rem;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(b.title)}</div>
<div style="font-size:.75rem;color:var(--text2);margin-bottom:8px">${esc(b.book_author||b.author_name||'β€”')}</div>
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
<span class="book-status ${sc}">${sl}</span>
${b.genre ? `<span class="chip">🏷️ ${esc(b.genre.split(',')[0].trim())}</span>` : ''}
${b.isbn ? `<span class="chip">πŸ”’ ${esc(b.isbn)}</span>` : ''}
</div>
</div>
</div>`;
}
/* ── Settings ────────────────────────────────────────── */
function loadSettings() {
updateEmbedSnippet();
}
function updateEmbedSnippet() {
const slug = _slug || localStorage.getItem('author_slug') || 'YOUR_SLUG';
document.getElementById('embed-snippet').textContent =
`<script src="${window.location.origin}/static/widget.js"\n data-slug="${slug}">\n<\/script>`;
}
async function saveSettings() {
try {
const name = document.getElementById('s-name').value.trim();
await auth.put(`/admin/${_slug}/profile`, { full_name: name });
toast('Settings saved', 'success');
document.getElementById('user-name').textContent = name;
} catch(e) { toast('Save failed: ' + e.message, 'error'); }
}
function copyEmbed() {
const text = document.getElementById('embed-snippet').textContent;
navigator.clipboard.writeText(text).then(() => toast('Embed code copied!','success')).catch(()=>toast('Copy failed','error'));
}
/* ── Utility ─────────────────────────────────────────── */
function esc(str) {
return String(str||'')
.replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
</script>
</body>
</html>