diff --git a/components/artists-section.tsx b/components/artists-section.tsx index d41308386..70758ce7e 100644 --- a/components/artists-section.tsx +++ b/components/artists-section.tsx @@ -2,175 +2,256 @@ import { useEffect, useMemo, useRef, useState } from "react" import Link from "next/link" +import { motion, AnimatePresence, useMotionValue, useTransform } from "framer-motion" import { useFeatureFlag } from "@/components/feature-flags-provider" import { Button } from "@/components/ui/button" -import { artists } from "@/data/artists" +import { artists as staticArtists } from "@/data/artists" +import { useActiveArtists } from "@/hooks/use-artists" +import type { PublicArtist } from "@/types/database" export function ArtistsSection() { - // Minimal animation: fade-in only (no parallax) - const [visibleCards, setVisibleCards] = useState([]) - const sectionRef = useRef(null) - const advancedNavAnimations = useFeatureFlag("ADVANCED_NAV_SCROLL_ANIMATIONS_ENABLED") - const allArtistIndices = useMemo(() => Array.from({ length: artists.length }, (_, idx) => idx), []) + // Fetch artists from database + const { data: dbArtistsData, isLoading, error } = useActiveArtists() - useEffect(() => { - if (!advancedNavAnimations) { - setVisibleCards(allArtistIndices) - return - } - setVisibleCards([]) - }, [advancedNavAnimations, allArtistIndices]) + // Merge static and database data + const artists = useMemo(() => { + // If still loading or error, use static data + if (isLoading || error || !dbArtistsData) { + return staticArtists + } - useEffect(() => { - if (!advancedNavAnimations) return - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - const cardIndex = Number.parseInt(entry.target.getAttribute("data-index") || "0") - setVisibleCards((prev) => [...new Set([...prev, cardIndex])]) - } + // Merge: use database portfolio images, keep static metadata + return staticArtists.map(staticArtist => { + const dbArtist = dbArtistsData.artists.find( + (db) => db.slug === staticArtist.slug || db.name === staticArtist.name + ) + + // If found in database, use its portfolio images + if (dbArtist && dbArtist.portfolioImages.length > 0) { + return { + ...staticArtist, + workImages: dbArtist.portfolioImages.map(img => img.url) + } + } + + // Fall back to static data + return staticArtist }) - }, - { threshold: 0.2, rootMargin: "0px 0px -10% 0px" }, + }, [dbArtistsData, isLoading, error]) + + // Minimal animation: fade-in only (no parallax) + const [visibleCards, setVisibleCards] = useState([]) + const [hoveredCard, setHoveredCard] = useState(null) + const [portfolioIndices, setPortfolioIndices] = useState>({}) + const sectionRef = useRef(null) + const advancedNavAnimations = useFeatureFlag("ADVANCED_NAV_SCROLL_ANIMATIONS_ENABLED") + const allArtistIndices = useMemo(() => Array.from({ length: artists.length }, (_, idx) => idx), [artists.length]) + + useEffect(() => { + if (!advancedNavAnimations) { + setVisibleCards(allArtistIndices) + return + } + setVisibleCards([]) + }, [advancedNavAnimations, allArtistIndices]) + + useEffect(() => { + if (!advancedNavAnimations) return + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const cardIndex = Number.parseInt(entry.target.getAttribute("data-index") || "0") + setVisibleCards((prev) => [...new Set([...prev, cardIndex])]) + } + }) + }, + { threshold: 0.2, rootMargin: "0px 0px -10% 0px" }, + ) + const cards = sectionRef.current?.querySelectorAll("[data-index]") + cards?.forEach((card) => observer.observe(card)) + return () => observer.disconnect() + }, [advancedNavAnimations]) + + const cardVisibilityClass = (index: number) => { + if (!advancedNavAnimations) return "opacity-100 translate-y-0" + return visibleCards.includes(index) ? "opacity-100 translate-y-0" : "opacity-0 translate-y-6" + } + + const cardTransitionDelay = (index: number) => { + if (!advancedNavAnimations) return undefined + return `${index * 40}ms` + } + + // Vary aspect ratio to create a subtle masonry rhythm + const aspectFor = (i: number) => { + const variants = ["aspect-[3/4]", "aspect-[4/5]", "aspect-square"] + return variants[i % variants.length] + } + + // Handle hover to cycle through portfolio images + const handleHoverStart = (artistIndex: number) => { + setHoveredCard(artistIndex) + const artist = artists[artistIndex] + if (artist.workImages.length > 0) { + setPortfolioIndices((prev) => { + const currentIndex = prev[artistIndex] ?? 0 + const nextIndex = (currentIndex + 1) % artist.workImages.length + return { ...prev, [artistIndex]: nextIndex } + }) + } + } + + const handleHoverEnd = () => { + setHoveredCard(null) + } + + const getPortfolioImage = (artistIndex: number) => { + const artist = artists[artistIndex] + if (artist.workImages.length === 0) return null + const imageIndex = portfolioIndices[artistIndex] ?? 0 + return artist.workImages[imageIndex] + } + + return ( +
+ {/* Faint logo texture */} +
+ +
+
+ + {/* Header */} +
+
+
+
+

ARTISTS

+

+ Our exceptional team of tattoo artists, each bringing unique expertise and artistic vision to create your perfect + tattoo. +

+
+
+ +
+
+
+
+ + {/* Masonry grid */} +
+
+ {/* columns-based masonry; tighter spacing and wider section */} +
+ {artists.map((artist, i) => { + const transitionDelay = cardTransitionDelay(i) + const portfolioImage = getPortfolioImage(i) + const isHovered = hoveredCard === i + + return ( +
+ + handleHoverStart(i)} + onHoverEnd={handleHoverEnd} + > + {/* Base layer: artist portrait */} +
+ {`${artist.name} +
+ + {/* Wipe overlay: portfolio image with curved boundary */} + + {isHovered && portfolioImage && ( + <> + {/* SVG clipPath with pronounced wave */} + + + + + + + + + {/* Portfolio image with curved clip */} +
+ {`${artist.name} +
+ + )} +
+ + {/* Minimal footer - only name */} +
+

{artist.name}

+

{artist.specialty}

+
+
+ +
+ ) + })} +
+
+
+ + {/* CTA Footer */} +
+
+

