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:
+101
-15
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user