impress/app/javascript/wardrobe-2020/WardrobePage/index.js
Matchu e300b2d342 Run Prettier on all wardrobe-2020 JS
Looks like the version of Prettier I just installed is v3, whereas our
last run in the impress-2020 repo was with v2. I don't think we had any
special config in that project, I think these are just changes to
Prettier's defaults, and I'm comfortable accepting them! (Mostly seems
like a lot of trailing commas.)
2023-10-24 16:45:49 -07:00

146 lines
5 KiB
JavaScript

import React from "react";
import { useToast } from "@chakra-ui/react";
import { emptySearchQuery } from "./SearchToolbar";
import ItemsAndSearchPanels from "./ItemsAndSearchPanels";
import SearchFooter from "./SearchFooter";
import SupportOnly from "./support/SupportOnly";
import useOutfitSaving from "./useOutfitSaving";
import useOutfitState, { OutfitStateContext } from "./useOutfitState";
import WardrobePageLayout from "./WardrobePageLayout";
import WardrobePreviewAndControls from "./WardrobePreviewAndControls";
/**
* WardrobePage is the most fun page on the site - it's where you create
* outfits!
*
* This page has two sections: the OutfitPreview, where we show the outfit as a
* big image; and the ItemsAndSearchPanels, which let you manage which items
* are in the outfit and find new ones.
*
* This component manages shared outfit state, and the fullscreen responsive
* page layout.
*/
function WardrobePage() {
const toast = useToast();
const { loading, error, outfitState, dispatchToOutfit } = useOutfitState();
const [searchQuery, setSearchQuery] = React.useState(emptySearchQuery);
// We manage outfit saving up here, rather than at the point of the UI where
// "Saving" indicators appear. That way, auto-saving still happens even when
// the indicator isn't on the page, e.g. when searching.
// NOTE: This only applies to navigations leaving the wardrobe-2020 app, not
// within!
const outfitSaving = useOutfitSaving(outfitState, dispatchToOutfit);
// TODO: I haven't found a great place for this error UI yet, and this case
// isn't very common, so this lil toast notification seems good enough!
React.useEffect(() => {
if (error) {
console.error(error);
toast({
title: "We couldn't load this outfit 😖",
description: "Please reload the page to try again. Sorry!",
status: "error",
isClosable: true,
duration: 999999999,
});
}
}, [error, toast]);
// For new outfits, we only block navigation while saving. For existing
// outfits, we block navigation while there are any unsaved changes.
const shouldBlockNavigation =
outfitSaving.canSaveOutfit &&
((outfitSaving.isNewOutfit && outfitSaving.isSaving) ||
(!outfitSaving.isNewOutfit && !outfitSaving.latestVersionIsSaved));
// In addition to a <Prompt /> for client-side nav, we need to block full nav!
React.useEffect(() => {
if (shouldBlockNavigation) {
const onBeforeUnload = (e) => {
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
e.preventDefault();
e.returnValue = "";
};
window.addEventListener("beforeunload", onBeforeUnload);
return () => window.removeEventListener("beforeunload", onBeforeUnload);
}
}, [shouldBlockNavigation]);
const title = `${outfitState.name || "Untitled outfit"} | Dress to Impress`;
React.useEffect(() => {
document.title = title;
}, [title]);
// NOTE: Most components pass around outfitState directly, to make the data
// relationships more explicit... but there are some deep components
// that need it, where it's more useful and more performant to access
// via context.
return (
<OutfitStateContext.Provider value={outfitState}>
<WardrobePageLayout
previewAndControls={
<WardrobePreviewAndControls
isLoading={loading}
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
/>
}
itemsAndMaybeSearchPanel={
<ItemsAndSearchPanels
loading={loading}
searchQuery={searchQuery}
onChangeSearchQuery={setSearchQuery}
outfitState={outfitState}
outfitSaving={outfitSaving}
dispatchToOutfit={dispatchToOutfit}
/>
}
searchFooter={
<SearchFooter
searchQuery={searchQuery}
onChangeSearchQuery={setSearchQuery}
outfitState={outfitState}
/>
}
/>
</OutfitStateContext.Provider>
);
}
/**
* SavedOutfitMetaTags renders the meta tags that we use to render pretty
* share cards for social media for saved outfits!
*/
function SavedOutfitMetaTags({ outfitState }) {
const updatedAtTimestamp = Math.floor(
new Date(outfitState.updatedAt).getTime() / 1000,
);
const imageUrl =
`https://impress-outfit-images.openneo.net/outfits` +
`/${encodeURIComponent(outfitState.id)}` +
`/v/${encodeURIComponent(updatedAtTimestamp)}` +
`/600.png`;
return (
<>
<meta
property="og:title"
content={outfitState.name || "Untitled outfit"}
/>
<meta property="og:type" content="website" />
<meta property="og:image" content={imageUrl} />
<meta property="og:url" content={outfitState.url} />
<meta property="og:site_name" content="Dress to Impress" />
<meta
property="og:description"
content="A custom Neopets outfit, designed on Dress to Impress!"
/>
</>
);
}
export default WardrobePage;