375 lines
11 KiB
JavaScript
375 lines
11 KiB
JavaScript
import React, { useState } from "react";
|
|
import { graphql, navigate, Link } from "gatsby";
|
|
import { GatsbyImage, getImage } from "gatsby-plugin-image";
|
|
import { Helmet } from "react-helmet";
|
|
import classnames from "classnames";
|
|
|
|
import ChevronLeft from '@spectrum-icons/workflow/ChevronLeft';
|
|
import ChevronRight from '@spectrum-icons/workflow/ChevronRight';
|
|
import Calendar from "@spectrum-icons/workflow/Calendar";
|
|
import Stopwatch from "@spectrum-icons/workflow/Stopwatch";
|
|
import Exposure from "@spectrum-icons/workflow/Exposure";
|
|
import Filmroll from "@spectrum-icons/workflow/Filmroll";
|
|
import Camera from "@spectrum-icons/workflow/Camera";
|
|
import Circle from "@spectrum-icons/workflow/Circle";
|
|
import Location from "@spectrum-icons/workflow/Location";
|
|
|
|
import {
|
|
getAspectRatio,
|
|
getMeta,
|
|
getName,
|
|
getShutterFractionFromExposureTime,
|
|
getVibrant,
|
|
getHelmetSafeBodyStyle,
|
|
hasName,
|
|
getCanonicalSize,
|
|
getGalleryPageUrl,
|
|
} from "../../utils";
|
|
import MetadataItem from "./MetadataItem";
|
|
|
|
const logKeyShortcut = (keyCode) => {
|
|
try {
|
|
// eslint-disable-next-line
|
|
window.plausible("KeyShortcut", { props: { keyCode } });
|
|
// eslint-disable-next-line
|
|
_paq.push(["trackEvent", "GalleryImage", "KeyShortcut", keyCode]);
|
|
} catch (e) {
|
|
/* do nothing */
|
|
}
|
|
};
|
|
|
|
const ArrowLinkClasses = `hover:underline text-vibrant-light hover:text-muted-light
|
|
lg:px-4 self-stretch flex items-center hover:bg-black/50 max-h-screen sticky top-0
|
|
`;
|
|
|
|
const GalleryImage = ({
|
|
data,
|
|
location: {
|
|
state
|
|
},
|
|
}) => {
|
|
const { sortedImageList, currentIndex, filterKeyword, sortKey } = state ?? {};
|
|
const image = data.file;
|
|
const ar = getAspectRatio(image);
|
|
|
|
const [zoom, setZoom] = useState(false);
|
|
|
|
const nextIndex =
|
|
sortedImageList && currentIndex < sortedImageList.length
|
|
? currentIndex + 1
|
|
: null;
|
|
const prevIndex =
|
|
sortedImageList && currentIndex > 0 ? currentIndex - 1 : null;
|
|
|
|
const nextImage = sortedImageList && sortedImageList[nextIndex];
|
|
const prevImage = sortedImageList && sortedImageList[prevIndex];
|
|
|
|
React.useEffect(() => {
|
|
const keyListener = (e) => {
|
|
switch (e.code) {
|
|
case "ArrowRight": {
|
|
logKeyShortcut(e.code);
|
|
if (nextImage) {
|
|
navigate(`/photogallery/${nextImage}/`, {
|
|
state: {
|
|
currentIndex: currentIndex + 1,
|
|
sortedImageList,
|
|
filterKeyword,
|
|
sortKey,
|
|
},
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
case "ArrowLeft": {
|
|
logKeyShortcut(e.code);
|
|
if (prevImage) {
|
|
navigate(`/photogallery/${prevImage}/`, {
|
|
state: {
|
|
currentIndex: currentIndex - 1,
|
|
sortedImageList,
|
|
filterKeyword,
|
|
sortKey,
|
|
},
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
case "Escape":
|
|
case "KeyG": {
|
|
logKeyShortcut(e.code);
|
|
navigate(
|
|
getGalleryPageUrl({ keyword: filterKeyword, sortKey }, image.base)
|
|
);
|
|
}
|
|
}
|
|
};
|
|
document.addEventListener("keydown", keyListener);
|
|
return () => {
|
|
document.removeEventListener("keydown", keyListener);
|
|
};
|
|
}, [
|
|
nextImage,
|
|
prevImage,
|
|
image.base,
|
|
currentIndex,
|
|
sortedImageList,
|
|
filterKeyword,
|
|
sortKey,
|
|
]);
|
|
|
|
const name = getName(image);
|
|
const { meta, dateTaken: dt } = getMeta(image);
|
|
let locationString;
|
|
if (meta.City || meta.State || meta.Location) {
|
|
const location = [meta.Location, meta.City, meta.State].filter(Boolean);
|
|
locationString = location.join(", ");
|
|
}
|
|
const vibrant = getVibrant(image, true);
|
|
const canonicalSize = getCanonicalSize(image);
|
|
|
|
const orientationClasses =
|
|
ar > 1
|
|
? "flex-col mx-auto"
|
|
: "portrait:mx-auto landscape:mx-5 landscape:flex-row-reverse portrait:flex-col";
|
|
const verticalPad = ar > 1 ? "170px" : "70px";
|
|
|
|
const shutterSpeed = React.useMemo(
|
|
() => getShutterFractionFromExposureTime(meta.ExposureTime || 0),
|
|
[meta]
|
|
);
|
|
const dateTaken = React.useMemo(() => new Date(dt), [dt]);
|
|
return (
|
|
<>
|
|
<Helmet>
|
|
<title>{name} - Gallery | Chuck Dries</title>
|
|
<body
|
|
className="text-vibrant-light bg-vibrant-dark"
|
|
style={getHelmetSafeBodyStyle(vibrant)}
|
|
/>
|
|
</Helmet>
|
|
<div className="min-h-screen flex flex-col justify-between">
|
|
<nav className="mt-1 ml-1 text-lg mb-4">
|
|
<button
|
|
className="hover:underline text-vibrant-light hover:text-muted-light arrow-left-before mr-1"
|
|
onClick={() => navigate(-1)}
|
|
type="button"
|
|
>
|
|
back
|
|
</button>
|
|
<Link
|
|
className="hover:underline text-vibrant-light hover:text-muted-light mx-1"
|
|
to="/"
|
|
>
|
|
home
|
|
</Link>
|
|
<Link
|
|
className="hover:underline text-vibrant-light hover:text-muted-light mx-1"
|
|
to={getGalleryPageUrl(
|
|
{ keyword: filterKeyword, sortKey },
|
|
image.base
|
|
)}
|
|
>
|
|
gallery <span className="bg-gray-300 text-black">esc</span>
|
|
</Link>
|
|
</nav>
|
|
<div className="flex justify-between flex-auto items-center lg:gap-2">
|
|
{prevImage && (
|
|
<Link
|
|
className={ArrowLinkClasses}
|
|
state={{
|
|
currentIndex: currentIndex - 1,
|
|
sortedImageList,
|
|
filterKeyword,
|
|
sortKey,
|
|
}}
|
|
to={`/photogallery/${prevImage}/`}
|
|
>
|
|
<span className="p-2 lg:p-4 bg-muted-light/25 rounded-full"><ChevronLeft /></span>
|
|
</Link>
|
|
)}
|
|
<div className={classnames("pb-2 flex", orientationClasses)}>
|
|
<div
|
|
className={classnames(
|
|
zoom ? "cursor-zoom-out" : "cursor-zoom-in",
|
|
"mb-2 self-center"
|
|
)}
|
|
onClick={() => setZoom((_zoom) => !_zoom)}
|
|
style={{
|
|
maxWidth: `calc(max(calc(100vh - ${verticalPad}), 500px) * ${ar})`,
|
|
}}
|
|
>
|
|
{zoom ? (
|
|
<img
|
|
alt={name}
|
|
src={image.publicURL}
|
|
style={{
|
|
width: canonicalSize.width / window.devicePixelRatio,
|
|
maxWidth: "unset",
|
|
}}
|
|
/>
|
|
) : (
|
|
<GatsbyImage
|
|
alt={name}
|
|
className="border-4 border-vibrant"
|
|
image={getImage(image)}
|
|
key={image.base}
|
|
loading="eager"
|
|
objectFit="contain"
|
|
/>
|
|
)}
|
|
</div>
|
|
<div
|
|
className={classnames(
|
|
"px-2 flex flex-row portrait:items-end mx-auto",
|
|
ar <= 1
|
|
? "pt-5 flex-col flex-auto text-right"
|
|
: "landscape:container portrait:pt-5 portrait:flex-col portrait:text-right"
|
|
)}
|
|
>
|
|
<div className="flex-auto mr-2">
|
|
<p className="text-muted-light font-mono text-sm m-0 mt-1">
|
|
{image.base}
|
|
</p>
|
|
{hasName(image) && (
|
|
<h1 className="text-4xl mt-0 font-serif">{name}</h1>
|
|
)}
|
|
<p className="landscape:mr-2">{meta.Caption}</p>
|
|
<a
|
|
className="cursor-pointer inline-block bg-muted-light text-vibrant-dark font-serif p-1 my-1 rounded"
|
|
download
|
|
href={image.publicURL}
|
|
onClick={() => {
|
|
try {
|
|
window.plausible("Download Wallpaper", {
|
|
props: { image: image.base },
|
|
});
|
|
} catch {
|
|
// do nothing
|
|
}
|
|
}}
|
|
>
|
|
Download wallpaper
|
|
</a>
|
|
</div>
|
|
{
|
|
<div
|
|
className="portrait:border-t-2 border-muted-light portrait:mt-2 mr-2 portrait:mb-1"
|
|
style={{ width: 30 }}
|
|
></div>
|
|
}
|
|
<div className="flex flex-col items-end">
|
|
<MetadataItem
|
|
data={dateTaken.toLocaleDateString()}
|
|
icon={<Calendar />}
|
|
title="date taken"
|
|
/>
|
|
<div className="flex justify-end gap-2 border border-vibrant-light pl-2 rounded">
|
|
<MetadataItem
|
|
data={shutterSpeed}
|
|
icon={<Stopwatch />}
|
|
title="shutter speed"
|
|
/>
|
|
{meta.FNumber && (
|
|
<MetadataItem
|
|
data={`f/${meta.FNumber}`}
|
|
icon={<Exposure />}
|
|
title="aperture"
|
|
/>
|
|
)}
|
|
<MetadataItem data={meta.ISO} icon={<Filmroll />} title="ISO" />
|
|
</div>
|
|
<MetadataItem
|
|
data={locationString}
|
|
icon={<Location />}
|
|
title="location"
|
|
/>
|
|
{(meta.Make || meta.Model) && (
|
|
<MetadataItem
|
|
data={[meta.Make, meta.Model].join(" ")}
|
|
icon={<Camera />}
|
|
title="camera"
|
|
/>
|
|
)}
|
|
{(meta.LensModel || meta.FocalLength) && (
|
|
<MetadataItem
|
|
data={[
|
|
meta.LensModel === "----" ? null : meta.LensModel,
|
|
meta.FocalLength && `${meta.FocalLength}mm`,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" @")}
|
|
icon={<Circle />}
|
|
title="lens"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{nextImage && (
|
|
<Link
|
|
className={ArrowLinkClasses}
|
|
state={{
|
|
currentIndex: currentIndex + 1,
|
|
sortedImageList,
|
|
filterKeyword,
|
|
sortKey,
|
|
}}
|
|
to={`/photogallery/${nextImage}/`}
|
|
>
|
|
<span className="p-2 lg:p-4 bg-muted-light/25 rounded-full"><ChevronRight /></span>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const query = graphql`
|
|
query GalleryImage($imageFilename: String) {
|
|
file(base: { eq: $imageFilename }) {
|
|
base
|
|
publicURL
|
|
childImageSharp {
|
|
fluid {
|
|
aspectRatio
|
|
}
|
|
gatsbyImageData(
|
|
layout: CONSTRAINED
|
|
placeholder: DOMINANT_COLOR
|
|
quality: 90
|
|
)
|
|
}
|
|
fields {
|
|
imageMeta {
|
|
dateTaken
|
|
meta {
|
|
Make
|
|
Model
|
|
ExposureTime
|
|
FNumber
|
|
ISO
|
|
DateTimeOriginal
|
|
CreateDate
|
|
ShutterSpeedValue
|
|
ApertureValue
|
|
FocalLength
|
|
LensModel
|
|
ObjectName
|
|
Caption
|
|
Location
|
|
City
|
|
State
|
|
}
|
|
vibrant {
|
|
...VibrantColors
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
export default GalleryImage;
|