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
- [ ] 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

View File

@ -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);

View File

@ -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 }) => {
</Link>
<Link
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>
</Link>
{pageContext.prevImage && (
{prevImage && (
<Link
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>
</Link>
)}
{pageContext.nextImage && (
{nextImage && (
<Link
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>
</Link>

View File

@ -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} */}
<div
className="w-full flex items-center flex-wrap"
style={{
position: "relative",
}}
>
{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 (
<Link
className={classNames(
"border-4 overflow-hidden",
debugHue && "border-8"
)}
id={image.base}
key={`${image.base}`}
state={{ modal: true }}
style={{
height,
width,
// borderColor: `hsl(${image.fields.imageMeta.dominantHue}, 100%, 50%)`
// borderColor: `rgb(${image.fields.imageMeta.vibrant.Vibrant.join(',')})`
borderColor: debugHue
? `hsl(
<div
className="w-full flex items-center flex-wrap"
style={{
position: "relative",
}}
>
{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 (
<Link
className={classNames(
"border-4 overflow-hidden",
debugHue && "border-8"
)}
id={image.base}
key={`${image.base}`}
state={{
...linkState,
sortedImageList,
currentIndex: i,
}}
style={{
height,
width,
// 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[1] * 100}%,
${image.fields.imageMeta.dominantHue[2] * 100}%
)`
: "black",
}}
to={`/photogallery/${image.base}`}
>
{debugHue && (
<span className="text-white z-20 absolute bg-black">
hsl(
{image.fields.imageMeta.dominantHue[0]},{" "}
{(image.fields.imageMeta.dominantHue[1] * 100).toFixed(2)}%,{" "}
{(image.fields.imageMeta.dominantHue[2] * 100).toFixed(2)}% )
</span>
)}
{debugRating && (
<span className="text-white z-20 absolute bg-black">
rating: {image.fields.imageMeta.meta.Rating}
</span>
)}
<GatsbyImage
alt={getName(image)}
className="w-full h-full"
image={getImage(image)}
objectFit="cover"
/>
</Link>
);
})}
</div>
: "black",
}}
to={`/photogallery/${image.base}`}
>
{debugHue && (
<span className="text-white z-20 absolute bg-black">
hsl(
{image.fields.imageMeta.dominantHue[0]},{" "}
{(image.fields.imageMeta.dominantHue[1] * 100).toFixed(2)}%,{" "}
{(image.fields.imageMeta.dominantHue[2] * 100).toFixed(2)}% )
</span>
)}
{debugRating && (
<span className="text-white z-20 absolute bg-black">
rating: {image.fields.imageMeta.meta.Rating}
</span>
)}
<GatsbyImage
alt={getName(image)}
className="w-full h-full"
image={getImage(image)}
objectFit="cover"
/>
</Link>
);
})}
</div>
</>
);
};

View File

@ -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}
/>
<div className="m-2">
<Picker
@ -211,6 +197,10 @@ const GalleryPage = ({ data }) => {
debugHue={sortKey === "hue_debug"}
debugRating={sortKey === "rating" && showDebug}
images={images}
linkState={{
sortKey,
filterKeyword,
}}
/>
</>
);

View File

@ -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, '');
};