import { useQuery } from '@tanstack/react-query' import { motion, AnimatePresence } from 'framer-motion' import { ShoppingBasket } from 'lucide-react' import { useActiveHeapMembers } from '../../hooks/useActiveHeapMembersQuery' import { useFilterStore } from '../../store/filterStore' import { photos as photosApi, heaps as heapsApi } from '../../services/api' import cardBg from '../../assets/card.png' /** How many thumbnails fan out across the stack at once. The newest is * drawn last (top), older ones fan back-left and back-right. */ const STACK_SIZE = 5 /** * Pinned card at the bottom of the LeftSidebar showing the currently * active heap. Renders nothing when no heap is active — the parent * layout collapses around it cleanly. * * The card is a fast nav shortcut + a satisfying landing spot for the P * pick action: every new pick optimistically updates the * ['heap-photo-ids', heapId] cache that the existing pick mutation * already maintains, so we just subscribe to the same query and let * framer-motion's AnimatePresence handle the entrance/exit animation * when ids appear or disappear. */ export function ActiveHeapCard() { const { activeHeap } = useActiveHeapMembers() const navigateToSection = useFilterStore((s) => s.navigateToSection) // Subscribes to the same cache key the pick mutation optimistically // updates. The data is already fetched (and kept fresh) by // useActiveHeapMembers above; this useQuery just gives us a render- // dependency on the array contents and stable insertion order. const { data: orderedIds = [] } = useQuery({ queryKey: ['heap-photo-ids', activeHeap?.id], queryFn: () => heapsApi.photoIds(activeHeap!.id), enabled: !!activeHeap, staleTime: 30_000, }) if (!activeHeap) return null // Show the latest STACK_SIZE photos. The backend returns ids in // insertion order so the last one in the array is the most recently // picked — that's the one we want at the front of the stack. const visible = orderedIds.slice(-STACK_SIZE) // We render newest LAST so it draws on top via z-index. Reverse so // index 0 is the back card and index N-1 is the front. const stack = visible.map((id, i) => ({ id, // Symmetric fan: front card has rotate=0, x=0; cards behind it // alternate left/right as you walk back through the stack. rotate: stackRotate(i, visible.length), x: stackOffsetX(i, visible.length), y: stackOffsetY(i, visible.length), z: i, })) return (
{/* Header — clickable, navigates to the heap section. */} {/* Stack row. Re-keyed on activeHeap.id so switching heaps tears * the animation context down cleanly instead of trying to * crossfade unrelated photos. The desert scene sits behind the * fanned thumbnails — `cover` + `bottom` keeps the dunes anchored * so the cacti frame the photos rather than the (transparent) sky. */}
{visible.length === 0 ? (
Pick photos with P to fill the heap
) : (
{stack.map((item) => ( ))}
)}
) } // ── Stack geometry ─────────────────────────────────────────────────────── // // `i` is the position in the visible array (0 = oldest, last = newest). // We want the newest card at center (rotate 0, x 0) and earlier cards // fanning symmetrically outward — so we score each card by how far it // is from the front, alternating sign. const X_STEP = 18 // pixels per fan step const Y_STEP = 2 // tiny vertical lift so the back cards peek above const ROTATE_STEP = 6 // degrees per fan step function stackRotate(i: number, len: number): number { // Distance from the front (newest). Front card → 0, then alternating // -1, +1, -2, +2 ... to spread cards outward. const fromFront = len - 1 - i if (fromFront === 0) return 0 const sign = fromFront % 2 === 1 ? -1 : 1 const magnitude = Math.ceil(fromFront / 2) return sign * magnitude * ROTATE_STEP } function stackOffsetX(i: number, len: number): number { const fromFront = len - 1 - i if (fromFront === 0) return 0 const sign = fromFront % 2 === 1 ? -1 : 1 const magnitude = Math.ceil(fromFront / 2) return sign * magnitude * X_STEP } function stackOffsetY(i: number, len: number): number { // Back cards lift up a couple pixels so they're visible above the // front card's top edge — gives the stack its sense of depth. const fromFront = len - 1 - i return fromFront * -Y_STEP }