Skip to content

new conf design — gallery strip #1999

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement custom zooming
  • Loading branch information
hasparus committed May 20, 2025
commit 93cd26b9c7685bda930bf19087029cba3fba6644
112 changes: 97 additions & 15 deletions src/app/conf/2025/components/gallery-strip/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
"use client"

import { useState } from "react"
import { useRef, useState } from "react"
import { motion, useMotionTemplate, useSpring } from "motion/react"
import { clsx } from "clsx"
import Image from "next-image-export-optimizer"
import type { StaticImageData } from "next/image"

import { Marquee } from "@/app/conf/_design-system/marquee"
import ZoomInIcon from "../../pixelarticons/zoom-in.svg?svgr"
import ZoomOutIcon from "../../pixelarticons/zoom-out.svg?svgr"

import { imagesByYear } from "./images"

Expand All @@ -15,6 +20,8 @@ export interface GalleryStripProps extends React.HTMLAttributes<HTMLElement> {}
export function GalleryStrip({ className, ...rest }: GalleryStripProps) {
const [selectedYear, setSelectedYear] = useState<Year>("2024")

const previousZoomedImage = useRef<HTMLElement | null>(null)

return (
<section
role="presentation"
Expand All @@ -27,7 +34,7 @@ export function GalleryStrip({ className, ...rest }: GalleryStripProps) {
key={year}
onClick={() => setSelectedYear(year)}
className={clsx(
"p-1 typography-menu",
"gql-focus-visible p-1 typography-menu",
selectedYear === year
? "bg-sec-light text-neu-900 dark:text-neu-0"
: "text-neu-800",
Expand All @@ -39,25 +46,100 @@ export function GalleryStrip({ className, ...rest }: GalleryStripProps) {
</div>

<div className="mt-6 w-full md:mt-10">
<Marquee gap={8} speed={35} speedOnHover={15} drag reverse>
<Marquee
gap={8}
speed={35}
speedOnHover={15}
drag
reverse
className="!overflow-visible"
>
{imagesByYear[selectedYear].map((image, i) => {
const key = `${selectedYear}-${i}`

return (
<div
key={`${selectedYear}-${i}`}
className="md:px-2"
role="presentation"
>
<Image
src={image}
alt=""
height={320}
className="pointer-events-none"
/>
</div>
<GalleryStripImage
key={key}
image={image}
previousZoomedImage={previousZoomedImage}
/>
)
})}
</Marquee>
</div>
</section>
)
}

function GalleryStripImage({
image,
previousZoomedImage,
}: {
image: StaticImageData
previousZoomedImage: React.MutableRefObject<HTMLElement | null>
}) {
const [isZoomed, setIsZoomed] = useState(false)
const scale = useSpring(1)
const transform = useMotionTemplate`translate3d(0,0,var(--translate-z,-16px)) scale(${scale})`

// if we set scale in useEffect the UI glitches
const zoomIn = (current: HTMLElement | null) => {
if (previousZoomedImage.current) {
previousZoomedImage.current.style.zIndex = "0"
previousZoomedImage.current.style.setProperty("--translate-z", "0px")
}

if (current) {
current.style.zIndex = "2"
current.style.setProperty("--translate-z", "16px")
}

previousZoomedImage.current = current

scale.set(1.665625)
setIsZoomed(true)
}

const zoomOut = () => {
scale.set(1)
setIsZoomed(false)
}

return (
<motion.div
role="presentation"
className="relative md:px-2"
style={{ transform }}
onPointerOut={event => {
const target = event.currentTarget
const relatedTarget = event.relatedTarget as Node | null

if (!relatedTarget || !target.contains(relatedTarget)) {
zoomOut()
}
}}
>
<Image
src={image}
alt=""
role="presentation"
width={799}
height={533}
className="pointer-events-none aspect-[799/533] h-[320px] w-auto object-cover"
/>
<button
type="button"
className="absolute right-2 top-0 z-[1] bg-neu-50/10 p-4"
onClick={event => {
isZoomed ? zoomOut() : zoomIn(event.currentTarget.parentElement)
}}
>
{isZoomed ? (
<ZoomOutIcon className="size-12" />
) : (
<ZoomInIcon className="size-12" />
)}
</button>
</motion.div>
)
}
5 changes: 5 additions & 0 deletions src/app/conf/2025/pixelarticons/zoom-in.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/app/conf/2025/pixelarticons/zoom-out.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.