site: /find/ filter toggles — Unused (default), All, plus Jokes/Quotes toggles

Replaces the old "Not in lists" filter with "Unused" (no list and no
promotedTo). Adds independent Jokes/Quotes toggles, off by default.
Filter + each toggle persist separately in localStorage. Card scoping uses
:global() so the filter selectors reach FindCard's CSS scope.

Also picks up the editorial-tag picker refactor in FindCard: in-card buttons
now POST to /api/edit-find-tag/ via a single delegated handler instead of
copying slash commands.
This commit is contained in:
2026-05-04 20:25:37 -04:00
parent 6c42d0c380
commit abd401d783
2 changed files with 180 additions and 28 deletions
+101 -15
View File
@@ -39,7 +39,13 @@ const lastUpdated = finds[0]?.data.date;
const inListSlugs = new Set(
(await getCollection('finds')).flatMap((l) => l.data.items)
);
const notInListCount = finds.filter((f) => !inListSlugs.has(f.id)).length;
const isJoke = (f: (typeof finds)[number]) => f.id.startsWith('joke-');
const isQuote = (f: (typeof finds)[number]) => f.id.startsWith('quote-');
const isUnused = (f: (typeof finds)[number]) =>
!inListSlugs.has(f.id) && !f.data.promotedTo;
const unusedCount = finds.filter(isUnused).length;
const jokeCount = finds.filter(isJoke).length;
const quoteCount = finds.filter(isQuote).length;
---
<BaseLayout
@@ -50,22 +56,42 @@ const notInListCount = finds.filter((f) => !inListSlugs.has(f.id)).length;
noindex
>
{import.meta.env.DEV && (
<div class="find-filter" role="group" aria-label="Filter">
<button type="button" data-find-filter-set="all" aria-current="true">
All ({finds.length})
<div class="find-filter-row">
<div class="find-filter" role="group" aria-label="Filter">
<button type="button" data-find-filter-set="unused" aria-current="true">
Unused ({unusedCount})
</button>
<button type="button" data-find-filter-set="all">
All ({finds.length})
</button>
</div>
<button
type="button"
class="find-toggle"
data-find-jokes-toggle
aria-pressed="false"
>
Jokes ({jokeCount})
</button>
<button type="button" data-find-filter-set="not-in-lists">
Not in lists ({notInListCount})
<button
type="button"
class="find-toggle"
data-find-quotes-toggle
aria-pressed="false"
>
Quotes ({quoteCount})
</button>
</div>
)}
<div data-find-filter="all">
<div data-find-filter="unused" data-find-jokes="hide" data-find-quotes="hide">
<Masonry initial={INITIAL_GRID} batch={BATCH_SIZE}>
{decorated.map(({ item, capturedAt }) => (
<FindCard
item={item}
inList={inListSlugs.has(item.id)}
unused={isUnused(item)}
joke={isJoke(item)}
quote={isQuote(item)}
capturedAt={capturedAt}
availableTags={editorialTags}
/>
@@ -101,13 +127,13 @@ const notInListCount = finds.filter((f) => !inListSlugs.has(f.id)).length;
refresh();
}
// Dev-only filter: toggle "all" vs "not-in-lists" on the masonry wrapper.
// Dev-only filter: toggle "unused" vs "all" on the masonry wrapper.
const wrapper = document.querySelector<HTMLDivElement>('[data-find-filter]');
const filterButtons = document.querySelectorAll<HTMLButtonElement>('[data-find-filter-set]');
const FILTER_KEY = 'find-filter';
if (wrapper && filterButtons.length) {
type FilterValue = 'all' | 'not-in-lists';
type FilterValue = 'unused' | 'all';
const apply = (value: FilterValue) => {
wrapper.setAttribute('data-find-filter', value);
for (const b of filterButtons) {
@@ -119,15 +145,40 @@ const notInListCount = finds.filter((f) => !inListSlugs.has(f.id)).length;
let saved: string | null = null;
try { saved = localStorage.getItem(FILTER_KEY); } catch (e) {}
if (saved === 'all' || saved === 'not-in-lists') apply(saved);
if (saved === 'unused' || saved === 'all') apply(saved);
for (const b of filterButtons) {
b.addEventListener('click', () => {
const v = b.dataset.findFilterSet;
if (v === 'all' || v === 'not-in-lists') apply(v);
if (v === 'unused' || v === 'all') apply(v);
});
}
}
// Dev-only category toggles: independent of the Unused/All filter.
const categoryToggles: { key: string; attr: string; selector: string }[] = [
{ key: 'find-jokes', attr: 'data-find-jokes', selector: '[data-find-jokes-toggle]' },
{ key: 'find-quotes', attr: 'data-find-quotes', selector: '[data-find-quotes-toggle]' },
];
for (const { key, attr, selector } of categoryToggles) {
const btn = document.querySelector<HTMLButtonElement>(selector);
if (!wrapper || !btn) continue;
const applyToggle = (show: boolean) => {
wrapper.setAttribute(attr, show ? 'show' : 'hide');
btn.setAttribute('aria-pressed', String(show));
try { localStorage.setItem(key, show ? '1' : '0'); } catch (e) {}
};
let saved: string | null = null;
try { saved = localStorage.getItem(key); } catch (e) {}
if (saved === '1') applyToggle(true);
btn.addEventListener('click', () => {
applyToggle(btn.getAttribute('aria-pressed') !== 'true');
});
}
</script>
<style>
@@ -157,10 +208,16 @@ const notInListCount = finds.filter((f) => !inListSlugs.has(f.id)).length;
font-weight: 700;
}
.find-filter-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
margin-bottom: 1.25rem;
}
.find-filter {
display: inline-flex;
gap: 0;
margin-bottom: 1.25rem;
border: 1px solid var(--rule);
border-radius: 999px;
overflow: hidden;
@@ -187,8 +244,37 @@ const notInListCount = finds.filter((f) => !inListSlugs.has(f.id)).length;
color: var(--bg);
}
/* Hide in-list cards when the filter is set to not-in-lists. */
[data-find-filter="not-in-lists"] [data-in-list="true"] {
.find-toggle {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.04em;
padding: 0.4rem 0.95rem;
margin: 0;
color: var(--muted);
background: none;
border: 1px solid var(--rule);
border-radius: 999px;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.find-toggle:hover { color: var(--accent); }
.find-toggle[aria-pressed="true"] {
background: var(--fg);
color: var(--bg);
border-color: var(--fg);
}
/* Hide non-unused cards when the filter is set to unused. */
[data-find-filter="unused"] :global([data-unused="false"]) {
display: none;
}
/* Hide joke cards when the Jokes toggle is off. */
[data-find-jokes="hide"] :global([data-joke="true"]) {
display: none;
}
/* Hide quote cards when the Quotes toggle is off. */
[data-find-quotes="hide"] :global([data-quote="true"]) {
display: none;
}
</style>