387 lines
12 KiB
JavaScript
387 lines
12 KiB
JavaScript
import React, { useState, useEffect } 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";
|
|
import Nav from "../Nav";
|
|
|
|
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 IconStyle = {
|
|
width: '24px',
|
|
margin: '0 4px'
|
|
}
|
|
|
|
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 [isClient, setIsClient] = useState(true);
|
|
|
|
useEffect(() => {
|
|
setIsClient(true);
|
|
}, []);
|
|
|
|
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"
|
|
: "portrait:mx-auto landscape:mx-5 landscape:flex-row-reverse portrait:flex-col";
|
|
const verticalPad = ar > 1 ? "250px" : "100px";
|
|
|
|
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 overflow-x-hidden">
|
|
<Nav
|
|
className="mb-4"
|
|
internalLinks={[
|
|
{ href: "/", label: "Home" },
|
|
{
|
|
href: getGalleryPageUrl(
|
|
{ keyword: filterKeyword, sortKey },
|
|
image.base
|
|
),
|
|
label: (
|
|
<>
|
|
Gallery <kbd className="font-normal">esc</kbd>
|
|
</>
|
|
),
|
|
},
|
|
]}
|
|
isClient={isClient}
|
|
/>
|
|
<div className="flex flex-auto items-center lg:gap-2 justify-between">
|
|
{prevImage ? (
|
|
<Link
|
|
className={ArrowLinkClasses}
|
|
state={{
|
|
currentIndex: currentIndex - 1,
|
|
sortedImageList,
|
|
filterKeyword,
|
|
sortKey,
|
|
}}
|
|
to={`/photogallery/${prevImage}/`}
|
|
>
|
|
<span className="p-1 lg:p-4 bg-muted-light/25 rounded-full">
|
|
<ChevronLeft />
|
|
</span>
|
|
</Link>
|
|
) : (
|
|
<div></div>
|
|
)}
|
|
<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-sans">{name}</h1>
|
|
)}
|
|
<p className="landscape:mr-2">{meta.Caption}</p>
|
|
<a
|
|
className="cursor-pointer inline-block bg-muted-light text-vibrant-dark font-sans 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 UNSAFE_style={IconStyle} />}
|
|
title="date taken"
|
|
/>
|
|
<div className="sm:flex justify-end gap-2 border border-vibrant-light pl-2 rounded">
|
|
<MetadataItem
|
|
data={shutterSpeed}
|
|
icon={<Stopwatch UNSAFE_style={IconStyle} />}
|
|
title="shutter speed"
|
|
/>
|
|
{meta.FNumber && (
|
|
<MetadataItem
|
|
data={`f/${meta.FNumber}`}
|
|
icon={<Exposure UNSAFE_style={IconStyle} />}
|
|
title="aperture"
|
|
/>
|
|
)}
|
|
<MetadataItem
|
|
data={meta.ISO}
|
|
icon={<Filmroll UNSAFE_style={IconStyle} />}
|
|
title="ISO"
|
|
/>
|
|
</div>
|
|
<MetadataItem
|
|
data={locationString}
|
|
icon={<Location UNSAFE_style={IconStyle} />}
|
|
title="location"
|
|
/>
|
|
{(meta.Make || meta.Model) && (
|
|
<MetadataItem
|
|
data={[meta.Make, meta.Model].join(" ")}
|
|
icon={<Camera UNSAFE_style={IconStyle} />}
|
|
title="camera"
|
|
/>
|
|
)}
|
|
{(meta.LensModel || meta.FocalLength) && (
|
|
<MetadataItem
|
|
data={[
|
|
meta.LensModel === "----" ? null : meta.LensModel,
|
|
meta.FocalLength && `${meta.FocalLength}mm`,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" @")}
|
|
icon={<Circle UNSAFE_style={IconStyle} />}
|
|
title="lens"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{nextImage ? (
|
|
<Link
|
|
className={ArrowLinkClasses}
|
|
state={{
|
|
currentIndex: currentIndex + 1,
|
|
sortedImageList,
|
|
filterKeyword,
|
|
sortKey,
|
|
}}
|
|
to={`/photogallery/${nextImage}/`}
|
|
>
|
|
<span className="p-1 lg:p-4 bg-muted-light/25 rounded-full">
|
|
<ChevronRight />
|
|
</span>
|
|
</Link>
|
|
) : (
|
|
<div></div>
|
|
)}
|
|
</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;
|