26e47a86cb
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>
133 lines
3.8 KiB
Vue
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>
|