/** * — Community photo gallery powered by Immich. * * Browse shared albums, view recent photos, and open the full * Immich interface for uploads and management. * * Attributes: * space — space slug (default: "demo") */ interface Album { id: string; albumName: string; description: string; assetCount: number; albumThumbnailAssetId: string | null; updatedAt: string; shared: boolean; } interface Asset { id: string; type: string; originalFileName: string; exifInfo?: { city?: string; country?: string; dateTimeOriginal?: string; make?: string; model?: string; }; fileCreatedAt: string; } class FolkPhotoGallery extends HTMLElement { private shadow: ShadowRoot; private space = "demo"; private view: "gallery" | "album" | "lightbox" = "gallery"; private albums: Album[] = []; private assets: Asset[] = []; private albumAssets: Asset[] = []; private selectedAlbum: Album | null = null; private lightboxAsset: Asset | null = null; private loading = false; private error = ""; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); } connectedCallback() { this.space = this.getAttribute("space") || "demo"; this.loadGallery(); } private getApiBase(): string { const path = window.location.pathname; const match = path.match(/^\/([^/]+)\/photos/); return match ? `/${match[1]}/photos` : ""; } private getImmichUrl(): string { return `https://${this.space}.rphotos.online`; } private async loadGallery() { this.loading = true; this.render(); const base = this.getApiBase(); try { const [albumsRes, assetsRes] = await Promise.all([ fetch(`${base}/api/albums`), fetch(`${base}/api/assets?size=24`), ]); const albumsData = await albumsRes.json(); const assetsData = await assetsRes.json(); this.albums = albumsData.albums || []; this.assets = assetsData.assets || []; } catch { this.error = "Could not connect to photo service. Make sure Immich is running."; } this.loading = false; this.render(); } private async loadAlbum(album: Album) { this.selectedAlbum = album; this.view = "album"; this.loading = true; this.render(); try { const base = this.getApiBase(); const res = await fetch(`${base}/api/albums/${album.id}`); const data = await res.json(); this.albumAssets = data.assets || []; } catch { this.error = "Failed to load album"; } this.loading = false; this.render(); } private openLightbox(asset: Asset) { this.lightboxAsset = asset; this.view = "lightbox"; this.render(); } private closeLightbox() { this.lightboxAsset = null; this.view = this.selectedAlbum ? "album" : "gallery"; this.render(); } private thumbUrl(assetId: string): string { const base = this.getApiBase(); return `${base}/api/assets/${assetId}/thumbnail`; } private originalUrl(assetId: string): string { const base = this.getApiBase(); return `${base}/api/assets/${assetId}/original`; } private formatDate(d: string): string { return new Date(d).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", }); } private render() { this.shadow.innerHTML = ` ${this.error ? `
${this.esc(this.error)}
` : ""} ${this.loading ? '
Loading photos...
' : ""} ${!this.loading ? this.renderView() : ""} ${this.view === "lightbox" && this.lightboxAsset ? this.renderLightbox() : ""} `; this.attachListeners(); } private renderView(): string { if (this.view === "album" && this.selectedAlbum) return this.renderAlbum(); return this.renderGalleryView(); } private renderGalleryView(): string { const hasContent = this.albums.length > 0 || this.assets.length > 0; return `
Photos
${!hasContent ? `
📸

No photos yet

Upload photos through Immich to see them here. Shared albums will appear automatically.

Open Immich to Upload
` : ""} ${this.albums.length > 0 ? `
Shared Albums
${this.albums.map((a) => `
${a.albumThumbnailAssetId ? `${this.esc(a.albumName)}` : '📷'}
${this.esc(a.albumName)}
${a.assetCount} photo${a.assetCount !== 1 ? "s" : ""}
`).join("")}
` : ""} ${this.assets.length > 0 ? `
Recent Photos
${this.assets.map((a) => `
${this.esc(a.originalFileName)}
`).join("")}
` : ""} `; } private renderAlbum(): string { const album = this.selectedAlbum!; return `
${this.esc(album.albumName)}
${this.albumAssets.length === 0 ? `
📷

Album is empty

Add photos to this album in Immich.

` : `
${this.albumAssets.map((a) => `
${this.esc(a.originalFileName)}
`).join("")}
`} `; } private renderLightbox(): string { const asset = this.lightboxAsset!; const info = asset.exifInfo; const location = [info?.city, info?.country].filter(Boolean).join(", "); const camera = [info?.make, info?.model].filter(Boolean).join(" "); return ` `; } private attachListeners() { // Album cards this.shadow.querySelectorAll("[data-album-id]").forEach((el) => { el.addEventListener("click", () => { const id = (el as HTMLElement).dataset.albumId!; const album = this.albums.find((a) => a.id === id); if (album) this.loadAlbum(album); }); }); // Photo cells this.shadow.querySelectorAll("[data-asset-id]").forEach((el) => { el.addEventListener("click", () => { const id = (el as HTMLElement).dataset.assetId!; const assets = this.view === "album" ? this.albumAssets : this.assets; const asset = assets.find((a) => a.id === id); if (asset) this.openLightbox(asset); }); }); // Back button this.shadow.querySelectorAll("[data-back]").forEach((el) => { el.addEventListener("click", () => { this.selectedAlbum = null; this.albumAssets = []; this.view = "gallery"; this.render(); }); }); // Lightbox close this.shadow.querySelector("[data-close-lightbox]")?.addEventListener("click", () => this.closeLightbox()); this.shadow.querySelector("[data-lightbox]")?.addEventListener("click", (e) => { if ((e.target as HTMLElement).matches("[data-lightbox]")) this.closeLightbox(); }); } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; } } customElements.define("folk-photo-gallery", FolkPhotoGallery);