diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 38a3ccf..5cfbdda 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -4,7 +4,6 @@ - @@ -14,7 +13,6 @@ import { onMounted, ref } from 'vue' import AppShell from './components/AppShell.vue' import AppSnackbar from './components/AppSnackbar.vue' import ImageViewer from './components/modal/ImageViewer.vue' -import PostModal from './components/posts/PostModal.vue' import { useModalStore } from './stores/modal.js' const modal = useModalStore() diff --git a/frontend/src/components/posts/PostCard.vue b/frontend/src/components/posts/PostCard.vue index c80a0ab..65194a0 100644 --- a/frontend/src/components/posts/PostCard.vue +++ b/frontend/src/components/posts/PostCard.vue @@ -1,8 +1,8 @@ @@ -14,6 +14,12 @@ @click.stop >{{ post.artist.name }} {{ relativeDate }} + + · {{ images.length }} image{{ images.length === 1 ? '' : 's' }} + + + · {{ attachments.length }} attachment{{ attachments.length === 1 ? '' : 's' }} + + - + + - + @@ -63,28 +76,85 @@ + + + + + {{ post.post_title }} + + + Post {{ post.external_post_id }} + + + + + + Loading full image list… + + + + + + + + (no description) + + + + Attachments + + + mdi-paperclip + {{ att.original_filename }} + ({{ formatBytes(att.size_bytes) }}) + + + + @@ -110,17 +229,22 @@ function onCardClick () { .fc-post-card { padding: 1rem; margin-bottom: 1rem; - cursor: pointer; container-type: inline-size; transition: border-color 0.15s ease; } -.fc-post-card:hover { +.fc-post-card:not(.fc-post-card--expanded) { + cursor: pointer; +} +.fc-post-card:not(.fc-post-card--expanded):hover { border-color: rgb(var(--v-theme-accent)); } .fc-post-card:focus-visible { outline: 2px solid rgb(var(--v-theme-accent)); outline-offset: 2px; } +.fc-post-card--expanded { + border-color: rgb(var(--v-theme-accent) / 0.6); +} .fc-post-card__head { display: flex; @@ -129,6 +253,7 @@ function onCardClick () { font-size: 0.8rem; color: rgb(var(--v-theme-on-surface-variant)); margin-bottom: 12px; + flex-wrap: wrap; } .fc-post-card__artist { color: rgb(var(--v-theme-on-surface)); @@ -136,8 +261,10 @@ function onCardClick () { font-weight: 600; } .fc-post-card__artist:hover { color: rgb(var(--v-theme-accent)); } -.fc-post-card__date { white-space: nowrap; } +.fc-post-card__date, +.fc-post-card__meta { white-space: nowrap; } +/* ---- COMPACT BODY ---- */ .fc-post-card__body { display: flex; flex-direction: column; @@ -148,13 +275,8 @@ function onCardClick () { flex-direction: row; gap: 24px; } - .fc-post-card__media { - flex: 0 0 50%; - } - .fc-post-card__text { - flex: 1 1 0; - min-width: 0; - } + .fc-post-card__media { flex: 0 0 50%; } + .fc-post-card__text { flex: 1 1 0; min-width: 0; } } .fc-post-card__hero { @@ -164,35 +286,24 @@ function onCardClick () { border-radius: 6px; } .fc-post-card__hero img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; + width: 100%; height: 100%; + object-fit: cover; display: block; } .fc-post-card__rail { - display: flex; - gap: 6px; - margin-top: 6px; + display: flex; gap: 6px; margin-top: 6px; } .fc-post-card__rail-cell { - width: 80px; - height: 80px; - overflow: hidden; - border-radius: 4px; + width: 80px; height: 80px; + overflow: hidden; border-radius: 4px; } .fc-post-card__rail-cell img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; + width: 100%; height: 100%; + object-fit: cover; display: block; } .fc-post-card__rail-more { - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; + width: 80px; height: 80px; + display: flex; align-items: center; justify-content: center; border: 1px dashed rgb(var(--v-theme-on-surface-variant)); border-radius: 4px; color: rgb(var(--v-theme-on-surface-variant)); @@ -201,8 +312,7 @@ function onCardClick () { .fc-post-card__title { font-family: 'Fraunces', Georgia, serif; - font-size: 18px; - font-weight: 500; + font-size: 18px; font-weight: 500; margin: 0 0 8px 0; color: rgb(var(--v-theme-on-surface)); display: -webkit-box; @@ -238,9 +348,7 @@ function onCardClick () { color: rgb(var(--v-theme-on-surface-variant)); } @container (min-width: 800px) { - .fc-post-card__desc { - -webkit-line-clamp: 5; - } + .fc-post-card__desc { -webkit-line-clamp: 5; } } .fc-post-card__atts { @@ -251,4 +359,60 @@ function onCardClick () { color: rgb(var(--v-theme-on-surface-variant)); } .fc-post-card__att-icon { color: rgb(var(--v-theme-on-surface-variant)); } + +/* ---- EXPANDED BODY ---- */ +.fc-post-card__expanded { + display: flex; + flex-direction: column; + gap: 20px; +} +.fc-post-card__title-full { + font-family: 'Fraunces', Georgia, serif; + font-size: 22px; + font-weight: 500; + margin: 0; + color: rgb(var(--v-theme-on-surface)); +} +@container (min-width: 800px) { + .fc-post-card__title-full { font-size: 26px; } +} +.fc-post-card__sec { margin: 0; } +.fc-post-card__h3 { + font-family: 'Fraunces', Georgia, serif; + font-size: 16px; + font-weight: 500; + margin: 0 0 8px 0; + color: rgb(var(--v-theme-on-surface)); +} +.fc-post-card__loading-hint { + margin-top: 8px; + font-size: 0.8rem; + color: rgb(var(--v-theme-on-surface-variant)); +} +.fc-post-card__desc-full { + font-size: 0.95rem; + line-height: 1.55; + color: rgb(var(--v-theme-on-surface)); +} +.fc-post-card__desc-full :deep(p) { margin: 0 0 12px 0; } +.fc-post-card__desc-full :deep(a) { color: rgb(var(--v-theme-accent)); } +.fc-post-card__atts-full { + display: flex; flex-wrap: wrap; gap: 8px; +} +.fc-post-card__att { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1px solid rgb(var(--v-theme-on-surface-variant)); + border-radius: 999px; + color: rgb(var(--v-theme-on-surface)); + text-decoration: none; + font-size: 0.85rem; +} +.fc-post-card__att:hover { + color: rgb(var(--v-theme-accent)); + border-color: rgb(var(--v-theme-accent)); +} +.fc-post-card__att-size { color: rgb(var(--v-theme-on-surface-variant)); } diff --git a/frontend/src/components/posts/PostModal.vue b/frontend/src/components/posts/PostModal.vue deleted file mode 100644 index 3c25ccb..0000000 --- a/frontend/src/components/posts/PostModal.vue +++ /dev/null @@ -1,211 +0,0 @@ - - { if (!v) close() }" - > - - - {{ post.source.platform }} - {{ post.artist.name }} - - {{ relativeDate }} - - - · {{ post.thumbnails.length }} image{{ post.thumbnails.length === 1 ? '' : 's' }} - - - · {{ post.attachments.length }} attachment{{ post.attachments.length === 1 ? '' : 's' }} - - - - - - - - - - - Loading full image list… - - - - - {{ post.post_title }} - - - - - - - - Attachments - - - mdi-paperclip - {{ att.original_filename }} - ({{ formatBytes(att.size_bytes) }}) - - - - - - - - - - - diff --git a/frontend/src/stores/modal.js b/frontend/src/stores/modal.js index 0f8665f..feffb15 100644 --- a/frontend/src/stores/modal.js +++ b/frontend/src/stores/modal.js @@ -11,9 +11,9 @@ export const useModalStore = defineStore('modal', () => { const error = ref(null) // Post-scoped cycle. When set, prev/next cycles within this array - // (used by PostModal's PostImageGrid clicks). When null, prev/next - // falls back to current.value.neighbors (the gallery-store-driven - // /api/gallery/image/ neighbors). + // (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/ neighbors). const postImageIds = ref(null) const postImageIndex = ref(0) diff --git a/frontend/src/stores/postModal.js b/frontend/src/stores/postModal.js deleted file mode 100644 index 64fd1c9..0000000 --- a/frontend/src/stores/postModal.js +++ /dev/null @@ -1,39 +0,0 @@ -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import { usePostsStore } from './posts.js' - -export const usePostModalStore = defineStore('postModal', () => { - const postsStore = usePostsStore() - - // Feed-shape on open; upgraded to detail shape (full description + - // uncapped thumbnails) once getPostFull resolves. - const currentPost = ref(null) - const detailLoaded = ref(false) - const error = ref(null) - const isOpen = computed(() => currentPost.value !== null) - - async function open (post) { - currentPost.value = post - detailLoaded.value = false - error.value = null - try { - const detail = await postsStore.getPostFull(post.id) - if (currentPost.value?.id === post.id) { - currentPost.value = detail - detailLoaded.value = true - } - } catch (e) { - // Keep feed-shape data; PostModal can still render title + truncated - // description + the up-to-6 feed thumbnails. Just surface the error. - error.value = e.message - } - } - - function close () { - currentPost.value = null - detailLoaded.value = false - error.value = null - } - - return { currentPost, detailLoaded, error, isOpen, open, close } -})
(no description)