fix: new user scroll fix, add image lightbox

This commit is contained in:
gosangam
2026-08-09 10:58:49 +05:30
parent ce373e3e03
commit df49e7afcc
4 changed files with 271 additions and 15 deletions
+66 -8
View File
@@ -40,6 +40,7 @@ import { useAuth } from "@/components/auth/auth-provider";
import { postSourceHref, quoteCardBgStyle } from "./post-card-shared";
import { PostActionsFooter } from "./post-actions-footer";
import { PostCardMenu } from "./post-card-menu";
import { PostImageLightbox } from "./post-image-lightbox";
import { FeedFollowButton } from "./feed-follow-button";
import { UpdateBackgroundModal } from "../update-background-modal";
import { MentionsDrawer } from "../mentions-drawer";
@@ -68,6 +69,8 @@ export function PostScrollCard({
const { user } = useAuth();
const isAuthor = !!user && user.id === post.user_id;
const [bgModal, setBgModal] = useState(false);
// Which attached photo the full-screen viewer is showing (null = closed).
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
const [expanded, setExpanded] = useState(false);
const [mentionsOpen, setMentionsOpen] = useState(false);
// Live mention count — seeded from the hydrated post, adjusted when the
@@ -176,7 +179,7 @@ export function PostScrollCard({
{/* Media — image carousel or gradient-with-quote */}
<div className="relative">
{hasImages ? (
<ImageCarousel images={images} />
<ImageCarousel images={images} onOpen={setLightboxIndex} />
) : (
<QuotePanel post={post} />
)}
@@ -225,14 +228,36 @@ export function PostScrollCard({
onClose={() => setMentionsOpen(false)}
onCountChange={(d) => setMentionCount((c) => Math.max(0, c + d))}
/>
<PostImageLightbox
images={images}
index={lightboxIndex}
caption={post.caption}
onIndexChange={setLightboxIndex}
onClose={() => setLightboxIndex(null)}
/>
</article>
);
}
// ─── Pieces ──────────────────────────────────────────────────────────────
function ImageCarousel({ images }: { images: PostImage[] }) {
function ImageCarousel({
images,
onOpen,
}: {
images: PostImage[];
/** Tapping a slide opens the full-screen viewer at that index. */
onOpen: (index: number) => void;
}) {
const [active, setActive] = useState(0);
const scrollerRef = useRef<HTMLDivElement>(null);
// Where the pointer went down, plus the scroller's position at that moment.
// Swiping the carousel and tapping a photo have to stay independent: the
// carousel scrolls natively (CSS scroll-snap), and browsers still fire a
// click after some swipe gestures. Comparing both the pointer travel AND
// whether the strip actually scrolled lets us drop those.
const tapStart = useRef({ x: 0, y: 0, scrollLeft: 0 });
function onScroll(e: React.UIEvent<HTMLDivElement>) {
const el = e.currentTarget;
@@ -241,9 +266,35 @@ function ImageCarousel({ images }: { images: PostImage[] }) {
setActive(Math.round(el.scrollLeft / w));
}
function onPointerDown(e: React.PointerEvent) {
tapStart.current = {
x: e.clientX,
y: e.clientY,
scrollLeft: scrollerRef.current?.scrollLeft ?? 0,
};
}
/** True only for a genuine tap — barely any pointer travel and no scroll. */
function wasTap(e: React.MouseEvent) {
const moved =
Math.abs(e.clientX - tapStart.current.x) > 8 ||
Math.abs(e.clientY - tapStart.current.y) > 8;
const scrolled =
Math.abs(
(scrollerRef.current?.scrollLeft ?? 0) - tapStart.current.scrollLeft,
) > 4;
return !moved && !scrolled;
}
if (images.length === 1) {
// Single image: no scroller, so every click is unambiguously a tap.
return (
<div className="relative aspect-square bg-muted">
<button
type="button"
onClick={() => onOpen(0)}
aria-label="View photo"
className="relative block w-full aspect-square bg-muted cursor-zoom-in outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
>
<Image
src={images[0].url}
alt=""
@@ -252,13 +303,14 @@ function ImageCarousel({ images }: { images: PostImage[] }) {
className="object-cover"
unoptimized
/>
</div>
</button>
);
}
return (
<div className="relative">
<div
ref={scrollerRef}
onScroll={onScroll}
className={cn(
"flex overflow-x-auto snap-x snap-mandatory",
@@ -266,11 +318,17 @@ function ImageCarousel({ images }: { images: PostImage[] }) {
)}
>
{images.map((img, i) => (
<div
<button
key={img.id}
className="relative w-full shrink-0 snap-start aspect-square bg-muted"
type="button"
onPointerDown={onPointerDown}
onClick={(e) => {
if (!wasTap(e)) return; // that was a swipe — let it scroll
onOpen(i);
}}
aria-roledescription="slide"
aria-label={`${i + 1} of ${images.length}`}
aria-label={`View photo ${i + 1} of ${images.length}`}
className="relative block w-full shrink-0 snap-start aspect-square bg-muted cursor-zoom-in outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
>
<Image
src={img.url}
@@ -280,7 +338,7 @@ function ImageCarousel({ images }: { images: PostImage[] }) {
className="object-cover"
unoptimized
/>
</div>
</button>
))}
</div>
<div className="absolute inset-x-0 bottom-3 flex items-center justify-center gap-1.5 pointer-events-none">