fc3f: ArtistsView + /artists route — search + platform filter + infinite scroll grid
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import SubscriptionsView from './views/SubscriptionsView.vue'
|
||||
import CredentialsView from './views/CredentialsView.vue'
|
||||
import DownloadsView from './views/DownloadsView.vue'
|
||||
import PostsView from './views/PostsView.vue'
|
||||
import ArtistsView from './views/ArtistsView.vue'
|
||||
|
||||
// The application's front door. `/` redirects here. Changing the front door
|
||||
// is a one-line edit (e.g. '/gallery' or '/tags').
|
||||
@@ -22,6 +23,7 @@ const routes = [
|
||||
// FC-2: image backbone
|
||||
{ path: '/showcase', name: 'showcase', component: ShowcaseView, meta: { title: 'Showcase' } },
|
||||
{ path: '/gallery', name: 'gallery', component: GalleryView, meta: { title: 'Gallery' } },
|
||||
{ path: '/artists', name: 'artists', component: ArtistsView, meta: { title: 'Artists' } },
|
||||
{ path: '/tags', name: 'tags', component: TagsView, meta: { title: 'Tags' } },
|
||||
// Artist detail — no meta.title (reached by clicking an artist, not nav).
|
||||
{ path: '/artist/:slug', name: 'artist', component: ArtistView },
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<v-container fluid class="py-6">
|
||||
|
||||
<div class="fc-artists__controls">
|
||||
<v-text-field
|
||||
v-model="search" density="compact" variant="outlined"
|
||||
prepend-inner-icon="mdi-magnify" hide-details
|
||||
placeholder="Search artists" clearable style="max-width: 320px;"
|
||||
/>
|
||||
<v-select
|
||||
v-model="platformModel"
|
||||
:items="platformItems"
|
||||
density="compact" variant="outlined" hide-details
|
||||
placeholder="Platform" clearable style="max-width: 220px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-alert v-if="store.error" type="error" variant="tonal" closable class="my-4">
|
||||
Failed to load: {{ store.error }}
|
||||
</v-alert>
|
||||
<v-alert v-else-if="store.isEmpty" type="info" variant="tonal" class="my-4">
|
||||
No artists match.
|
||||
</v-alert>
|
||||
|
||||
<div class="fc-artists__grid">
|
||||
<ArtistCard v-for="c in store.cards" :key="c.id" :card="c" />
|
||||
</div>
|
||||
|
||||
<div v-if="store.loading" class="fc-artists__sentinel">
|
||||
<v-progress-circular indeterminate color="accent" size="28" />
|
||||
</div>
|
||||
<div v-else-if="store.hasMore" ref="sentinelEl" class="fc-artists__sentinel" />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useArtistDirectoryStore } from '../stores/artistDirectory.js'
|
||||
import { usePlatformsStore } from '../stores/platforms.js'
|
||||
import ArtistCard from '../components/discovery/ArtistCard.vue'
|
||||
|
||||
const store = useArtistDirectoryStore()
|
||||
const platformsStore = usePlatformsStore()
|
||||
|
||||
const search = ref('')
|
||||
const platformModel = ref(null)
|
||||
const sentinelEl = ref(null)
|
||||
|
||||
const platformItems = ref([])
|
||||
|
||||
let debounce = null
|
||||
watch(search, (v) => {
|
||||
if (debounce) clearTimeout(debounce)
|
||||
debounce = setTimeout(() => store.setQuery(v || ''), 300)
|
||||
})
|
||||
watch(platformModel, (v) => store.setPlatform(v || null))
|
||||
|
||||
let observer = null
|
||||
function attachObserver() {
|
||||
if (observer) observer.disconnect()
|
||||
if (!sentinelEl.value) return
|
||||
observer = new IntersectionObserver(([e]) => {
|
||||
if (e.isIntersecting && store.hasMore && !store.loading) store.loadMore()
|
||||
}, { rootMargin: '600px' })
|
||||
observer.observe(sentinelEl.value)
|
||||
}
|
||||
watch(sentinelEl, attachObserver)
|
||||
|
||||
onMounted(async () => {
|
||||
await platformsStore.loadAll()
|
||||
platformItems.value = (platformsStore.platforms || []).map(p => ({
|
||||
title: p.key, value: p.key,
|
||||
}))
|
||||
store.reset()
|
||||
attachObserver()
|
||||
})
|
||||
onUnmounted(() => observer && observer.disconnect())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-artists__controls {
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
flex-wrap: wrap; margin-bottom: 16px;
|
||||
}
|
||||
.fc-artists__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.fc-artists__sentinel {
|
||||
display: flex; justify-content: center; padding: 32px 0; min-height: 60px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user