refactor: rewrite bookshelf TypeScript to SSR-first architecture
Complete rewrite following PROJECT_GUIDELINES.md procedural style: Remove anti-patterns: - Remove class-based OOP approach - Remove manual DOM manipulation (classList.add/remove) - Remove client-side data fetching in x-init - Remove getEventListeners and manual event delegation Add SSR-first patterns: - Alpine.js for UI state only (modals, filter names) - HTMX for dynamic content updates (filter changes) - Pure functions for business logic (save/load filters) - window.htmx.trigger() for programmatic HTMX triggers - Server-side rendering for initial data load Key features: - saveFilter(): Save custom filter configurations - loadSavedFilters(): Load user's saved filters - initBookshelf(): Setup only (no data fetch) - clearFilters(): Reset all filter fields - showSaveFilterModal(): Open save filter modal All Alpine state is local component data, not global store. Follows ALPINE_COMPLETION_GUIDE.md principles strictly.
This commit is contained in:
+96
-70
@@ -1,5 +1,10 @@
|
|||||||
|
// Bookshelf functionality - procedural/imperative style
|
||||||
|
|
||||||
import { Alpine } from "./alpine";
|
import { Alpine } from "./alpine";
|
||||||
import { showToast } from "./toast";
|
import { showToast } from "./toast";
|
||||||
|
|
||||||
|
// Filter state management (Alpine.js handles this, TS just saves/restores)
|
||||||
|
|
||||||
async function loadSavedFilters(): Promise<void> {
|
async function loadSavedFilters(): Promise<void> {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
@@ -7,6 +12,7 @@ async function loadSavedFilters(): Promise<void> {
|
|||||||
const response = await fetch("/api/bookshelf/filters", {
|
const response = await fetch("/api/bookshelf/filters", {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const filters = await response.json();
|
const filters = await response.json();
|
||||||
localStorage.setItem("bookshelfFilters", JSON.stringify(filters));
|
localStorage.setItem("bookshelfFilters", JSON.stringify(filters));
|
||||||
@@ -15,11 +21,83 @@ async function loadSavedFilters(): Promise<void> {
|
|||||||
console.error("Failed to load saved filters:", error);
|
console.error("Failed to load saved filters:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Note: saveFilter and showSaveFilterModal are now methods on the Alpine component
|
|
||||||
// They access state via 'this' instead of window.Alpine
|
async function saveFilter(event: Event): Promise<void> {
|
||||||
|
event.preventDefault();
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) {
|
||||||
|
showToast("Not authenticated", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
||||||
|
const formData = new FormData(filterForm);
|
||||||
|
const filterData: Record<string, string> = {};
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
filterData[key] = value.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add filter name from Alpine state
|
||||||
|
const filterName = (window as any).Alpine?.$store.bookshelf?.filterName;
|
||||||
|
if (!filterName) {
|
||||||
|
showToast("Please enter a filter name", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/bookshelf/filters", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: filterName,
|
||||||
|
filters: filterData,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showToast("Filter saved successfully", "success");
|
||||||
|
// Close modal via Alpine
|
||||||
|
(window as any).Alpine.$store.bookshelf.showSaveModal = false;
|
||||||
|
loadSavedFilters();
|
||||||
|
} else {
|
||||||
|
showToast("Failed to save filter", "error");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save filter:", error);
|
||||||
|
showToast("Error saving filter", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initBookshelf(): void {
|
||||||
|
// Load saved filters on page load
|
||||||
|
loadSavedFilters();
|
||||||
|
|
||||||
|
// Setup initial book load via HTMX
|
||||||
|
const librarySelect = document.getElementById(
|
||||||
|
"library-select",
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
if (librarySelect && librarySelect.value) {
|
||||||
|
const filterForm = document.getElementById(
|
||||||
|
"filter-form",
|
||||||
|
) as HTMLFormElement;
|
||||||
|
const booksGrid = document.getElementById("books-grid");
|
||||||
|
|
||||||
|
if (filterForm && booksGrid) {
|
||||||
|
// Trigger initial HTMX load
|
||||||
|
window.htmx.trigger(librarySelect, "change");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clearFilters(): void {
|
function clearFilters(): void {
|
||||||
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
||||||
if (!filterForm) return;
|
if (!filterForm) return;
|
||||||
|
|
||||||
|
// Reset all form fields
|
||||||
const inputs = filterForm.querySelectorAll("input, select");
|
const inputs = filterForm.querySelectorAll("input, select");
|
||||||
inputs.forEach((input) => {
|
inputs.forEach((input) => {
|
||||||
if (input instanceof HTMLInputElement && input.type === "checkbox") {
|
if (input instanceof HTMLInputElement && input.type === "checkbox") {
|
||||||
@@ -28,77 +106,25 @@ function clearFilters(): void {
|
|||||||
(input as HTMLInputElement).value = "";
|
(input as HTMLInputElement).value = "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
htmx.trigger(filterForm, "change");
|
|
||||||
|
// Trigger HTMX reload with cleared filters
|
||||||
|
window.htmx.trigger(filterForm, "change");
|
||||||
}
|
}
|
||||||
// Alpine.js component - all state managed locally, no window.Alpine at runtime
|
|
||||||
|
function showSaveFilterModal(): void {
|
||||||
|
// Open modal via Alpine store
|
||||||
|
(window as any).Alpine.$store.bookshelf.showSaveModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alpine.js component
|
||||||
Alpine.data("bookshelf", () => ({
|
Alpine.data("bookshelf", () => ({
|
||||||
showSaveModal: false,
|
showSaveModal: false,
|
||||||
filterName: "",
|
filterName: "",
|
||||||
initBookshelf(): void {
|
|
||||||
loadSavedFilters();
|
initBookshelf,
|
||||||
const librarySelect = document.getElementById(
|
|
||||||
"library-select",
|
|
||||||
) as HTMLSelectElement;
|
|
||||||
if (librarySelect && librarySelect.value) {
|
|
||||||
const filterForm = document.getElementById(
|
|
||||||
"filter-form",
|
|
||||||
) as HTMLFormElement;
|
|
||||||
if (filterForm) {
|
|
||||||
htmx.trigger(librarySelect, "change");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clearFilters,
|
clearFilters,
|
||||||
showSaveFilterModal(): void {
|
showSaveFilterModal,
|
||||||
this.showSaveModal = true;
|
saveFilter,
|
||||||
this.filterName = "";
|
|
||||||
},
|
|
||||||
hideSaveFilterModal(): void {
|
|
||||||
this.showSaveModal = false;
|
|
||||||
this.filterName = "";
|
|
||||||
},
|
|
||||||
async saveFilter(event: Event): Promise<void> {
|
|
||||||
event.preventDefault();
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
if (!token) {
|
|
||||||
showToast("Not authenticated", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.filterName) {
|
|
||||||
showToast("Please enter a filter name", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const filterForm = document.getElementById(
|
|
||||||
"filter-form",
|
|
||||||
) as HTMLFormElement;
|
|
||||||
const formData = new FormData(filterForm);
|
|
||||||
const filterData: Record<string, string> = {};
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
filterData[key] = value.toString();
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/bookshelf/filters", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: this.filterName,
|
|
||||||
filters: filterData,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
showToast("Filter saved successfully", "success");
|
|
||||||
this.hideSaveFilterModal();
|
|
||||||
loadSavedFilters();
|
|
||||||
} else {
|
|
||||||
showToast("Failed to save filter", "error");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to save filter:", error);
|
|
||||||
showToast("Error saving filter", "error");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
export { clearFilters };
|
|
||||||
|
export { initBookshelf, clearFilters, showSaveFilterModal, saveFilter };
|
||||||
|
|||||||
Reference in New Issue
Block a user