- Rename project from 'bookmann' to 'shelf' - Move all backend/ contents to root level (flatten structure) - Update Go module name from 'bookmann' to 'shelf' - Update all import paths to use new 'shelf' module - Update Dockerfile to work without backend/ subdirectory - Update docker-compose.yml to use new structure and rename containers - Update .gitignore for new file paths - Update README.md with new project name and structure - Regenerate database code with new module imports
457 lines
24 KiB
HTML
457 lines
24 KiB
HTML
{{template "base.html" .}}
|
|
|
|
{{define "title"}}Dashboard - Bookmann{{end}}
|
|
|
|
{{define "content"}}
|
|
<!-- Navigation Header -->
|
|
<nav class="border-b" style="border-color: var(--border); background-color: var(--bg-secondary)">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between items-center h-16">
|
|
<div class="flex items-center">
|
|
<h1 class="text-xl font-bold" style="color: var(--text-primary)">📚 Bookmann</h1>
|
|
</div>
|
|
<div class="flex items-center space-x-4">
|
|
<select id="theme-select" class="px-3 py-2 border rounded text-sm" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" onchange="changeTheme()">
|
|
<option value="tokyo-night">Tokyo Night</option>
|
|
<option value="dracula">Dracula</option>
|
|
<option value="nord">Nord</option>
|
|
<option value="solarized-dark">Solarized Dark</option>
|
|
<option value="monokai">Monokai</option>
|
|
<option value="one-dark-pro">One Dark Pro</option>
|
|
<option value="material-dark">Material Dark</option>
|
|
<option value="catppuccin-mocha">Catppuccin Mocha</option>
|
|
<option value="catppuccin-macchiato">Catppuccin Macchiato</option>
|
|
<option value="catppuccin-frappe">Catppuccin Frappé</option>
|
|
<option value="catppuccin-latte">Catppuccin Latte</option>
|
|
</select>
|
|
<span style="color: var(--text-secondary)">Welcome, {{.User.Username}}!</span>
|
|
<button onclick="logout()" class="btn-primary px-4 py-2 rounded text-sm">
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<!-- Header Section -->
|
|
<div class="mb-8">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h2 class="text-3xl font-bold" style="color: var(--text-primary)">Your Ebook Library</h2>
|
|
<p style="color: var(--text-secondary)">Manage your ebook collection and reading progress</p>
|
|
</div>
|
|
<button onclick="showCreateForm()" class="btn-primary px-6 py-3 rounded-lg font-medium">
|
|
+ Add New Ebook
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search and Filter -->
|
|
<div class="mb-6">
|
|
<div class="flex flex-col sm:flex-row gap-4">
|
|
<input type="text" id="search-input" placeholder="Search ebooks..." class="flex-1 px-4 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onkeyup="filterEbooks()">
|
|
<select id="sort-select" class="px-4 py-2 border rounded-lg" style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border)" onchange="loadEbooks()">
|
|
<option value="created_at DESC">Newest First</option>
|
|
<option value="title ASC">Title A-Z</option>
|
|
<option value="author ASC">Author A-Z</option>
|
|
<option value="created_at ASC">Oldest First</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create/Edit Form (Hidden by default) -->
|
|
<div id="ebook-form" class="card p-6 rounded-lg border mb-6" style="display: none;">
|
|
<h3 id="form-title" class="text-xl font-semibold mb-4" style="color: var(--text-primary)">Add New Ebook</h3>
|
|
<form id="ebook-form-element" hx-post="/api/ebooks" hx-target="#form-result" hx-swap="innerHTML" hx-on:htmx:after-request="handleFormResponse()" hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Title *</label>
|
|
<input type="text" name="title" id="form-title-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Author</label>
|
|
<input type="text" name="author" id="form-author-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">ISBN</label>
|
|
<input type="text" name="isbn" id="form-isbn-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Series</label>
|
|
<input type="text" name="series" id="form-series-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Series Number</label>
|
|
<input type="number" name="series_number" id="form-series-number-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Publisher</label>
|
|
<input type="text" name="publisher" id="form-publisher-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Date Published</label>
|
|
<input type="date" name="date_published" id="form-date-published-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">ASIN</label>
|
|
<input type="text" name="asin" id="form-asin-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Tags</label>
|
|
<input type="text" name="tags" id="form-tags-input" placeholder="fiction, adventure, ..." class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
|
</div>
|
|
<div class="md:col-span-2">
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Contributors</label>
|
|
<input type="text" name="contributors" id="form-contributors-input" placeholder="Editor Name, Illustrator Name" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
|
</div>
|
|
<div class="md:col-span-2">
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Description</label>
|
|
<textarea name="description" id="form-description-input" rows="3" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)"></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">File Path *</label>
|
|
<input type="text" name="file_path" id="form-file-path-input" placeholder="/uploads/book.epub" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">File Size (bytes) *</label>
|
|
<input type="number" name="file_size" id="form-file-size-input" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">MIME Type *</label>
|
|
<input type="text" name="mime_type" id="form-mime-type-input" placeholder="application/epub+zip" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1" style="color: var(--text-secondary)">Cover Image Path</label>
|
|
<input type="text" name="cover_image_path" id="form-cover-image-path-input" placeholder="/uploads/cover.jpg" class="w-full px-3 py-2 border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">
|
|
</div>
|
|
</div>
|
|
<div class="mt-6 flex justify-end space-x-3">
|
|
<button type="button" onclick="hideCreateForm()" class="px-4 py-2 border rounded" style="border-color: var(--border); color: var(--text-secondary)">Cancel</button>
|
|
<button type="submit" class="btn-primary px-6 py-2 rounded">Save Ebook</button>
|
|
</div>
|
|
</form>
|
|
<div id="form-result" class="mt-4"></div>
|
|
</div>
|
|
|
|
<!-- Ebooks Grid -->
|
|
<div id="ebooks-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<!-- Ebooks will be loaded here -->
|
|
</div>
|
|
|
|
<!-- Loading indicator -->
|
|
<div id="loading" class="text-center py-8" style="color: var(--text-secondary)">
|
|
Loading your ebooks...
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div id="empty-state" class="text-center py-12" style="display: none;">
|
|
<div class="text-6xl mb-4">📚</div>
|
|
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No ebooks yet</h3>
|
|
<p style="color: var(--text-secondary)">Start building your library by adding your first ebook</p>
|
|
<button onclick="showCreateForm()" class="btn-primary px-6 py-3 rounded-lg font-medium mt-4">
|
|
Add Your First Ebook
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div id="pagination" class="mt-8 flex justify-center space-x-2" style="display: none;">
|
|
<button id="prev-btn" onclick="changePage(-1)" class="px-4 py-2 border rounded disabled:opacity-50" style="border-color: var(--border); color: var(--text-secondary)">Previous</button>
|
|
<span id="page-info" style="color: var(--text-secondary)"></span>
|
|
<button id="next-btn" onclick="changePage(1)" class="px-4 py-2 border rounded disabled:opacity-50" style="border-color: var(--border); color: var(--text-secondary)">Next</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ebook Detail Modal -->
|
|
<div id="ebook-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style="display: none; z-index: 1000;">
|
|
<div class="card max-w-2xl w-full mx-4 rounded-lg border max-h-screen overflow-y-auto" style="background-color: var(--bg-secondary);">
|
|
<div class="p-6">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<h3 id="modal-title" class="text-xl font-semibold" style="color: var(--text-primary)"></h3>
|
|
<button onclick="closeModal()" class="text-xl" style="color: var(--text-secondary)">×</button>
|
|
</div>
|
|
<div id="modal-content">
|
|
<!-- Content will be loaded here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentPage = 1;
|
|
let currentSort = 'created_at DESC';
|
|
let allEbooks = [];
|
|
|
|
function showCreateForm() {
|
|
document.getElementById('ebook-form').style.display = 'block';
|
|
document.getElementById('form-title').textContent = 'Add New Ebook';
|
|
document.getElementById('ebook-form-element').setAttribute('hx-post', '/api/ebooks');
|
|
clearForm();
|
|
document.getElementById('form-title-input').focus();
|
|
}
|
|
|
|
function showEditForm(ebook) {
|
|
document.getElementById('ebook-form').style.display = 'block';
|
|
document.getElementById('form-title').textContent = 'Edit Ebook';
|
|
document.getElementById('ebook-form-element').setAttribute('hx-put', `/api/ebooks/${ebook.id}`);
|
|
populateForm(ebook);
|
|
document.getElementById('form-title-input').focus();
|
|
}
|
|
|
|
function hideCreateForm() {
|
|
document.getElementById('ebook-form').style.display = 'none';
|
|
clearForm();
|
|
}
|
|
|
|
function clearForm() {
|
|
document.getElementById('ebook-form-element').reset();
|
|
}
|
|
|
|
function populateForm(ebook) {
|
|
document.getElementById('form-title-input').value = ebook.title || '';
|
|
document.getElementById('form-author-input').value = ebook.author || '';
|
|
document.getElementById('form-isbn-input').value = ebook.isbn || '';
|
|
document.getElementById('form-series-input').value = ebook.series || '';
|
|
document.getElementById('form-series-number-input').value = ebook.series_number || '';
|
|
document.getElementById('form-publisher-input').value = ebook.publisher || '';
|
|
document.getElementById('form-date-published-input').value = ebook.date_published || '';
|
|
document.getElementById('form-asin-input').value = ebook.asin || '';
|
|
document.getElementById('form-tags-input').value = ebook.tags || '';
|
|
document.getElementById('form-contributors-input').value = ebook.contributors || '';
|
|
document.getElementById('form-description-input').value = ebook.description || '';
|
|
document.getElementById('form-file-path-input').value = ebook.file_path || '';
|
|
document.getElementById('form-file-size-input').value = ebook.file_size || '';
|
|
document.getElementById('form-mime-type-input').value = ebook.mime_type || '';
|
|
document.getElementById('form-cover-image-path-input').value = ebook.cover_image_path || '';
|
|
}
|
|
|
|
function handleFormResponse() {
|
|
if (event.detail.xhr.status >= 200 && event.detail.xhr.status < 300) {
|
|
hideCreateForm();
|
|
loadEbooks();
|
|
}
|
|
}
|
|
|
|
function loadEbooks() {
|
|
const loading = document.getElementById('loading');
|
|
const container = document.getElementById('ebooks-container');
|
|
const emptyState = document.getElementById('empty-state');
|
|
|
|
loading.style.display = 'block';
|
|
container.innerHTML = '';
|
|
|
|
const sort = document.getElementById('sort-select').value;
|
|
currentSort = sort;
|
|
|
|
fetch(`/api/ebooks?limit=12&offset=${(currentPage - 1) * 12}`, {
|
|
headers: {
|
|
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
loading.style.display = 'none';
|
|
allEbooks = data;
|
|
|
|
if (data.length === 0) {
|
|
emptyState.style.display = 'block';
|
|
document.getElementById('pagination').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
emptyState.style.display = 'none';
|
|
renderEbooks(data);
|
|
})
|
|
.catch(error => {
|
|
loading.style.display = 'none';
|
|
console.error('Error loading ebooks:', error);
|
|
container.innerHTML = '<div class="col-span-full text-center py-8" style="color: var(--text-secondary)">Error loading ebooks. Please try again.</div>';
|
|
});
|
|
}
|
|
|
|
function renderEbooks(ebooks) {
|
|
const container = document.getElementById('ebooks-container');
|
|
container.innerHTML = '';
|
|
|
|
ebooks.forEach(ebook => {
|
|
const card = createEbookCard(ebook);
|
|
container.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function createEbookCard(ebook) {
|
|
const card = document.createElement('div');
|
|
card.className = 'card p-4 rounded-lg border hover:shadow-lg transition-shadow';
|
|
card.style.cssText = `background-color: var(--bg-secondary); border-color: var(--border);`;
|
|
|
|
const coverUrl = ebook.cover_image_path ? ebook.cover_image_path : '/static/placeholder-book.svg';
|
|
|
|
card.innerHTML = `
|
|
<div class="flex flex-col h-full">
|
|
<div class="flex-1">
|
|
<div class="aspect-w-3 aspect-h-4 mb-3 overflow-hidden rounded">
|
|
<img src="${coverUrl}" alt="Cover" class="w-full h-48 object-cover rounded" onerror="this.src='/static/placeholder-book.svg'">
|
|
</div>
|
|
<h3 class="font-semibold text-lg mb-1 line-clamp-2" style="color: var(--text-primary)">${ebook.title}</h3>
|
|
${ebook.author ? `<p class="text-sm mb-2" style="color: var(--text-secondary)">by ${ebook.author}</p>` : ''}
|
|
${ebook.series ? `<p class="text-sm mb-2" style="color: var(--text-secondary)">${ebook.series}${ebook.series_number ? ' #' + ebook.series_number : ''}</p>` : ''}
|
|
<div class="flex flex-wrap gap-1 mb-3">
|
|
${ebook.tags ? ebook.tags.split(',').slice(0, 3).map(tag => `<span class="px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">${tag.trim()}</span>`).join('') : ''}
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-between items-center mt-4">
|
|
<div class="flex space-x-2">
|
|
<button onclick="viewEbook('${ebook.id}')" class="px-3 py-1 text-sm border rounded hover:opacity-80" style="border-color: var(--border); color: var(--text-secondary)">View</button>
|
|
<button onclick="showEditForm(${JSON.stringify(ebook).replace(/"/g, '"')})" class="px-3 py-1 text-sm border rounded hover:opacity-80" style="border-color: var(--border); color: var(--text-secondary)">Edit</button>
|
|
</div>
|
|
<button onclick="deleteEbook('${ebook.id}')" class="px-3 py-1 text-sm text-red-500 hover:text-red-700">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return card;
|
|
}
|
|
|
|
function viewEbook(id) {
|
|
fetch(`/api/ebooks/${id}`, {
|
|
headers: {
|
|
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(ebook => {
|
|
showEbookModal(ebook);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading ebook:', error);
|
|
});
|
|
}
|
|
|
|
function showEbookModal(ebook) {
|
|
const modal = document.getElementById('ebook-modal');
|
|
const title = document.getElementById('modal-title');
|
|
const content = document.getElementById('modal-content');
|
|
|
|
title.textContent = ebook.title;
|
|
|
|
content.innerHTML = `
|
|
<div class="space-y-4">
|
|
<div class="flex space-x-4">
|
|
<img src="${ebook.cover_image_path || '/static/placeholder-book.svg'}" alt="Cover" class="w-32 h-40 object-cover rounded" onerror="this.src='/static/placeholder-book.svg'">
|
|
<div class="flex-1">
|
|
<h4 class="font-semibold mb-2" style="color: var(--text-primary)">${ebook.title}</h4>
|
|
${ebook.author ? `<p style="color: var(--text-secondary)">Author: ${ebook.author}</p>` : ''}
|
|
${ebook.publisher ? `<p style="color: var(--text-secondary)">Publisher: ${ebook.publisher}</p>` : ''}
|
|
${ebook.date_published ? `<p style="color: var(--text-secondary)">Published: ${new Date(ebook.date_published).toLocaleDateString()}</p>` : ''}
|
|
${ebook.isbn ? `<p style="color: var(--text-secondary)">ISBN: ${ebook.isbn}</p>` : ''}
|
|
${ebook.asin ? `<p style="color: var(--text-secondary)">ASIN: ${ebook.asin}</p>` : ''}
|
|
${ebook.series ? `<p style="color: var(--text-secondary)">Series: ${ebook.series}${ebook.series_number ? ' #' + ebook.series_number : ''}</p>` : ''}
|
|
</div>
|
|
</div>
|
|
${ebook.description ? `<div><h5 class="font-semibold mb-2" style="color: var(--text-primary)">Description</h5><p style="color: var(--text-secondary)">${ebook.description}</p></div>` : ''}
|
|
${ebook.tags ? `<div><h5 class="font-semibold mb-2" style="color: var(--text-primary)">Tags</h5><div class="flex flex-wrap gap-1">${ebook.tags.split(',').map(tag => `<span class="px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">${tag.trim()}</span>`).join('')}</div></div>` : ''}
|
|
${ebook.contributors ? `<div><h5 class="font-semibold mb-2" style="color: var(--text-primary)">Contributors</h5><p style="color: var(--text-secondary)">${ebook.contributors}</p></div>` : ''}
|
|
<div class="border-t pt-4" style="border-color: var(--border)">
|
|
<h5 class="font-semibold mb-2" style="color: var(--text-primary)">File Information</h5>
|
|
<p style="color: var(--text-secondary)">Size: ${formatFileSize(ebook.file_size)} | Type: ${ebook.mime_type}</p>
|
|
<p style="color: var(--text-secondary)">Path: ${ebook.file_path}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
modal.style.display = 'flex';
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('ebook-modal').style.display = 'none';
|
|
}
|
|
|
|
function deleteEbook(id) {
|
|
if (confirm('Are you sure you want to delete this ebook?')) {
|
|
fetch(`/api/ebooks/${id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
loadEbooks();
|
|
} else {
|
|
alert('Error deleting ebook');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error deleting ebook:', error);
|
|
alert('Error deleting ebook');
|
|
});
|
|
}
|
|
}
|
|
|
|
function filterEbooks() {
|
|
const searchTerm = document.getElementById('search-input').value.toLowerCase();
|
|
const filteredEbooks = allEbooks.filter(ebook =>
|
|
ebook.title.toLowerCase().includes(searchTerm) ||
|
|
(ebook.author && ebook.author.toLowerCase().includes(searchTerm)) ||
|
|
(ebook.tags && ebook.tags.toLowerCase().includes(searchTerm))
|
|
);
|
|
renderEbooks(filteredEbooks);
|
|
}
|
|
|
|
function changePage(direction) {
|
|
currentPage += direction;
|
|
if (currentPage < 1) currentPage = 1;
|
|
loadEbooks();
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (!bytes) return 'Unknown';
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('user');
|
|
window.location.href = '/';
|
|
}
|
|
|
|
function changeTheme() {
|
|
const theme = document.getElementById('theme-select').value;
|
|
applyTheme(theme);
|
|
fetch('/api/auth/theme', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
|
},
|
|
body: JSON.stringify({ theme })
|
|
}).catch(err => console.log('Theme save failed', err));
|
|
}
|
|
|
|
function loadUserTheme() {
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
fetch('/api/auth/profile', {
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
}).then(res => res.json()).then(data => {
|
|
if (data.theme) applyTheme(data.theme);
|
|
}).catch(() => {});
|
|
}
|
|
}
|
|
|
|
// Load ebooks on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadTheme();
|
|
loadUserTheme();
|
|
document.getElementById('theme-select').value = localStorage.getItem('theme') || 'tokyo-night';
|
|
loadEbooks();
|
|
});
|
|
</script>
|
|
{{end}} |