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; -}