11"use client"
22
3- import { useState } from "react"
3+ import { useRef , useState } from "react"
4+ import { motion , useMotionTemplate , useSpring } from "motion/react"
45import { clsx } from "clsx"
56import Image from "next-image-export-optimizer"
7+ import type { StaticImageData } from "next/image"
8+
69import { Marquee } from "@/app/conf/_design-system/marquee"
10+ import ZoomInIcon from "../../pixelarticons/zoom-in.svg?svgr"
11+ import ZoomOutIcon from "../../pixelarticons/zoom-out.svg?svgr"
712
813import { imagesByYear } from "./images"
914
@@ -15,6 +20,8 @@ export interface GalleryStripProps extends React.HTMLAttributes<HTMLElement> {}
1520export function GalleryStrip ( { className, ...rest } : GalleryStripProps ) {
1621 const [ selectedYear , setSelectedYear ] = useState < Year > ( "2024" )
1722
23+ const previousZoomedImage = useRef < HTMLElement | null > ( null )
24+
1825 return (
1926 < section
2027 role = "presentation"
@@ -27,7 +34,7 @@ export function GalleryStrip({ className, ...rest }: GalleryStripProps) {
2734 key = { year }
2835 onClick = { ( ) => setSelectedYear ( year ) }
2936 className = { clsx (
30- "p-1 typography-menu" ,
37+ "gql-focus-visible p-1 typography-menu" ,
3138 selectedYear === year
3239 ? "bg-sec-light text-neu-900 dark:text-neu-0"
3340 : "text-neu-800" ,
@@ -39,25 +46,100 @@ export function GalleryStrip({ className, ...rest }: GalleryStripProps) {
3946 </ div >
4047
4148 < div className = "mt-6 w-full md:mt-10" >
42- < Marquee gap = { 8 } speed = { 35 } speedOnHover = { 15 } drag reverse >
49+ < Marquee
50+ gap = { 8 }
51+ speed = { 35 }
52+ speedOnHover = { 15 }
53+ drag
54+ reverse
55+ className = "!overflow-visible"
56+ >
4357 { imagesByYear [ selectedYear ] . map ( ( image , i ) => {
58+ const key = `${ selectedYear } -${ i } `
59+
4460 return (
45- < div
46- key = { `${ selectedYear } -${ i } ` }
47- className = "md:px-2"
48- role = "presentation"
49- >
50- < Image
51- src = { image }
52- alt = ""
53- height = { 320 }
54- className = "pointer-events-none"
55- />
56- </ div >
61+ < GalleryStripImage
62+ key = { key }
63+ image = { image }
64+ previousZoomedImage = { previousZoomedImage }
65+ />
5766 )
5867 } ) }
5968 </ Marquee >
6069 </ div >
6170 </ section >
6271 )
6372}
73+
74+ function GalleryStripImage ( {
75+ image,
76+ previousZoomedImage,
77+ } : {
78+ image : StaticImageData
79+ previousZoomedImage : React . MutableRefObject < HTMLElement | null >
80+ } ) {
81+ const [ isZoomed , setIsZoomed ] = useState ( false )
82+ const scale = useSpring ( 1 )
83+ const transform = useMotionTemplate `translate3d(0,0,var(--translate-z,-16px)) scale(${ scale } )`
84+
85+ // if we set scale in useEffect the UI glitches
86+ const zoomIn = ( current : HTMLElement | null ) => {
87+ if ( previousZoomedImage . current ) {
88+ previousZoomedImage . current . style . zIndex = "0"
89+ previousZoomedImage . current . style . setProperty ( "--translate-z" , "0px" )
90+ }
91+
92+ if ( current ) {
93+ current . style . zIndex = "2"
94+ current . style . setProperty ( "--translate-z" , "16px" )
95+ }
96+
97+ previousZoomedImage . current = current
98+
99+ scale . set ( 1.665625 )
100+ setIsZoomed ( true )
101+ }
102+
103+ const zoomOut = ( ) => {
104+ scale . set ( 1 )
105+ setIsZoomed ( false )
106+ }
107+
108+ return (
109+ < motion . div
110+ role = "presentation"
111+ className = "relative md:px-2"
112+ style = { { transform } }
113+ onPointerOut = { event => {
114+ const target = event . currentTarget
115+ const relatedTarget = event . relatedTarget as Node | null
116+
117+ if ( ! relatedTarget || ! target . contains ( relatedTarget ) ) {
118+ zoomOut ( )
119+ }
120+ } }
121+ >
122+ < Image
123+ src = { image }
124+ alt = ""
125+ role = "presentation"
126+ width = { 799 }
127+ height = { 533 }
128+ className = "pointer-events-none aspect-[799/533] h-[320px] w-auto object-cover"
129+ />
130+ < button
131+ type = "button"
132+ className = "absolute right-2 top-0 z-[1] bg-neu-50/10 p-4"
133+ onClick = { event => {
134+ isZoomed ? zoomOut ( ) : zoomIn ( event . currentTarget . parentElement )
135+ } }
136+ >
137+ { isZoomed ? (
138+ < ZoomOutIcon className = "size-12" />
139+ ) : (
140+ < ZoomInIcon className = "size-12" />
141+ ) }
142+ </ button >
143+ </ motion . div >
144+ )
145+ }
0 commit comments