fc3e: PostsFilterBar — artist autocomplete + platform v-select with URL-driven hydration

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 18:55:45 -04:00
parent 581f4aea79
commit a4f960c22c
@@ -0,0 +1,140 @@
<template>
<div class="fc-posts-filters">
<v-autocomplete
v-model="artistModel"
:items="artistOptions"
:loading="artistLoading"
:search="artistQuery"
item-title="name"
item-value="id"
label="Artist"
density="compact"
hide-details
clearable
no-filter
return-object
style="min-width: 240px"
@update:search="onArtistSearch"
@update:model-value="onArtistPicked"
/>
<v-select
v-model="platformModel"
:items="platformItems"
label="Platform"
density="compact"
hide-details
clearable
style="min-width: 180px"
@update:model-value="emitFilters"
/>
<v-btn
v-if="hasFilters"
variant="text" size="small"
@click="clearAll"
>Clear</v-btn>
</div>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useSourcesStore } from '../../stores/sources.js'
import { usePlatformsStore } from '../../stores/platforms.js'
const props = defineProps({
artistId: { type: Number, default: null },
platform: { type: String, default: null },
})
const emit = defineEmits(['update:filters'])
const sourcesStore = useSourcesStore()
const platformsStore = usePlatformsStore()
// Artist autocomplete state
const artistQuery = ref('')
const artistOptions = ref([])
const artistLoading = ref(false)
const artistModel = ref(null)
// Platform v-select state
const platformItems = computed(() =>
(platformsStore.platforms || []).map(p => ({ title: p.key, value: p.key }))
)
const platformModel = ref(props.platform)
const hasFilters = computed(
() => artistModel.value != null || platformModel.value != null
)
let _searchTimer = null
function onArtistSearch(q) {
artistQuery.value = q
clearTimeout(_searchTimer)
if (!q || !q.trim()) {
artistOptions.value = artistModel.value ? [artistModel.value] : []
return
}
_searchTimer = setTimeout(async () => {
artistLoading.value = true
try {
const results = await sourcesStore.autocompleteArtist(q, 20)
artistOptions.value = results
} finally {
artistLoading.value = false
}
}, 200)
}
function onArtistPicked() {
emitFilters()
}
function emitFilters() {
emit('update:filters', {
artist_id: artistModel.value?.id ?? null,
platform: platformModel.value || null,
})
}
function clearAll() {
artistModel.value = null
platformModel.value = null
artistOptions.value = []
artistQuery.value = ''
emitFilters()
}
onMounted(async () => {
await platformsStore.loadAll()
// If a deep-link arrives with an artist_id, hydrate the selected display.
if (props.artistId != null) {
const seed = { id: props.artistId, name: `Artist #${props.artistId}` }
artistModel.value = seed
artistOptions.value = [seed]
}
})
watch(() => props.artistId, (val) => {
if (val == null) {
artistModel.value = null
} else if (artistModel.value?.id !== val) {
const seed = { id: val, name: `Artist #${val}` }
artistModel.value = seed
artistOptions.value = [seed]
}
})
watch(() => props.platform, (val) => {
if (platformModel.value !== val) platformModel.value = val || null
})
</script>
<style scoped>
.fc-posts-filters {
display: flex;
gap: 0.75rem;
align-items: center;
padding-bottom: 1rem;
}
</style>