From 509a7958cfdd807420f14334c3eaef140b4ebf3a Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 4 Jun 2026 17:29:19 -0400 Subject: [PATCH] feat(posts): images open the modal; only text expands in place Post cards no longer expand the whole card on click (the old two-click path to the images). The card is compact-only now: - Hero / rail thumbs / the +N tile are buttons that open the post-scoped image modal (modal.open(id, { postImageIds })) so you view big + arrow through ALL the post's images. The feed caps thumbnails at 6, so for posts with more we lazily getPostFull to get the complete id list; +N opens at the first hidden image. - The description is the ONLY in-place expansion: a Show more / Show less toggle shown only when the text is actually truncated (server description_truncated flag OR a measured CSS-clamp overflow, ResizeObserver-guarded). Expanding loads description_full when server-truncated and renders it unclamped. - Attachments: download chips now render inline in the compact card (the feed already carries download_url), since the expanded view is gone. Removes PostImageGrid.vue (the mosaic, now unused). Tests cover show-more visibility + image-click opening the scoped modal. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/posts/PostCard.vue | 358 ++++++++---------- .../src/components/posts/PostImageGrid.vue | 63 --- frontend/src/stores/modal.js | 6 +- frontend/test/components/postCard.spec.js | 61 ++- 4 files changed, 207 insertions(+), 281 deletions(-) delete mode 100644 frontend/src/components/posts/PostImageGrid.vue diff --git a/frontend/src/components/posts/PostCard.vue b/frontend/src/components/posts/PostCard.vue index a45f0a5..29aa76d 100644 --- a/frontend/src/components/posts/PostCard.vue +++ b/frontend/src/components/posts/PostCard.vue @@ -1,29 +1,18 @@ - - diff --git a/frontend/src/stores/modal.js b/frontend/src/stores/modal.js index 05876d0..3859b61 100644 --- a/frontend/src/stores/modal.js +++ b/frontend/src/stores/modal.js @@ -18,9 +18,9 @@ export const useModalStore = defineStore('modal', () => { const inflight = useInflightToken() // 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/ neighbors). + // (used by PostCard image clicks — the modal is scoped to that post's + // images). 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/test/components/postCard.spec.js b/frontend/test/components/postCard.spec.js index 1e39004..3625ca1 100644 --- a/frontend/test/components/postCard.spec.js +++ b/frontend/test/components/postCard.spec.js @@ -1,26 +1,61 @@ // @vitest-environment happy-dom -import { describe, it, expect } from 'vitest' +import { describe, it, expect, vi } from 'vitest' +import { flushPromises } from '@vue/test-utils' import PostCard from '../../src/components/posts/PostCard.vue' +import { useModalStore } from '../../src/stores/modal.js' import { freshPinia, mountComponent } from '../support/mountComponent.js' +const now = new Date().toISOString() +const BASE = { + id: 1, external_post_id: 'P1', + post_title: 'Hello World', + post_url: 'https://x/1', post_date: now, downloaded_at: now, + artist: { id: 1, name: 'Sabu', slug: 'sabu' }, + source: { id: 1, platform: 'subscribestar' }, + thumbnails: [], thumbnails_more: 0, attachments: [], +} + describe('PostCard', () => { it('renders the HTML-stripped title and the artist', () => { - const pinia = freshPinia() - const now = new Date().toISOString() - const post = { - id: 1, external_post_id: 'P1', - post_title: 'Hello World', - post_url: 'https://x/1', post_date: now, downloaded_at: now, - description_plain: 'a description', - artist: { id: 1, name: 'Sabu', slug: 'sabu' }, - source: { id: 1, platform: 'subscribestar' }, - thumbnails: [], thumbnails_more: 0, attachments: [], - } - const w = mountComponent(PostCard, { props: { post }, pinia }) + const post = { ...BASE, description_plain: 'a description' } + const w = mountComponent(PostCard, { props: { post }, pinia: freshPinia() }) const t = w.text() expect(t).toContain('Hello World') // plain title expect(t).not.toContain('') // tags stripped expect(t).toContain('Sabu') // artist (RouterLink slot) }) + + it('shows a "Show more" toggle only when the description is truncated', () => { + const truncated = mountComponent(PostCard, { + props: { post: { ...BASE, description_plain: 'lots of text…', description_truncated: true } }, + pinia: freshPinia(), + }) + expect(truncated.text()).toContain('Show more') + + const full = mountComponent(PostCard, { + props: { post: { ...BASE, description_plain: 'short', description_truncated: false } }, + pinia: freshPinia(), + }) + expect(full.text()).not.toContain('Show more') + }) + + it('clicking an image opens the post-scoped modal (never expands the card)', async () => { + const pinia = freshPinia() + const modal = useModalStore() + const openSpy = vi.spyOn(modal, 'open').mockResolvedValue() + const post = { + ...BASE, + description_plain: 'd', + thumbnails: [ + { image_id: 10, thumbnail_url: '/a' }, + { image_id: 11, thumbnail_url: '/b' }, + ], + thumbnails_more: 0, + } + const w = mountComponent(PostCard, { props: { post }, pinia }) + await w.find('.fc-post-card__hero').trigger('click') + await flushPromises() + expect(openSpy).toHaveBeenCalledWith(10, { postImageIds: [10, 11] }) + }) })