From 16c9e1a25da520104970637571933817c78740c1 Mon Sep 17 00:00:00 2001 From: Matchu Date: Thu, 15 Sep 2022 04:03:51 -0700 Subject: [PATCH] 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! --- pages/outfits/[outfitId].tsx | 102 ++++++---------------- src/app/WardrobePage/index.js | 114 ++++++++++++++++--------- src/app/WardrobePage/useOutfitState.js | 12 ++- 3 files changed, 112 insertions(+), 116 deletions(-) diff --git a/pages/outfits/[outfitId].tsx b/pages/outfits/[outfitId].tsx index d679409..b81c823 100644 --- a/pages/outfits/[outfitId].tsx +++ b/pages/outfits/[outfitId].tsx @@ -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 = ({ outfit }) => { - return ( - <> - - {outfit.name || "Untitled outfit"} | Dress to Impress - - - - - ); +const WardrobePageWrapper: NextPageWithLayout = () => { + return ; }; 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 ( - <> - - - - - - - - ); -} - 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; diff --git a/src/app/WardrobePage/index.js b/src/app/WardrobePage/index.js index 746a1ea..ffc8df2 100644 --- a/src/app/WardrobePage/index.js +++ b/src/app/WardrobePage/index.js @@ -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() { // 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 ( - - - - + <> + + + {outfitState.name || "Untitled outfit"} | Dress to Impress + + {outfitState.id && } + + + + + - {/* - * 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 - */} - + {/* + * 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={} + + } + 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 ( + <> + - + + + + + + ); } diff --git a/src/app/WardrobePage/useOutfitState.js b/src/app/WardrobePage/useOutfitState.js index f08b957..c137763 100644 --- a/src/app/WardrobePage/useOutfitState.js +++ b/src/app/WardrobePage/useOutfitState.js @@ -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,