"use client" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import Link from "next/link" import { Button } from "@/components/ui/button" import { artists as staticArtists } from "@/data/artists" import { useActiveArtists } from "@/hooks/use-artists" import type { PublicArtist } from "@/types/database" import { cn } from "@/lib/utils" type ArtistGridSet = { key: string items: PublicArtist[] } const GRID_SIZE = 8 const GRID_INTERVAL = 12000 export function ArtistsSection() { const { data: dbArtistsData, isLoading, error } = useActiveArtists() const artists = useMemo(() => { if (isLoading || error || !dbArtistsData) { return staticArtists } return staticArtists.map((staticArtist) => { const dbArtist = dbArtistsData.artists.find( (db) => db.slug === staticArtist.slug || db.name === staticArtist.name ) if (dbArtist && dbArtist.portfolioImages.length > 0) { return { ...staticArtist, workImages: dbArtist.portfolioImages.map((img) => img.url), } } return staticArtist }) }, [dbArtistsData, error, isLoading]) const artistsRef = useRef(artists) const gridSetsRef = useRef([]) const activeSetRef = useRef(0) const [gridSets, setGridSets] = useState([]) const [activeSetIndex, setActiveSetIndex] = useState(0) const [previousSetIndex, setPreviousSetIndex] = useState(null) const [centerIndex, setCenterIndex] = useState(0) artistsRef.current = artists gridSetsRef.current = gridSets activeSetRef.current = activeSetIndex const shuffleArtists = useCallback((input: PublicArtist[]) => { const array = [...input] for (let i = array.length - 1; i > 0; i -= 1) { const j = Math.floor(Math.random() * (i + 1)) ;[array[i], array[j]] = [array[j], array[i]] } return array }, []) const ensureGridCount = useCallback( (pool: PublicArtist[], chunk: PublicArtist[]) => { if (chunk.length >= GRID_SIZE) { return chunk.slice(0, GRID_SIZE) } const topUpSource = shuffleArtists(pool) const needed = GRID_SIZE - chunk.length return [...chunk, ...topUpSource.slice(0, needed)] }, [shuffleArtists] ) const createKey = () => Math.random().toString(36).slice(2) const regenerateSets = useCallback(() => { const pool = artistsRef.current if (pool.length === 0) { setGridSets([]) setActiveSetIndex(0) setPreviousSetIndex(null) return } const shuffled = shuffleArtists(pool) const batches: ArtistGridSet[] = [] for (let i = 0; i < shuffled.length; i += GRID_SIZE) { const slice = ensureGridCount(pool, shuffled.slice(i, i + GRID_SIZE)) batches.push({ key: `${createKey()}-${i}`, items: slice }) } if (batches.length === 1) { const alternate = ensureGridCount(pool, shuffleArtists(pool)) batches.push({ key: `${createKey()}-alt`, items: alternate }) } setGridSets(batches) setActiveSetIndex(0) setPreviousSetIndex(null) }, [ensureGridCount, shuffleArtists]) useEffect(() => { regenerateSets() }, [artists, regenerateSets]) const advanceSet = useCallback(() => { if (gridSetsRef.current.length === 0) { return } setPreviousSetIndex(activeSetRef.current) setActiveSetIndex((prev) => { const next = prev + 1 if (next >= gridSetsRef.current.length) { regenerateSets() setCenterIndex(0) return 0 } setCenterIndex(0) return next }) }, [regenerateSets]) const rotateCarousel = useCallback((direction: 'next' | 'prev') => { if (gridSetsRef.current.length === 0) return const currentSet = gridSetsRef.current[activeSetRef.current] if (!currentSet) return setCenterIndex((prev) => { const nextIndex = direction === 'next' ? (prev + 1) % currentSet.items.length : (prev - 1 + currentSet.items.length) % currentSet.items.length // If we've looped back to start on 'next', advance to next set if (direction === 'next' && nextIndex === 0 && prev === currentSet.items.length - 1) { setTimeout(() => advanceSet(), 0) } return nextIndex }) }, [advanceSet]) useEffect(() => { if (gridSets.length === 0) { return } const interval = window.setInterval(() => { rotateCarousel('next') }, GRID_INTERVAL) return () => window.clearInterval(interval) }, [rotateCarousel, gridSets.length]) const displayIndices = useMemo(() => { const indices = new Set() indices.add(activeSetIndex) if (previousSetIndex !== null && previousSetIndex !== activeSetIndex) { indices.add(previousSetIndex) } return Array.from(indices) }, [activeSetIndex, previousSetIndex]) const getArtistImage = (artist: PublicArtist) => { // Try faceImage from static data first if ((artist as any).faceImage) { return (artist as any).faceImage } // Fall back to first work image if (artist.workImages && artist.workImages.length > 0) { return artist.workImages[0] } // Final fallback return "/placeholder.svg" } return (
Resident & Guest Artists

Artists Who Know What They're Doing

Cover-up specialists, illustrative work, anime, and fine line. Each artist brings years of experience and their own style. Custom work and flash drops.

{gridSets[activeSetIndex] && (
{gridSets[activeSetIndex].items.map((artist, index) => { const href = `/artists/${artist.slug}` const image = getArtistImage(artist) // Calculate position relative to center with wrapping for infinite loop let positionFromCenter = index - centerIndex const totalCards = gridSets[activeSetIndex].items.length // Wrap around for continuous loop effect if (positionFromCenter < -2) { positionFromCenter += totalCards } else if (positionFromCenter > totalCards / 2) { positionFromCenter -= totalCards } // Only show center + 2 behind on each side const isVisible = Math.abs(positionFromCenter) <= 2 // Calculate transforms for stacked deck effect const isCenterCard = positionFromCenter === 0 // Cards stack behind based on position let translateY = 0 let translateX = 0 let scale = 1 let opacity = 1 let zIndex = 10 if (positionFromCenter > 0) { // Cards to the right (future cards) - stack behind on right translateY = positionFromCenter * 20 translateX = positionFromCenter * 40 scale = 1 - positionFromCenter * 0.08 opacity = Math.max(0, 1 - positionFromCenter * 0.4) zIndex = 10 - positionFromCenter } else if (positionFromCenter < 0) { // Cards to the left (past cards) - slide out to left and fade translateX = positionFromCenter * 150 opacity = Math.max(0, 1 + positionFromCenter * 0.5) zIndex = 10 + positionFromCenter } if (!isVisible) return null return ( {`${artist.name}

{artist.name}

{(artist as any).specialty || "Tattoo Artist"}

) })} {/* Navigation buttons */}
)}
) }