From 3b63d62b958621dcc0169dd511857532615de10e Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 4 Mar 2026 12:11:06 -0800 Subject: [PATCH] feat: add page-flip animation, click and swipe navigation to deck viewer Click left/right halves to navigate. Swipe on touch devices. 3D perspective page-flip animation on transitions. Co-Authored-By: Claude Opus 4.6 --- components/sponsorship-flipbook.tsx | 103 ++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/components/sponsorship-flipbook.tsx b/components/sponsorship-flipbook.tsx index c8aeaa0..3f8e44a 100644 --- a/components/sponsorship-flipbook.tsx +++ b/components/sponsorship-flipbook.tsx @@ -1,37 +1,100 @@ "use client" -import { useState } from "react" +import { useState, useRef, useCallback } from "react" import Image from "next/image" import { ChevronLeft, ChevronRight } from "lucide-react" import { Button } from "@/components/ui/button" const TOTAL_SLIDES = 12 +const SWIPE_THRESHOLD = 50 export default function SponsorshipFlipbook() { const [currentSlide, setCurrentSlide] = useState(0) + const [isFlipping, setIsFlipping] = useState(false) + const [flipDirection, setFlipDirection] = useState<"left" | "right">("left") + const touchStartX = useRef(0) + const touchEndX = useRef(0) - const prevSlide = () => setCurrentSlide((s) => Math.max(0, s - 1)) - const nextSlide = () => setCurrentSlide((s) => Math.min(TOTAL_SLIDES - 1, s + 1)) + const goTo = useCallback( + (direction: "prev" | "next") => { + if (isFlipping) return + if (direction === "prev" && currentSlide === 0) return + if (direction === "next" && currentSlide >= TOTAL_SLIDES - 1) return + + setFlipDirection(direction === "next" ? "left" : "right") + setIsFlipping(true) + + setTimeout(() => { + setCurrentSlide((s) => + direction === "next" + ? Math.min(TOTAL_SLIDES - 1, s + 1) + : Math.max(0, s - 1) + ) + setIsFlipping(false) + }, 400) + }, + [currentSlide, isFlipping] + ) + + const handleClick = (e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect() + const clickX = e.clientX - rect.left + if (clickX < rect.width / 2) { + goTo("prev") + } else { + goTo("next") + } + } + + const handleTouchStart = (e: React.TouchEvent) => { + touchStartX.current = e.touches[0].clientX + touchEndX.current = e.touches[0].clientX + } + + const handleTouchMove = (e: React.TouchEvent) => { + touchEndX.current = e.touches[0].clientX + } + + const handleTouchEnd = () => { + const diff = touchStartX.current - touchEndX.current + if (Math.abs(diff) > SWIPE_THRESHOLD) { + goTo(diff > 0 ? "next" : "prev") + } + } return (
-
+
- {Array.from({ length: TOTAL_SLIDES }, (_, i) => ( -
- {`Sponsorship -
- ))} + {`Sponsorship
@@ -39,7 +102,7 @@ export default function SponsorshipFlipbook() {