TheBird

Gallery.jsx

4.7 kB · jsx · 127 lines

1import { useEffect, useRef } from "react";2import { grid } from "../lib/shop.ts";3import { Icon } from "./Icon.jsx";45const typing = (target) => target instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName);67export function Carousel({ slides, index, onIndex, keys = false, pixel = false, full = false, hold, onFull, label }) {8  const many = slides.length > 1;9  const one = slides[index] ?? slides[0];10  const strip = useRef(null);11  const swiped = useRef(false);12  const from = useRef([0, 0]);13  const first = useRef(true);14  const step = (delta) => onIndex((((index + delta) % slides.length) + slides.length) % slides.length);1516  useEffect(() => {17    if (first.current) {18      first.current = false;19      return;20    }21    strip.current?.children[index]?.scrollIntoView({ block: "nearest", inline: "center", behavior: "smooth" });22  }, [index]);2324  useEffect(() => {25    if (!keys) return;26    const down = (event) => {27      if (event.metaKey || event.ctrlKey || event.altKey || typing(event.target)) return;28      if (event.key === "ArrowLeft") step(-1);29      else if (event.key === "ArrowRight") step(1);30      else return;31      event.preventDefault();32    };33    document.addEventListener("keydown", down);34    return () => document.removeEventListener("keydown", down);35  });3637  return (38    <div className={pixel ? "gallery pixel" : "gallery"} ref={hold}>39      <div className="stage">40        {one ? (41          <button42            type="button"43            className={full ? "shot zoom" : "shot"}44            aria-label={full ? "All images" : undefined}45            onPointerDown={(event) => {46              from.current = [event.clientX, event.clientY];47              swiped.current = false;48            }}49            onPointerUp={(event) => {50              const dx = event.clientX - from.current[0];51              if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(event.clientY - from.current[1])) {52                swiped.current = true;53                step(dx < 0 ? 1 : -1);54              }55            }}56            onClick={() => {57              if (full && !swiped.current && matchMedia("(min-width: 900px)").matches) onFull?.();58            }}59          >60            <img src={one.full} alt={one.alt} width="1200" height="1200" fetchPriority={keys ? "high" : undefined} decoding="async" />61          </button>62        ) : null}63        {full ? (64          <button type="button" className="arrow full" aria-label="All images" onClick={() => onFull?.()}>65            <Icon name="fullscreen" />66          </button>67        ) : null}68        {many ? (69          <>70            <button type="button" className="arrow prev" aria-label="Previous" onClick={() => step(-1)}>71              <Icon name="chevron_left" />72            </button>73            <button type="button" className="arrow next" aria-label="Next" onClick={() => step(1)}>74              <Icon name="chevron_right" />75            </button>76            <span className="count">{`${index + 1} / ${slides.length}`}</span>77          </>78        ) : null}79      </div>80      {many ? (81        <div className="thumbs" ref={strip}>82          {slides.map((slide, i) => (83            <button type="button" key={slide.style} aria-pressed={i === index ? "true" : "false"} aria-label={slide.alt || `${label} ${i + 1}`} onClick={() => onIndex(i)}>84              <img src={slide.thumb} alt="" width="400" height="400" loading="lazy" decoding="async" />85            </button>86          ))}87        </div>88      ) : null}89    </div>90  );91}9293export const mockupSlides = (images, title) =>94  images.map((image) => ({95    style: image.style,96    thumb: grid(image.url, 400),97    full: grid(image.url, 1200),98    link: grid(image.url, 2000),99    alt: image.alt || title,100    name: image.style,101  }));102103export function Mockups({ images, title, width = 800, onJump }) {104  return (105    <div className="shots">106      {images.map((image, i) => (107        <button type="button" key={image.style} aria-label={image.alt || title} onClick={() => onJump?.(i)}>108          <img src={grid(image.url, width)} alt="" width={width} height={width} loading="lazy" decoding="async" />109        </button>110      ))}111    </div>112  );113}114115export function FullGrid({ images, title, box, onJump }) {116  return (117    <dialog className="all" aria-label="All images" ref={box} onClick={(event) => event.target === box.current && box.current?.close()}>118      <div className="bar">119        <span className="fine">{`${images.length} images`}</span>120        <button type="button" className="tool" aria-label="Close" onClick={() => box.current?.close()}>121          <Icon name="close" />122        </button>123      </div>124      <Mockups images={images} title={title} width={1200} onJump={onJump} />125    </dialog>126  );127}