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
+79 -13
View File
@@ -6,13 +6,17 @@ import SourceBadge from './SourceBadge.astro';
interface Props {
item: FindItem;
inList?: boolean;
unused?: boolean;
joke?: boolean;
quote?: boolean;
capturedAt?: Date;
availableTags?: string[];
}
const { item, inList, capturedAt, availableTags = [] } = Astro.props;
const { item, unused, joke, quote, capturedAt, availableTags = [] } = Astro.props;
const hero = findHero(item.id);
const showInListAttr = import.meta.env.DEV && inList !== undefined;
const showUnusedAttr = import.meta.env.DEV && unused !== undefined;
const showJokeAttr = import.meta.env.DEV && joke !== undefined;
const showQuoteAttr = import.meta.env.DEV && quote !== undefined;
const selectedTags: string[] = item.data.editorialTags ?? [];
const selectedSet = new Set(selectedTags);
@@ -34,7 +38,9 @@ function host(url: string) {
<article
class:list={['find-card', { 'has-hero': !!hero }]}
data-in-list={showInListAttr ? String(inList) : undefined}
data-unused={showUnusedAttr ? String(unused) : undefined}
data-joke={showJokeAttr ? String(joke) : undefined}
data-quote={showQuoteAttr ? String(quote) : undefined}
>
{import.meta.env.DEV && (
<label class="edit-checkbox" aria-label={`Select ${item.data.name}`}>
@@ -71,7 +77,7 @@ function host(url: string) {
)}
{import.meta.env.DEV && (
<div class="edit-tags">
<div class="edit-tags" data-edit-tags-root data-slug={item.id}>
{selectedTags.length > 0 && (
<div class="edit-tags-selected" aria-hidden="true">
{selectedTags.map((tag) => (
@@ -87,11 +93,8 @@ function host(url: string) {
'edit-tag',
{ 'is-selected': selectedSet.has(tag), 'is-delete': tag === 'delete' },
]}
data-edit-action="copy"
data-command={`/edit-find-tag ${item.id} ${tag}`}
data-confirm={selectedSet.has(tag)
? `Remove "${tag}" from "${item.data.name}".`
: `Tag "${item.data.name}" with "${tag}".`}
data-edit-tag-toggle
data-tag={tag}
>
{tag}
</button>
@@ -99,9 +102,7 @@ function host(url: string) {
<button
type="button"
class="edit-tag edit-tag-prompt"
data-edit-action="tag-prompt"
data-slug={item.id}
data-name={item.data.name}
data-edit-tag-prompt
aria-label="Add custom tag"
title="Add custom tag"
>
@@ -113,6 +114,71 @@ function host(url: string) {
</div>
</article>
{import.meta.env.DEV && (
<script>
if (!(window as any).__editFindTagsBound) {
(window as any).__editFindTagsBound = true;
const VALID_TAG = /^[a-z0-9][a-z0-9-]{0,40}$/i;
async function postEdit(slug: string, tag: string, mode?: 'add' | 'remove') {
const res = await fetch('/api/edit-find-tag/', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ slug, tag, mode }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json() as Promise<{ slug: string; tag: string; editorialTags: string[] }>;
}
function findRoot(el: HTMLElement): HTMLElement | null {
return el.closest<HTMLElement>('[data-edit-tags-root]');
}
document.addEventListener('click', async (e) => {
const target = e.target as HTMLElement;
const toggle = target.closest<HTMLButtonElement>('[data-edit-tag-toggle]');
const prompt = target.closest<HTMLButtonElement>('[data-edit-tag-prompt]');
if (!toggle && !prompt) return;
e.preventDefault();
e.stopPropagation();
const root = findRoot(toggle ?? prompt!);
const slug = root?.dataset.slug;
if (!slug) return;
let tag: string | null;
if (toggle) {
tag = toggle.dataset.tag ?? null;
} else {
tag = window.prompt('New editorial tag (a-z, 0-9, hyphen):');
if (!tag) return;
tag = tag.trim().toLowerCase();
if (!VALID_TAG.test(tag)) {
alert('Invalid tag — must be a-z / 0-9 / hyphen, max 40 chars.');
return;
}
}
if (!tag) return;
const btn = toggle ?? prompt!;
btn.disabled = true;
try {
await postEdit(slug, tag);
// HMR will reload the page on the file change; no need to update DOM here.
} catch (err) {
alert(`Edit failed: ${err instanceof Error ? err.message : String(err)}`);
} finally {
btn.disabled = false;
}
});
}
</script>
)}
<style>
.find-card {
position: relative;