1
1
"use client"
2
2
3
- import { useState } from "react"
3
+ import { useRef , useState } from "react"
4
+ import { motion , useMotionTemplate , useSpring } from "motion/react"
4
5
import { clsx } from "clsx"
5
6
import Image from "next-image-export-optimizer"
7
+ import type { StaticImageData } from "next/image"
8
+
6
9
import { 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"
7
12
8
13
import { imagesByYear } from "./images"
9
14
@@ -15,6 +20,8 @@ export interface GalleryStripProps extends React.HTMLAttributes<HTMLElement> {}
15
20
export function GalleryStrip ( { className, ...rest } : GalleryStripProps ) {
16
21
const [ selectedYear , setSelectedYear ] = useState < Year > ( "2024" )
17
22
23
+ const previousZoomedImage = useRef < HTMLElement | null > ( null )
24
+
18
25
return (
19
26
< section
20
27
role = "presentation"
@@ -27,7 +34,7 @@ export function GalleryStrip({ className, ...rest }: GalleryStripProps) {
27
34
key = { year }
28
35
onClick = { ( ) => setSelectedYear ( year ) }
29
36
className = { clsx (
30
- "p-1 typography-menu" ,
37
+ "gql-focus-visible p-1 typography-menu" ,
31
38
selectedYear === year
32
39
? "bg-sec-light text-neu-900 dark:text-neu-0"
33
40
: "text-neu-800" ,
@@ -39,25 +46,100 @@ export function GalleryStrip({ className, ...rest }: GalleryStripProps) {
39
46
</ div >
40
47
41
48
< 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
+ >
43
57
{ imagesByYear [ selectedYear ] . map ( ( image , i ) => {
58
+ const key = `${ selectedYear } -${ i } `
59
+
44
60
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
+ />
57
66
)
58
67
} ) }
59
68
</ Marquee >
60
69
</ div >
61
70
</ section >
62
71
)
63
72
}
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