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}