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