From 067da33025cbf5ed92707efa1558a658135e310e Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Mon, 30 Sep 2024 11:48:44 -0700 Subject: [PATCH] Replace trades pages with redirects to Classic DTI The immediate motivation here is that there's slowness rn, and I'm digging through the slow query log for suspicious targets (hard to tell when even normal queries are getting slowed down by whatever the culprit is!), and Impress 2020's constantly-recalcuated trade activity dates were one of them. So, rather than update this page to use the new table columns Classic now uses, I figured, let's Delete Things and reduce surface area. The re-merge continues! If we're lucky, this change alone will stop the bursts of slowness. If not, we'll keep looking for suspicious targets! --- next.config.js | 11 + pages/items/[itemId]/trades/offering.tsx | 54 --- pages/items/[itemId]/trades/seeking.tsx | 54 --- src/app/ItemPage.js | 62 +-- src/app/ItemTradesPage.js | 517 ----------------------- 5 files changed, 47 insertions(+), 651 deletions(-) delete mode 100644 pages/items/[itemId]/trades/offering.tsx delete mode 100644 pages/items/[itemId]/trades/seeking.tsx delete mode 100644 src/app/ItemTradesPage.js diff --git a/next.config.js b/next.config.js index f7c64e8..1147494 100644 --- a/next.config.js +++ b/next.config.js @@ -29,6 +29,17 @@ module.exports = { destination: "/user/:userId/lists", permanent: true, }, + { + source: "/items/:itemId/trades/offering", + destination: + "https://impress.openneo.net/items/:itemId/trades/offering", + permanent: true, + }, + { + source: "/items/:itemId/trades/seeking", + destination: "https://impress.openneo.net/items/:itemId/trades/seeking", + permanent: true, + }, ]; }, }; diff --git a/pages/items/[itemId]/trades/offering.tsx b/pages/items/[itemId]/trades/offering.tsx deleted file mode 100644 index 3e6a4a6..0000000 --- a/pages/items/[itemId]/trades/offering.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { GetServerSideProps } from "next"; -import { ItemTradesOfferingPage } from "../../../../src/app/ItemTradesPage"; -import { gql, loadGraphqlQuery } from "../../../../src/server/ssr-graphql"; -// @ts-ignore doesn't understand module.exports -import { oneDay, oneWeek } from "../../../../src/server/util"; - -export default function ItemTradesOfferingPageWrapper() { - return ; -} - -export const getServerSideProps: GetServerSideProps = async ({ - params, - res, -}) => { - if (params?.itemId == null) { - throw new Error(`assertion error: itemId param is missing`); - } - - // Load the most important, most stable item data to get onto the page ASAP. - // We'll cache it real hard, to help it load extra-fast for popular items! - const { errors, graphqlState } = await loadGraphqlQuery({ - query: gql` - query ItemsTradesOffering_GetServerSideProps($itemId: ID!) { - item(id: $itemId) { - id - name - thumbnailUrl - description - isNc - isPb - createdAt - } - } - `, - variables: { itemId: params.itemId }, - }); - if (errors) { - console.warn( - `[SSR: /items/[itemId]/trades/offering] Skipping GraphQL preloading, got errors:` - ); - for (const error of errors) { - console.warn(`[SSR: /items/[itemId]/trades/offering]`, error); - } - return { props: { graphqlState: {} } }; - } - - // Cache this very aggressively, because it's such stable data! - res.setHeader( - "Cache-Control", - `public, s-maxage=${oneDay}, stale-while-revalidate=${oneWeek}` - ); - - return { props: { graphqlState } }; -}; diff --git a/pages/items/[itemId]/trades/seeking.tsx b/pages/items/[itemId]/trades/seeking.tsx deleted file mode 100644 index f8fa05b..0000000 --- a/pages/items/[itemId]/trades/seeking.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { GetServerSideProps } from "next"; -import { ItemTradesSeekingPage } from "../../../../src/app/ItemTradesPage"; -import { gql, loadGraphqlQuery } from "../../../../src/server/ssr-graphql"; -// @ts-ignore doesn't understand module.exports -import { oneDay, oneWeek } from "../../../../src/server/util"; - -export default function ItemTradesSeekingPageWrapper() { - return ; -} - -export const getServerSideProps: GetServerSideProps = async ({ - params, - res, -}) => { - if (params?.itemId == null) { - throw new Error(`assertion error: itemId param is missing`); - } - - // Load the most important, most stable item data to get onto the page ASAP. - // We'll cache it real hard, to help it load extra-fast for popular items! - const { errors, graphqlState } = await loadGraphqlQuery({ - query: gql` - query ItemsTradesSeeking_GetServerSideProps($itemId: ID!) { - item(id: $itemId) { - id - name - thumbnailUrl - description - isNc - isPb - createdAt - } - } - `, - variables: { itemId: params.itemId }, - }); - if (errors) { - console.warn( - `[SSR: /items/[itemId]/trades/seeking] Skipping GraphQL preloading, got errors:` - ); - for (const error of errors) { - console.warn(`[SSR: /items/[itemId]/trades/seeking]`, error); - } - return { props: { graphqlState: {} } }; - } - - // Cache this very aggressively, because it's such stable data! - res.setHeader( - "Cache-Control", - `public, s-maxage=${oneDay}, stale-while-revalidate=${oneWeek}` - ); - - return { props: { graphqlState } }; -}; diff --git a/src/app/ItemPage.js b/src/app/ItemPage.js index bbed293..f4da1a5 100644 --- a/src/app/ItemPage.js +++ b/src/app/ItemPage.js @@ -92,7 +92,7 @@ export function ItemPageContent({ itemId, isEmbedded = false }) { } } `, - { variables: { itemId }, returnPartialData: true } + { variables: { itemId }, returnPartialData: true }, ); if (error) { @@ -302,7 +302,7 @@ const ItemPageOwnWantListsDropdownButton = React.forwardRef( ); - } + }, ); function ItemPageOwnWantListsDropdownContent({ closetLists, item }) { @@ -336,7 +336,7 @@ function ItemPageOwnWantsListsDropdownRow({ closetList, item }) { } } `, - { context: { sendAuth: true } } + { context: { sendAuth: true } }, ); const [sendRemoveFromListMutation] = useMutation( @@ -352,7 +352,7 @@ function ItemPageOwnWantsListsDropdownRow({ closetList, item }) { } } `, - { context: { sendAuth: true } } + { context: { sendAuth: true } }, ); const onChange = React.useCallback( @@ -397,7 +397,13 @@ function ItemPageOwnWantsListsDropdownRow({ closetList, item }) { }); } }, - [closetList, item, sendAddToListMutation, sendRemoveFromListMutation, toast] + [ + closetList, + item, + sendAddToListMutation, + sendRemoveFromListMutation, + toast, + ], ); return ( @@ -445,7 +451,7 @@ function ItemPageOwnButton({ itemId, isChecked }) { context: { sendAuth: true }, }, ], - } + }, ); const [sendRemoveMutation] = useMutation( @@ -476,7 +482,7 @@ function ItemPageOwnButton({ itemId, isChecked }) { context: { sendAuth: true }, }, ], - } + }, ); return ( @@ -571,7 +577,7 @@ function ItemPageWantButton({ itemId, isChecked }) { context: { sendAuth: true }, }, ], - } + }, ); const [sendRemoveMutation] = useMutation( @@ -602,7 +608,7 @@ function ItemPageWantButton({ itemId, isChecked }) { context: { sendAuth: true }, }, ], - } + }, ); return ( @@ -676,7 +682,7 @@ function ItemPageTradeLinks({ itemId, isEmbedded }) { } } `, - { variables: { itemId } } + { variables: { itemId } }, ); if (error) { @@ -690,7 +696,7 @@ function ItemPageTradeLinks({ itemId, isEmbedded }) { (Math.random() > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC"), - [] + [], ); const [petState, setPetState] = React.useState({ // We'll fill these in once the canonical appearance data arrives. @@ -787,11 +793,11 @@ function ItemPageOutfitPreview({ itemId }) { }); const [preferredSpeciesId, setPreferredSpeciesId] = useLocalStorage( "DTIItemPreviewPreferredSpeciesId", - null + null, ); const [preferredColorId, setPreferredColorId] = useLocalStorage( "DTIItemPreviewPreferredColorId", - null + null, ); const setPetStateFromUserAction = React.useCallback( @@ -826,7 +832,7 @@ function ItemPageOutfitPreview({ itemId }) { return newPetState; }), - [setPreferredColorId, setPreferredSpeciesId] + [setPreferredColorId, setPreferredSpeciesId], ); // We don't need to reload this query when preferred species/color change, so @@ -845,7 +851,11 @@ function ItemPageOutfitPreview({ itemId }) { // query after this loads, because our Apollo cache can't detect the // shared item appearance. (For standard colors though, our logic to // cover standard-color switches works for this preloading too.) - const { loading: loadingGQL, error: errorGQL, data } = useQuery( + const { + loading: loadingGQL, + error: errorGQL, + data, + } = useQuery( gql` query ItemPageOutfitPreview( $itemId: ID! @@ -920,7 +930,7 @@ function ItemPageOutfitPreview({ itemId }) { appearanceId: canonicalPetAppearance?.id, }); }, - } + }, ); const compatibleBodies = @@ -936,7 +946,7 @@ function ItemPageOutfitPreview({ itemId }) { compatibleBodies.length === 1 && !compatibleBodies[0].representsAllBodies && (data?.item?.name || "").includes( - data?.item?.canonicalAppearance?.body?.canonicalAppearance?.species?.name + data?.item?.canonicalAppearance?.body?.canonicalAppearance?.species?.name, ); const couldProbablyModelMoreData = !isProbablySpeciesSpecific; @@ -982,7 +992,7 @@ function ItemPageOutfitPreview({ itemId }) { appearanceId: null, }); }, - [valids, idealPose, setPetStateFromUserAction] + [valids, idealPose, setPetStateFromUserAction], ); const borderColor = useColorModeValue("green.700", "green.400"); @@ -1201,8 +1211,8 @@ function ExpandOnGroupHover({ children, ...props }) { // I don't think this is possible, but I'd like to know if it happens! logAndCapture( new Error( - `Measurer node not ready during effect. Transition won't be smooth.` - ) + `Measurer node not ready during effect. Transition won't be smooth.`, + ), ); return; } @@ -1283,8 +1293,8 @@ export function ItemZonesInfo({ const sortedZonesAndTheirBodies = [...zoneLabelsAndTheirBodies].sort((a, b) => buildSortKeyForZoneLabelsAndTheirBodies(a).localeCompare( - buildSortKeyForZoneLabelsAndTheirBodies(b) - ) + buildSortKeyForZoneLabelsAndTheirBodies(b), + ), ); const restrictedZoneLabels = [ @@ -1296,8 +1306,8 @@ export function ItemZonesInfo({ // preview available in the list has the zones listed here. const bodyGroups = new Set( zoneLabelsAndTheirBodies.map(({ bodies }) => - bodies.map((b) => b.id).join(",") - ) + bodies.map((b) => b.id).join(","), + ), ); const showBodyInfo = bodyGroups.size > 1; diff --git a/src/app/ItemTradesPage.js b/src/app/ItemTradesPage.js deleted file mode 100644 index 25c15f1..0000000 --- a/src/app/ItemTradesPage.js +++ /dev/null @@ -1,517 +0,0 @@ -import React from "react"; -import { ClassNames } from "@emotion/react"; -import { - Box, - Button, - Flex, - Skeleton, - useColorModeValue, - useToken, -} from "@chakra-ui/react"; -import gql from "graphql-tag"; -import { useQuery } from "@apollo/client"; -import Link from "next/link"; -import { useRouter } from "next/router"; - -import { Heading2 } from "./util"; -import ItemPageLayout from "./ItemPageLayout"; -import useCurrentUser from "./components/useCurrentUser"; -import { ChevronDownIcon, ChevronUpIcon } from "@chakra-ui/icons"; -import Head from "next/head"; - -export function ItemTradesOfferingPage() { - return ( - - ); -} - -export function ItemTradesSeekingPage() { - return ( - - ); -} - -function ItemTradesPage({ - title, - userHeading, - compareColumnLabel, - tradesQuery, -}) { - const { query } = useRouter(); - const { itemId } = query; - - const { error, data } = useQuery( - gql` - query ItemTradesPage($itemId: ID!) { - item(id: $itemId) { - id - name - isNc - isPb - thumbnailUrl - description - createdAt - ncTradeValueText - } - } - `, - { variables: { itemId }, returnPartialData: true } - ); - - if (error) { - return {error.message}; - } - - return ( - <> - - {data?.item?.name && ( - - {data?.item?.name} | {title} | Dress to Impress - - )} - - - - {title} - - - - - ); -} - -function ItemTradesTable({ - itemId, - userHeading, - compareColumnLabel, - tradesQuery, -}) { - const { isLoggedIn } = useCurrentUser(); - const { loading, error, data } = useQuery(tradesQuery, { - variables: { itemId }, - context: { sendAuth: true }, - }); - - const [isShowingInactiveTrades, setIsShowingInactiveTrades] = React.useState( - false - ); - - const shouldShowCompareColumn = isLoggedIn; - - // We partially randomize trade sorting, but we want it to stay stable across - // re-renders. To do this, we can use `getTradeSortKey`, which will either - // build a new sort key for the trade, or return the cached one from the - // `tradeSortKeys` map. - const tradeSortKeys = React.useMemo(() => new Map(), []); - const getTradeSortKey = (trade) => { - if (!tradeSortKeys.has(trade.id)) { - tradeSortKeys.set( - trade.id, - getVaguelyRandomizedTradeSortKey( - trade.user.lastTradeActivity, - trade.user.matchingItems.length - ) - ); - } - return tradeSortKeys.get(trade.id); - }; - - const allTrades = [...(data?.item?.trades || [])]; - - // Only trades from users active within the last 6 months are shown by - // default. The user can toggle to the full view, though! - const sixMonthsAgo = new Date(); - sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); - const activeTrades = allTrades.filter( - (t) => new Date(t.user.lastTradeActivity) > sixMonthsAgo - ); - - const trades = isShowingInactiveTrades ? allTrades : activeTrades; - trades.sort((a, b) => getTradeSortKey(b).localeCompare(getTradeSortKey(a))); - - const numInactiveTrades = allTrades.length - activeTrades.length; - - if (error) { - return {error.message}; - } - - const minorColumnWidth = { - base: shouldShowCompareColumn ? "23%" : "30%", - md: "20ex", - }; - - return ( - - {({ css }) => ( - - - - - - {/* A small wording tweak to fit better on the xsmall screens! */} - Last active - Last edit - - {shouldShowCompareColumn && ( - - - {compareColumnLabel} - - Matches - - )} - - {userHeading} - - List - - - - {loading && ( - <> - - - - - - - )} - {!loading && - trades.length > 0 && - trades.map((trade) => ( - - ))} - {!loading && trades.length === 0 && ( - - - No trades yet! - - - )} - - - {numInactiveTrades > 0 && ( - - - - )} - - )} - - ); -} - -function ItemTradesTableRow({ - href, - username, - listName, - lastTradeActivity, - matchingItems, - shouldShowCompareColumn, -}) { - const { push: pushHistory } = useRouter(); - const onClick = React.useCallback(() => pushHistory(href), [ - pushHistory, - href, - ]); - const focusBackground = useColorModeValue("gray.100", "gray.600"); - - const sortedMatchingItems = [...matchingItems].sort((a, b) => - a.name.localeCompare(b.name) - ); - - return ( - - {({ css }) => ( - - - {formatVagueDate(lastTradeActivity)} - - {shouldShowCompareColumn && ( - - {matchingItems.length > 0 ? ( - - {sortedMatchingItems.slice(0, 4).map((item) => ( - - - {item.name} - - - ))} - {matchingItems.length > 4 && ( - + {matchingItems.length - 4} more - )} - - ) : ( - <> - No matches - None - - )} - - )} - {username} - - - - {listName} - - - - - )} - - ); -} - -function ItemTradesTableRowSkeleton({ shouldShowCompareColumn }) { - return ( - - - X - - - X - - - X - - {shouldShowCompareColumn && ( - - X - - )} - - ); -} - -function ItemTradesTableCell({ children, as = "td", ...props }) { - const borderColor = useColorModeValue("gray.300", "gray.400"); - const borderColorCss = useToken("colors", borderColor); - const borderRadiusCss = useToken("radii", "md"); - - return ( - - {({ css }) => ( - - {children} - - )} - - ); -} - -function isThisWeek(date) { - const startOfThisWeek = new Date(); - startOfThisWeek.setDate(startOfThisWeek.getDate() - 7); - return date > startOfThisWeek; -} - -const shortMonthYearFormatter = new Intl.DateTimeFormat("en", { - month: "short", - year: "numeric", -}); - -function formatVagueDate(dateString) { - const date = new Date(dateString); - - if (isThisWeek(date)) { - return "This week"; - } - - return shortMonthYearFormatter.format(date); -} - -function getVaguelyRandomizedTradeSortKey(dateString, numMatchingItems) { - const date = new Date(dateString); - const hasMatchingItems = numMatchingItems >= 1; - - // "This week" sorts after all other dates, but with a random factor! I don't - // want people worrying about gaming themselves up to the very top, just be - // active and trust the system 😅 (I figure that, if you care enough to "game" - // the system by faking activity every week, you probably also care enough to - // be... making real trades every week lmao) - // - // We also prioritize having matches, but we don't bother to sort _how many_ - // matches, to decrease the power of gaming with large honeypot lists, and - // because it's hard to judge how good matches are anyway. - if (isThisWeek(date)) { - const matchingItemsKey = hasMatchingItems - ? "ZZmatchingZZ" - : "AAnotmatchingAA"; - return `ZZZthisweekZZZ-${matchingItemsKey}-${Math.random()}`; - } - - return dateString; -}