v272vjg35: Swipe through a post's media on mobile slides

This commit is contained in:
2026-10-20 08:26:54 -06:00
parent adcd0cfd96
commit b97dddfc4c
3 changed files with 86 additions and 2 deletions
+36 -2
View File
@@ -82,8 +82,42 @@ function SlideMedia({ media, author }: { media: CardMedia; author: string }) {
);
}
/** Several media items in one slide: a sideways scroll-snap track with a dot per item. */
function MediaTrack({ items, author }: { items: CardMedia[]; author: string }) {
const track = useRef<HTMLDivElement>(null);
const [active, setActive] = useState(0);
const onScroll = () => {
const el = track.current;
if (!el) return;
const next = Math.min(items.length - 1, Math.max(0, Math.round(el.scrollLeft / Math.max(el.clientWidth, 1))));
if (next === active) return;
setActive(next);
// A video stops when it is swiped away.
el.querySelectorAll<HTMLElement>(":scope > .slide-pane").forEach((pane, i) => {
if (i !== next) pane.querySelectorAll("video").forEach((v) => v.pause());
});
};
return (
<>
<div className="slide-track" data-testid="slide-track" ref={track} onScroll={onScroll}>
{items.map((m, i) => (
<div className="slide-pane" key={i} aria-label={`Media ${i + 1} of ${items.length}`}>
<SlideMedia media={m} author={author} />
</div>
))}
</div>
<div className="slide-dots" data-testid="slide-dots" aria-hidden="true">
{items.map((_, i) => (
<span key={i} className={`slide-dot${i === active ? " slide-dot-on" : ""}`} />
))}
</div>
</>
);
}
function PostSlide({ post, index, current, p }: { post: CardPost; index: number; current: boolean; p: FeedViewProps }) {
const media = post.media.find((m) => m.kind === "video" || m.src) ?? null;
const items = post.media.filter((m) => m.kind === "video" || m.src);
const media = items[0] ?? null;
const own = canDelete(post, p.feedUserId) && !!p.onDelete;
// While a video plays, the author and text step aside so the native controls stay clear.
const [playing, setPlaying] = useState(false);
@@ -100,7 +134,7 @@ function PostSlide({ post, index, current, p }: { post: CardPost; index: number;
>
{media ? (
<>
<SlideMedia media={media} author={post.authorName} />
{items.length > 1 ? <MediaTrack items={items} author={post.authorName} /> : <SlideMedia media={media} author={post.authorName} />}
<div className="slide-scrim-top" aria-hidden="true" />
<div className="slide-scrim" aria-hidden="true" />
</>
+16
View File
@@ -89,6 +89,22 @@ export const FIXTURE_FEED_ITEMS: unknown[] = [
mediaItems: [{ mediaType: "video", thumbnailUrl: placeholderImage(210, "Weekly reviews"), mp4Url: FIXTURE_VIDEO_URL, videoDuration: 154, order: 0 }],
},
},
{
type: "post",
publishedAt: "2026-10-18T21:20:00Z",
likesCount: 22,
commentsCount: 2,
post: {
id: "p7",
content: "Workshop recap: two slides and the demo recording. Swipe sideways for all three.",
author: author("a7", "Leila Haddad"),
mediaItems: [
{ mediaType: "image", imageUrl: placeholderImage(140, "Slide one"), order: 0 },
{ mediaType: "image", imageUrl: placeholderImage(260, "Slide two"), order: 1 },
{ mediaType: "video", thumbnailUrl: placeholderImage(20, "Demo recording"), mp4Url: FIXTURE_VIDEO_URL, videoDuration: 73, order: 2 },
],
},
},
{
type: "post",
publishedAt: "2026-10-18T19:05:00Z",
+34
View File
@@ -56,6 +56,40 @@
.slide-play { z-index: 4; }
.slide-dur { top: 128px; right: 16px; z-index: 4; }
/* Several media items: a sideways snap track; each pane is one full-slide item. */
.slide-track {
position: absolute;
inset: 0;
display: flex;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
.slide-track::-webkit-scrollbar { display: none; }
.slide-pane { position: relative; flex: 0 0 100%; height: 100%; scroll-snap-align: start; scroll-snap-stop: always; }
.slide-dots {
position: absolute;
left: 0;
right: 0;
bottom: 84px;
z-index: 4;
display: flex;
justify-content: center;
gap: 5px;
pointer-events: none;
}
.slide-dot {
display: block;
width: 5px;
height: 5px;
border-radius: 3px;
background: var(--on-media-line);
transition: width .5s var(--ease), background-color .5s;
}
.slide-dot-on { width: 18px; background: var(--accent); }
.slide-scrim,
.slide-scrim-top { position: absolute; left: 0; right: 0; z-index: 1; pointer-events: none; }
.slide-scrim { bottom: 0; height: 58%; background: linear-gradient(180deg, var(--media-fade-0), var(--media-fade)); }