Spaces:
Running
Running
| import { CONFIG } from "./config.js"; | |
| import { Store } from "./store.js"; | |
| import { Bag } from "./bag.js"; | |
| import { TRACKS } from "./tracks.js"; | |
| import { fetchAllSpacesWithRetry } from "./api.js"; | |
| import { mountCard } from "./embed.js"; | |
| import * as ui from "./ui.js"; | |
| import { initOAuth, signIn, signOut, likeOnHf } from "./oauth.js"; | |
| const $ = (id) => document.getElementById(id); | |
| const state = { | |
| store: new Store({ key: CONFIG.storageKey }), | |
| all: [], // all view models | |
| byId: new Map(), | |
| bag: null, | |
| filterTrack: null, // null | "Backyard" | "Wood" | |
| current: null, // current vm | |
| controller: null, // mountCard controller | |
| oauth: null, | |
| }; | |
| function idsForTrack(track) { | |
| const list = track ? state.all.filter((v) => v.track === track) : state.all; | |
| return list.map((v) => v.id); | |
| } | |
| function eligibleIds() { return idsForTrack(state.filterTrack); } | |
| function prefetchNext() { | |
| if (!CONFIG.prefetchNext || !state.bag) return; | |
| const peekId = state.bag.bag[state.bag.cursor]; | |
| const vm = peekId && state.byId.get(peekId); | |
| if (!vm) return; | |
| let link = document.getElementById("prefetch-link"); | |
| if (!link) { | |
| link = document.createElement("link"); | |
| link.id = "prefetch-link"; | |
| link.rel = "preconnect"; | |
| document.head.append(link); | |
| } | |
| link.href = vm.embedUrl; | |
| } | |
| function show(vm) { | |
| if (!vm) return; | |
| state.controller?.destroy(); | |
| state.current = vm; | |
| state.store.markSeen(vm.id); | |
| ui.renderCard(vm, state.store); | |
| ui.setProgress(state.bag.progress()); | |
| state.controller = mountCard($("stage"), vm, { onSkip: next }); | |
| if (CONFIG.resumeRound) state.store.saveRound({ ...state.bag.serialize(), track: state.filterTrack }); | |
| prefetchNext(); | |
| } | |
| function next() { show(state.byId.get(state.bag.next())); } | |
| function prev() { const id = state.bag.prev(); if (id) show(state.byId.get(id)); } | |
| function cycleTrack() { | |
| const order = [null, ...TRACKS]; | |
| const nextTrack = order[(order.indexOf(state.filterTrack) + 1) % order.length]; | |
| const ids = idsForTrack(nextTrack); | |
| if (!ids.length) { ui.toast(`No spaces in ${nextTrack}`); return; } | |
| state.filterTrack = nextTrack; | |
| ui.setTrackFilterLabel(state.filterTrack); | |
| state.bag.setIds(ids); | |
| next(); | |
| } | |
| function wireKeys() { | |
| document.addEventListener("keydown", (e) => { | |
| const tag = e.target.tagName; | |
| if (tag === "INPUT" || tag === "TEXTAREA") return; | |
| switch (e.key) { | |
| case " ": case "ArrowRight": e.preventDefault(); next(); break; | |
| case "ArrowLeft": prev(); break; | |
| case "l": case "L": $("btn-like").click(); break; | |
| case "s": case "S": $("btn-save").click(); break; | |
| case "o": case "O": if (state.current) window.open(state.current.hfUrl, "_blank"); break; | |
| case "?": ui.showShortcuts(); break; | |
| } | |
| }); | |
| } | |
| function wireButtons() { | |
| $("btn-next").onclick = next; | |
| $("btn-prev").onclick = prev; | |
| $("btn-track").onclick = cycleTrack; | |
| ui.setTrackFilterLabel(null); | |
| $("btn-save").onclick = () => { | |
| if (!state.current) return; | |
| const on = state.store.toggleSaved(state.current.id); | |
| ui.setSaveState(on); | |
| ui.setSavedCount(state.store.savedIds().length); | |
| ui.toast(on ? "Saved 💾" : "Removed from saved"); | |
| }; | |
| $("btn-saved").onclick = () => { | |
| const vms = state.store.savedIds().map((id) => state.byId.get(id)).filter(Boolean); | |
| ui.openDrawer(vms, { | |
| onOpenHere: (id) => { $("drawer").hidden = true; show(state.byId.get(id)); }, | |
| onUnsave: (id) => { state.store.toggleSaved(id); ui.setSavedCount(state.store.savedIds().length); }, | |
| }); | |
| }; | |
| ui.setSavedCount(state.store.savedIds().length); | |
| $("btn-like").onclick = async () => { | |
| const vm = state.current; | |
| if (!vm) return; | |
| const turningOn = !state.store.isLiked(vm.id); | |
| ui.setLikeState(turningOn); // optimistic | |
| const where = await likeOnHf(vm.id, turningOn, state.oauth?.accessToken); | |
| if (where === "hf") { | |
| state.store.setHfLiked(vm.id, turningOn); | |
| vm.likes = Math.max(0, (vm.likes || 0) + (turningOn ? 1 : -1)); | |
| ui.setHfLikes(vm.likes); | |
| ui.toast(turningOn ? "Liked on Hugging Face 💚" : "Unliked"); | |
| } else { | |
| state.store.setLocalLiked(vm.id, turningOn); | |
| ui.toast(state.oauth | |
| ? "Couldn't like on HF — saved locally. Open on HF to like officially ↗" | |
| : "Saved your like locally — sign in to like on HF ↗"); | |
| } | |
| }; | |
| } | |
| function renderAuth() { | |
| ui.renderAuth(state.oauth?.userInfo, { | |
| onSignIn: signIn, | |
| onSignOut: () => { signOut(); state.oauth = null; renderAuth(); }, | |
| }); | |
| } | |
| function showError(msg) { | |
| $("stage").innerHTML = `<div class="fallback"><strong>${msg}</strong> | |
| <div class="fallback-actions"> | |
| <a class="btn" target="_blank" rel="noopener" href="${CONFIG.apiBase}/${CONFIG.org}">Visit the org ↗</a> | |
| <button class="btn btn-ghost" onclick="location.reload()">Retry</button> | |
| </div></div>`; | |
| } | |
| async function boot() { | |
| wireKeys(); | |
| wireButtons(); | |
| try { | |
| state.all = await fetchAllSpacesWithRetry(); | |
| } catch { | |
| showError("Couldn't reach the Hugging Face API. Check your connection and retry."); | |
| return; | |
| } | |
| if (!state.all.length) { showError("No projects found on the trail yet."); return; } | |
| state.byId = new Map(state.all.map((v) => [v.id, v])); | |
| state.oauth = await initOAuth(); | |
| renderAuth(); | |
| const saved = CONFIG.resumeRound ? state.store.loadRound() : null; | |
| state.filterTrack = saved?.track ?? null; | |
| ui.setTrackFilterLabel(state.filterTrack); | |
| const canResume = saved && saved.track === state.filterTrack; | |
| state.bag = new Bag(eligibleIds(), { state: canResume ? saved : null }); | |
| next(); | |
| if (!localStorage.getItem("bsr:hinted")) { | |
| ui.toast("Spot a project · 👍 like · 💾 save · 🧭 Next (or hit Space). Press ? for keys.", 6000); | |
| localStorage.setItem("bsr:hinted", "1"); | |
| } | |
| } | |
| boot(); | |