Migrate to Vite / Vite+
Modernize building and such
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as Model from "model";
|
||||
import { Picture } from "components/picture";
|
||||
import * as Model from "@/model";
|
||||
import { Picture } from "@/components/picture";
|
||||
|
||||
export interface Props {
|
||||
image: Model.Image;
|
||||
@@ -48,9 +48,7 @@ export const BigPicture: React.FC<Props> = ({
|
||||
const [pinchState, setPinchState] = React.useState<PinchState | null>(null);
|
||||
const [isPointerPanning, setIsPointerPanning] = React.useState(false);
|
||||
const tapStartRef = React.useRef<Point | null>(null);
|
||||
const lastTapRef = React.useRef<{ time: number; x: number; y: number } | null>(
|
||||
null
|
||||
);
|
||||
const lastTapRef = React.useRef<{ time: number; x: number; y: number } | null>(null);
|
||||
|
||||
const onEscape = React.useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
@@ -58,7 +56,7 @@ export const BigPicture: React.FC<Props> = ({
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose]
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const goNext = React.useCallback(() => {
|
||||
@@ -103,7 +101,7 @@ export const BigPicture: React.FC<Props> = ({
|
||||
y: Math.min(maxPanY, Math.max(-maxPanY, nextPan.y)),
|
||||
};
|
||||
},
|
||||
[image.height, image.width, width, zoom]
|
||||
[image.height, image.width, width, zoom],
|
||||
);
|
||||
|
||||
const applyZoomAt = React.useCallback(
|
||||
@@ -124,7 +122,7 @@ export const BigPicture: React.FC<Props> = ({
|
||||
setZoom(clampedZoom);
|
||||
setPan(clampPan(nextPan, clampedZoom));
|
||||
},
|
||||
[clampPan, getViewportCenter, pan.x, pan.y, zoom]
|
||||
[clampPan, getViewportCenter, pan.x, pan.y, zoom],
|
||||
);
|
||||
|
||||
const startSwipe = React.useCallback((touch: React.Touch) => {
|
||||
@@ -162,7 +160,7 @@ export const BigPicture: React.FC<Props> = ({
|
||||
startSwipe(touch);
|
||||
tapStartRef.current = { x: touch.clientX, y: touch.clientY };
|
||||
},
|
||||
[startSwipe, zoom]
|
||||
[startSwipe, zoom],
|
||||
);
|
||||
|
||||
const onTouchEnd = React.useCallback(
|
||||
@@ -171,8 +169,7 @@ export const BigPicture: React.FC<Props> = ({
|
||||
const tapStart = tapStartRef.current;
|
||||
const now = Date.now();
|
||||
const isTap =
|
||||
tapStart &&
|
||||
Math.hypot(tapStart.x - touch.clientX, tapStart.y - touch.clientY) < 10;
|
||||
tapStart && Math.hypot(tapStart.x - touch.clientX, tapStart.y - touch.clientY) < 10;
|
||||
const lastTap = lastTapRef.current;
|
||||
|
||||
if (isTap && lastTap && now - lastTap.time < 350) {
|
||||
@@ -182,10 +179,7 @@ export const BigPicture: React.FC<Props> = ({
|
||||
const focal = rect
|
||||
? { x: touch.clientX - rect.left, y: touch.clientY - rect.top }
|
||||
: undefined;
|
||||
const fitScale = Math.max(
|
||||
image.width / width,
|
||||
image.height / (window.innerHeight - 80)
|
||||
);
|
||||
const fitScale = Math.max(image.width / width, image.height / (window.innerHeight - 80));
|
||||
const targetZoom = zoom > 1.05 ? 1 : Math.min(4, fitScale);
|
||||
applyZoomAt(targetZoom, focal);
|
||||
setIsDragging(false);
|
||||
@@ -242,7 +236,17 @@ export const BigPicture: React.FC<Props> = ({
|
||||
setDragOffset(0);
|
||||
}
|
||||
},
|
||||
[applyZoomAt, goNext, goPrevious, image.height, image.width, pinchState, touchStart, width, zoom]
|
||||
[
|
||||
applyZoomAt,
|
||||
goNext,
|
||||
goPrevious,
|
||||
image.height,
|
||||
image.width,
|
||||
pinchState,
|
||||
touchStart,
|
||||
width,
|
||||
zoom,
|
||||
],
|
||||
);
|
||||
|
||||
const onTouchMove = React.useCallback(
|
||||
@@ -272,8 +276,8 @@ export const BigPicture: React.FC<Props> = ({
|
||||
x: prev.x + (touch.clientX - panStart.x),
|
||||
y: prev.y + (touch.clientY - panStart.y),
|
||||
},
|
||||
zoom
|
||||
)
|
||||
zoom,
|
||||
),
|
||||
);
|
||||
setPanStart({ x: touch.clientX, y: touch.clientY });
|
||||
return;
|
||||
@@ -292,7 +296,7 @@ export const BigPicture: React.FC<Props> = ({
|
||||
}
|
||||
setDragOffset(dx);
|
||||
},
|
||||
[applyZoomAt, clampPan, panStart, pinchState, touchStart, zoom]
|
||||
[applyZoomAt, clampPan, panStart, pinchState, touchStart, zoom],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -327,7 +331,7 @@ export const BigPicture: React.FC<Props> = ({
|
||||
}
|
||||
applyZoomAt(nextZoom, focal);
|
||||
},
|
||||
[applyZoomAt, zoom]
|
||||
[applyZoomAt, zoom],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -352,8 +356,8 @@ export const BigPicture: React.FC<Props> = ({
|
||||
x: prev.x + e.movementX,
|
||||
y: prev.y + e.movementY,
|
||||
},
|
||||
zoom
|
||||
)
|
||||
zoom,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -377,7 +381,7 @@ export const BigPicture: React.FC<Props> = ({
|
||||
e.preventDefault();
|
||||
setIsPointerPanning(true);
|
||||
},
|
||||
[zoom]
|
||||
[zoom],
|
||||
);
|
||||
|
||||
const slideWidth = width;
|
||||
@@ -404,27 +408,18 @@ export const BigPicture: React.FC<Props> = ({
|
||||
onTouchEnd={onTouchEnd}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
<div
|
||||
className={`BigPicture-track${isDragging ? " is-dragging" : ""}`}
|
||||
style={trackStyle}
|
||||
>
|
||||
<div className={`BigPicture-track${isDragging ? " is-dragging" : ""}`} style={trackStyle}>
|
||||
{[previousImage, image, nextImage].map((img) => {
|
||||
const imgScaleWidth = img.width / width;
|
||||
const imgScaleHeight = img.height / (window.innerHeight - 80);
|
||||
const imgScale = Math.max(imgScaleWidth, imgScaleHeight);
|
||||
const isCurrent = img.src === image.src;
|
||||
return (
|
||||
<div
|
||||
className="BigPicture-frame"
|
||||
key={img.src}
|
||||
style={slideStyle}
|
||||
>
|
||||
<div className="BigPicture-frame" key={img.src} style={slideStyle}>
|
||||
<div
|
||||
className="BigPicture-imageWrapper"
|
||||
style={
|
||||
isCurrent
|
||||
? { transform: `translate3d(${pan.x}px, ${pan.y}px, 0)` }
|
||||
: undefined
|
||||
isCurrent ? { transform: `translate3d(${pan.x}px, ${pan.y}px, 0)` } : undefined
|
||||
}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as Model from "model";
|
||||
import { Picture } from "components/picture";
|
||||
import * as Model from "@/model";
|
||||
import { Picture } from "@/components/picture";
|
||||
|
||||
export interface Props {
|
||||
images: Model.Image[];
|
||||
@@ -30,13 +30,7 @@ const badness = (row: Model.Image[], width: number, height: number): number => {
|
||||
return (rowHeight - height) * (rowHeight - height);
|
||||
};
|
||||
|
||||
export const Grid: React.FC<Props> = ({
|
||||
images,
|
||||
onImageSelected,
|
||||
pageBottom,
|
||||
width,
|
||||
height,
|
||||
}) => {
|
||||
export const Grid: React.FC<Props> = ({ images, onImageSelected, pageBottom, width, height }) => {
|
||||
const rowsMemo = React.useRef<Map<number, Map<number, BadList>>>(new Map());
|
||||
const targetRowHeight = width > 900 ? ROW_HEIGHT : MOBILE_ROW_HEIGHT;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as Model from "model";
|
||||
import { Grid } from "components/grid";
|
||||
import * as Model from "@/model";
|
||||
import { Grid } from "@/components/grid";
|
||||
|
||||
export interface Props {
|
||||
imageSet: Model.ImageSet;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import * as Model from "model";
|
||||
import * as Model from "@/model";
|
||||
|
||||
export interface Props {
|
||||
image: Model.Image;
|
||||
@@ -9,20 +9,7 @@ export interface Props {
|
||||
defer?: boolean;
|
||||
}
|
||||
|
||||
interface SrcSetInfo {
|
||||
jpeg: string;
|
||||
webp: string;
|
||||
avif: string;
|
||||
bestSrc: string;
|
||||
}
|
||||
|
||||
export const Picture: React.FC<Props> = ({
|
||||
image,
|
||||
onClick,
|
||||
height,
|
||||
width,
|
||||
defer,
|
||||
}) => {
|
||||
export const Picture: React.FC<Props> = ({ image, onClick, height, width, defer }) => {
|
||||
const [isMounted, setIsMounted] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -37,8 +24,7 @@ export const Picture: React.FC<Props> = ({
|
||||
let bestScale = Infinity;
|
||||
|
||||
Model.SIZES.forEach((size) => {
|
||||
const derivedWidth =
|
||||
image.width > image.height ? size : (image.width / image.height) * size;
|
||||
const derivedWidth = image.width > image.height ? size : (image.width / image.height) * size;
|
||||
|
||||
const scale = derivedWidth / width;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react";
|
||||
import * as Model from "model";
|
||||
import { BigPicture } from "components/big_picture";
|
||||
import { ImageSet } from "components/image_set";
|
||||
import { SetCover } from "components/set_cover";
|
||||
import * as Model from "@/model";
|
||||
import { BigPicture } from "@/components/big_picture";
|
||||
import { ImageSet } from "@/components/image_set";
|
||||
import { SetCover } from "@/components/set_cover";
|
||||
|
||||
export interface Props {}
|
||||
|
||||
@@ -34,13 +34,9 @@ const formatHash = (set: Model.ImageSet) =>
|
||||
set.description.replace(/[^a-zA-Z0-9-_]/g, "-");
|
||||
|
||||
export const Root: React.FC<Props> = () => {
|
||||
const [data, setData] = React.useState<Model.Data | null>(null);
|
||||
const [selectedImage, setSelectedImage] = React.useState<Model.Image | null>(
|
||||
null
|
||||
);
|
||||
const [selectedSet, setSelectedSet] = React.useState<Model.ImageSet | null>(
|
||||
null
|
||||
);
|
||||
const [data] = React.useState<Model.Data>(Model.data);
|
||||
const [selectedImage, setSelectedImage] = React.useState<Model.Image | null>(null);
|
||||
const [selectedSet, setSelectedSet] = React.useState<Model.ImageSet | null>(null);
|
||||
const [dimensions, setDimensions] = React.useState(() => {
|
||||
const height = viewHeight();
|
||||
return {
|
||||
@@ -91,26 +87,7 @@ export const Root: React.FC<Props> = () => {
|
||||
}, [data]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let isMounted = true;
|
||||
window
|
||||
.fetch(Model.dataUrl)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
if (isMounted) {
|
||||
setData(json);
|
||||
}
|
||||
})
|
||||
.catch((e) => console.error("Error fetching data", e));
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (data) {
|
||||
loadHash();
|
||||
}
|
||||
loadHash();
|
||||
}, [data, loadHash]);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -142,10 +119,7 @@ export const Root: React.FC<Props> = () => {
|
||||
React.useEffect(() => {
|
||||
if (selectedSet) {
|
||||
document.title =
|
||||
selectedSet.location +
|
||||
" – " +
|
||||
selectedSet.description +
|
||||
" – Skiing - Aaron Gutierrez";
|
||||
selectedSet.location + " – " + selectedSet.description + " – Skiing - Aaron Gutierrez";
|
||||
} else {
|
||||
document.title = "Skiing - Aaron Gutierrez";
|
||||
}
|
||||
@@ -160,7 +134,7 @@ export const Root: React.FC<Props> = () => {
|
||||
}
|
||||
setSelectedImage(img);
|
||||
},
|
||||
[selectedImage]
|
||||
[selectedImage],
|
||||
);
|
||||
|
||||
const onSetSelected = React.useCallback((set: Model.ImageSet) => {
|
||||
@@ -217,9 +191,6 @@ export const Root: React.FC<Props> = () => {
|
||||
}, [selectedImage, selectedSet]);
|
||||
|
||||
const renderSets = () => {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
if (selectedSet) {
|
||||
return (
|
||||
<ImageSet
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as Model from "model";
|
||||
import { Picture } from "components/picture";
|
||||
import * as Model from "@/model";
|
||||
import { Picture } from "@/components/picture";
|
||||
|
||||
export interface Props {
|
||||
imageSet: Model.ImageSet;
|
||||
@@ -12,22 +12,13 @@ export const SetCover: React.FC<Props> = ({ imageSet, onClick, width }) => {
|
||||
const coverImage = imageSet.images[0];
|
||||
const isTall = coverImage.height > coverImage.width;
|
||||
|
||||
const height = isTall
|
||||
? width
|
||||
: (coverImage.height / coverImage.width) * width;
|
||||
const height = isTall ? width : (coverImage.height / coverImage.width) * width;
|
||||
|
||||
const normalizedWidth = isTall
|
||||
? (coverImage.width / coverImage.height) * width
|
||||
: width;
|
||||
const normalizedWidth = isTall ? (coverImage.width / coverImage.height) * width : width;
|
||||
|
||||
return (
|
||||
<div className="SetCover" onClick={onClick}>
|
||||
<Picture
|
||||
image={coverImage}
|
||||
onClick={() => {}}
|
||||
height={height}
|
||||
width={normalizedWidth}
|
||||
/>
|
||||
<Picture image={coverImage} onClick={() => {}} height={height} width={normalizedWidth} />
|
||||
<h2>
|
||||
<span className="SetCover-location">{imageSet.location}</span>
|
||||
<span className="SetCover-description">{imageSet.description}</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Root } from "components/root";
|
||||
import "@/styles.css";
|
||||
import { Root } from "@/components/root";
|
||||
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const SIZES = [2400, 1600, 1200, 800, 600, 400, 200];
|
||||
import galleryData from "../img/data.json";
|
||||
|
||||
export const dataUrl = "img/data.json";
|
||||
export const SIZES = [2400, 1600, 1200, 800, 600, 400, 200];
|
||||
|
||||
export interface Data {
|
||||
sets: ImageSet[];
|
||||
@@ -17,3 +17,5 @@ export interface Image {
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const data = galleryData as Data;
|
||||
|
||||
213
src/styles.css
Normal file
213
src/styles.css
Normal file
@@ -0,0 +1,213 @@
|
||||
html,
|
||||
body {
|
||||
background-color: #fff;
|
||||
color: #335;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial,
|
||||
sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body.no-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #69c;
|
||||
cursor: pointer;
|
||||
font-size: 45px;
|
||||
font-weight: lighter;
|
||||
line-height: 60px;
|
||||
margin: 30px 30px 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #666;
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
line-height: 30px;
|
||||
margin: 0 30px 30px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #69c;
|
||||
font-size: 18px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ImageSet h2 {
|
||||
border-bottom: 1px solid #eef;
|
||||
}
|
||||
|
||||
.Root-setCovers {
|
||||
display: grid;
|
||||
gap: 30px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.SetCover {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ImageSet-location,
|
||||
.ImageSet-description,
|
||||
.SetCover-location,
|
||||
.SetCover-description {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ImageSet-location:after,
|
||||
.SetCover-location:after {
|
||||
content: " · ";
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.ImageSet-navigation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.Grid {
|
||||
margin-bottom: 45px;
|
||||
}
|
||||
|
||||
.Grid-row {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.Grid img {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.BigPicture {
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.BigPicture-viewport {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
touch-action: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.BigPicture-track {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.BigPicture-track.is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.BigPicture-frame {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.BigPicture-frame.is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.BigPicture-imageWrapper {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.BigPicture-zoomTarget {
|
||||
transition: transform 0.2s ease;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.BigPicture img {
|
||||
transition-duration: 0.2s;
|
||||
transition-property: height, width, opacity;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.BigPicture-footer {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
max-width: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.BigPicture-footerLink {
|
||||
color: #69c;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 0 18px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.BigPicture-footerLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html,
|
||||
body {
|
||||
background-color: #111;
|
||||
color: #ccf;
|
||||
}
|
||||
|
||||
.ImageSet h2 {
|
||||
border-color: #335;
|
||||
color: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.Root-setCovers {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.Root-setCovers {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user