"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 = 16 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) 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() return 0 } return next }) }, [regenerateSets]) useEffect(() => { if (gridSets.length === 0) { return } const interval = window.setInterval(() => { advanceSet() }, GRID_INTERVAL) return () => window.clearInterval(interval) }, [advanceSet, 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) => { const candidate = (artist as any).faceImage || artist.workImages?.[0] if (candidate) { return candidate } return "/placeholder.svg" } return (
Resident & Guest Artists

A Collective of Story-Driven Tattoo Artists

United Tattoo is home to cover-up virtuosos, illustrative explorers, anime specialists, and fine line minimalists. Every artist curates their chair with intention—offering custom narratives, flash experiments, and collaborative pieces that evolve with you.

What to Expect

Consultation-first Process

Artist pairing • Mood-boards • Aftercare guides • CalDAV-synced scheduling

Specialties

Layered Stylescapes

Black & grey realism • Neo-traditional color • Bold cover-ups • Fine line botanicals

Let's Plan Your Piece

Choose your artist, share your story, and build a tattoo ritual around intentional ink.

) }