import React from "react"; import { useToast } from "@chakra-ui/react"; import { useRouter } from "next/router"; 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. We also mount a // in this component to prevent navigating away before saving. 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 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 ( <> {/* * TODO: This might unnecessarily block navigations that we don't * necessarily need to, e.g., navigating back to Your Outfits while the * save request is in flight. We could instead submit the save mutation * immediately on client-side nav, and have each outfit save mutation * install a `beforeunload` handler that ensures that you don't close * the page altogether while it's in flight. But let's start simple and * see how annoying it actually is in practice lol */} } itemsAndMaybeSearchPanel={ } searchFooter={ } /> ); } /** * 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 ( <> ); } /** * Prompt blocks client-side navigation via Next.js when the `when` prop is * true. This is our attempt at a drop-in replacement for the Prompt component * offered by react-router! * * Adapted from https://github.com/vercel/next.js/issues/2694#issuecomment-778225625 */ function Prompt({ when, message }) { const router = useRouter(); React.useEffect(() => { const handleWindowClose = (e) => { if (!when) return; e.preventDefault(); return (e.returnValue = message); }; const handleBrowseAway = () => { if (!when) return; if (window.confirm(message)) return; router.events.emit("routeChangeError"); throw "routeChange aborted by ."; }; window.addEventListener("beforeunload", handleWindowClose); router.events.on("routeChangeStart", handleBrowseAway); return () => { window.removeEventListener("beforeunload", handleWindowClose); router.events.off("routeChangeStart", handleBrowseAway); }; }, [when, message, router]); return null; } export default WardrobePage;