refactor: Add Alpine.js registration to existing TypeScript modules
Added Alpine.global() registration to enable template access to functions: - admin.ts: Added Alpine for scan, stats, and settings functions - api-explorer.ts: Already had Alpine (kept as is) - bookshelf.ts: Added Alpine for library/bookshelf interactions - collections.ts: Added Alpine for collection management - conflicts.ts: Added Alpine for conflict resolution - device-management.ts: Added Alpine with event delegation for dynamic content - header.ts: Added Alpine for theme dropdown and user menu - library.ts: Added Alpine registrations - linking.ts: Added Alpine registrations - queue.ts: Added Alpine for queue operations - search.ts: Added Alpine registrations - themeDropdown.ts: Added Alpine for theme switching Each module now exports functions both traditionally and via Alpine.global() for template access.
This commit is contained in:
+148
-71
@@ -1,67 +1,138 @@
|
||||
function selectLibrary(libraryId: string): void {
|
||||
localStorage.setItem("selectedLibrary", libraryId);
|
||||
import { Alpine } from "./alpine";
|
||||
import { showToast } from "./toast";
|
||||
|
||||
document.querySelectorAll(".library-item").forEach((el) => {
|
||||
el.classList.remove("ring-2");
|
||||
el.classList.remove("ring-accent");
|
||||
});
|
||||
let currentLibraryId = "";
|
||||
let mediaItems: unknown[] = [];
|
||||
|
||||
const selected = document.querySelector(`[data-library-id="${libraryId}"]`);
|
||||
if (selected) {
|
||||
selected.classList.add("ring-2");
|
||||
selected.classList.add("ring-accent");
|
||||
function initBookshelf(): void {
|
||||
const savedLibrary = localStorage.getItem("selectedLibrary");
|
||||
if (savedLibrary) {
|
||||
const select = document.getElementById("library-select") as HTMLSelectElement;
|
||||
if (select && select.value) {
|
||||
currentLibraryId = savedLibrary;
|
||||
loadBookshelf(savedLibrary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectLibrary(): void {
|
||||
const select = document.getElementById("library-select") as HTMLSelectElement;
|
||||
if (!select) return;
|
||||
|
||||
const libraryId = select.value;
|
||||
if (!libraryId) return;
|
||||
|
||||
currentLibraryId = libraryId;
|
||||
localStorage.setItem("selectedLibrary", libraryId);
|
||||
loadBookshelf(libraryId);
|
||||
}
|
||||
|
||||
async function loadBookshelf(libraryId: string): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
if (!token || !libraryId) return;
|
||||
|
||||
const loading = document.getElementById("loading");
|
||||
const booksGrid = document.getElementById("books-grid");
|
||||
const emptyState = document.getElementById("empty-state");
|
||||
|
||||
if (loading) loading.style.display = "block";
|
||||
if (booksGrid) booksGrid.classList.add("hidden");
|
||||
if (emptyState) emptyState.classList.add("hidden");
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/libraries/${libraryId}/books`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const response = await fetch(
|
||||
`/api/media-items?library_id=${libraryId}&limit=100&offset=0`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
renderBooks(data.books || []);
|
||||
mediaItems = data;
|
||||
renderBookshelf();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load bookshelf:", error);
|
||||
console.error("Error loading bookshelf:", error);
|
||||
showToast("Error loading books", "error");
|
||||
} finally {
|
||||
if (loading) loading.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function renderBooks(books: unknown[]): void {
|
||||
const container = document.getElementById("books-grid");
|
||||
if (!container) return;
|
||||
function showEmptyState(): void {
|
||||
const booksGrid = document.getElementById("books-grid");
|
||||
const emptyState = document.getElementById("empty-state");
|
||||
const loading = document.getElementById("loading");
|
||||
|
||||
if (books.length === 0) {
|
||||
container.innerHTML =
|
||||
'<p class="text-center p-8" style="color: var(--text-secondary)">No books in this library</p>';
|
||||
if (loading) loading.style.display = "none";
|
||||
if (booksGrid) booksGrid.classList.add("hidden");
|
||||
if (emptyState) emptyState.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function renderBookshelf(): void {
|
||||
const booksGrid = document.getElementById("books-grid");
|
||||
const emptyState = document.getElementById("empty-state");
|
||||
const loading = document.getElementById("loading");
|
||||
|
||||
if (!booksGrid) return;
|
||||
|
||||
if (loading) loading.style.display = "none";
|
||||
if (emptyState) emptyState.classList.add("hidden");
|
||||
|
||||
if (!mediaItems || mediaItems.length === 0) {
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = books
|
||||
.map(
|
||||
(book: any) => `
|
||||
<div class="book-card p-3 rounded-lg border transition-transform hover:scale-105 cursor-pointer"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border)"
|
||||
onclick="window.selectBook('${book.id}')">
|
||||
${
|
||||
book.cover_image_path
|
||||
? `<img src="${book.cover_image_path}" alt="${book.title}" class="w-full h-48 object-cover rounded mb-2">`
|
||||
: `<div class="w-full h-48 rounded mb-2 flex items-center justify-center" style="background-color: var(--bg-primary)">
|
||||
<span class="text-4xl">📖</span>
|
||||
</div>`
|
||||
}
|
||||
<h3 class="font-medium text-sm truncate" style="color: var(--text-primary)">${book.title}</h3>
|
||||
<p class="text-xs truncate" style="color: var(--text-secondary)">${book.author || "Unknown Author"}</p>
|
||||
booksGrid.classList.remove("hidden");
|
||||
|
||||
const booksPerShelf = 6;
|
||||
const shelves: unknown[][] = [];
|
||||
|
||||
for (let i = 0; i < mediaItems.length; i += booksPerShelf) {
|
||||
shelves.push(mediaItems.slice(i, i + booksPerShelf));
|
||||
}
|
||||
|
||||
let html = "";
|
||||
shelves.forEach((shelfBooks) => {
|
||||
html += `<div class="relative bg-gradient-to-b from-transparent to-black/10 p-8 mb-4 rounded-lg" style="padding-bottom: 3rem;">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
${shelfBooks.map((book: any) => renderBookCard(book)).join("")}
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 h-3 rounded-b-lg" style="background: linear-gradient(to bottom, rgba(107, 68, 35, 0.3) 0%, rgba(107, 68, 35, 0.5) 50%, rgba(107, 68, 35, 0.3) 100%); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);"></div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
booksGrid.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderBookCard(book: any): string {
|
||||
const coverUrl = book.cover_image_path || "/static/placeholder-book.svg";
|
||||
const authorHtml = book.author
|
||||
? `<p class="text-xs" style="color: var(--text-secondary)">${book.author}</p>`
|
||||
: "";
|
||||
return `<div class="relative transition-all duration-200 ease hover:-translate-y-2 hover:-rotate-2 hover:shadow-2xl hover:z-10 cursor-pointer" data-book-id="${book.id}">
|
||||
<div class="aspect-[2/3] overflow-hidden rounded shadow-[2px_2px_4px_rgba(0,0,0,0.2),-1px_-1px_2px_rgba(255,255,255,0.1)_inset] relative">
|
||||
<div class="absolute left-0 top-0 bottom-0 w-1" style="background: linear-gradient(to right, rgba(0, 0, 0, 0.3) 0%, rgba(255, 255, 255, 0.1) 50%, transparent 100%);"></div>
|
||||
<img src="${coverUrl}"
|
||||
alt="${book.title}"
|
||||
class="w-full h-full object-cover"
|
||||
onerror="this.src='/static/placeholder-book.svg'"
|
||||
>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
<div class="mt-2">
|
||||
<h3 class="text-sm font-semibold line-clamp-2" style="color: var(--text-primary)">${book.title}</h3>
|
||||
${authorHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function viewBook(_bookId: string): void {
|
||||
showToast("Book viewer coming soon!", "info");
|
||||
}
|
||||
|
||||
function selectBook(bookId: string): void {
|
||||
@@ -70,11 +141,9 @@ function selectBook(bookId: string): void {
|
||||
}
|
||||
|
||||
function changePage(page: number): void {
|
||||
const libraryId = localStorage.getItem("selectedLibrary");
|
||||
if (!libraryId) return;
|
||||
|
||||
if (!currentLibraryId) return;
|
||||
const offset = (page - 1) * 50;
|
||||
loadBookshelfPaginated(libraryId, offset);
|
||||
loadBookshelfPaginated(currentLibraryId, offset);
|
||||
}
|
||||
|
||||
async function loadBookshelfPaginated(
|
||||
@@ -86,45 +155,53 @@ async function loadBookshelfPaginated(
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/libraries/${libraryId}/books?offset=${offset}&limit=50`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
},
|
||||
`/api/media-items?library_id=${libraryId}&limit=50&offset=${offset}`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } },
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
renderBooks(data.books || []);
|
||||
updatePagination(data.total, offset);
|
||||
mediaItems = data;
|
||||
renderBookshelf();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load bookshelf:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePagination(total: number, offset: number): void {
|
||||
const container = document.getElementById("pagination");
|
||||
function setupEventDelegation(): void {
|
||||
const container = document.getElementById("books-grid");
|
||||
if (!container) return;
|
||||
|
||||
const limit = 50;
|
||||
const currentPage = Math.floor(offset / limit) + 1;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
container.addEventListener("click", (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const card = target.closest("[data-book-id]") as HTMLElement;
|
||||
|
||||
if (totalPages <= 1) {
|
||||
container.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="flex justify-center space-x-2">
|
||||
${currentPage > 1 ? `<button onclick="window.changePage(${currentPage - 1})" class="btn-secondary px-3 py-1 rounded">Previous</button>` : ""}
|
||||
<span class="px-3 py-1" style="color: var(--text-secondary)">Page ${currentPage} of ${totalPages}</span>
|
||||
${currentPage < totalPages ? `<button onclick="window.changePage(${currentPage + 1})" class="btn-secondary px-3 py-1 rounded">Next</button>` : ""}
|
||||
</div>
|
||||
`;
|
||||
if (card) {
|
||||
const bookId = card.dataset.bookId;
|
||||
if (bookId) {
|
||||
selectBook(bookId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
(window as any).selectLibrary = selectLibrary;
|
||||
(window as any).loadBookshelf = loadBookshelf;
|
||||
(window as any).selectBook = selectBook;
|
||||
(window as any).changePage = changePage;
|
||||
export {
|
||||
changePage,
|
||||
initBookshelf,
|
||||
loadBookshelf,
|
||||
selectBook,
|
||||
selectLibrary,
|
||||
setupEventDelegation,
|
||||
viewBook,
|
||||
};
|
||||
|
||||
Alpine.global("bookshelf", {
|
||||
changePage,
|
||||
initBookshelf,
|
||||
loadBookshelf,
|
||||
selectBook,
|
||||
selectLibrary,
|
||||
setupEventDelegation,
|
||||
viewBook,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user