[WIP] Migrate outfit page, with known bug
Okay so there's a bug here where navigating directly to /outfits/new?species=X&color=Y will reset to a Blue Acara, because Next.js statically renders the Blue Acara on build, and then rehydrates a Blue Acara on load, and then updates the real page query in—and our state management for outfits doesn't *listen* to URL changes, it only *emits* them. It'd be good to consider like… changing that? It's tricky because our state model is… not simple, when you consider that we have both local state and URL state and saved-outfit state in play. But it could be done! But there might be another option too. I'll take a look at this after moving the home page, which will give me the chance to see what the experience navigating in from there is like!
This commit is contained in:
parent
5d28c36e8a
commit
eb602556bf
10 changed files with 167 additions and 86 deletions
|
@ -2,34 +2,38 @@
|
||||||
// extra SSR for outfit sharing meta tags!
|
// extra SSR for outfit sharing meta tags!
|
||||||
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import { NextPageWithLayout } from "../_app";
|
||||||
|
import WardrobePage from "../../src/app/WardrobePage";
|
||||||
|
// @ts-ignore: doesn't understand module.exports
|
||||||
import connectToDb from "../../src/server/db";
|
import connectToDb from "../../src/server/db";
|
||||||
|
// @ts-ignore: doesn't understand module.exports
|
||||||
import { normalizeRow } from "../../src/server/util";
|
import { normalizeRow } from "../../src/server/util";
|
||||||
|
import { GetServerSideProps } from "next";
|
||||||
|
|
||||||
// import NextIndexWrapper from '../../src'
|
type Outfit = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
type PageProps = {
|
||||||
|
outfit: Outfit;
|
||||||
|
};
|
||||||
|
|
||||||
// next/dynamic is used to prevent breaking incompatibilities
|
const WardrobePageWrapper: NextPageWithLayout<PageProps> = ({ outfit }) => {
|
||||||
// with SSR from window.SOME_VAR usage, if this is not used
|
|
||||||
// next/dynamic can be removed to take advantage of SSR/prerendering
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
// try changing "ssr" to true below to test for incompatibilities, if
|
|
||||||
// no errors occur the above static import can be used instead and the
|
|
||||||
// below removed
|
|
||||||
const App = dynamic(() => import("../../src/app/App"), { ssr: false });
|
|
||||||
|
|
||||||
export default function Page({ outfit, ...props }) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{outfit.name || "Untitled outfit"} | Dress to Impress</title>
|
<title>{outfit.name || "Untitled outfit"} | Dress to Impress</title>
|
||||||
<OutfitMetaTags outfit={outfit} />
|
<OutfitMetaTags outfit={outfit} />
|
||||||
</Head>
|
</Head>
|
||||||
<App {...props} />
|
<WardrobePage />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function OutfitMetaTags({ outfit }) {
|
WardrobePageWrapper.renderWithLayout = (children) => children;
|
||||||
|
|
||||||
|
function OutfitMetaTags({ outfit }: { outfit: Outfit }) {
|
||||||
const updatedAtTimestamp = Math.floor(
|
const updatedAtTimestamp = Math.floor(
|
||||||
new Date(outfit.updatedAt).getTime() / 1000
|
new Date(outfit.updatedAt).getTime() / 1000
|
||||||
);
|
);
|
||||||
|
@ -57,8 +61,13 @@ function OutfitMetaTags({ outfit }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps({ params }) {
|
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
|
||||||
const outfit = await loadOutfitData(params.id);
|
const outfitId = params?.outfitId;
|
||||||
|
if (typeof outfitId !== "string") {
|
||||||
|
throw new Error(`assertion failed: outfitId route param is missing`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outfit = await loadOutfitData(outfitId);
|
||||||
if (outfit == null) {
|
if (outfit == null) {
|
||||||
return { notFound: true };
|
return { notFound: true };
|
||||||
}
|
}
|
||||||
|
@ -72,9 +81,9 @@ export async function getServerSideProps({ params }) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
async function loadOutfitData(id) {
|
async function loadOutfitData(id: string) {
|
||||||
const db = await connectToDb();
|
const db = await connectToDb();
|
||||||
const [rows] = await db.query(`SELECT * FROM outfits WHERE id = ?;`, [id]);
|
const [rows] = await db.query(`SELECT * FROM outfits WHERE id = ?;`, [id]);
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
|
@ -83,3 +92,5 @@ async function loadOutfitData(id) {
|
||||||
|
|
||||||
return normalizeRow(rows[0]);
|
return normalizeRow(rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default WardrobePageWrapper;
|
|
@ -1,19 +0,0 @@
|
||||||
// This is just a copy of our higher-level catch-all page.
|
|
||||||
// That way, /outfits/new renders as normal, but /outfits/:slug
|
|
||||||
// does the SSR thing!
|
|
||||||
|
|
||||||
// import NextIndexWrapper from '../../src'
|
|
||||||
|
|
||||||
// next/dynamic is used to prevent breaking incompatibilities
|
|
||||||
// with SSR from window.SOME_VAR usage, if this is not used
|
|
||||||
// next/dynamic can be removed to take advantage of SSR/prerendering
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
// try changing "ssr" to true below to test for incompatibilities, if
|
|
||||||
// no errors occur the above static import can be used instead and the
|
|
||||||
// below removed
|
|
||||||
const App = dynamic(() => import("../../src/app/App"), { ssr: false });
|
|
||||||
|
|
||||||
export default function Page(props) {
|
|
||||||
return <App {...props} />;
|
|
||||||
}
|
|
10
pages/outfits/new.tsx
Normal file
10
pages/outfits/new.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import WardrobePage from "../../src/app/WardrobePage";
|
||||||
|
import type { NextPageWithLayout } from "../_app";
|
||||||
|
|
||||||
|
const WardrobePageWrapper: NextPageWithLayout = () => {
|
||||||
|
return <WardrobePage />;
|
||||||
|
};
|
||||||
|
|
||||||
|
WardrobePageWrapper.renderWithLayout = (children) => children;
|
||||||
|
|
||||||
|
export default WardrobePageWrapper;
|
|
@ -7,13 +7,9 @@ import {
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
|
|
||||||
import PageLayout from "./PageLayout";
|
import PageLayout from "./PageLayout";
|
||||||
import WardrobePageLayout from "./WardrobePage/WardrobePageLayout";
|
|
||||||
import { loadable } from "./util";
|
import { loadable } from "./util";
|
||||||
|
|
||||||
const HomePage = loadable(() => import("./HomePage"));
|
const HomePage = loadable(() => import("./HomePage"));
|
||||||
const WardrobePage = loadable(() => import("./WardrobePage"), {
|
|
||||||
fallback: <WardrobePageLayout />,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App is the entry point of our application. There's not a ton of exciting
|
* App is the entry point of our application. There's not a ton of exciting
|
||||||
|
@ -27,12 +23,6 @@ function App() {
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/outfits/new">
|
|
||||||
<WardrobePage />
|
|
||||||
</Route>
|
|
||||||
<Route path="/outfits/:id">
|
|
||||||
<WardrobePage />
|
|
||||||
</Route>
|
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<PageLayout hideHomeLink>
|
<PageLayout hideHomeLink>
|
||||||
<HomePage />
|
<HomePage />
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { EditIcon, DeleteIcon, InfoIcon } from "@chakra-ui/icons";
|
import { EditIcon, DeleteIcon, InfoIcon } from "@chakra-ui/icons";
|
||||||
import { Link } from "react-router-dom";
|
import Link from "next/link";
|
||||||
import { loadable } from "../util";
|
import { loadable } from "../util";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -249,13 +249,13 @@ function ItemActionButton({ icon, label, to, onClick }) {
|
||||||
<ClassNames>
|
<ClassNames>
|
||||||
{({ css }) => (
|
{({ css }) => (
|
||||||
<Tooltip label={label} placement="top">
|
<Tooltip label={label} placement="top">
|
||||||
<IconButton
|
<LinkOrButton
|
||||||
as={to ? Link : "button"}
|
component={IconButton}
|
||||||
|
href={to}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="gray.400"
|
color="gray.400"
|
||||||
to={to}
|
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={css`
|
className={css`
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -286,6 +286,19 @@ function ItemActionButton({ icon, label, to, onClick }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LinkOrButton({ href, component = Button, ...props }) {
|
||||||
|
const ButtonComponent = component;
|
||||||
|
if (href != null) {
|
||||||
|
return (
|
||||||
|
<Link href={href} passHref>
|
||||||
|
<ButtonComponent as="a" {...props} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <ButtonComponent {...props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ItemListContainer is a container for Item components! Wrap your Item
|
* ItemListContainer is a container for Item components! Wrap your Item
|
||||||
* components in this to ensure a consistent list layout.
|
* components in this to ensure a consistent list layout.
|
||||||
|
|
|
@ -35,7 +35,6 @@ import {
|
||||||
WarningTwoIcon,
|
WarningTwoIcon,
|
||||||
} from "@chakra-ui/icons";
|
} from "@chakra-ui/icons";
|
||||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Delay,
|
Delay,
|
||||||
|
@ -50,6 +49,7 @@ import { IoCloudUploadOutline } from "react-icons/io5";
|
||||||
import { MdMoreVert } from "react-icons/md";
|
import { MdMoreVert } from "react-icons/md";
|
||||||
import { buildOutfitUrl } from "./useOutfitState";
|
import { buildOutfitUrl } from "./useOutfitState";
|
||||||
import { gql, useMutation } from "@apollo/client";
|
import { gql, useMutation } from "@apollo/client";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ItemsPanel shows the items in the current outfit, and lets the user toggle
|
* ItemsPanel shows the items in the current outfit, and lets the user toggle
|
||||||
|
@ -455,7 +455,7 @@ function OutfitHeading({ outfitState, outfitSaving, dispatchToOutfit }) {
|
||||||
function DeleteOutfitMenuItem({ outfitState }) {
|
function DeleteOutfitMenuItem({ outfitState }) {
|
||||||
const { id, name } = outfitState;
|
const { id, name } = outfitState;
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const history = useHistory();
|
const { push: pushHistory } = useRouter();
|
||||||
|
|
||||||
const [sendDeleteOutfitMutation, { loading, error }] = useMutation(
|
const [sendDeleteOutfitMutation, { loading, error }] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
|
@ -506,7 +506,7 @@ function DeleteOutfitMenuItem({ outfitState }) {
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
sendDeleteOutfitMutation({ variables: { id } })
|
sendDeleteOutfitMutation({ variables: { id } })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
history.push(`/your-outfits`);
|
pushHistory(`/your-outfits`);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
/* handled in error UI */
|
/* handled in error UI */
|
||||||
|
|
|
@ -33,7 +33,7 @@ import {
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
} from "@chakra-ui/icons";
|
} from "@chakra-ui/icons";
|
||||||
import { MdPause, MdPlayArrow } from "react-icons/md";
|
import { MdPause, MdPlayArrow } from "react-icons/md";
|
||||||
import { Link } from "react-router-dom";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { getBestImageUrlForLayer } from "../components/OutfitPreview";
|
import { getBestImageUrlForLayer } from "../components/OutfitPreview";
|
||||||
import HTML5Badge, { layerUsesHTML5 } from "../components/HTML5Badge";
|
import HTML5Badge, { layerUsesHTML5 } from "../components/HTML5Badge";
|
||||||
|
@ -318,14 +318,15 @@ function BackButton({ outfitState }) {
|
||||||
outfitState.creator && outfitState.creator.id === currentUser.id;
|
outfitState.creator && outfitState.creator.id === currentUser.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlButton
|
<Link href={outfitBelongsToCurrentUser ? "/your-outfits" : "/"} passHref>
|
||||||
as={Link}
|
<ControlButton
|
||||||
to={outfitBelongsToCurrentUser ? "/your-outfits" : "/"}
|
as="a"
|
||||||
icon={<ArrowBackIcon />}
|
icon={<ArrowBackIcon />}
|
||||||
aria-label="Leave this outfit"
|
aria-label="Leave this outfit"
|
||||||
d="inline-flex" // Not sure why <a> requires this to style right! ^^`
|
d="inline-flex" // Not sure why <a> requires this to style right! ^^`
|
||||||
data-test-id="wardrobe-nav-back-button"
|
data-test-id="wardrobe-nav-back-button"
|
||||||
/>
|
/>
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Prompt } from "react-router-dom";
|
|
||||||
import { useToast } from "@chakra-ui/react";
|
import { useToast } from "@chakra-ui/react";
|
||||||
import { loadable } from "../util";
|
import { loadable } from "../util";
|
||||||
|
|
||||||
|
@ -11,6 +10,7 @@ import useOutfitState, { OutfitStateContext } from "./useOutfitState";
|
||||||
import { usePageTitle } from "../util";
|
import { usePageTitle } from "../util";
|
||||||
import WardrobePageLayout from "./WardrobePageLayout";
|
import WardrobePageLayout from "./WardrobePageLayout";
|
||||||
import WardrobePreviewAndControls from "./WardrobePreviewAndControls";
|
import WardrobePreviewAndControls from "./WardrobePreviewAndControls";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const WardrobeDevHacks = loadable(() => import("./WardrobeDevHacks"));
|
const WardrobeDevHacks = loadable(() => import("./WardrobeDevHacks"));
|
||||||
|
|
||||||
|
@ -119,4 +119,36 @@ function WardrobePage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <Prompt>.";
|
||||||
|
};
|
||||||
|
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;
|
export default WardrobePage;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useToast } from "@chakra-ui/react";
|
import { useToast } from "@chakra-ui/react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useRouter } from "next/router";
|
||||||
import { useDebounce } from "../util";
|
import { useDebounce } from "../util";
|
||||||
import useCurrentUser from "../components/useCurrentUser";
|
import useCurrentUser from "../components/useCurrentUser";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
|
@ -9,7 +9,7 @@ import { outfitStatesAreEqual } from "./useOutfitState";
|
||||||
|
|
||||||
function useOutfitSaving(outfitState, dispatchToOutfit) {
|
function useOutfitSaving(outfitState, dispatchToOutfit) {
|
||||||
const { isLoggedIn, id: currentUserId } = useCurrentUser();
|
const { isLoggedIn, id: currentUserId } = useCurrentUser();
|
||||||
const history = useHistory();
|
const { pathname, push: pushHistory } = useRouter();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// There's not a way to reset an Apollo mutation state to clear out the error
|
// There's not a way to reset an Apollo mutation state to clear out the error
|
||||||
|
@ -158,8 +158,8 @@ function useOutfitSaving(outfitState, dispatchToOutfit) {
|
||||||
// Navigate to the new saved outfit URL. Our Apollo cache should pick
|
// Navigate to the new saved outfit URL. Our Apollo cache should pick
|
||||||
// up the data from this mutation response, and combine it with the
|
// up the data from this mutation response, and combine it with the
|
||||||
// existing cached data, to make this smooth without any loading UI.
|
// existing cached data, to make this smooth without any loading UI.
|
||||||
if (history.location.pathname !== `/outfits/${outfit.id}`) {
|
if (pathname !== `/outfits/[outfitId]`) {
|
||||||
history.push(`/outfits/${outfit.id}`);
|
pushHistory(`/outfits/${outfit.id}`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -175,7 +175,7 @@ function useOutfitSaving(outfitState, dispatchToOutfit) {
|
||||||
// It's important that this callback _doesn't_ change when the outfit
|
// It's important that this callback _doesn't_ change when the outfit
|
||||||
// changes, so that the auto-save effect is only responding to the
|
// changes, so that the auto-save effect is only responding to the
|
||||||
// debounced state!
|
// debounced state!
|
||||||
[sendSaveOutfitMutation, history, toast]
|
[sendSaveOutfitMutation, pathname, pushHistory, toast]
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveOutfit = React.useCallback(
|
const saveOutfit = React.useCallback(
|
||||||
|
|
|
@ -2,9 +2,9 @@ import React from "react";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import produce, { enableMapSet } from "immer";
|
import produce, { enableMapSet } from "immer";
|
||||||
import { useQuery, useApolloClient } from "@apollo/client";
|
import { useQuery, useApolloClient } from "@apollo/client";
|
||||||
import { useLocation, useParams } from "react-router-dom";
|
|
||||||
|
|
||||||
import { itemAppearanceFragment } from "../components/useOutfitAppearance";
|
import { itemAppearanceFragment } from "../components/useOutfitAppearance";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
enableMapSet();
|
enableMapSet();
|
||||||
|
|
||||||
|
@ -249,8 +249,13 @@ function useOutfitState() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep the URL up-to-date. (We don't listen to it, though 😅)
|
// Keep the URL up-to-date. (We don't listen to it, though 😅)
|
||||||
|
// TODO: Seems like we should hook this in with the actual router... I'm
|
||||||
|
// avoiding it rn, but I'm worried Next.js won't necessarily play nice
|
||||||
|
// with this hack, even though react-router did. Hard to predict!
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
window.history.replaceState(null, "", url);
|
if (typeof history !== "undefined") {
|
||||||
|
history.replaceState(null, "", url);
|
||||||
|
}
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -369,39 +374,74 @@ const EMPTY_CUSTOMIZATION_STATE = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function useParseOutfitUrl() {
|
function useParseOutfitUrl() {
|
||||||
const { id } = useParams();
|
const { query } = useRouter();
|
||||||
const { search } = useLocation();
|
const { outfitId } = query;
|
||||||
|
|
||||||
// We memoize this to make `outfitStateWithoutExtras` an even more reliable
|
// We memoize this to make `outfitStateWithoutExtras` an even more reliable
|
||||||
// stable object!
|
// stable object!
|
||||||
const memoizedOutfitState = React.useMemo(() => {
|
const memoizedOutfitState = React.useMemo(() => {
|
||||||
// For the /outfits/:id page, ignore the query string, and just wait for the
|
// For the /outfits/:id page, ignore the query string, and just wait for the
|
||||||
// outfit data to load in!
|
// outfit data to load in!
|
||||||
if (id != null) {
|
if (outfitId != null) {
|
||||||
return {
|
return {
|
||||||
...EMPTY_CUSTOMIZATION_STATE,
|
...EMPTY_CUSTOMIZATION_STATE,
|
||||||
id,
|
id: outfitId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, parse the query string, and fill in default values for anything
|
// Otherwise, parse the query string, and fill in default values for anything
|
||||||
// not specified.
|
// not specified.
|
||||||
const urlParams = new URLSearchParams(search);
|
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
name: urlParams.get("name"),
|
name: getValueFromQuery(query.name),
|
||||||
speciesId: urlParams.get("species") || "1",
|
speciesId: getValueFromQuery(query.species) || "1",
|
||||||
colorId: urlParams.get("color") || "8",
|
colorId: getValueFromQuery(query.color) || "8",
|
||||||
pose: urlParams.get("pose") || "HAPPY_FEM",
|
pose: getValueFromQuery(query.pose) || "HAPPY_FEM",
|
||||||
appearanceId: urlParams.get("state") || null,
|
appearanceId: getValueFromQuery(query.state) || null,
|
||||||
wornItemIds: new Set(urlParams.getAll("objects[]")),
|
wornItemIds: new Set(getListFromQuery(query["objects[]"])),
|
||||||
closetedItemIds: new Set(urlParams.getAll("closet[]")),
|
closetedItemIds: new Set(getListFromQuery(query["closet[]"])),
|
||||||
};
|
};
|
||||||
}, [id, search]);
|
}, [outfitId, query]);
|
||||||
|
|
||||||
return memoizedOutfitState;
|
return memoizedOutfitState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getValueFromQuery reads the given value from Next's `router.query` as a
|
||||||
|
* single value. For example:
|
||||||
|
*
|
||||||
|
* ?foo=bar -> "bar" -> "bar"
|
||||||
|
* ?foo=bar&foo=baz -> ["bar", "baz"] -> "bar"
|
||||||
|
* ?lol=huh -> undefined -> null
|
||||||
|
*/
|
||||||
|
function getValueFromQuery(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value[0];
|
||||||
|
} else if (value != null) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getListFromQuery reads the given value from Next's `router.query` as a list
|
||||||
|
* of values. For example:
|
||||||
|
*
|
||||||
|
* ?foo=bar -> "bar" -> ["bar"]
|
||||||
|
* ?foo=bar&foo=baz -> ["bar", "baz"] -> ["bar", "baz"]
|
||||||
|
* ?lol=huh -> undefined -> []
|
||||||
|
*/
|
||||||
|
function getListFromQuery(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
} else if (value != null) {
|
||||||
|
return [value];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getOutfitStateFromOutfitData(outfit) {
|
function getOutfitStateFromOutfitData(outfit) {
|
||||||
if (!outfit) {
|
if (!outfit) {
|
||||||
return EMPTY_CUSTOMIZATION_STATE;
|
return EMPTY_CUSTOMIZATION_STATE;
|
||||||
|
@ -602,7 +642,10 @@ function getZonesAndItems(itemsById, wornItemIds, closetedItemIds) {
|
||||||
export function buildOutfitUrl(outfitState, { withoutOutfitId = false } = {}) {
|
export function buildOutfitUrl(outfitState, { withoutOutfitId = false } = {}) {
|
||||||
const { id } = outfitState;
|
const { id } = outfitState;
|
||||||
|
|
||||||
const { origin } = window.location;
|
const origin =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? window.location.origin
|
||||||
|
: "https://impress-2020.openneo.net";
|
||||||
|
|
||||||
if (id && !withoutOutfitId) {
|
if (id && !withoutOutfitId) {
|
||||||
return origin + `/outfits/${id}`;
|
return origin + `/outfits/${id}`;
|
||||||
|
|
Loading…
Reference in a new issue