4d2c464045
PostCard and PostModal competed for the same data and rendered redundant chrome (header twice, image grid twice, attachment list twice). The wider PostCard layout we shipped 2026-05-27 has enough real estate to be the canonical post surface, so collapse the two into one. Compact (default) state is unchanged: hero + 3-cell rail + truncated title + 3/5-line description + attachment count badge. Whole-card click expands in place. Expanded state shows: full title, mosaic of ALL post images via PostImageGrid (uncapped, lazy-loaded via getPostFull), full sanitized-HTML description with paragraph wrapping, attachments as downloadable pill links. Click the chevron in the header to collapse; mosaic image clicks open ImageViewer scoped to the post (modalStore's postImageIds path is preserved — only the comment changed). Per-card local state — no global modal store. Each PostCard owns its own expanded ref and lazy-loaded detail; collapsing a card discards neither (so re-expand is instant after the first fetch). Deleted: PostModal.vue, postModal.js store. Removed the App.vue mount.
137 lines
4.3 KiB
JavaScript
137 lines
4.3 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { useApi } from '../composables/useApi.js'
|
|
|
|
export const useModalStore = defineStore('modal', () => {
|
|
const api = useApi()
|
|
|
|
const currentImageId = ref(null)
|
|
const current = ref(null)
|
|
const loading = ref(false)
|
|
const error = ref(null)
|
|
|
|
// Post-scoped cycle. When set, prev/next cycles within this array
|
|
// (used by PostCard's expanded-mosaic PostImageGrid clicks). When
|
|
// null, prev/next falls back to current.value.neighbors (the
|
|
// gallery-store-driven /api/gallery/image/<id> neighbors).
|
|
const postImageIds = ref(null)
|
|
const postImageIndex = ref(0)
|
|
|
|
async function open (id, opts = {}) {
|
|
currentImageId.value = id
|
|
loading.value = true
|
|
error.value = null
|
|
// Update post-scoped state if caller passed it; otherwise clear so
|
|
// the next open() from gallery context uses neighbors mode.
|
|
if (opts.postImageIds != null) {
|
|
postImageIds.value = opts.postImageIds
|
|
postImageIndex.value = opts.postImageIds.indexOf(id)
|
|
if (postImageIndex.value < 0) postImageIndex.value = 0
|
|
} else if (opts.clearPostScope !== false && postImageIds.value != null) {
|
|
postImageIds.value = null
|
|
postImageIndex.value = 0
|
|
}
|
|
try {
|
|
current.value = await api.get(`/api/gallery/image/${id}`)
|
|
} catch (e) {
|
|
error.value = e.message
|
|
current.value = null
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function close () {
|
|
currentImageId.value = null
|
|
current.value = null
|
|
error.value = null
|
|
postImageIds.value = null
|
|
postImageIndex.value = 0
|
|
}
|
|
|
|
async function goPrev () {
|
|
if (postImageIds.value != null) {
|
|
if (postImageIndex.value > 0) {
|
|
const newIdx = postImageIndex.value - 1
|
|
const newId = postImageIds.value[newIdx]
|
|
postImageIndex.value = newIdx
|
|
await open(newId, { postImageIds: postImageIds.value })
|
|
}
|
|
return
|
|
}
|
|
if (current.value && current.value.neighbors?.prev_id) {
|
|
await open(current.value.neighbors.prev_id)
|
|
}
|
|
}
|
|
|
|
async function goNext () {
|
|
if (postImageIds.value != null) {
|
|
if (postImageIndex.value < postImageIds.value.length - 1) {
|
|
const newIdx = postImageIndex.value + 1
|
|
const newId = postImageIds.value[newIdx]
|
|
postImageIndex.value = newIdx
|
|
await open(newId, { postImageIds: postImageIds.value })
|
|
}
|
|
return
|
|
}
|
|
if (current.value && current.value.neighbors?.next_id) {
|
|
await open(current.value.neighbors.next_id)
|
|
}
|
|
}
|
|
|
|
async function reloadTags () {
|
|
if (!currentImageId.value) return
|
|
const tags = await api.get(`/api/images/${currentImageId.value}/tags`)
|
|
if (current.value) current.value.tags = tags
|
|
}
|
|
|
|
async function removeTag (tagId) {
|
|
if (!currentImageId.value) return
|
|
const prev = current.value.tags
|
|
current.value.tags = current.value.tags.filter(t => t.id !== tagId)
|
|
try {
|
|
await api.delete(`/api/images/${currentImageId.value}/tags/${tagId}`)
|
|
await api.post(`/api/images/${currentImageId.value}/suggestions/dismiss`, {
|
|
body: { tag_id: tagId },
|
|
})
|
|
} catch (e) {
|
|
current.value.tags = prev
|
|
window.__fcToast?.({ text: `Failed to remove tag: ${e.message}`, type: 'error' })
|
|
throw e
|
|
}
|
|
}
|
|
|
|
async function addExistingTag (tagId) {
|
|
if (!currentImageId.value) return
|
|
await api.post(`/api/images/${currentImageId.value}/tags`, {
|
|
body: { tag_id: tagId, source: 'manual' },
|
|
})
|
|
await reloadTags()
|
|
}
|
|
|
|
async function createAndAdd ({ name, kind, fandom_id = null }) {
|
|
const tag = await api.post('/api/tags', { body: { name, kind, fandom_id } })
|
|
await addExistingTag(tag.id)
|
|
}
|
|
|
|
const isOpen = computed(() => currentImageId.value !== null)
|
|
const canPrev = computed(() => {
|
|
if (postImageIds.value != null) return postImageIndex.value > 0
|
|
return current.value?.neighbors?.prev_id != null
|
|
})
|
|
const canNext = computed(() => {
|
|
if (postImageIds.value != null) {
|
|
return postImageIndex.value < (postImageIds.value.length - 1)
|
|
}
|
|
return current.value?.neighbors?.next_id != null
|
|
})
|
|
|
|
return {
|
|
currentImageId, current, loading, error,
|
|
postImageIds, postImageIndex,
|
|
isOpen, canPrev, canNext,
|
|
open, close, goPrev, goNext,
|
|
reloadTags, removeTag, addExistingTag, createAndAdd,
|
|
}
|
|
})
|