/** * — browseable provider directory. * Shows a grid of provider cards with search, capability filter, and proximity sorting. */ class FolkProviderDirectory extends HTMLElement { private shadow: ShadowRoot; private providers: any[] = []; private capabilities: string[] = []; private selectedCap = ""; private searchQuery = ""; private userLat: number | null = null; private userLng: number | null = null; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); } connectedCallback() { this.render(); this.loadProviders(); } private getApiBase(): string { const path = window.location.pathname; const parts = path.split("/").filter(Boolean); return parts.length >= 2 ? `/${parts[0]}/providers` : "/demo/providers"; } private async loadProviders() { try { const params = new URLSearchParams(); if (this.selectedCap) params.set("capability", this.selectedCap); if (this.userLat && this.userLng) { params.set("lat", String(this.userLat)); params.set("lng", String(this.userLng)); } params.set("limit", "100"); const res = await fetch(`${this.getApiBase()}/api/providers?${params}`); const data = await res.json(); this.providers = data.providers || []; // Collect unique capabilities const capSet = new Set(); for (const p of this.providers) { for (const cap of (p.capabilities || [])) capSet.add(cap); } this.capabilities = Array.from(capSet).sort(); this.render(); } catch (e) { console.error("Failed to load providers:", e); } } private render() { const filtered = this.providers.filter((p) => { if (this.searchQuery) { const q = this.searchQuery.toLowerCase(); const name = (p.name || "").toLowerCase(); const city = (p.location?.city || "").toLowerCase(); const country = (p.location?.country || "").toLowerCase(); if (!name.includes(q) && !city.includes(q) && !country.includes(q)) return false; } return true; }); this.shadow.innerHTML = `

\u{1F3ED} Provider Directory

${this.capabilities.length > 0 ? `
All ${this.capabilities.map((cap) => ` ${cap} `).join("")}
` : ""} ${filtered.length === 0 ? `
No providers found
` : `
${filtered.map((p) => `

${this.esc(p.name)}

${this.esc(p.location?.city || "")}${p.location?.region ? `, ${this.esc(p.location.region)}` : ""} ${this.esc(p.location?.country || "")}
\u2713 Active ${p.distance_km !== undefined ? `${p.distance_km} km` : ""}
${(p.capabilities || []).map((cap: string) => `${this.esc(cap)}`).join("")}
${p.turnaround?.standard_days ? `
\u23F1 ${p.turnaround.standard_days} days standard${p.turnaround.rush_days ? ` / ${p.turnaround.rush_days} days rush (+${p.turnaround.rush_surcharge_pct || 0}%)` : ""}
` : ""}
`).join("")}
`} `; // Event listeners this.shadow.querySelector(".search")?.addEventListener("input", (e) => { this.searchQuery = (e.target as HTMLInputElement).value; this.render(); }); this.shadow.querySelector(".locate-btn")?.addEventListener("click", () => { if (this.userLat) { this.userLat = null; this.userLng = null; this.loadProviders(); } else { navigator.geolocation?.getCurrentPosition( (pos) => { this.userLat = pos.coords.latitude; this.userLng = pos.coords.longitude; this.loadProviders(); }, () => { console.warn("Geolocation denied"); } ); } }); this.shadow.querySelectorAll(".cap[data-cap]").forEach((el) => { el.addEventListener("click", () => { this.selectedCap = (el as HTMLElement).dataset.cap || ""; this.loadProviders(); }); }); } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } } customElements.define("folk-provider-directory", FolkProviderDirectory);