feat(sidebar,tags): per-tab counts and aggregated Tags badge
- /tags: each tab pill shows its own count (labels/keywords = distinct tags, ratings/colors = photos covered). Labels and marks queries become always-enabled on the route so every pill resolves immediately; keywords stays lazy. - LeftSidebar: Map badge now reads from the shared `['geo']` cache so it matches /map's "N geotagged" footer instead of count.places (distinct locations). Tags badge sums the four inner counts; keywords contributes lazily once /tags?tab=keywords is visited. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,10 +78,14 @@
|
||||
}
|
||||
|
||||
// ── Data sources ─────────────────────────────────────────────────────────
|
||||
// Labels and marks are always enabled while /tags is mounted so every
|
||||
// tab pill can render its count badge, not just the active tab. Both
|
||||
// share queryKeys with the sidebar so the fetch is deduped. Keywords
|
||||
// stays lazy (it's an O(1000-photo getPhoto fan-out) — heavy).
|
||||
const labelsQuery = createQuery<PpLabel[]>(() => ({
|
||||
queryKey: ['labels'],
|
||||
queryFn: listLabels,
|
||||
enabled: isAuthenticated() && activeTab === 'labels'
|
||||
enabled: isAuthenticated()
|
||||
}));
|
||||
|
||||
const keywordsQuery = createQuery<AggregatedKeyword[]>(() => ({
|
||||
@@ -94,7 +98,7 @@
|
||||
const marksQuery = createQuery<PhotoMarksMap>(() => ({
|
||||
queryKey: ['marks'],
|
||||
queryFn: getAllMarks,
|
||||
enabled: isAuthenticated() && (activeTab === 'ratings' || activeTab === 'colors'),
|
||||
enabled: isAuthenticated(),
|
||||
staleTime: 60_000
|
||||
}));
|
||||
|
||||
@@ -181,6 +185,34 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Per-tab badge counts ─────────────────────────────────────────────────
|
||||
// Each tab pill shows what its grid covers: distinct labels/keywords for
|
||||
// the bucket-style tabs, photo-count for the fixed-cardinality ones
|
||||
// (ratings/colors), matching how the sidebar's Tags badge aggregates.
|
||||
// `undefined` means the underlying query hasn't resolved yet — the badge
|
||||
// is skipped rather than showing a misleading 0.
|
||||
const labelsCount = $derived<number | undefined>(labelsQuery.data?.length);
|
||||
const keywordsCount = $derived<number | undefined>(keywordsQuery.data?.length);
|
||||
const ratedPhotosCount = $derived<number | undefined>(
|
||||
marksQuery.data ? countMarked(marksQuery.data, 'rating') : undefined
|
||||
);
|
||||
const coloredPhotosCount = $derived<number | undefined>(
|
||||
marksQuery.data ? countMarked(marksQuery.data, 'color') : undefined
|
||||
);
|
||||
function countMarked(marks: PhotoMarksMap, field: 'rating' | 'color'): number {
|
||||
let n = 0;
|
||||
for (const m of Object.values(marks)) {
|
||||
if (field === 'rating' ? (m.rating ?? 0) > 0 : Boolean(m.color)) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
function tabCount(tab: Tab): number | undefined {
|
||||
if (tab === 'labels') return labelsCount;
|
||||
if (tab === 'keywords') return keywordsCount;
|
||||
if (tab === 'ratings') return ratedPhotosCount;
|
||||
return coloredPhotosCount;
|
||||
}
|
||||
|
||||
// ── Sorted full lists per tab (most-common first), then page slice ───────
|
||||
const labelsSorted = $derived(
|
||||
[...(labelsQuery.data ?? [])].sort((a, b) => (b.PhotoCount ?? 0) - (a.PhotoCount ?? 0))
|
||||
@@ -285,14 +317,24 @@
|
||||
reads consistently across the app. -->
|
||||
<div class="flex items-center gap-1">
|
||||
{#each TABS as t (t.id)}
|
||||
{@const count = tabCount(t.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border px-2 py-0.5 text-[11px] {activeTab === t.id
|
||||
class="inline-flex items-center gap-1 rounded border px-2 py-0.5 text-[11px] {activeTab === t.id
|
||||
? 'border-primary/40 bg-primary/10 text-primary'
|
||||
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'}"
|
||||
onclick={() => setTab(t.id)}
|
||||
>
|
||||
{t.label}
|
||||
{#if count !== undefined}
|
||||
<span
|
||||
class="tabular-nums text-[10px] {activeTab === t.id
|
||||
? 'text-primary/70'
|
||||
: 'text-muted-foreground/70'}"
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user