From 861e8baeb87e711e3cdacacaacfff2a8828c3b75 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 15 May 2026 21:12:19 -0400 Subject: [PATCH] feat(fc2c-i): pinia stores for showcase, tag directory, artist --- frontend/src/stores/artist.js | 63 +++++++++++++++++++++++++++++ frontend/src/stores/showcase.js | 45 +++++++++++++++++++++ frontend/src/stores/tagDirectory.js | 57 ++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 frontend/src/stores/artist.js create mode 100644 frontend/src/stores/showcase.js create mode 100644 frontend/src/stores/tagDirectory.js diff --git a/frontend/src/stores/artist.js b/frontend/src/stores/artist.js new file mode 100644 index 0000000..a8e250e --- /dev/null +++ b/frontend/src/stores/artist.js @@ -0,0 +1,63 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useApi } from '../composables/useApi.js' + +const PAGE = 60 + +export const useArtistStore = defineStore('artist', () => { + const api = useApi() + const overview = ref(null) + const images = ref([]) + const nextCursor = ref(null) + const loading = ref(false) + const imagesLoading = ref(false) + const error = ref(null) + const notFound = ref(false) + let started = false + + async function load(slug) { + overview.value = null + images.value = [] + nextCursor.value = null + notFound.value = false + started = false + error.value = null + loading.value = true + try { + overview.value = await api.get(`/api/artist/${encodeURIComponent(slug)}`) + await loadMoreImages(slug) + } catch (e) { + if (e.status === 404) notFound.value = true + else error.value = e.message + } finally { + loading.value = false + } + } + + async function loadMoreImages(slug) { + if (imagesLoading.value) return + if (started && nextCursor.value === null) return + imagesLoading.value = true + try { + const params = { limit: PAGE } + if (nextCursor.value) params.cursor = nextCursor.value + const body = await api.get( + `/api/artist/${encodeURIComponent(slug)}/images`, { params } + ) + images.value.push(...body.images) + nextCursor.value = body.next_cursor + started = true + } catch (e) { + error.value = e.message + } finally { + imagesLoading.value = false + } + } + + const hasMoreImages = computed(() => !started || nextCursor.value !== null) + + return { + overview, images, loading, imagesLoading, error, notFound, + hasMoreImages, load, loadMoreImages + } +}) diff --git a/frontend/src/stores/showcase.js b/frontend/src/stores/showcase.js new file mode 100644 index 0000000..7e72488 --- /dev/null +++ b/frontend/src/stores/showcase.js @@ -0,0 +1,45 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useApi } from '../composables/useApi.js' + +const PAGE = 60 + +export const useShowcaseStore = defineStore('showcase', () => { + const api = useApi() + const images = ref([]) + const loading = ref(false) + const error = ref(null) + const exhausted = ref(false) + const seen = new Set() + + async function fetchPage() { + if (loading.value) return + loading.value = true + error.value = null + try { + const body = await api.get('/api/showcase', { params: { limit: PAGE } }) + const fresh = body.images.filter(i => !seen.has(i.id)) + if (fresh.length === 0) { exhausted.value = true; return } + for (const i of fresh) seen.add(i.id) + images.value.push(...fresh) + } catch (e) { + error.value = e.message + } finally { + loading.value = false + } + } + + async function shuffle() { + images.value = [] + seen.clear() + exhausted.value = false + await fetchPage() + } + + const hasMore = computed(() => !exhausted.value) + const isEmpty = computed( + () => !loading.value && images.value.length === 0 && error.value === null + ) + + return { images, loading, error, hasMore, isEmpty, fetchPage, shuffle } +}) diff --git a/frontend/src/stores/tagDirectory.js b/frontend/src/stores/tagDirectory.js new file mode 100644 index 0000000..e0ffe46 --- /dev/null +++ b/frontend/src/stores/tagDirectory.js @@ -0,0 +1,57 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useApi } from '../composables/useApi.js' + +const PAGE = 60 + +export const useTagDirectoryStore = defineStore('tagDirectory', () => { + const api = useApi() + const cards = ref([]) + const nextCursor = ref(null) + const loading = ref(false) + const error = ref(null) + const kind = ref(null) + const q = ref('') + let started = false + + async function loadMore() { + if (loading.value) return + if (started && nextCursor.value === null) return + loading.value = true + error.value = null + try { + const params = { limit: PAGE } + if (kind.value) params.kind = kind.value + if (q.value) params.q = q.value + if (nextCursor.value) params.cursor = nextCursor.value + const body = await api.get('/api/tags/directory', { params }) + cards.value.push(...body.cards) + nextCursor.value = body.next_cursor + started = true + } catch (e) { + error.value = e.message + } finally { + loading.value = false + } + } + + async function reset() { + cards.value = [] + nextCursor.value = null + started = false + await loadMore() + } + + function setKind(k) { kind.value = k; reset() } + function setQuery(text) { q.value = text; reset() } + + const hasMore = computed(() => !started || nextCursor.value !== null) + const isEmpty = computed( + () => !loading.value && cards.value.length === 0 && error.value === null + ) + + return { + cards, loading, error, kind, q, hasMore, isEmpty, + loadMore, reset, setKind, setQuery + } +})