store sort and filterkeyword in urls and history state instead of localstorage
This commit is contained in:
parent
8ac3dd27dd
commit
4bfd662138
6
TODO.md
6
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
|
||||
|
@ -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);
|
||||
|
@ -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">⭠</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">⭢</span>
|
||||
</Link>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
25
src/utils.js
25
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, '');
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user