READY?

+

+ Choose your artist and start your tattoo journey with United Tattoo. +

+ +
+
+
) - const cards = sectionRef.current?.querySelectorAll("[data-index]") - cards?.forEach((card) => observer.observe(card)) - return () => observer.disconnect() - }, [advancedNavAnimations]) - - const cardVisibilityClass = (index: number) => { - if (!advancedNavAnimations) return "opacity-100 translate-y-0" - return visibleCards.includes(index) ? "opacity-100 translate-y-0" : "opacity-0 translate-y-6" - } - - const cardTransitionDelay = (index: number) => { - if (!advancedNavAnimations) return undefined - return `${index * 40}ms` - } - - // Vary aspect ratio to create a subtle masonry rhythm - const aspectFor = (i: number) => { - const variants = ["aspect-[3/4]", "aspect-[4/5]", "aspect-square"] - return variants[i % variants.length] - } - - return ( -
- {/* Faint logo texture */} -
- -
-
- - {/* Header */} -
-
-
-
-

ARTISTS

-

- Our exceptional team of tattoo artists, each bringing unique expertise and artistic vision to create your perfect - tattoo. -

-
-
- -
-
-
-
- - {/* Masonry grid */} -
-
- {/* columns-based masonry; tighter spacing and wider section */} -
- {artists.map((artist, i) => { - const transitionDelay = cardTransitionDelay(i) - return ( -
-
- {/* Imagery: use only the artist portrait */} -
- {`${artist.name} -
- - {/* Softer hover wash (replaces heavy overlay) */} -
- - {/* Top-left experience pill */} -
- - {artist.experience} - -
- - {/* Minimal footer */} -
-

{artist.name}

-

{artist.specialty}

- -
- - -
-
-
-
- ) - })} -
-
-
- - {/* CTA Footer */} -
-
-

READY?

-

- Choose your artist and start your tattoo journey with United Tattoo. -

- -
-
-
- ) } diff --git a/package-lock.json b/package-lock.json index 888121f18..13c701518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "cmdk": "latest", "date-fns": "latest", "embla-carousel-react": "8.5.1", + "framer-motion": "^12.23.24", "geist": "^1.3.1", "ical.js": "^1.5.0", "input-otp": "latest", @@ -16895,6 +16896,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -19006,6 +19034,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 54bc98f90..7af5cc6e0 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "cmdk": "latest", "date-fns": "latest", "embla-carousel-react": "8.5.1", + "framer-motion": "^12.23.24", "geist": "^1.3.1", "ical.js": "^1.5.0", "input-otp": "latest",