diff --git a/TODO.md b/TODO.md index 3e11d04..54bad08 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,9 @@ - [ ] Resume/Projects portfolio - [ ] Blog -- [ ] tags +- [x] tags - [ ] photo stories/collection pages - [ ] typescript (w/ graphql codegen) +- [ ] CMS - [x] gallery sort and filter preserved in url (use-query or whatever?) -- [ ] nav next/prev buttons use saved gallery sort and filter +- [x] nav next/prev buttons use saved gallery sort and filter +- [ ] bug: "gallery" buttin in nav should reset filter and sort selections diff --git a/gatsby-node.js b/gatsby-node.js index 6c1ce53..9755f2b 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -220,16 +220,16 @@ exports.createPages = async ({ graphql, actions, reporter }) => { ); edges.forEach(({ node }, index) => { - const nextImage = - index === edges.length - 1 ? null : edges[index + 1].node.base; - const prevImage = index === 0 ? null : edges[index - 1].node.base; + // const nextImage = + // index === edges.length - 1 ? null : edges[index + 1].node.base; + // const prevImage = index === 0 ? null : edges[index - 1].node.base; const page = { path: `photogallery/${node.base}`, component: galleryImageTemplate, context: { imageFilename: node.base, - nextImage, - prevImage, + // nextImage, + // prevImage, }, }; createPage(page); diff --git a/src/components/GalleryImage/GalleryImage.js b/src/components/GalleryImage/GalleryImage.js index e222a21..723755a 100644 --- a/src/components/GalleryImage/GalleryImage.js +++ b/src/components/GalleryImage/GalleryImage.js @@ -21,6 +21,7 @@ import { getHelmetSafeBodyStyle, hasName, getCanonicalSize, + getGalleryPageUrl, } from "../../utils"; import MetadataItem from "./MetadataItem"; @@ -35,33 +36,64 @@ const logKeyShortcut = (keyCode) => { } }; -const GalleryImage = ({ data, pageContext }) => { +const GalleryImage = ({ + data, + location: { + state: { sortedImageList, currentIndex, filterKeyword, sortKey }, + }, +}) => { 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 (pageContext.nextImage) { - navigate(`/photogallery/${pageContext.nextImage}/`); + if (nextImage) { + navigate(`/photogallery/${nextImage}/`, { + state: { + currentIndex: currentIndex + 1, + sortedImageList, + filterKeyword, + sortKey, + }, + }); } return; } case "ArrowLeft": { logKeyShortcut(e.code); - if (pageContext.prevImage) { - navigate(`/photogallery/${pageContext.prevImage}/`); + if (prevImage) { + navigate(`/photogallery/${prevImage}/`, { + state: { + currentIndex: currentIndex - 1, + sortedImageList, + filterKeyword, + sortKey, + }, + }); } return; } case "Escape": case "KeyG": { logKeyShortcut(e.code); - navigate(`/photogallery/#${image.base}`); + navigate( + getGalleryPageUrl({ keyword: filterKeyword, sortKey }, image.base) + ); } } }; @@ -69,7 +101,15 @@ const GalleryImage = ({ data, pageContext }) => { return () => { document.removeEventListener("keydown", keyListener); }; - }, [pageContext, image.base]); + }, [ + nextImage, + prevImage, + image.base, + currentIndex, + sortedImageList, + filterKeyword, + sortKey, + ]); const name = getName(image); const { meta, dateTaken: dt } = getMeta(image); @@ -118,22 +158,37 @@ const GalleryImage = ({ data, pageContext }) => { gallery esc - {pageContext.prevImage && ( + {prevImage && ( previous )} - {pageContext.nextImage && ( + {nextImage && ( next diff --git a/src/components/MasonryGallery.js b/src/components/MasonryGallery.js index d0e2cbc..b16c29f 100644 --- a/src/components/MasonryGallery.js +++ b/src/components/MasonryGallery.js @@ -13,11 +13,12 @@ const MasonryGallery = ({ aspectsByBreakpoint: aspectTargetsByBreakpoint, debugHue, debugRating, + linkState, }) => { const breakpoints = React.useMemo( () => R.pick(R.keys(aspectTargetsByBreakpoint), themeBreakpoints), [aspectTargetsByBreakpoint] - ); + ); const { breakpoint } = useBreakpoint(breakpoints, "sm"); @@ -65,81 +66,89 @@ const MasonryGallery = ({ [aspectRatios, targetAspect] ); + const sortedImageList = React.useMemo( + () => images.map((image) => image.base), + [images] + ); + let cursor = 0; return ( <> - {/* {breakpoint} */} -
- {images.map((image, i) => { - let currentRow = rows[cursor]; - if (rows[i]) { - cursor = i; - currentRow = rows[i]; - } - const rowAspectRatioSum = currentRow.aspect; - const ar = getAspectRatio(image); - let width; - let height = `calc(100vw / ${rowAspectRatioSum} - 10px)`; - if (rowAspectRatioSum < targetAspect * 0.66) { - // incomplete row, render stuff at "ideal" sizes instead of filling width - width = `calc(100vw / ${targetAspect / ar})`; - height = "unset"; - } else { - const widthNumber = ((ar / rowAspectRatioSum) * 100).toFixed(7); - width = `${widthNumber}%`; - } - return ( - + {images.map((image, i) => { + let currentRow = rows[cursor]; + if (rows[i]) { + cursor = i; + currentRow = rows[i]; + } + const rowAspectRatioSum = currentRow.aspect; + const ar = getAspectRatio(image); + let width; + let height = `calc(100vw / ${rowAspectRatioSum} - 10px)`; + if (rowAspectRatioSum < targetAspect * 0.66) { + // incomplete row, render stuff at "ideal" sizes instead of filling width + width = `calc(100vw / ${targetAspect / ar})`; + height = "unset"; + } else { + const widthNumber = ((ar / rowAspectRatioSum) * 100).toFixed(7); + width = `${widthNumber}%`; + } + return ( + - {debugHue && ( - - hsl( - {image.fields.imageMeta.dominantHue[0]},{" "} - {(image.fields.imageMeta.dominantHue[1] * 100).toFixed(2)}%,{" "} - {(image.fields.imageMeta.dominantHue[2] * 100).toFixed(2)}% ) - - )} - {debugRating && ( - - rating: {image.fields.imageMeta.meta.Rating} - - )} - - - ); - })} -
+ : "black", + }} + to={`/photogallery/${image.base}`} + > + {debugHue && ( + + hsl( + {image.fields.imageMeta.dominantHue[0]},{" "} + {(image.fields.imageMeta.dominantHue[1] * 100).toFixed(2)}%,{" "} + {(image.fields.imageMeta.dominantHue[2] * 100).toFixed(2)}% ) + + )} + {debugRating && ( + + rating: {image.fields.imageMeta.meta.Rating} + + )} + + + ); + })} + ); }; diff --git a/src/pages/photogallery/[...].js b/src/pages/photogallery/[...].js index 2cfd54a..17b74ee 100644 --- a/src/pages/photogallery/[...].js +++ b/src/pages/photogallery/[...].js @@ -7,6 +7,7 @@ import { Picker, Item } from "@adobe/react-spectrum"; import MasonryGallery from "../../components/MasonryGallery"; import KeywordsPicker from "../../components/KeywordsPicker"; +import { getGalleryPageUrl } from "../../utils"; const SORT_KEYS = { hue: ["fields", "imageMeta", "vibrantHue"], @@ -15,27 +16,10 @@ const SORT_KEYS = { date: [], }; -const getUrl = ({ keyword, sortKey }) => { - const url = new URL(window.location.toString()); - if (keyword !== undefined) { - if (keyword === null) { - url.searchParams.delete("filter"); - } else { - url.searchParams.set("filter", keyword); - } - } - if (sortKey !== undefined) { - if (sortKey === "rating") { - url.searchParams.delete("sort"); - } else { - url.searchParams.set("sort", sortKey); - } - } - return url.toString(); -}; - const GalleryPage = ({ data }) => { - const [keyword, _setKeyword] = React.useState(null); + const hash = window.location.hash.replace("#", ""); + + const [filterKeyword, _setKeyword] = React.useState(null); const [sortKey, _setSortKey] = React.useState("rating"); const showDebug = typeof window !== "undefined" && @@ -51,10 +35,13 @@ const GalleryPage = ({ data }) => { // do nothing } _setKeyword(newKeyword); - localStorage?.setItem("photogallery.keyword", newKeyword); - window.history.replaceState(null, "", getUrl({ keyword: newKeyword })); + window.history.replaceState( + null, + "", + getGalleryPageUrl({ keyword: newKeyword, sortKey }, hash) + ); }, - [_setKeyword] + [_setKeyword, sortKey, hash] ); const setSortKey = React.useCallback( @@ -67,48 +54,47 @@ const GalleryPage = ({ data }) => { // do nothing } _setSortKey(newSortKey); - localStorage?.setItem("photogallery.sortkey2", newSortKey); - window.history.replaceState(null, "", getUrl({ sortKey: newSortKey })); + window.history.replaceState( + null, + "", + getGalleryPageUrl({ sortKey: newSortKey, keyword: filterKeyword }, hash) + ); }, - [_setSortKey] + [_setSortKey, filterKeyword, hash] ); - React.useEffect(() => { - const url = new URL(window.location.toString()); - - const sortKeyFromUrl = url.searchParams.get("sort"); - const sortKeyFromStorage = localStorage.getItem("photogallery.sortkey2"); - if (sortKeyFromUrl || sortKeyFromStorage) { - setSortKey(sortKeyFromUrl || sortKeyFromStorage); - } - - const filterKeyFromUrl = url.searchParams.get("filter"); - const filterKeyFromStorage = localStorage.getItem("photogallery.keyword"); - if (filterKeyFromUrl || filterKeyFromStorage !== "null") { - setKeyword(filterKeyFromUrl || filterKeyFromStorage); - } - }, [setSortKey, setKeyword]); - const scrollIntoView = React.useCallback(() => { - if (!window.location.hash) { + if (!hash) { return; } - const el = document.getElementById(window.location.hash.split("#")[1]); + const el = document.getElementById(hash); if (!el) { return; } el.scrollIntoView({ block: "center", }); - }, []); + }, [hash]); React.useEffect(() => { + const url = new URL(window.location.toString()); + + const sortKeyFromUrl = url.searchParams.get("sort"); + if (sortKeyFromUrl) { + _setSortKey(sortKeyFromUrl, false); + } + + const filterKeyFromUrl = url.searchParams.get("filter"); + if (filterKeyFromUrl) { + _setKeyword(filterKeyFromUrl, false); + } + // hacky but it works for now setTimeout(() => { // don't scroll into view if user got here with back button scrollIntoView(); }, 100); - }, [scrollIntoView]); + }, [setSortKey, setKeyword, scrollIntoView]); const images = React.useMemo( () => @@ -124,16 +110,16 @@ const GalleryPage = ({ data }) => { return -1 * (date1.getTime() - date2.getTime()); }) : R.sort(R.descend(R.path(SORT_KEYS[sortKey]))), - keyword + filterKeyword ? R.filter((image) => R.includes( - keyword, + filterKeyword, R.path(["fields", "imageMeta", "meta", "Keywords"], image) ) ) : R.identity )(data.allFile.nodes), - [data, sortKey, keyword] + [data, sortKey, filterKeyword] ); return ( @@ -183,7 +169,7 @@ const GalleryPage = ({ data }) => { // "sunset", ]} onChange={setKeyword} - value={keyword} + value={filterKeyword} />
{ debugHue={sortKey === "hue_debug"} debugRating={sortKey === "rating" && showDebug} images={images} + linkState={{ + sortKey, + filterKeyword, + }} /> ); diff --git a/src/utils.js b/src/utils.js index ec644fa..98630ec 100644 --- a/src/utils.js +++ b/src/utils.js @@ -79,3 +79,28 @@ export const getShutterFractionFromExposureTime = (exposureTime) => { } return `${numerator}/${denominator}`; }; + +export const getGalleryPageUrl = ( + { keyword, sortKey }, + hash +) => { + const url = new URL(`${window.location.origin}/photogallery/`); + if (keyword !== undefined) { + if (keyword === null) { + url.searchParams.delete("filter"); + } else { + url.searchParams.set("filter", keyword); + } + } + if (sortKey !== undefined) { + if (sortKey === "rating") { + url.searchParams.delete("sort"); + } else { + url.searchParams.set("sort", sortKey); + } + } + if (hash) { + url.hash = hash; + } + return url.href.toString().replace(url.origin, ''); +};