Migrate to Vite / Vite+

Modernize building and such
This commit is contained in:
2026-04-03 21:12:56 -07:00
parent 445f95340a
commit f04fd0fada
26 changed files with 8080 additions and 4180 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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>

View File

@@ -1,4 +1,5 @@
import { Root } from "components/root";
import "@/styles.css";
import { Root } from "@/components/root";
import { createRoot } from "react-dom/client";

View File

@@ -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
View 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;
}
}