store sort and filterkeyword in urls and history state instead of localstorage

This commit is contained in:
Chuck Dries 2022-07-10 17:09:58 -07:00
parent 8ac3dd27dd
commit 4bfd662138
No known key found for this signature in database
GPG Key ID: A00B7AEAE1DC5BE6
6 changed files with 219 additions and 138 deletions

View File

@ -1,7 +1,9 @@
- [ ] Resume/Projects portfolio - [ ] Resume/Projects portfolio
- [ ] Blog - [ ] Blog
- [ ] tags - [x] tags
- [ ] photo stories/collection pages - [ ] photo stories/collection pages
- [ ] typescript (w/ graphql codegen) - [ ] typescript (w/ graphql codegen)
- [ ] CMS
- [x] gallery sort and filter preserved in url (use-query or whatever?) - [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

View File

@ -220,16 +220,16 @@ exports.createPages = async ({ graphql, actions, reporter }) => {
); );
edges.forEach(({ node }, index) => { edges.forEach(({ node }, index) => {
const nextImage = // const nextImage =
index === edges.length - 1 ? null : edges[index + 1].node.base; // index === edges.length - 1 ? null : edges[index + 1].node.base;
const prevImage = index === 0 ? null : edges[index - 1].node.base; // const prevImage = index === 0 ? null : edges[index - 1].node.base;
const page = { const page = {
path: `photogallery/${node.base}`, path: `photogallery/${node.base}`,
component: galleryImageTemplate, component: galleryImageTemplate,
context: { context: {
imageFilename: node.base, imageFilename: node.base,
nextImage, // nextImage,
prevImage, // prevImage,
}, },
}; };
createPage(page); createPage(page);

View File

@ -21,6 +21,7 @@ import {
getHelmetSafeBodyStyle, getHelmetSafeBodyStyle,
hasName, hasName,
getCanonicalSize, getCanonicalSize,
getGalleryPageUrl,
} from "../../utils"; } from "../../utils";
import MetadataItem from "./MetadataItem"; 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 image = data.file;
const ar = getAspectRatio(image); const ar = getAspectRatio(image);
const [zoom, setZoom] = useState(false); 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(() => { React.useEffect(() => {
const keyListener = (e) => { const keyListener = (e) => {
switch (e.code) { switch (e.code) {
case "ArrowRight": { case "ArrowRight": {
logKeyShortcut(e.code); logKeyShortcut(e.code);
if (pageContext.nextImage) { if (nextImage) {
navigate(`/photogallery/${pageContext.nextImage}/`); navigate(`/photogallery/${nextImage}/`, {
state: {
currentIndex: currentIndex + 1,
sortedImageList,
filterKeyword,
sortKey,
},
});
} }
return; return;
} }
case "ArrowLeft": { case "ArrowLeft": {
logKeyShortcut(e.code); logKeyShortcut(e.code);
if (pageContext.prevImage) { if (prevImage) {
navigate(`/photogallery/${pageContext.prevImage}/`); navigate(`/photogallery/${prevImage}/`, {
state: {
currentIndex: currentIndex - 1,
sortedImageList,
filterKeyword,
sortKey,
},
});
} }
return; return;
} }
case "Escape": case "Escape":
case "KeyG": { case "KeyG": {
logKeyShortcut(e.code); logKeyShortcut(e.code);
navigate(`/photogallery/#${image.base}`); navigate(
getGalleryPageUrl({ keyword: filterKeyword, sortKey }, image.base)
);
} }
} }
}; };
@ -69,7 +101,15 @@ const GalleryImage = ({ data, pageContext }) => {
return () => { return () => {
document.removeEventListener("keydown", keyListener); document.removeEventListener("keydown", keyListener);
}; };
}, [pageContext, image.base]); }, [
nextImage,
prevImage,
image.base,
currentIndex,
sortedImageList,
filterKeyword,
sortKey,
]);
const name = getName(image); const name = getName(image);
const { meta, dateTaken: dt } = getMeta(image); const { meta, dateTaken: dt } = getMeta(image);
@ -118,22 +158,37 @@ const GalleryImage = ({ data, pageContext }) => {
</Link> </Link>
<Link <Link
className="hover:underline text-vibrant-light hover:text-muted-light mx-1" className="hover:underline text-vibrant-light hover:text-muted-light mx-1"
to={`/photogallery/#${image.base}`} to={getGalleryPageUrl(
{ keyword: filterKeyword, sortKey },
image.base
)}
> >
gallery <span className="bg-gray-300 text-black">esc</span> gallery <span className="bg-gray-300 text-black">esc</span>
</Link> </Link>
{pageContext.prevImage && ( {prevImage && (
<Link <Link
className="hover:underline text-vibrant-light hover:text-muted-light mx-1" className="hover:underline text-vibrant-light hover:text-muted-light mx-1"
to={`/photogallery/${pageContext.prevImage}/`} state={{
currentIndex: currentIndex - 1,
sortedImageList,
filterKeyword,
sortKey,
}}
to={`/photogallery/${prevImage}/`}
> >
previous <span className="bg-gray-300 text-black">&#11104;</span> previous <span className="bg-gray-300 text-black">&#11104;</span>
</Link> </Link>
)} )}
{pageContext.nextImage && ( {nextImage && (
<Link <Link
className="hover:underline text-vibrant-light hover:text-muted-light mx-1" className="hover:underline text-vibrant-light hover:text-muted-light mx-1"
to={`/photogallery/${pageContext.nextImage}/`} state={{
currentIndex: currentIndex + 1,
sortedImageList,
filterKeyword,
sortKey,
}}
to={`/photogallery/${nextImage}/`}
> >
next <span className="bg-gray-300 text-black">&#11106;</span> next <span className="bg-gray-300 text-black">&#11106;</span>
</Link> </Link>

View File

@ -13,11 +13,12 @@ const MasonryGallery = ({
aspectsByBreakpoint: aspectTargetsByBreakpoint, aspectsByBreakpoint: aspectTargetsByBreakpoint,
debugHue, debugHue,
debugRating, debugRating,
linkState,
}) => { }) => {
const breakpoints = React.useMemo( const breakpoints = React.useMemo(
() => R.pick(R.keys(aspectTargetsByBreakpoint), themeBreakpoints), () => R.pick(R.keys(aspectTargetsByBreakpoint), themeBreakpoints),
[aspectTargetsByBreakpoint] [aspectTargetsByBreakpoint]
); );
const { breakpoint } = useBreakpoint(breakpoints, "sm"); const { breakpoint } = useBreakpoint(breakpoints, "sm");
@ -65,81 +66,89 @@ const MasonryGallery = ({
[aspectRatios, targetAspect] [aspectRatios, targetAspect]
); );
const sortedImageList = React.useMemo(
() => images.map((image) => image.base),
[images]
);
let cursor = 0; let cursor = 0;
return ( return (
<> <>
{/* {breakpoint} */} <div
<div className="w-full flex items-center flex-wrap"
className="w-full flex items-center flex-wrap" style={{
style={{ position: "relative",
position: "relative", }}
}} >
> {images.map((image, i) => {
{images.map((image, i) => { let currentRow = rows[cursor];
let currentRow = rows[cursor]; if (rows[i]) {
if (rows[i]) { cursor = i;
cursor = i; currentRow = rows[i];
currentRow = rows[i]; }
} const rowAspectRatioSum = currentRow.aspect;
const rowAspectRatioSum = currentRow.aspect; const ar = getAspectRatio(image);
const ar = getAspectRatio(image); let width;
let width; let height = `calc(100vw / ${rowAspectRatioSum} - 10px)`;
let height = `calc(100vw / ${rowAspectRatioSum} - 10px)`; if (rowAspectRatioSum < targetAspect * 0.66) {
if (rowAspectRatioSum < targetAspect * 0.66) { // incomplete row, render stuff at "ideal" sizes instead of filling width
// incomplete row, render stuff at "ideal" sizes instead of filling width width = `calc(100vw / ${targetAspect / ar})`;
width = `calc(100vw / ${targetAspect / ar})`; height = "unset";
height = "unset"; } else {
} else { const widthNumber = ((ar / rowAspectRatioSum) * 100).toFixed(7);
const widthNumber = ((ar / rowAspectRatioSum) * 100).toFixed(7); width = `${widthNumber}%`;
width = `${widthNumber}%`; }
} return (
return ( <Link
<Link className={classNames(
className={classNames( "border-4 overflow-hidden",
"border-4 overflow-hidden", debugHue && "border-8"
debugHue && "border-8" )}
)} id={image.base}
id={image.base} key={`${image.base}`}
key={`${image.base}`} state={{
state={{ modal: true }} ...linkState,
style={{ sortedImageList,
height, currentIndex: i,
width, }}
// borderColor: `hsl(${image.fields.imageMeta.dominantHue}, 100%, 50%)` style={{
// borderColor: `rgb(${image.fields.imageMeta.vibrant.Vibrant.join(',')})` height,
borderColor: debugHue width,
? `hsl( // borderColor: `hsl(${image.fields.imageMeta.dominantHue}, 100%, 50%)`
// borderColor: `rgb(${image.fields.imageMeta.vibrant.Vibrant.join(',')})`
borderColor: debugHue
? `hsl(
${image.fields.imageMeta.dominantHue[0]}, ${image.fields.imageMeta.dominantHue[0]},
${image.fields.imageMeta.dominantHue[1] * 100}%, ${image.fields.imageMeta.dominantHue[1] * 100}%,
${image.fields.imageMeta.dominantHue[2] * 100}% ${image.fields.imageMeta.dominantHue[2] * 100}%
)` )`
: "black", : "black",
}} }}
to={`/photogallery/${image.base}`} to={`/photogallery/${image.base}`}
> >
{debugHue && ( {debugHue && (
<span className="text-white z-20 absolute bg-black"> <span className="text-white z-20 absolute bg-black">
hsl( hsl(
{image.fields.imageMeta.dominantHue[0]},{" "} {image.fields.imageMeta.dominantHue[0]},{" "}
{(image.fields.imageMeta.dominantHue[1] * 100).toFixed(2)}%,{" "} {(image.fields.imageMeta.dominantHue[1] * 100).toFixed(2)}%,{" "}
{(image.fields.imageMeta.dominantHue[2] * 100).toFixed(2)}% ) {(image.fields.imageMeta.dominantHue[2] * 100).toFixed(2)}% )
</span> </span>
)} )}
{debugRating && ( {debugRating && (
<span className="text-white z-20 absolute bg-black"> <span className="text-white z-20 absolute bg-black">
rating: {image.fields.imageMeta.meta.Rating} rating: {image.fields.imageMeta.meta.Rating}
</span> </span>
)} )}
<GatsbyImage <GatsbyImage
alt={getName(image)} alt={getName(image)}
className="w-full h-full" className="w-full h-full"
image={getImage(image)} image={getImage(image)}
objectFit="cover" objectFit="cover"
/> />
</Link> </Link>
); );
})} })}
</div> </div>
</> </>
); );
}; };

View File

@ -7,6 +7,7 @@ import { Picker, Item } from "@adobe/react-spectrum";
import MasonryGallery from "../../components/MasonryGallery"; import MasonryGallery from "../../components/MasonryGallery";
import KeywordsPicker from "../../components/KeywordsPicker"; import KeywordsPicker from "../../components/KeywordsPicker";
import { getGalleryPageUrl } from "../../utils";
const SORT_KEYS = { const SORT_KEYS = {
hue: ["fields", "imageMeta", "vibrantHue"], hue: ["fields", "imageMeta", "vibrantHue"],
@ -15,27 +16,10 @@ const SORT_KEYS = {
date: [], 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 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 [sortKey, _setSortKey] = React.useState("rating");
const showDebug = const showDebug =
typeof window !== "undefined" && typeof window !== "undefined" &&
@ -51,10 +35,13 @@ const GalleryPage = ({ data }) => {
// do nothing // do nothing
} }
_setKeyword(newKeyword); _setKeyword(newKeyword);
localStorage?.setItem("photogallery.keyword", newKeyword); window.history.replaceState(
window.history.replaceState(null, "", getUrl({ keyword: newKeyword })); null,
"",
getGalleryPageUrl({ keyword: newKeyword, sortKey }, hash)
);
}, },
[_setKeyword] [_setKeyword, sortKey, hash]
); );
const setSortKey = React.useCallback( const setSortKey = React.useCallback(
@ -67,48 +54,47 @@ const GalleryPage = ({ data }) => {
// do nothing // do nothing
} }
_setSortKey(newSortKey); _setSortKey(newSortKey);
localStorage?.setItem("photogallery.sortkey2", newSortKey); window.history.replaceState(
window.history.replaceState(null, "", getUrl({ sortKey: newSortKey })); 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(() => { const scrollIntoView = React.useCallback(() => {
if (!window.location.hash) { if (!hash) {
return; return;
} }
const el = document.getElementById(window.location.hash.split("#")[1]); const el = document.getElementById(hash);
if (!el) { if (!el) {
return; return;
} }
el.scrollIntoView({ el.scrollIntoView({
block: "center", block: "center",
}); });
}, []); }, [hash]);
React.useEffect(() => { 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 // hacky but it works for now
setTimeout(() => { setTimeout(() => {
// don't scroll into view if user got here with back button // don't scroll into view if user got here with back button
scrollIntoView(); scrollIntoView();
}, 100); }, 100);
}, [scrollIntoView]); }, [setSortKey, setKeyword, scrollIntoView]);
const images = React.useMemo( const images = React.useMemo(
() => () =>
@ -124,16 +110,16 @@ const GalleryPage = ({ data }) => {
return -1 * (date1.getTime() - date2.getTime()); return -1 * (date1.getTime() - date2.getTime());
}) })
: R.sort(R.descend(R.path(SORT_KEYS[sortKey]))), : R.sort(R.descend(R.path(SORT_KEYS[sortKey]))),
keyword filterKeyword
? R.filter((image) => ? R.filter((image) =>
R.includes( R.includes(
keyword, filterKeyword,
R.path(["fields", "imageMeta", "meta", "Keywords"], image) R.path(["fields", "imageMeta", "meta", "Keywords"], image)
) )
) )
: R.identity : R.identity
)(data.allFile.nodes), )(data.allFile.nodes),
[data, sortKey, keyword] [data, sortKey, filterKeyword]
); );
return ( return (
@ -183,7 +169,7 @@ const GalleryPage = ({ data }) => {
// "sunset", // "sunset",
]} ]}
onChange={setKeyword} onChange={setKeyword}
value={keyword} value={filterKeyword}
/> />
<div className="m-2"> <div className="m-2">
<Picker <Picker
@ -211,6 +197,10 @@ const GalleryPage = ({ data }) => {
debugHue={sortKey === "hue_debug"} debugHue={sortKey === "hue_debug"}
debugRating={sortKey === "rating" && showDebug} debugRating={sortKey === "rating" && showDebug}
images={images} images={images}
linkState={{
sortKey,
filterKeyword,
}}
/> />
</> </>
); );

View File

@ -79,3 +79,28 @@ export const getShutterFractionFromExposureTime = (exposureTime) => {
} }
return `${numerator}/${denominator}`; 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, '');
};