Simplify page title & SSR for saved outfit
Now we can just use our usual pattern: preload some GraphQL data, and render the title and such in the page component itself! Whew!
This commit is contained in:
parent
e42f39f49b
commit
16c9e1a25d
3 changed files with 112 additions and 116 deletions
|
@ -1,96 +1,46 @@
|
|||
// This is a copy of our higher-level catch-all page, but with some
|
||||
// extra SSR for outfit sharing meta tags!
|
||||
|
||||
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";
|
||||
// @ts-ignore: doesn't understand module.exports
|
||||
import { normalizeRow } from "../../src/server/util";
|
||||
import { GetServerSideProps } from "next";
|
||||
import { gql, loadGraphqlQuery } from "../../src/server/ssr-graphql";
|
||||
|
||||
type Outfit = {
|
||||
id: string;
|
||||
name: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type PageProps = {
|
||||
outfit: Outfit;
|
||||
};
|
||||
|
||||
const WardrobePageWrapper: NextPageWithLayout<PageProps> = ({ outfit }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{outfit.name || "Untitled outfit"} | Dress to Impress</title>
|
||||
<OutfitMetaTags outfit={outfit} />
|
||||
</Head>
|
||||
<WardrobePage />
|
||||
</>
|
||||
);
|
||||
const WardrobePageWrapper: NextPageWithLayout = () => {
|
||||
return <WardrobePage />;
|
||||
};
|
||||
|
||||
WardrobePageWrapper.renderWithLayout = (children) => children;
|
||||
|
||||
function OutfitMetaTags({ outfit }: { outfit: Outfit }) {
|
||||
const updatedAtTimestamp = Math.floor(
|
||||
new Date(outfit.updatedAt).getTime() / 1000
|
||||
);
|
||||
const outfitUrl =
|
||||
`https://impress-2020.openneo.net/outfits` +
|
||||
`/${encodeURIComponent(outfit.id)}`;
|
||||
const imageUrl =
|
||||
`https://impress-outfit-images.openneo.net/outfits` +
|
||||
`/${encodeURIComponent(outfit.id)}` +
|
||||
`/v/${encodeURIComponent(updatedAtTimestamp)}` +
|
||||
`/600.png`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<meta property="og:title" content={outfit.name || "Untitled outfit"} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={imageUrl} />
|
||||
<meta property="og:url" content={outfitUrl} />
|
||||
<meta property="og:site_name" content="Dress to Impress" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A custom Neopets outfit, designed on Dress to Impress!"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
|
||||
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) {
|
||||
const { data, errors, graphqlState } = await loadGraphqlQuery({
|
||||
query: gql`
|
||||
query OutfitsOutfitId_GetServerSideProps($outfitId: ID!) {
|
||||
outfit(id: $outfitId) {
|
||||
id
|
||||
name
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { outfitId },
|
||||
});
|
||||
if (errors) {
|
||||
console.warn(
|
||||
`[SSR: /outfits/[outfitId]] Skipping GraphQL preloading, got errors:`
|
||||
);
|
||||
for (const error of errors) {
|
||||
console.warn(`[SSR: /outfits/[outfitId]]`, error);
|
||||
}
|
||||
return { props: { outfit: null, graphqlState: {} } };
|
||||
}
|
||||
if (data?.outfit == null) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
outfit: {
|
||||
id: outfit.id,
|
||||
name: outfit.name,
|
||||
updatedAt: outfit.updatedAt.toISOString(),
|
||||
},
|
||||
},
|
||||
};
|
||||
return { props: { graphqlState } };
|
||||
};
|
||||
|
||||
async function loadOutfitData(id: string) {
|
||||
const db = await connectToDb();
|
||||
const [rows] = await db.query(`SELECT * FROM outfits WHERE id = ?;`, [id]);
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizeRow(rows[0]);
|
||||
}
|
||||
|
||||
export default WardrobePageWrapper;
|
||||
|
|
|
@ -7,10 +7,10 @@ import SearchFooter from "./SearchFooter";
|
|||
import SupportOnly from "./support/SupportOnly";
|
||||
import useOutfitSaving from "./useOutfitSaving";
|
||||
import useOutfitState, { OutfitStateContext } from "./useOutfitState";
|
||||
import { usePageTitle } from "../util";
|
||||
import WardrobePageLayout from "./WardrobePageLayout";
|
||||
import WardrobePreviewAndControls from "./WardrobePreviewAndControls";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
|
||||
const WardrobeDevHacks = loadable(() => import("./WardrobeDevHacks"));
|
||||
|
||||
|
@ -35,8 +35,6 @@ function WardrobePage() {
|
|||
// <Prompt /> in this component to prevent navigating away before saving.
|
||||
const outfitSaving = useOutfitSaving(outfitState, dispatchToOutfit);
|
||||
|
||||
usePageTitle(outfitState.name || "Untitled outfit");
|
||||
|
||||
// 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(() => {
|
||||
|
@ -78,44 +76,84 @@ function WardrobePage() {
|
|||
// that need it, where it's more useful and more performant to access
|
||||
// via context.
|
||||
return (
|
||||
<OutfitStateContext.Provider value={outfitState}>
|
||||
<SupportOnly>
|
||||
<WardrobeDevHacks />
|
||||
</SupportOnly>
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{outfitState.name || "Untitled outfit"} | Dress to Impress
|
||||
</title>
|
||||
{outfitState.id && <SavedOutfitMetaTags outfitState={outfitState} />}
|
||||
</Head>
|
||||
<OutfitStateContext.Provider value={outfitState}>
|
||||
<SupportOnly>
|
||||
<WardrobeDevHacks />
|
||||
</SupportOnly>
|
||||
|
||||
{/*
|
||||
* 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
|
||||
*/}
|
||||
<Prompt
|
||||
when={shouldBlockNavigation}
|
||||
message="Are you sure you want to leave? Your changes might not be saved."
|
||||
/>
|
||||
{/*
|
||||
* 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
|
||||
*/}
|
||||
<Prompt
|
||||
when={shouldBlockNavigation}
|
||||
message="Are you sure you want to leave? Your changes might not be saved."
|
||||
/>
|
||||
|
||||
<WardrobePageLayout
|
||||
previewAndControls={
|
||||
<WardrobePreviewAndControls
|
||||
isLoading={loading}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
}
|
||||
itemsAndMaybeSearchPanel={
|
||||
<ItemsAndSearchPanels
|
||||
loading={loading}
|
||||
outfitState={outfitState}
|
||||
outfitSaving={outfitSaving}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
}
|
||||
searchFooter={<SearchFooter />}
|
||||
<WardrobePageLayout
|
||||
previewAndControls={
|
||||
<WardrobePreviewAndControls
|
||||
isLoading={loading}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
}
|
||||
itemsAndMaybeSearchPanel={
|
||||
<ItemsAndSearchPanels
|
||||
loading={loading}
|
||||
outfitState={outfitState}
|
||||
outfitSaving={outfitSaving}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
}
|
||||
searchFooter={<SearchFooter />}
|
||||
/>
|
||||
</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"}
|
||||
/>
|
||||
</OutfitStateContext.Provider>
|
||||
<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!"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ function useOutfitState() {
|
|||
outfit(id: $id) {
|
||||
id
|
||||
name
|
||||
updatedAt
|
||||
creator {
|
||||
id
|
||||
}
|
||||
|
@ -69,6 +70,7 @@ function useOutfitState() {
|
|||
);
|
||||
|
||||
const creator = outfitData?.outfit?.creator;
|
||||
const updatedAt = outfitData?.outfit?.updatedAt;
|
||||
|
||||
// We memoize this to make `outfitStateWithoutExtras` an even more reliable
|
||||
// stable object!
|
||||
|
@ -94,9 +96,14 @@ function useOutfitState() {
|
|||
// data isn't loaded yet, then this will be a customization state with
|
||||
// partial data, and that's okay.)
|
||||
let outfitState;
|
||||
if (urlOutfitState.id === localOutfitState.id) {
|
||||
if (
|
||||
urlOutfitState.id === localOutfitState.id &&
|
||||
localOutfitState.speciesId != null &&
|
||||
localOutfitState.colorId != null
|
||||
) {
|
||||
// Use the reducer state: they're both for the same saved outfit, or both
|
||||
// for an unsaved outfit (null === null).
|
||||
// for an unsaved outfit (null === null). But we don't use it when it's
|
||||
// *only* got the ID, and no other fields yet.
|
||||
console.debug("[useOutfitState] Choosing local outfit state");
|
||||
outfitState = localOutfitState;
|
||||
} else if (urlOutfitState.id && urlOutfitState.id === savedOutfitState.id) {
|
||||
|
@ -228,6 +235,7 @@ function useOutfitState() {
|
|||
const outfitStateWithExtras = {
|
||||
id,
|
||||
creator,
|
||||
updatedAt,
|
||||
zonesAndItems,
|
||||
incompatibleItems,
|
||||
name,
|
||||
|
|
Loading…
Reference in a new issue