const API_ROOT = window.location.origin; import ImgLoader from "./imgLoader.js"; import AuthSession from "./sessionAuth.js"; import { fallbackToRedirect, openAuthPopup, waitForAuthPopup } from "./authPopup.js"; import "./pxme-gallery.js"; import "./components/pxme-image-editor.js"; const sampleItems = { images: [ { id: "1", title: "Sample Image 1", description: "This is a sample image description.", uri: "../test/images/th.jpg", thumbnailUri: "../test/thumbnails/th_thumbnail.jpg", categories: ["Nature", "Landscape"], }, { id: "2", title: "Sample Image 2", description: "This is another sample image description.", uri: "../test/images/1000015181.jpg", thumbnailUri: "../test/thumbnails/1000015181_thumbnail.jpg", categories: ["City", "Architecture"], }, ], total: 2, count: 2, limit: 10, offset: 0, }; const placeholderItem = { id: "placeholder", title: "Placeholder Image", description: "This is a placeholder image.", uri: "../test/images/placeholder.jpg", thumbnailUri: "/thumbnails/placeholder_thumbnail.jpg", categories: ["Placeholder"], }; if (typeof window !== "undefined") { window.sampleItems = sampleItems; window.placeholderItem = placeholderItem; } const { fetchImages, fetchImage } = ImgLoader({ apiRoot: API_ROOT, sampleItems: sampleItems, }); const maxItems = parseInt(new URLSearchParams(window.location.search).get("limit")) || 100; // Active filter state let personFilter = new URLSearchParams(window.location.search).get("person") || ""; let categoryFilter = new URLSearchParams(window.location.search).get("category") || ""; let searchFilter = new URLSearchParams(window.location.search).get("search") || ""; let sortOrder = new URLSearchParams(window.location.search).get("sort") || "newest"; let nextOffset = 0; const gallery = document.querySelector("pxme-gallery"); const toolbar = document.querySelector("#pxme-toolbar"); const hero = document.querySelector("#pxme-hero"); const pending = document.querySelector("#pxme-pending"); let loggedIn = false; function setHeroStatus(message) { if (!hero) return; if (message) { hero.setAttribute("status", message); } else { hero.removeAttribute("status"); } if (typeof hero.render === "function") { hero.render(); } } document.addEventListener("hero-signin", async () => { setHeroStatus("Waiting for Google sign-in to complete..."); const popup = openAuthPopup(); if (!popup) { setHeroStatus("Popup blocked. Redirecting to the standard sign-in page..."); fallbackToRedirect(); return; } try { await waitForAuthPopup(popup); setHeroStatus("Sign-in complete. Loading your gallery..."); window.location.reload(); } catch (error) { const code = error instanceof Error ? error.message : "auth_error"; if (code === "popup_blocked" || code === "auth_timeout") { setHeroStatus("Popup unavailable. Redirecting to the standard sign-in page..."); fallbackToRedirect(); return; } if (code === "auth_cancelled") { setHeroStatus("Sign-in window closed before completion."); return; } setHeroStatus("We could not complete sign-in. Please try again."); } }); const loadMoreItems = () => { if (nextOffset === null) return; gallery.showMoreLoader(); fetchImages(nextOffset, maxItems, personFilter, categoryFilter, sortOrder, searchFilter) .then((data) => { nextOffset = data.nextOffset; gallery.appendItems(data); }) .catch((error) => { console.error("Error fetching more images:", error); }); }; gallery.loadMore = loadMoreItems; /** * Load images from scratch with current filter state. */ function loadGallery() { if (!loggedIn) { gallery.items = { images: [], count: 0, total: 0 }; return; } nextOffset = 0; gallery.showLoading(); fetchImages(0, maxItems, personFilter, categoryFilter, sortOrder, searchFilter) .then((data) => { nextOffset = data.nextOffset; gallery.items = data; }) .catch((error) => { console.error("Error fetching images:", error); gallery.items = { images: [placeholderItem], count: 1, total: 1, }; }); } // Listen for filter changes from pxme-filter component document.addEventListener("filter-change", (e) => { const { people, categories } = e.detail; personFilter = people.length ? people[0] : ""; // Multiple selected categories use OR semantics: join with commas and let the // backend match images that have ANY of them (CAT-17). categoryFilter = categories.length ? categories.join(",") : ""; // Update URL params for shareability const url = new URL(window.location); if (personFilter) { url.searchParams.set("person", personFilter); } else { url.searchParams.delete("person"); } if (categoryFilter) { url.searchParams.set("category", categoryFilter); } else { url.searchParams.delete("category"); } window.history.replaceState({}, "", url); loadGallery(); }); // Listen for search changes from pxme-search component document.addEventListener("search-change", (e) => { searchFilter = e.detail.search || ""; // Update URL params for shareability const url = new URL(window.location); if (searchFilter) { url.searchParams.set("search", searchFilter); } else { url.searchParams.delete("search"); } window.history.replaceState({}, "", url); loadGallery(); }); // Listen for sort changes from pxme-toolbar component document.addEventListener("sort-change", (e) => { sortOrder = e.detail.sort; // Update URL const url = new URL(window.location); url.searchParams.set("sort", sortOrder); window.history.replaceState({}, "", url); loadGallery(); }); /** * Fetch a single image by ID and update its card in the gallery. */ async function updateSingleImage(imageId) { const imageData = await fetchImage(imageId); if (imageData) { gallery.updateImage(imageId, imageData); } } // Check admin status and enable editing if admin, and connect SSE if authenticated const auth = AuthSession({ authUri: "/auth/session" }); // UAT-only: show the "Test login (UAT)" link on the hero when the server has // the test-auth flag on. A 200 means the route exists; anything else (404 in // prod, 405, network) hides the link. Never leaks credentials. if (hero) { fetch("/auth/test/login", { method: "GET", credentials: "include" }) .then((res) => { if (res.status === 200) { hero.setAttribute("test-login", "true"); hero.render?.(); } }) .catch(() => {}); } auth.isLoggedIn().then((isLoggedIn) => { if (!isLoggedIn) { toolbar.hidden = true; gallery.hidden = true; pending.hidden = true; hero.hidden = false; return; } auth.getSession().then((session) => { if (!session?.user?.approved) { toolbar.hidden = true; gallery.hidden = true; hero.hidden = true; pending.hidden = false; if (session.user.email) { pending.setAttribute("email", session.user.email); pending.render?.(); } return; } loggedIn = true; toolbar.hidden = false; gallery.hidden = false; hero.hidden = true; pending.hidden = true; loadGallery(); }); }); // Listen for edit-image events from the gallery document.addEventListener("edit-image", async (e) => { const { image } = e.detail; // Remove any existing editor const existing = document.querySelector("pxme-image-editor"); if (existing) existing.remove(); const editor = document.createElement("pxme-image-editor"); editor.isAdmin = (await auth.isAdmin()) || false; editor.image = image; document.body.appendChild(editor); }); // Refresh gallery when the image editor signals changes were made document.addEventListener("image-updated", (e) => { const imageId = e.detail?.id; if (imageId) { updateSingleImage(imageId); } else { loadGallery(); } });