Spaces:
Running
Running
| <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 & 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,'&').replace(/</g,'<') | |
| .replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| </script> | |
| </body> | |
| </html> | |