/** * — Book grid with search, tags, and upload. * * Displays community books in a responsive grid. Clicking a book * navigates to the flipbook reader. Authenticated users can upload. */ interface BookData { id: string; slug: string; title: string; author: string | null; description: string | null; pdf_size_bytes: number; page_count: number; tags: string[]; cover_color: string; contributor_name: string | null; featured: boolean; view_count: number; created_at: string; } export class FolkBookShelf extends HTMLElement { private _books: BookData[] = []; private _filtered: BookData[] = []; private _spaceSlug = "personal"; private _searchTerm = ""; private _selectedTag: string | null = null; static get observedAttributes() { return ["space-slug"]; } set books(val: BookData[]) { this._books = val; this._filtered = val; this.render(); } get books() { return this._books; } set spaceSlug(val: string) { this._spaceSlug = val; } connectedCallback() { this.attachShadow({ mode: "open" }); this.render(); if (this._spaceSlug === "demo" || this.getAttribute("space") === "demo") { this.loadDemoBooks(); } } private loadDemoBooks() { this.books = [ { id: "b1", slug: "amusing-ourselves-to-death", title: "Amusing Ourselves to Death", author: "Neil Postman", description: "A prophetic look at what happens when politics, journalism, education, and even religion become subject to the demands of entertainment. Originally published in 1985, this book remains incredibly relevant in the age of social media and constant digital distraction.", pdf_size_bytes: 2457600, page_count: 231, tags: ["media criticism", "society", "technology", "culture"], cover_color: "#1e3a5f", contributor_name: null, featured: true, view_count: 523, created_at: "2025-11-10" }, { id: "b2", slug: "interference", title: "Interference: A Grand Scientific Musical Theory", author: "Richard Merrick", description: "A groundbreaking exploration of Harmonic Interference Theory - a set of principles explaining music perception using the physics of harmonic standing waves, connecting music theory to cymatics, Fibonacci sequences, and the fundamental patterns found throughout nature.", pdf_size_bytes: 8388608, page_count: 524, tags: ["music theory", "harmonics", "cymatics", "science", "perception"], cover_color: "#7c3aed", contributor_name: null, featured: true, view_count: 312, created_at: "2025-12-05" }, { id: "b3", slug: "governing-the-commons", title: "Governing the Commons", author: "Elinor Ostrom", description: "Analysis of collective action and the governance of common-pool resources", pdf_size_bytes: 2457600, page_count: 280, tags: ["economics", "governance"], cover_color: "#2563eb", contributor_name: "Community Library", featured: true, view_count: 342, created_at: "2026-01-15" }, { id: "b4", slug: "the-mushroom-at-the-end-of-the-world", title: "The Mushroom at the End of the World", author: "Anna Lowenhaupt Tsing", description: "On the possibility of life in capitalist ruins", pdf_size_bytes: 3145728, page_count: 352, tags: ["ecology", "anthropology"], cover_color: "#059669", contributor_name: null, featured: false, view_count: 187, created_at: "2026-01-20" }, { id: "b5", slug: "doughnut-economics", title: "Doughnut Economics", author: "Kate Raworth", description: "Seven ways to think like a 21st-century economist", pdf_size_bytes: 1887436, page_count: 320, tags: ["economics"], cover_color: "#d97706", contributor_name: "Reading Circle", featured: false, view_count: 256, created_at: "2026-02-01" }, { id: "b6", slug: "entangled-life", title: "Entangled Life", author: "Merlin Sheldrake", description: "How fungi make our worlds, change our minds, and shape our futures", pdf_size_bytes: 2621440, page_count: 368, tags: ["ecology", "science"], cover_color: "#0891b2", contributor_name: "Mycofi Lab", featured: false, view_count: 431, created_at: "2026-02-10" }, ]; } attributeChangedCallback(name: string, _old: string, val: string) { if (name === "space-slug") this._spaceSlug = val; } private get allTags(): string[] { const tags = new Set(); for (const b of this._books) { for (const t of b.tags || []) tags.add(t); } return Array.from(tags).sort(); } private applyFilters() { let result = this._books; if (this._searchTerm) { const term = this._searchTerm.toLowerCase(); result = result.filter( (b) => b.title.toLowerCase().includes(term) || (b.author && b.author.toLowerCase().includes(term)) || (b.description && b.description.toLowerCase().includes(term)) ); } if (this._selectedTag) { const tag = this._selectedTag; result = result.filter((b) => b.tags?.includes(tag)); } this._filtered = result; } private formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1048576) return `${(bytes / 1024).toFixed(0)} KB`; return `${(bytes / 1048576).toFixed(1)} MB`; } private render() { if (!this.shadowRoot) return; const tags = this.allTags; const books = this._filtered; this.shadowRoot.innerHTML = `
Library
${tags.length > 0 ? `
${tags.map((t) => `${t}`).join("")}
` : ""} ${books.length === 0 ? `

No books yet

Upload a PDF to share with the community

` : `` } `; this.bindEvents(); } private bindEvents() { if (!this.shadowRoot) return; // Search const searchInput = this.shadowRoot.querySelector(".search-input") as HTMLInputElement; searchInput?.addEventListener("input", () => { this._searchTerm = searchInput.value; this.applyFilters(); this.updateGrid(); }); // Tags this.shadowRoot.querySelectorAll(".tag").forEach((el) => { el.addEventListener("click", () => { const tag = (el as HTMLElement).dataset.tag!; if (this._selectedTag === tag) { this._selectedTag = null; el.classList.remove("active"); } else { this.shadowRoot!.querySelectorAll(".tag").forEach((t) => t.classList.remove("active")); this._selectedTag = tag; el.classList.add("active"); } this.applyFilters(); this.updateGrid(); }); }); // Upload modal const uploadBtn = this.shadowRoot.querySelector(".upload-btn"); const overlay = this.shadowRoot.querySelector(".modal-overlay") as HTMLElement; const cancelBtn = this.shadowRoot.querySelector(".btn-cancel"); const submitBtn = this.shadowRoot.querySelector(".btn-submit") as HTMLButtonElement; const dropZone = this.shadowRoot.querySelector(".drop-zone") as HTMLElement; const fileInput = this.shadowRoot.querySelector('input[type="file"]') as HTMLInputElement; const titleInput = this.shadowRoot.querySelector('input[name="title"]') as HTMLInputElement; const errorEl = this.shadowRoot.querySelector(".error-msg") as HTMLElement; let selectedFile: File | null = null; uploadBtn?.addEventListener("click", () => { overlay.hidden = false; }); cancelBtn?.addEventListener("click", () => { overlay.hidden = true; selectedFile = null; }); overlay?.addEventListener("click", (e) => { if (e.target === overlay) overlay.hidden = true; }); dropZone?.addEventListener("click", () => fileInput?.click()); dropZone?.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("dragover"); }); dropZone?.addEventListener("dragleave", () => dropZone.classList.remove("dragover")); dropZone?.addEventListener("drop", (e) => { e.preventDefault(); dropZone.classList.remove("dragover"); const file = (e as DragEvent).dataTransfer?.files[0]; if (file?.type === "application/pdf") { selectedFile = file; dropZone.innerHTML = `${file.name}`; if (titleInput.value) submitBtn.disabled = false; } }); fileInput?.addEventListener("change", () => { if (fileInput.files?.[0]) { selectedFile = fileInput.files[0]; dropZone.innerHTML = `${selectedFile.name}`; if (titleInput.value) submitBtn.disabled = false; } }); titleInput?.addEventListener("input", () => { submitBtn.disabled = !titleInput.value.trim() || !selectedFile; }); submitBtn?.addEventListener("click", async () => { if (!selectedFile || !titleInput.value.trim()) return; submitBtn.disabled = true; submitBtn.textContent = "Uploading..."; errorEl.hidden = true; const formData = new FormData(); formData.append("pdf", selectedFile); formData.append("title", titleInput.value.trim()); const authorInput = this.shadowRoot!.querySelector('input[name="author"]') as HTMLInputElement; const descInput = this.shadowRoot!.querySelector('textarea[name="description"]') as HTMLTextAreaElement; const tagsInput = this.shadowRoot!.querySelector('input[name="tags"]') as HTMLInputElement; const licenseInput = this.shadowRoot!.querySelector('input[name="license"]') as HTMLInputElement; if (authorInput.value) formData.append("author", authorInput.value); if (descInput.value) formData.append("description", descInput.value); if (tagsInput.value) formData.append("tags", tagsInput.value); if (licenseInput.value) formData.append("license", licenseInput.value); // Get auth token const token = localStorage.getItem("encryptid_token"); if (!token) { errorEl.textContent = "Please sign in first (use the identity button in the header)"; errorEl.hidden = false; submitBtn.disabled = false; submitBtn.textContent = "Upload"; return; } try { const res = await fetch(`/${this._spaceSlug}/rbooks/api/books`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: formData, }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || "Upload failed"); } // Navigate to the new book window.location.href = `/${this._spaceSlug}/rbooks/read/${data.slug}`; } catch (e: any) { errorEl.textContent = e.message; errorEl.hidden = false; submitBtn.disabled = false; submitBtn.textContent = "Upload"; } }); } private updateGrid() { // Re-render just the grid portion (lightweight update) this.render(); } private escapeHtml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } } customElements.define("folk-book-shelf", FolkBookShelf);