/** * — file browsing, upload, share links, and memory cards. * * Attributes: * space="slug" — shared space to browse (default: "default") */ class FolkFileBrowser extends HTMLElement { private shadow: ShadowRoot; private space = "default"; private files: any[] = []; private cards: any[] = []; private tab: "files" | "cards" = "files"; private loading = false; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); } connectedCallback() { this.space = this.getAttribute("space") || "default"; this.render(); this.loadFiles(); this.loadCards(); } private async loadFiles() { this.loading = true; this.render(); try { const base = this.getApiBase(); const res = await fetch(`${base}/api/files?space=${encodeURIComponent(this.space)}`); const data = await res.json(); this.files = data.files || []; } catch (e) { console.error("[FileBrowser] Error loading files:", e); } this.loading = false; this.render(); } private async loadCards() { try { const base = this.getApiBase(); const res = await fetch(`${base}/api/cards?space=${encodeURIComponent(this.space)}`); const data = await res.json(); this.cards = data.cards || []; } catch { this.cards = []; } } private getApiBase(): string { const path = window.location.pathname; const match = path.match(/^\/([^/]+)\/files/); return match ? `/${match[1]}/files` : ""; } private formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; } private formatDate(d: string): string { return new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); } private mimeIcon(mime: string): string { if (mime?.startsWith("image/")) return "\uD83D\uDDBC\uFE0F"; if (mime?.startsWith("video/")) return "\uD83C\uDFA5"; if (mime?.startsWith("audio/")) return "\uD83C\uDFB5"; if (mime?.includes("pdf")) return "\uD83D\uDCC4"; if (mime?.includes("zip") || mime?.includes("tar") || mime?.includes("gz")) return "\uD83D\uDCE6"; if (mime?.includes("text") || mime?.includes("json") || mime?.includes("xml")) return "\uD83D\uDCDD"; return "\uD83D\uDCC1"; } private cardTypeIcon(type: string): string { const icons: Record = { note: "\uD83D\uDCDD", idea: "\uD83D\uDCA1", task: "\u2705", reference: "\uD83D\uDD17", quote: "\uD83D\uDCAC", }; return icons[type] || "\uD83D\uDCDD"; } private async handleUpload(e: Event) { e.preventDefault(); const form = this.shadow.querySelector("#upload-form") as HTMLFormElement; if (!form) return; const fileInput = form.querySelector('input[type="file"]') as HTMLInputElement; if (!fileInput?.files?.length) return; const formData = new FormData(); formData.append("file", fileInput.files[0]); formData.append("space", this.space); const titleInput = form.querySelector('input[name="title"]') as HTMLInputElement; if (titleInput?.value) formData.append("title", titleInput.value); try { const base = this.getApiBase(); const res = await fetch(`${base}/api/files`, { method: "POST", body: formData }); if (res.ok) { form.reset(); this.loadFiles(); } else { const err = await res.json(); alert(`Upload failed: ${err.error || "Unknown error"}`); } } catch (e) { alert("Upload failed — network error"); } } private async handleDelete(fileId: string) { if (!confirm("Delete this file?")) return; try { const base = this.getApiBase(); await fetch(`${base}/api/files/${fileId}`, { method: "DELETE" }); this.loadFiles(); } catch {} } private async handleShare(fileId: string) { try { const base = this.getApiBase(); const res = await fetch(`${base}/api/files/${fileId}/share`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ expires_in_hours: 72 }), }); const data = await res.json(); if (data.share?.url) { const fullUrl = `${window.location.origin}${this.getApiBase()}${data.share.url}`; await navigator.clipboard.writeText(fullUrl).catch(() => {}); alert(`Share link copied!\n${fullUrl}\nExpires in 72 hours.`); } } catch { alert("Failed to create share link"); } } private async handleCreateCard(e: Event) { e.preventDefault(); const form = this.shadow.querySelector("#card-form") as HTMLFormElement; if (!form) return; const title = (form.querySelector('input[name="card-title"]') as HTMLInputElement)?.value; const body = (form.querySelector('textarea[name="card-body"]') as HTMLTextAreaElement)?.value; const cardType = (form.querySelector('select[name="card-type"]') as HTMLSelectElement)?.value; if (!title) return; try { const base = this.getApiBase(); const res = await fetch(`${base}/api/cards`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, body, card_type: cardType, shared_space: this.space }), }); if (res.ok) { form.reset(); this.loadCards(); } } catch {} } private async handleDeleteCard(cardId: string) { if (!confirm("Delete this card?")) return; try { const base = this.getApiBase(); await fetch(`${base}/api/cards/${cardId}`, { method: "DELETE" }); this.loadCards(); } catch {} } private render() { const filesActive = this.tab === "files"; this.shadow.innerHTML = `
\uD83D\uDCC1 Files
\uD83C\uDFB4 Memory Cards
${filesActive ? this.renderFilesTab() : this.renderCardsTab()} `; this.shadow.querySelectorAll(".tab-btn").forEach((btn) => { btn.addEventListener("click", () => { this.tab = (btn as HTMLElement).dataset.tab as "files" | "cards"; this.render(); }); }); const uploadForm = this.shadow.querySelector("#upload-form"); if (uploadForm) uploadForm.addEventListener("submit", (e) => this.handleUpload(e)); const cardForm = this.shadow.querySelector("#card-form"); if (cardForm) cardForm.addEventListener("submit", (e) => this.handleCreateCard(e)); this.shadow.querySelectorAll("[data-action]").forEach((btn) => { const action = (btn as HTMLElement).dataset.action!; const id = (btn as HTMLElement).dataset.id!; btn.addEventListener("click", () => { if (action === "delete") this.handleDelete(id); else if (action === "share") this.handleShare(id); else if (action === "delete-card") this.handleDeleteCard(id); else if (action === "download") { const base = this.getApiBase(); window.open(`${base}/api/files/${id}/download`, "_blank"); } }); }); } private renderFilesTab(): string { return `

Upload File

${this.loading ? '
Loading files...
' : ""} ${!this.loading && this.files.length === 0 ? '
No files yet. Upload one above.
' : ""} ${ !this.loading && this.files.length > 0 ? `
${this.files .map( (f) => `
${this.mimeIcon(f.mime_type)}
${this.esc(f.title || f.original_filename)}
${this.formatSize(f.file_size)} · ${this.formatDate(f.created_at)}
`, ) .join("")}
` : "" } `; } private renderCardsTab(): string { return `

New Memory Card

${this.cards.length === 0 ? '
No memory cards yet.
' : ""} ${ this.cards.length > 0 ? `
${this.cards .map( (c) => `
${this.cardTypeIcon(c.card_type)} ${this.esc(c.title)} ${c.card_type}
${c.body ? `
${this.esc(c.body)}
` : ""}
`, ) .join("")}
` : "" } `; } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; } } customElements.define("folk-file-browser", FolkFileBrowser);