From 21da20d927f205d2021fe0065af0ccad0dea9827 Mon Sep 17 00:00:00 2001 From: Nicholai Date: Mon, 20 Oct 2025 18:02:25 -0600 Subject: [PATCH] feat(artists/mobile): add lightbox swipe, preserve scroll on modal, animated filter pills, carousel dots indicators, and a11y roles; desktop untouched --- components/artist-portfolio.tsx | 83 +++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/components/artist-portfolio.tsx b/components/artist-portfolio.tsx index 8d08025e4..cd1a9b6a6 100644 --- a/components/artist-portfolio.tsx +++ b/components/artist-portfolio.tsx @@ -5,11 +5,11 @@ import Image from "next/image" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import Link from "next/link" -import { ArrowLeft, Instagram, ExternalLink, Loader2, DollarSign } from "lucide-react" +import { Instagram, ExternalLink, Loader2, DollarSign } from "lucide-react" import { useArtist } from "@/hooks/use-artist-data" import { useIsMobile } from "@/hooks/use-mobile" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Carousel, CarouselContent, CarouselItem } from "@/components/ui/carousel" +import { type CarouselApi, Carousel, CarouselContent, CarouselItem } from "@/components/ui/carousel" import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area" interface ArtistPortfolioProps { @@ -22,6 +22,10 @@ export function ArtistPortfolio({ artistId }: ArtistPortfolioProps) { const [scrollY, setScrollY] = useState(0) const [mobileView, setMobileView] = useState<"grid" | "carousel">("grid") const isMobile = useIsMobile() + // carousel indicator state (mobile) + const [carouselApi, setCarouselApi] = useState(null) + const [carouselCount, setCarouselCount] = useState(0) + const [carouselCurrent, setCarouselCurrent] = useState(0) // Fetch artist data from API const { data: artist, isLoading, error } = useArtist(artistId) @@ -29,6 +33,7 @@ export function ArtistPortfolio({ artistId }: ArtistPortfolioProps) { // keep a reference to the last focused thumbnail so we can return focus on modal close const lastFocusedRef = useRef(null) const closeButtonRef = useRef(null) + const touchStartX = useRef(null) useEffect(() => { // Enable parallax only on desktop to avoid jank on mobile @@ -38,6 +43,38 @@ export function ArtistPortfolio({ artistId }: ArtistPortfolioProps) { return () => window.removeEventListener("scroll", handleScroll) }, [isMobile]) + // Preserve scroll position when modal opens/closes + useEffect(() => { + if (!selectedImage) return + const y = window.scrollY + const { body } = document + body.style.position = "fixed" + body.style.top = `-${y}px` + body.style.left = "0" + body.style.right = "0" + return () => { + const top = body.style.top + body.style.position = "" + body.style.top = "" + body.style.left = "" + body.style.right = "" + const restoreY = Math.abs(parseInt(top || "0", 10)) + window.scrollTo(0, restoreY) + } + }, [selectedImage]) + + // Carousel indicators state wiring + useEffect(() => { + if (!carouselApi) return + setCarouselCount(carouselApi.scrollSnapList().length) + setCarouselCurrent(carouselApi.selectedScrollSnap()) + const onSelect = () => setCarouselCurrent(carouselApi.selectedScrollSnap()) + carouselApi.on("select", onSelect) + return () => { + carouselApi.off("select", onSelect) + } + }, [carouselApi]) + // Derived lists (safe when `artist` is undefined during initial renders) const portfolioImages = artist?.portfolioImages || [] @@ -401,8 +438,8 @@ export function ArtistPortfolio({ artistId }: ArtistPortfolioProps) { {/* Category Filter - horizontal pills */} {categories.length > 1 && (
- -
+ +
{categories.map((category) => { const count = category === "All" ? portfolioImages.length @@ -412,8 +449,8 @@ export function ArtistPortfolio({ artistId }: ArtistPortfolioProps) {
) : ( -
- +
+ {filteredPortfolio.map((item) => ( @@ -506,6 +543,18 @@ export function ArtistPortfolio({ artistId }: ArtistPortfolioProps) {
{filteredPortfolio.length} pieces
+ {/* Dots indicators */} +
+ {Array.from({ length: carouselCount }).map((_, i) => ( +
)} @@ -572,6 +621,24 @@ export function ArtistPortfolio({ artistId }: ArtistPortfolioProps) {
e.stopPropagation()} + onTouchStart={(e) => { + touchStartX.current = e.touches[0].clientX + }} + onTouchEnd={(e) => { + if (touchStartX.current == null) return + const dx = e.changedTouches[0].clientX - touchStartX.current + const threshold = 40 + if (Math.abs(dx) > threshold) { + if (dx < 0) { + const next = (currentIndex + 1) % filteredPortfolio.length + goToIndex(next) + } else { + const prev = (currentIndex - 1 + filteredPortfolio.length) % filteredPortfolio.length + goToIndex(prev) + } + } + touchStartX.current = null + }} > {/* Prev */}