Skip to content

Commit 9a55f1d

Browse files
TMHSDigitalclaude
andcommitted
Shortlist shows what the set owes; copy a link to the view; download the results (#110)
- owes.mjs owesGroups(): the stack page's obligations as short groups; the shortlist panel lists them under the entries (credits, notices, per-file checks, open questions, unclear, no credit needed) - beside the result count: Copy link to this view, and Download these as CSV (the same catalogCsv as catalog.csv, served as exports.js) or JSON Checked in a browser: with ?licence=cc-by, 23 shown and 23 rows in each download, the link copies, and the owes groups render. Closes #110 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpkeXQPaUhxjgqdqnk6BrJ
1 parent 623388a commit 9a55f1d

7 files changed

Lines changed: 105 additions & 2 deletions

File tree

‎site/lib/lib.test.mjs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2381,6 +2381,23 @@ await section("entry check registry and validateRepo (#255)", async () => {
23812381
eq("validateRepo: it counts the entries", run.entryCount > 400, true);
23822382
});
23832383

2384+
/* shortlist owes and view actions (#110) ----------------------------------- */
2385+
await section("shortlist owes and view actions (#110)", async () => {
2386+
const { owes, owesGroups } = await import("./owes.mjs");
2387+
const e = (id, over) => ({ id, name: id.toUpperCase(), status: "active", attribution_required: false, commercial: true, ...over });
2388+
const groups = owesGroups(owes([
2389+
{ need: "shortlist", entry: e("a", { attribution_required: true, attribution_string: "Art by A" }) },
2390+
{ need: "shortlist", entry: e("b", { commercial: "varies" }) },
2391+
{ need: "shortlist", entry: e("c", { status: "needs-review" }) },
2392+
]));
2393+
eq("owes groups: only kinds with entries, in order", groups.map((g) => g.key).join(), "credits,perFile,openQuestions,noCredit");
2394+
eq("owes groups: entries listed under each", groups.find((g) => g.key === "noCredit").entries.map((x) => x.id).join(), "b,c");
2395+
eq("owes groups: nothing picked, nothing owed", owesGroups(owes([])).length, 0);
2396+
const fsm = (await import("node:fs")).default;
2397+
const tpl = fsm.readFileSync(new URL("../public/index.html", import.meta.url), "utf8");
2398+
for (const id of ["shortlist-owes", "view-actions", "copy-view-link"]) has(`homepage has #${id}`, tpl, `id="${id}"`);
2399+
});
2400+
23842401
/* report (keep last) ------------------------------------------------------ */
23852402
if (failures.length) {
23862403
console.error(`lib.test failed (${failures.length}):`);

‎site/lib/owes.mjs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@ export function owes(picked) {
2929
};
3030
}
3131

32+
/**
33+
* The owes() result as short groups for the shortlist panel (#110): one per
34+
* kind of obligation that has entries, in the stack page's order, each with
35+
* the entries it covers. Empty groups are left out.
36+
*/
37+
export function owesGroups(owed) {
38+
return [
39+
["credits", "Credits to ship", owed.credits],
40+
["notices", "Notices to keep with the files", owed.notices],
41+
["perFile", "Check each file", owed.perFile],
42+
["openQuestions", "Open questions", owed.openQuestions],
43+
["unclear", "Unclear credit: read the Notes", owed.unclear],
44+
["noCredit", "No credit needed", owed.noCredit],
45+
]
46+
.filter(([, , items]) => items.length)
47+
.map(([key, heading, items]) => ({ key, heading, entries: items.map((i) => i.entry) }));
48+
}
49+
3250
/**
3351
* A templated credit line marks each part the reader fills in with braces:
3452
* `"{title}" by Kevin MacLeod`. The validator (V35) refuses other

‎site/lib/shared.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ export const BROWSER_MODULES = {
291291
"cards.mjs": "cards.mjs", // the entry card template, for app.js and the category pages
292292
"shared.mjs": "shared.mjs",
293293
"client-state.js": "client-state.mjs", // shortlist parsing, escaping and the entry back link
294+
"exports.js": "exports.mjs", // "Download these" on the catalog: the same CSV as catalog.csv
294295
};
295296

296297
/**
@@ -300,7 +301,7 @@ export const BROWSER_MODULES = {
300301
* lib.test.mjs walks the real import graph and fails if a list drifts.
301302
*/
302303
export const PAGE_MODULES = {
303-
home: ["app.js", "search.js", "cards.mjs", "shared.mjs", "views.js", "shortlist.js", "owes.js", "client-state.js"],
304+
home: ["app.js", "search.js", "cards.mjs", "shared.mjs", "views.js", "shortlist.js", "owes.js", "client-state.js", "exports.js"],
304305
entry: ["shortlist.js", "owes.js", "client-state.js"],
305306
category: ["category.js", "views.js", "shortlist.js", "owes.js", "client-state.js"],
306307
notFound: ["not-found.js", "search.js", "shared.mjs"],

‎site/public/app.js‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { activeFilterChips, FILTER_DEFAULTS, FILTERS, gapIssueUrl, closestEntries, computeResults as computeResultsFor, passesFilters as passes, placementKey, queryTests as compileQuery, readState, scoreFields, searchFields, shouldGroup as shouldGroupFor, slugOf, sorterFor, SORTS, stateQuery, ENGINES } from "./search.js";
22
import { CARD_LIMIT, LIST_LIMIT, entryCardHtml, escapeHtml, groupHeadingHtml, groupMoreHtml, PERSPECTIVE_LABELS } from "./cards.mjs";
33
import { installViewToggles } from "./views.js";
4+
import { catalogCsv } from "./exports.js";
45
import { isListed, PERSPECTIVES } from "./shared.mjs";
56

67
(() => {
@@ -524,9 +525,13 @@ import { isListed, PERSPECTIVES } from "./shared.mjs";
524525
const SEARCH_DELAY = 120;
525526
let searchTimer = 0;
526527

528+
// The entries the current view shows, for "Download these" (#110).
529+
let lastList = [];
530+
527531
function apply({ push = false } = {}) {
528532
clearTimeout(searchTimer);
529533
const results = computeResults();
534+
lastList = results.list;
530535
syncCategoryChips(results);
531536
renderActiveFilters();
532537
renderGrid(results);
@@ -580,6 +585,39 @@ import { isListed, PERSPECTIVES } from "./shared.mjs";
580585

581586
/* ----------------------------------------------------------- controls */
582587

588+
/** "Copy link to this view" and "Download these" beside the result count (#110). */
589+
function bindViewActions() {
590+
const host = $("#view-actions");
591+
if (!host) return;
592+
host.hidden = false;
593+
const status = $("#view-actions-status");
594+
$("#copy-view-link").addEventListener("click", async () => {
595+
const url = location.href.split("#")[0] + "#catalog";
596+
try {
597+
await navigator.clipboard.writeText(url);
598+
status.textContent = "Link copied";
599+
} catch {
600+
// Clipboard refused: show the link so it can be copied by hand.
601+
status.textContent = url;
602+
}
603+
});
604+
for (const btn of $$("[data-results-export]", host)) {
605+
btn.addEventListener("click", () => {
606+
const csv = btn.getAttribute("data-results-export") === "csv";
607+
const text = csv ? catalogCsv(lastList, data.site?.siteUrl || "") : `${JSON.stringify(lastList, null, 2)}\n`;
608+
const url = URL.createObjectURL(new Blob([text], { type: csv ? "text/csv" : "application/json" }));
609+
const link = document.createElement("a");
610+
link.href = url;
611+
link.download = csv ? "free-game-dev-assets.csv" : "free-game-dev-assets.json";
612+
document.body.append(link);
613+
link.click();
614+
link.remove();
615+
setTimeout(() => URL.revokeObjectURL(url), 1000);
616+
status.textContent = `Downloaded ${lastList.length} ${lastList.length === 1 ? "entry" : "entries"}`;
617+
});
618+
}
619+
}
620+
583621
function bindControls() {
584622
const search = $("#search");
585623
search.value = state.q;
@@ -808,6 +846,7 @@ import { isListed, PERSPECTIVES } from "./shared.mjs";
808846
adoptNodes();
809847
renderCategoryFilters();
810848
bindControls();
849+
bindViewActions();
811850
syncControls();
812851
apply();
813852
restoreScroll();

‎site/public/index.html‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ <h2 id="catalog-title">Catalog</h2>
162162
</select>
163163
</label>
164164
<p class="result-count" id="result-count" aria-live="polite" tabindex="-1"><!--RESULT_COUNT--></p>
165+
<p class="view-actions" id="view-actions" hidden>
166+
<button type="button" class="link-button" id="copy-view-link">Copy link to this view</button>
167+
<button type="button" class="link-button" data-results-export="csv">Download these (CSV)</button>
168+
<button type="button" class="link-button" data-results-export="json">Download these (JSON)</button>
169+
<span class="copy-status" id="view-actions-status" role="status"></span>
170+
</p>
165171
<p class="current-group" id="current-group"></p>
166172
</div>
167173

@@ -183,6 +189,7 @@ <h2 id="catalog-title">Catalog</h2>
183189
<h3 id="shortlist-title">Your shortlist (<span id="shortlist-count">0</span>)</h3>
184190
<p class="filter-note">Kept in this browser only. The CREDITS file lists what these entries owe: credit lines, notices to keep, files to check one by one, and open questions.</p>
185191
<ul class="shortlist-items" id="shortlist-items"></ul>
192+
<div class="shortlist-owes" id="shortlist-owes" aria-label="What these entries owe"></div>
186193
<p class="shortlist-actions">
187194
<button type="button" class="btn-ghost" data-shortlist-action="md">Download CREDITS.md</button>
188195
<button type="button" class="btn-ghost" data-shortlist-action="txt">Download CREDITS.txt</button>

‎site/public/shortlist.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* kept in this browser only, and exported as one CREDITS file built by the
44
* same code as the stack pages' "What this stack owes" (owes.js).
55
*/
6-
import { creditsFile, owes } from "./owes.js";
6+
import { creditsFile, owes, owesGroups } from "./owes.js";
77
import { escapeHtml, parseShortlist } from "./client-state.js";
88

99
const KEY = "fgda:shortlist";
@@ -114,6 +114,13 @@ function renderPanel() {
114114
<button type="button" class="link-button" data-shortlist-remove="${escapeHtml(id)}">Remove<span class="sr-only"> ${escapeHtml(e.name)}</span></button></li>`;
115115
})
116116
.join("");
117+
// What the set owes, as on a stack page (#110).
118+
const owed = document.getElementById("shortlist-owes");
119+
if (owed) {
120+
owed.innerHTML = owesGroups(owes(ids.map((id) => ({ need: "shortlist", entry: byId[id] }))))
121+
.map((g) => `<p><strong>${escapeHtml(g.heading)} (${g.entries.length}):</strong> ${g.entries.map((e) => escapeHtml(e.name)).join(", ")}</p>`)
122+
.join("");
123+
}
117124
}
118125

119126
function update(next) {

‎site/public/styles.css‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,3 +2284,17 @@ body.card-focus .to-top {
22842284
min-height: 44px;
22852285
}
22862286
}
2287+
2288+
/* What the shortlist owes, and the actions beside the result count (#110). */
2289+
.shortlist-owes p {
2290+
margin: 0.25rem 0;
2291+
font-size: 0.9rem;
2292+
}
2293+
.view-actions {
2294+
display: flex;
2295+
flex-wrap: wrap;
2296+
gap: 0.25rem 1rem;
2297+
align-items: baseline;
2298+
margin: 0.25rem 0 0;
2299+
font-size: 0.9rem;
2300+
}

0 commit comments

Comments
 (0)