feat(community): complete Layer A subcategory tagging (#118)
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
Release / release (push) Waiting to run

- RecipeBrowserPanel: fix onTagSearchInput using '_all' domain slug
  (backend validates domain — was silently returning empty results)
- RecipeDetailPanel: fetch and display accepted community category tags
  on recipe open; accepted tags shown with accent chip + checkmark,
  pending tags shown in muted style
- browserAPI.listRecipeTags() was already in api.ts but not consumed —
  now wired into RecipeDetailPanel onMounted as a background fetch
This commit is contained in:
pyr0ball 2026-04-25 23:31:30 -07:00
parent 12ab63e2fb
commit 95e76edaea
2 changed files with 67 additions and 3 deletions

View file

@ -527,8 +527,10 @@ function onTagSearchInput() {
tagSearchDebounce = setTimeout(async () => {
tagModal.value.searching = true
try {
// Re-use the browser API: browse all recipes filtered by title substring
const res = await browserAPI.browse('_all', '_all', { page: 1, q })
// Use the first available domain with category=_all to search all recipes by title.
// Domain must be a real domain slug '_all' is not valid at the browse endpoint.
const searchDomain = domains.value[0]?.id ?? 'cuisine'
const res = await browserAPI.browse(searchDomain, '_all', { page: 1, q })
tagModal.value.results = (res.recipes ?? []).slice(0, 8).map(
(r: { id: number; title: string }) => ({ id: r.id, title: r.title })
)

View file

@ -225,6 +225,23 @@
</ol>
</details>
<!-- Community tags accepted location tags from other users -->
<div v-if="communityTags.length > 0" class="detail-section community-tags-section">
<h3 class="section-label">Community categories</h3>
<div class="community-tags-list">
<span
v-for="tag in communityTags"
:key="tag.id"
class="community-tag-chip"
:class="{ 'community-tag-chip--accepted': tag.accepted }"
:title="tag.accepted ? 'Confirmed by the community' : 'Pending confirmation'"
>
{{ tag.domain }} {{ tag.category }}<template v-if="tag.subcategory"> {{ tag.subcategory }}</template>
<span v-if="tag.accepted" class="community-tag-check" aria-label="Confirmed"></span>
</span>
</div>
</div>
<!-- Bottom padding so last step isn't hidden behind sticky footer -->
<div style="height: var(--spacing-xl)" />
</div>
@ -354,7 +371,7 @@
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { useRecipesStore } from '../stores/recipes'
import { useSavedRecipesStore } from '../stores/savedRecipes'
import { inventoryAPI, recipesAPI } from '../services/api'
import { inventoryAPI, recipesAPI, browserAPI } from '../services/api'
import type { RecipeSuggestion, GroceryLink, StepAnalysis } from '../services/api'
import SaveRecipeModal from './SaveRecipeModal.vue'
@ -386,6 +403,12 @@ onMounted(() => {
)
;(focusable ?? dialogRef.value)?.focus()
})
// Load community tags in the background non-critical, silently skip on error
browserAPI.listRecipeTags(props.recipe.id).then((tags) => {
communityTags.value = tags
}).catch(() => {
// Community tags are supplemental; silently skip on error
})
})
onUnmounted(() => {
@ -411,6 +434,10 @@ const isSaved = computed(() => savedStore.isSaved(props.recipe.id))
const cookDone = ref(false)
// Community tags
type CommunityTag = { id: number; domain: string; category: string; subcategory: string | null; pseudonym: string; upvotes: number; accepted: boolean }
const communityTags = ref<CommunityTag[]>([])
// Leftover shelf-life
type LeftoversData = { fridge_days: number; freeze_days: number | null; freeze_by_day: number | null; storage_advice: string }
const leftovers = ref<LeftoversData | null>(null)
@ -1628,4 +1655,39 @@ details[open].steps-collapsible .steps-collapsible-summary::before {
font-size: 1rem;
line-height: 1;
}
/* ── Community tags section ──────────────────────────────── */
.community-tags-section {
padding-top: var(--spacing-sm);
}
.community-tags-list {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.community-tag-chip {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 2px var(--spacing-sm);
border-radius: var(--radius-pill, 999px);
font-size: var(--font-size-xs, 0.72rem);
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
white-space: nowrap;
}
.community-tag-chip--accepted {
background: rgba(124, 111, 205, 0.12);
color: var(--color-accent, #7c6fcd);
border-color: rgba(124, 111, 205, 0.3);
}
.community-tag-check {
font-size: 0.65rem;
opacity: 0.8;
}
</style>