388 lines
10 KiB
TypeScript
388 lines
10 KiB
TypeScript
import * as React from "react";
|
||
import * as R from "ramda";
|
||
import { graphql, Link, navigate, PageProps } from "gatsby";
|
||
import { Helmet } from "react-helmet";
|
||
// import { Picker, Item } from "@adobe/react-spectrum";
|
||
|
||
import MasonryGallery from "../components/MasonryGallery";
|
||
import KeywordsPicker from "../components/KeywordsPicker";
|
||
import {
|
||
compareDates,
|
||
getGalleryPageUrl,
|
||
getHelmetSafeBodyStyle,
|
||
getVibrantStyle,
|
||
} from "../utils";
|
||
import Nav from "../components/Nav";
|
||
import { Item, Select } from "../components/Select";
|
||
import { Switch } from "../components/Switch";
|
||
import ColorPalette from "@spectrum-icons/workflow/ColorPalette";
|
||
|
||
const SORT_KEYS = {
|
||
hue: ["fields", "imageMeta", "vibrantHue"],
|
||
rating: ["fields", "imageMeta", "meta", "Rating"],
|
||
// hue_debug: ["fields", "imageMeta", "dominantHue", 0],
|
||
hue_debug: ["fields", "imageMeta", "dominantHue", "0"],
|
||
date: ["fields", "imageMeta", "dateTaken"],
|
||
datePublished: ["fields", "imageMeta", "datePublished"],
|
||
} as const;
|
||
|
||
export type GalleryImage =
|
||
Queries.GalleryPageQueryQuery["all"]["nodes"][number];
|
||
|
||
function smartCompareDates(
|
||
key: keyof typeof SORT_KEYS,
|
||
left: GalleryImage,
|
||
right: GalleryImage
|
||
) {
|
||
let diff = compareDates(SORT_KEYS[key], left, right);
|
||
if (diff !== 0) {
|
||
return diff;
|
||
}
|
||
return compareDates(SORT_KEYS.date, left, right);
|
||
}
|
||
|
||
const GalleryPage = ({
|
||
data,
|
||
location,
|
||
}: PageProps<Queries.GalleryPageQueryQuery>) => {
|
||
const hash = location.hash ? location.hash.replace("#", "") : "";
|
||
|
||
const params = new URLSearchParams(location.search);
|
||
const filterKeyword = params.get("filter");
|
||
const sortKey = params.get("sort") ?? "rating";
|
||
|
||
const showDebug = Boolean(params.get("debug")?.length);
|
||
const [showPalette, setShowPalette] = React.useState(false);
|
||
|
||
const onKeywordPick = React.useCallback((newKeyword: string | null) => {
|
||
if (newKeyword) {
|
||
try {
|
||
window.plausible("Filter Keyword", {
|
||
props: { keyword: newKeyword },
|
||
});
|
||
} catch (e) {
|
||
// do nothing
|
||
}
|
||
}
|
||
}, []);
|
||
|
||
const setSortKey = React.useCallback(
|
||
(newSortKey: string) => {
|
||
try {
|
||
window.plausible("Sort Gallery", {
|
||
props: { key: newSortKey },
|
||
});
|
||
} catch (e) {
|
||
// do nothing
|
||
}
|
||
navigate(
|
||
getGalleryPageUrl(
|
||
{ sortKey: newSortKey, keyword: filterKeyword, showDebug },
|
||
hash
|
||
),
|
||
{ replace: true }
|
||
);
|
||
},
|
||
[filterKeyword, hash, showDebug]
|
||
);
|
||
|
||
const removeHash = React.useCallback(() => {
|
||
if (!hash.length) {
|
||
return;
|
||
}
|
||
// const url = new URL(
|
||
// typeof window !== "undefined"
|
||
// ? window.location.href.toString()
|
||
// : "https://chuckdries.com/photogallery/"
|
||
// );
|
||
|
||
// url.hash = "";
|
||
// window.history.replaceState(null, "", url.href.toString());
|
||
navigate(getGalleryPageUrl({ sortKey, keyword: filterKeyword, showDebug}, ""), { replace: true })
|
||
window.removeEventListener("wheel", removeHash);
|
||
window.removeEventListener("touchmove", removeHash);
|
||
}, [hash, sortKey, filterKeyword, showDebug]);
|
||
|
||
React.useEffect(() => {
|
||
window.addEventListener("wheel", removeHash);
|
||
window.addEventListener("touchmove", removeHash);
|
||
return () => {
|
||
window.removeEventListener("wheel", removeHash);
|
||
window.removeEventListener("touchmove", removeHash);
|
||
}
|
||
}, [removeHash]);
|
||
|
||
React.useEffect(() => {
|
||
// hacky but it works for now
|
||
requestAnimationFrame(() => {
|
||
// don't scroll into view if user got here with back button or if we just cleared it
|
||
if (!hash.length) {
|
||
return;
|
||
}
|
||
const el = document.getElementById(hash);
|
||
if (!el) {
|
||
console.log("⚠️failed to find hash");
|
||
return;
|
||
}
|
||
console.log("scrolling into view manually");
|
||
el.scrollIntoView({
|
||
block: hash.startsWith("all") ? "start" : "center",
|
||
});
|
||
});
|
||
}, [hash]);
|
||
|
||
const images: GalleryImage[] = React.useMemo(() => {
|
||
const sort =
|
||
sortKey === "date" || sortKey === "datePublished"
|
||
? R.sort((node1: typeof data["all"]["nodes"][number], node2) =>
|
||
smartCompareDates(sortKey, node1, node2)
|
||
)
|
||
: R.sort(
|
||
// @ts-ignore
|
||
R.descend(R.path<GalleryImage>(SORT_KEYS[sortKey]))
|
||
);
|
||
|
||
const filter = filterKeyword
|
||
? R.filter((image) =>
|
||
R.includes(
|
||
filterKeyword,
|
||
R.pathOr([], ["fields", "imageMeta", "meta", "Keywords"], image)
|
||
)
|
||
)
|
||
: R.identity;
|
||
|
||
try {
|
||
const ret = R.pipe(
|
||
// @ts-ignore
|
||
sort,
|
||
filter
|
||
)(data.all.nodes) as any;
|
||
return ret;
|
||
} catch (e) {
|
||
console.log("caught images!", e);
|
||
return [];
|
||
}
|
||
}, [data, sortKey, filterKeyword]);
|
||
|
||
const recents = React.useMemo(() => {
|
||
return R.sort(
|
||
(left, right) => smartCompareDates("datePublished", left, right),
|
||
data.recents.nodes
|
||
);
|
||
}, [data]);
|
||
|
||
const dataFn = React.useCallback(
|
||
(image: GalleryImage): string | null => {
|
||
if (!showDebug) {
|
||
return null;
|
||
}
|
||
if (sortKey === "rating") {
|
||
return `[${R.pathOr(null, SORT_KEYS.rating, image)}] ${image.base}`;
|
||
}
|
||
if (sortKey === "datePublished") {
|
||
const date = R.pathOr(null, SORT_KEYS.datePublished, image);
|
||
if (!date) {
|
||
return null;
|
||
}
|
||
return new Date(date).toLocaleString();
|
||
}
|
||
return null;
|
||
},
|
||
[showDebug, sortKey]
|
||
);
|
||
|
||
return (
|
||
<>
|
||
{/* @ts-ignore */}
|
||
<Helmet>
|
||
<title>Photo Gallery | Chuck Dries</title>
|
||
<body
|
||
className="bg-white transition-color"
|
||
// @ts-ignore
|
||
style={getHelmetSafeBodyStyle(
|
||
// @ts-ignore shrug
|
||
getVibrantStyle({
|
||
Muted: [0, 0, 0],
|
||
LightMuted: [0, 0, 0],
|
||
Vibrant: [0, 0, 0],
|
||
LightVibrant: [0, 0, 0],
|
||
DarkMuted: [238, 238, 238],
|
||
DarkVibrant: [238, 238, 238],
|
||
})
|
||
)}
|
||
/>
|
||
</Helmet>
|
||
<div className="top-0 z-10">
|
||
<div className="bg-vibrant-dark text-light-vibrant pb-1">
|
||
<Nav
|
||
className="mb-4"
|
||
internalLinks={[
|
||
{ href: "/", label: "Home" },
|
||
{ href: "/photogallery/", label: "Gallery" },
|
||
]}
|
||
/>
|
||
</div>
|
||
<div className="gradient pb-6">
|
||
<div className="px-4 md:px-8 flex items-baseline">
|
||
<h3 className="mx-2 font-bold" id="recently">
|
||
Recently published
|
||
</h3>
|
||
{sortKey !== "datePublished" && (
|
||
<Link
|
||
className="underline cursor-pointer text-gray-500"
|
||
to="?sort=datePublished#all"
|
||
>
|
||
show more
|
||
</Link>
|
||
)}
|
||
</div>
|
||
<MasonryGallery
|
||
aspectsByBreakpoint={{
|
||
xs: 3,
|
||
sm: 3,
|
||
md: 4,
|
||
lg: 4,
|
||
xl: 5,
|
||
"2xl": 6,
|
||
"3xl": 8,
|
||
}}
|
||
images={recents}
|
||
singleRow
|
||
/>
|
||
</div>
|
||
<div className="px-4 md:px-8 mt-2 pt-2">
|
||
<h3 className="mx-2 font-bold" id="all">
|
||
All images
|
||
</h3>
|
||
</div>
|
||
<div className="flex flex-col lg:flex-row lg:items-end justify-between px-4 md:px-8 sm:mx-auto">
|
||
<KeywordsPicker
|
||
getHref={(val, selected) =>
|
||
selected
|
||
? getGalleryPageUrl({ keyword: null, sortKey, showDebug }, hash)
|
||
: getGalleryPageUrl({ keyword: val, sortKey, showDebug }, hash)
|
||
}
|
||
keywords={[
|
||
"Boyce Thompson Arboretum",
|
||
"winter",
|
||
"night",
|
||
"coast",
|
||
// "city",
|
||
"landscape",
|
||
"flowers",
|
||
"product",
|
||
// "waterfall",
|
||
// "fireworks",
|
||
// "panoramic",
|
||
"Portland Japanese Garden",
|
||
// "shoot the light",
|
||
// "sunset",
|
||
]}
|
||
onPick={onKeywordPick}
|
||
value={filterKeyword}
|
||
/>
|
||
<div className="my-2 mr-2 flex flex-row items-end">
|
||
<div className="border border-gray-400 rounded mr-2">
|
||
<Switch
|
||
isSelected={showPalette}
|
||
onChange={(val) => setShowPalette(val)}
|
||
>
|
||
<ColorPalette
|
||
UNSAFE_style={{
|
||
width: "24px",
|
||
margin: "0 4px",
|
||
}}
|
||
/>
|
||
</Switch>
|
||
</div>
|
||
<Select
|
||
label="Sort by..."
|
||
// @ts-ignore
|
||
onSelectionChange={setSortKey}
|
||
selectedKey={sortKey}
|
||
>
|
||
<Item key="rating">Curated</Item>
|
||
<Item key="datePublished">Date published</Item>
|
||
<Item key="date">Date taken</Item>
|
||
<Item key="hue">Hue</Item>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<MasonryGallery
|
||
aspectsByBreakpoint={{
|
||
xs: 2,
|
||
sm: 2,
|
||
md: 3,
|
||
lg: 4,
|
||
xl: 5,
|
||
"2xl": 6.1,
|
||
"3xl": 8,
|
||
}}
|
||
dataFn={dataFn}
|
||
debugHue={sortKey === "hue_debug"}
|
||
images={images}
|
||
linkState={{
|
||
sortKey,
|
||
filterKeyword,
|
||
}}
|
||
showPalette={showPalette}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export const query = graphql`
|
||
query GalleryPageQuery {
|
||
recents: allFile(
|
||
filter: { sourceInstanceName: { eq: "gallery" } }
|
||
sort: { fields: { imageMeta: { datePublished: DESC } } }
|
||
limit: 10
|
||
) {
|
||
...GalleryImageFile
|
||
}
|
||
all: allFile(
|
||
filter: { sourceInstanceName: { eq: "gallery" } }
|
||
sort: { fields: { imageMeta: { dateTaken: DESC } } }
|
||
) {
|
||
...GalleryImageFile
|
||
}
|
||
}
|
||
|
||
fragment GalleryImageFile on FileConnection {
|
||
nodes {
|
||
base
|
||
childImageSharp {
|
||
fluid {
|
||
aspectRatio
|
||
}
|
||
gatsbyImageData(
|
||
layout: CONSTRAINED
|
||
height: 550
|
||
placeholder: DOMINANT_COLOR
|
||
)
|
||
}
|
||
fields {
|
||
imageMeta {
|
||
vibrantHue
|
||
dominantHue
|
||
dateTaken
|
||
datePublished
|
||
meta {
|
||
Keywords
|
||
Rating
|
||
ObjectName
|
||
CreateDate
|
||
ModifyDate
|
||
}
|
||
vibrant {
|
||
...VibrantColors
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
export default GalleryPage;
|