Files
FabledCurator/frontend/src/components/gallery/GalleryGrid.vue
T
bvandeusen 26e47a86cb
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 12s
CI / frontend-build (push) Successful in 22s
CI / integration (push) Successful in 2m58s
fix(gallery): render similar-mode results (flat list when no date groups)
The 'See all similar' takeover fetched /api/gallery/similar fine (200, ~100
results) but the grid showed nothing: GalleryGrid renders ONLY by iterating
store.dateGroups, and similar-mode returns date_groups=[] (results are ranked
by cosine distance, not chronological). Zero groups → zero tiles despite
store.images being full. Add a flat fallback: when there are no date groups
but images exist, render them as one ungrouped list in ranked order (no date
headers). The modal Related strip was unaffected (it renders its images
directly). Test locks both the flat and grouped paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:24:52 -04:00

133 lines
3.8 KiB
Vue

<template>
<div class="fc-gallery-grid">
<div v-if="store.loading && store.images.length === 0" class="fc-gallery-grid__skeleton">
<div v-for="i in 12" :key="i" class="fc-gallery-grid__skeleton-item" />
</div>
<template v-for="group in store.dateGroups" :key="`${group.year}-${group.month}`">
<h2 class="fc-gallery-grid__date-header" :id="dateHeaderId(group)">
{{ monthLabel(group.year, group.month) }}
</h2>
<div class="fc-gallery-grid__items">
<GalleryItem
v-for="img in imagesForGroup(group)"
:key="img.id" :image="img" @open="$emit('open', img.id)"
/>
</div>
</template>
<!-- Similar-mode (and any non-date-grouped result set) returns no date
groups the results are ranked, not chronological. Render them as a
single flat list in their given order rather than nothing. -->
<div
v-if="!store.dateGroups.length && store.images.length"
class="fc-gallery-grid__items"
>
<GalleryItem
v-for="img in store.images"
:key="img.id" :image="img" @open="$emit('open', img.id)"
/>
</div>
<div v-if="store.loading" class="fc-gallery-grid__sentinel">
<v-progress-circular indeterminate color="accent" size="28" />
</div>
<div v-else-if="store.hasMore" ref="sentinelEl" class="fc-gallery-grid__sentinel" />
<div v-else-if="store.images.length" class="fc-gallery-grid__end text-caption">
End of gallery.
</div>
<v-alert
v-if="store.error" type="error" variant="tonal" closable class="my-4"
>
Failed to load: {{ store.error }}
</v-alert>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useGalleryStore } from '../../stores/gallery.js'
import { useInfiniteScroll } from '../../composables/useInfiniteScroll.js'
import GalleryItem from './GalleryItem.vue'
defineEmits(['open'])
const store = useGalleryStore()
const sentinelEl = ref(null)
useInfiniteScroll(sentinelEl, () => {
if (store.hasMore && !store.loading) store.loadMore()
})
function dateHeaderId(group) { return `fc-month-${group.year}-${group.month}` }
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
function monthLabel(y, m) { return `${MONTHS[m - 1]} ${y}` }
function imagesForGroup(group) {
const ids = new Set(group.image_ids)
return store.images.filter(i => ids.has(i.id))
}
</script>
<style scoped>
.fc-gallery-grid {
display: flex;
flex-direction: column;
}
.fc-gallery-grid__date-header {
font-family: 'Fraunces', Georgia, serif;
font-size: 20px;
font-weight: 500;
margin: 24px 0 12px;
padding-bottom: 6px;
border-bottom: 1px solid rgb(var(--v-theme-surface-light));
color: rgb(var(--v-theme-on-surface));
}
.fc-gallery-grid__items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 8px;
}
.fc-gallery-grid__sentinel {
display: flex; justify-content: center; align-items: center;
padding: 32px 0;
min-height: 60px;
}
.fc-gallery-grid__end {
text-align: center;
padding: 32px 0;
color: rgb(var(--v-theme-on-surface-variant, var(--v-theme-on-surface)));
}
.fc-gallery-grid__skeleton {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 8px;
}
.fc-gallery-grid__skeleton-item {
aspect-ratio: 1;
background: linear-gradient(
90deg,
rgb(var(--v-theme-surface)) 0%,
rgb(var(--v-theme-surface-light)) 50%,
rgb(var(--v-theme-surface)) 100%
);
background-size: 200% 100%;
animation: fc-shimmer 1.4s infinite;
border-radius: 4px;
}
@keyframes fc-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@media (max-width: 600px) {
.fc-gallery-grid__items,
.fc-gallery-grid__skeleton {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 4px;
}
}
</style>