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!
This commit is contained in:
parent
35479bf80e
commit
067da33025
5 changed files with 47 additions and 651 deletions
|
@ -29,6 +29,17 @@ module.exports = {
|
||||||
destination: "/user/:userId/lists",
|
destination: "/user/:userId/lists",
|
||||||
permanent: true,
|
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,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 <ItemTradesOfferingPage />;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 } };
|
|
||||||
};
|
|
|
@ -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 <ItemTradesSeekingPage />;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 } };
|
|
||||||
};
|
|
|
@ -92,7 +92,7 @@ export function ItemPageContent({ itemId, isEmbedded = false }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
{ variables: { itemId }, returnPartialData: true }
|
{ variables: { itemId }, returnPartialData: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -302,7 +302,7 @@ const ItemPageOwnWantListsDropdownButton = React.forwardRef(
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function ItemPageOwnWantListsDropdownContent({ closetLists, item }) {
|
function ItemPageOwnWantListsDropdownContent({ closetLists, item }) {
|
||||||
|
@ -336,7 +336,7 @@ function ItemPageOwnWantsListsDropdownRow({ closetList, item }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
{ context: { sendAuth: true } }
|
{ context: { sendAuth: true } },
|
||||||
);
|
);
|
||||||
|
|
||||||
const [sendRemoveFromListMutation] = useMutation(
|
const [sendRemoveFromListMutation] = useMutation(
|
||||||
|
@ -352,7 +352,7 @@ function ItemPageOwnWantsListsDropdownRow({ closetList, item }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
{ context: { sendAuth: true } }
|
{ context: { sendAuth: true } },
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChange = React.useCallback(
|
const onChange = React.useCallback(
|
||||||
|
@ -397,7 +397,13 @@ function ItemPageOwnWantsListsDropdownRow({ closetList, item }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[closetList, item, sendAddToListMutation, sendRemoveFromListMutation, toast]
|
[
|
||||||
|
closetList,
|
||||||
|
item,
|
||||||
|
sendAddToListMutation,
|
||||||
|
sendRemoveFromListMutation,
|
||||||
|
toast,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -445,7 +451,7 @@ function ItemPageOwnButton({ itemId, isChecked }) {
|
||||||
context: { sendAuth: true },
|
context: { sendAuth: true },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [sendRemoveMutation] = useMutation(
|
const [sendRemoveMutation] = useMutation(
|
||||||
|
@ -476,7 +482,7 @@ function ItemPageOwnButton({ itemId, isChecked }) {
|
||||||
context: { sendAuth: true },
|
context: { sendAuth: true },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -571,7 +577,7 @@ function ItemPageWantButton({ itemId, isChecked }) {
|
||||||
context: { sendAuth: true },
|
context: { sendAuth: true },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [sendRemoveMutation] = useMutation(
|
const [sendRemoveMutation] = useMutation(
|
||||||
|
@ -602,7 +608,7 @@ function ItemPageWantButton({ itemId, isChecked }) {
|
||||||
context: { sendAuth: true },
|
context: { sendAuth: true },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -676,7 +682,7 @@ function ItemPageTradeLinks({ itemId, isEmbedded }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
{ variables: { itemId } }
|
{ variables: { itemId } },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -690,7 +696,7 @@ function ItemPageTradeLinks({ itemId, isEmbedded }) {
|
||||||
</Box>
|
</Box>
|
||||||
<SubtleSkeleton isLoaded={!loading}>
|
<SubtleSkeleton isLoaded={!loading}>
|
||||||
<ItemPageTradeLink
|
<ItemPageTradeLink
|
||||||
href={`/items/${itemId}/trades/offering`}
|
href={`https://impress.openneo.net/items/${itemId}/trades/offering`}
|
||||||
count={data?.item?.numUsersOfferingThis || 0}
|
count={data?.item?.numUsersOfferingThis || 0}
|
||||||
label="offering"
|
label="offering"
|
||||||
colorScheme="green"
|
colorScheme="green"
|
||||||
|
@ -699,7 +705,7 @@ function ItemPageTradeLinks({ itemId, isEmbedded }) {
|
||||||
</SubtleSkeleton>
|
</SubtleSkeleton>
|
||||||
<SubtleSkeleton isLoaded={!loading}>
|
<SubtleSkeleton isLoaded={!loading}>
|
||||||
<ItemPageTradeLink
|
<ItemPageTradeLink
|
||||||
href={`/items/${itemId}/trades/seeking`}
|
href={`https://impress.openneo.net/items/${itemId}/trades/seeking`}
|
||||||
count={data?.item?.numUsersSeekingThis || 0}
|
count={data?.item?.numUsersSeekingThis || 0}
|
||||||
label="seeking"
|
label="seeking"
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
|
@ -769,7 +775,7 @@ function IconCheckbox({ icon, isChecked, ...props }) {
|
||||||
function ItemPageOutfitPreview({ itemId }) {
|
function ItemPageOutfitPreview({ itemId }) {
|
||||||
const idealPose = React.useMemo(
|
const idealPose = React.useMemo(
|
||||||
() => (Math.random() > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC"),
|
() => (Math.random() > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC"),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
const [petState, setPetState] = React.useState({
|
const [petState, setPetState] = React.useState({
|
||||||
// We'll fill these in once the canonical appearance data arrives.
|
// We'll fill these in once the canonical appearance data arrives.
|
||||||
|
@ -787,11 +793,11 @@ function ItemPageOutfitPreview({ itemId }) {
|
||||||
});
|
});
|
||||||
const [preferredSpeciesId, setPreferredSpeciesId] = useLocalStorage(
|
const [preferredSpeciesId, setPreferredSpeciesId] = useLocalStorage(
|
||||||
"DTIItemPreviewPreferredSpeciesId",
|
"DTIItemPreviewPreferredSpeciesId",
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
const [preferredColorId, setPreferredColorId] = useLocalStorage(
|
const [preferredColorId, setPreferredColorId] = useLocalStorage(
|
||||||
"DTIItemPreviewPreferredColorId",
|
"DTIItemPreviewPreferredColorId",
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const setPetStateFromUserAction = React.useCallback(
|
const setPetStateFromUserAction = React.useCallback(
|
||||||
|
@ -826,7 +832,7 @@ function ItemPageOutfitPreview({ itemId }) {
|
||||||
|
|
||||||
return newPetState;
|
return newPetState;
|
||||||
}),
|
}),
|
||||||
[setPreferredColorId, setPreferredSpeciesId]
|
[setPreferredColorId, setPreferredSpeciesId],
|
||||||
);
|
);
|
||||||
|
|
||||||
// We don't need to reload this query when preferred species/color change, so
|
// 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
|
// query after this loads, because our Apollo cache can't detect the
|
||||||
// shared item appearance. (For standard colors though, our logic to
|
// shared item appearance. (For standard colors though, our logic to
|
||||||
// cover standard-color switches works for this preloading too.)
|
// cover standard-color switches works for this preloading too.)
|
||||||
const { loading: loadingGQL, error: errorGQL, data } = useQuery(
|
const {
|
||||||
|
loading: loadingGQL,
|
||||||
|
error: errorGQL,
|
||||||
|
data,
|
||||||
|
} = useQuery(
|
||||||
gql`
|
gql`
|
||||||
query ItemPageOutfitPreview(
|
query ItemPageOutfitPreview(
|
||||||
$itemId: ID!
|
$itemId: ID!
|
||||||
|
@ -920,7 +930,7 @@ function ItemPageOutfitPreview({ itemId }) {
|
||||||
appearanceId: canonicalPetAppearance?.id,
|
appearanceId: canonicalPetAppearance?.id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const compatibleBodies =
|
const compatibleBodies =
|
||||||
|
@ -936,7 +946,7 @@ function ItemPageOutfitPreview({ itemId }) {
|
||||||
compatibleBodies.length === 1 &&
|
compatibleBodies.length === 1 &&
|
||||||
!compatibleBodies[0].representsAllBodies &&
|
!compatibleBodies[0].representsAllBodies &&
|
||||||
(data?.item?.name || "").includes(
|
(data?.item?.name || "").includes(
|
||||||
data?.item?.canonicalAppearance?.body?.canonicalAppearance?.species?.name
|
data?.item?.canonicalAppearance?.body?.canonicalAppearance?.species?.name,
|
||||||
);
|
);
|
||||||
const couldProbablyModelMoreData = !isProbablySpeciesSpecific;
|
const couldProbablyModelMoreData = !isProbablySpeciesSpecific;
|
||||||
|
|
||||||
|
@ -982,7 +992,7 @@ function ItemPageOutfitPreview({ itemId }) {
|
||||||
appearanceId: null,
|
appearanceId: null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[valids, idealPose, setPetStateFromUserAction]
|
[valids, idealPose, setPetStateFromUserAction],
|
||||||
);
|
);
|
||||||
|
|
||||||
const borderColor = useColorModeValue("green.700", "green.400");
|
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!
|
// I don't think this is possible, but I'd like to know if it happens!
|
||||||
logAndCapture(
|
logAndCapture(
|
||||||
new Error(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1283,8 +1293,8 @@ export function ItemZonesInfo({
|
||||||
|
|
||||||
const sortedZonesAndTheirBodies = [...zoneLabelsAndTheirBodies].sort((a, b) =>
|
const sortedZonesAndTheirBodies = [...zoneLabelsAndTheirBodies].sort((a, b) =>
|
||||||
buildSortKeyForZoneLabelsAndTheirBodies(a).localeCompare(
|
buildSortKeyForZoneLabelsAndTheirBodies(a).localeCompare(
|
||||||
buildSortKeyForZoneLabelsAndTheirBodies(b)
|
buildSortKeyForZoneLabelsAndTheirBodies(b),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const restrictedZoneLabels = [
|
const restrictedZoneLabels = [
|
||||||
|
@ -1296,8 +1306,8 @@ export function ItemZonesInfo({
|
||||||
// preview available in the list has the zones listed here.
|
// preview available in the list has the zones listed here.
|
||||||
const bodyGroups = new Set(
|
const bodyGroups = new Set(
|
||||||
zoneLabelsAndTheirBodies.map(({ bodies }) =>
|
zoneLabelsAndTheirBodies.map(({ bodies }) =>
|
||||||
bodies.map((b) => b.id).join(",")
|
bodies.map((b) => b.id).join(","),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const showBodyInfo = bodyGroups.size > 1;
|
const showBodyInfo = bodyGroups.size > 1;
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
|
||||||
<ItemTradesPage
|
|
||||||
title="Trades: Offering"
|
|
||||||
userHeading="Owner"
|
|
||||||
compareColumnLabel="Trade for your…"
|
|
||||||
tradesQuery={gql`
|
|
||||||
query ItemTradesTableOffering($itemId: ID!) {
|
|
||||||
item(id: $itemId) {
|
|
||||||
id
|
|
||||||
trades: tradesOffering {
|
|
||||||
id
|
|
||||||
user {
|
|
||||||
id
|
|
||||||
username
|
|
||||||
lastTradeActivity
|
|
||||||
matchingItems: itemsTheyWantThatCurrentUserOwns {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closetList {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ItemTradesSeekingPage() {
|
|
||||||
return (
|
|
||||||
<ItemTradesPage
|
|
||||||
title="Trades: Seeking"
|
|
||||||
userHeading="Seeker"
|
|
||||||
compareColumnLabel="Trade for their…"
|
|
||||||
tradesQuery={gql`
|
|
||||||
query ItemTradesTableSeeking($itemId: ID!) {
|
|
||||||
item(id: $itemId) {
|
|
||||||
id
|
|
||||||
trades: tradesSeeking {
|
|
||||||
id
|
|
||||||
user {
|
|
||||||
id
|
|
||||||
username
|
|
||||||
lastTradeActivity
|
|
||||||
matchingItems: itemsTheyOwnThatCurrentUserWants {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closetList {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <Box color="red.400">{error.message}</Box>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
{data?.item?.name && (
|
|
||||||
<title>
|
|
||||||
{data?.item?.name} | {title} | Dress to Impress
|
|
||||||
</title>
|
|
||||||
)}
|
|
||||||
</Head>
|
|
||||||
<ItemPageLayout item={data?.item}>
|
|
||||||
<Heading2 marginTop="6" marginBottom="4">
|
|
||||||
{title}
|
|
||||||
</Heading2>
|
|
||||||
<ItemTradesTable
|
|
||||||
itemId={itemId}
|
|
||||||
userHeading={userHeading}
|
|
||||||
compareColumnLabel={compareColumnLabel}
|
|
||||||
tradesQuery={tradesQuery}
|
|
||||||
/>
|
|
||||||
</ItemPageLayout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <Box color="red.400">{error.message}</Box>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const minorColumnWidth = {
|
|
||||||
base: shouldShowCompareColumn ? "23%" : "30%",
|
|
||||||
md: "20ex",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClassNames>
|
|
||||||
{({ css }) => (
|
|
||||||
<Box>
|
|
||||||
<Box
|
|
||||||
as="table"
|
|
||||||
width="100%"
|
|
||||||
boxShadow="md"
|
|
||||||
className={css`
|
|
||||||
/* Chakra doesn't have props for these! */
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0;
|
|
||||||
table-layout: fixed;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Box as="thead" fontSize={{ base: "xs", sm: "sm" }}>
|
|
||||||
<Box as="tr">
|
|
||||||
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
|
||||||
{/* A small wording tweak to fit better on the xsmall screens! */}
|
|
||||||
<Box display={{ base: "none", sm: "block" }}>Last active</Box>
|
|
||||||
<Box display={{ base: "block", sm: "none" }}>Last edit</Box>
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
{shouldShowCompareColumn && (
|
|
||||||
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
|
||||||
<Box display={{ base: "none", sm: "block" }}>
|
|
||||||
{compareColumnLabel}
|
|
||||||
</Box>
|
|
||||||
<Box display={{ base: "block", sm: "none" }}>Matches</Box>
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
)}
|
|
||||||
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
|
||||||
{userHeading}
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
<ItemTradesTableCell as="th">List</ItemTradesTableCell>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box as="tbody">
|
|
||||||
{loading && (
|
|
||||||
<>
|
|
||||||
<ItemTradesTableRowSkeleton
|
|
||||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
||||||
/>
|
|
||||||
<ItemTradesTableRowSkeleton
|
|
||||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
||||||
/>
|
|
||||||
<ItemTradesTableRowSkeleton
|
|
||||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
||||||
/>
|
|
||||||
<ItemTradesTableRowSkeleton
|
|
||||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
||||||
/>
|
|
||||||
<ItemTradesTableRowSkeleton
|
|
||||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!loading &&
|
|
||||||
trades.length > 0 &&
|
|
||||||
trades.map((trade) => (
|
|
||||||
<ItemTradesTableRow
|
|
||||||
key={trade.id}
|
|
||||||
href={`/user/${trade.user.id}/lists#list-${trade.closetList.id}`}
|
|
||||||
username={trade.user.username}
|
|
||||||
listName={trade.closetList.name}
|
|
||||||
lastTradeActivity={trade.user.lastTradeActivity}
|
|
||||||
matchingItems={trade.user.matchingItems}
|
|
||||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{!loading && trades.length === 0 && (
|
|
||||||
<Box as="tr">
|
|
||||||
<ItemTradesTableCell
|
|
||||||
colSpan={shouldShowCompareColumn ? 4 : 3}
|
|
||||||
textAlign="center"
|
|
||||||
fontStyle="italic"
|
|
||||||
>
|
|
||||||
No trades yet!
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{numInactiveTrades > 0 && (
|
|
||||||
<Flex justify="center">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
marginTop="4"
|
|
||||||
onClick={() => setIsShowingInactiveTrades((s) => !s)}
|
|
||||||
>
|
|
||||||
{isShowingInactiveTrades ? (
|
|
||||||
<>
|
|
||||||
<ChevronUpIcon marginRight="2" />
|
|
||||||
Hide {numInactiveTrades} older trades
|
|
||||||
<ChevronUpIcon marginLeft="2" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ChevronDownIcon marginRight="2" />
|
|
||||||
Show {numInactiveTrades} more older trades
|
|
||||||
<ChevronDownIcon marginLeft="2" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</ClassNames>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<ClassNames>
|
|
||||||
{({ css }) => (
|
|
||||||
<Box
|
|
||||||
as="tr"
|
|
||||||
cursor="pointer"
|
|
||||||
_hover={{ background: focusBackground }}
|
|
||||||
_focusWithin={{ background: focusBackground }}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<ItemTradesTableCell fontSize="xs">
|
|
||||||
{formatVagueDate(lastTradeActivity)}
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
{shouldShowCompareColumn && (
|
|
||||||
<ItemTradesTableCell fontSize="xs">
|
|
||||||
{matchingItems.length > 0 ? (
|
|
||||||
<Box as="ul">
|
|
||||||
{sortedMatchingItems.slice(0, 4).map((item) => (
|
|
||||||
<Box key={item.id} as="li">
|
|
||||||
<Box
|
|
||||||
lineHeight="1.5"
|
|
||||||
maxHeight="1.5em"
|
|
||||||
overflow="hidden"
|
|
||||||
textOverflow="ellipsis"
|
|
||||||
whiteSpace="nowrap"
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
{matchingItems.length > 4 && (
|
|
||||||
<Box as="li">+ {matchingItems.length - 4} more</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Box display={{ base: "none", sm: "block" }}>No matches</Box>
|
|
||||||
<Box display={{ base: "block", sm: "none" }}>None</Box>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
)}
|
|
||||||
<ItemTradesTableCell fontSize="xs">{username}</ItemTradesTableCell>
|
|
||||||
<ItemTradesTableCell fontSize="sm">
|
|
||||||
<Link href={href} passHref>
|
|
||||||
<Box
|
|
||||||
as="a"
|
|
||||||
className={css`
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
tr:hover &,
|
|
||||||
tr:focus-within & {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{listName}
|
|
||||||
</Box>
|
|
||||||
</Link>
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</ClassNames>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemTradesTableRowSkeleton({ shouldShowCompareColumn }) {
|
|
||||||
return (
|
|
||||||
<Box as="tr">
|
|
||||||
<ItemTradesTableCell>
|
|
||||||
<Skeleton width="100%">X</Skeleton>
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
<ItemTradesTableCell>
|
|
||||||
<Skeleton width="100%">X</Skeleton>
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
<ItemTradesTableCell>
|
|
||||||
<Skeleton width="100%">X</Skeleton>
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
{shouldShowCompareColumn && (
|
|
||||||
<ItemTradesTableCell>
|
|
||||||
<Skeleton width="100%">X</Skeleton>
|
|
||||||
</ItemTradesTableCell>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemTradesTableCell({ children, as = "td", ...props }) {
|
|
||||||
const borderColor = useColorModeValue("gray.300", "gray.400");
|
|
||||||
const borderColorCss = useToken("colors", borderColor);
|
|
||||||
const borderRadiusCss = useToken("radii", "md");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClassNames>
|
|
||||||
{({ css }) => (
|
|
||||||
<Box
|
|
||||||
as={as}
|
|
||||||
paddingX="4"
|
|
||||||
paddingY="2"
|
|
||||||
textAlign="left"
|
|
||||||
className={css`
|
|
||||||
/* Lol sigh, getting this right is way more involved than I wish it
|
|
||||||
* were. What I really want is border-collapse and a simple 1px border,
|
|
||||||
* but that disables border-radius. So, we homebrew it by giving all
|
|
||||||
* cells bottom and right borders, but only the cells on the edges a
|
|
||||||
* top or left border; and then target the exact 4 corner cells to
|
|
||||||
* round them. Pretty old-school tbh 🙃 */
|
|
||||||
|
|
||||||
border-bottom: 1px solid ${borderColorCss};
|
|
||||||
border-right: 1px solid ${borderColorCss};
|
|
||||||
|
|
||||||
thead tr:first-of-type & {
|
|
||||||
border-top: 1px solid ${borderColorCss};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
border-left: 1px solid ${borderColorCss};
|
|
||||||
}
|
|
||||||
|
|
||||||
thead tr:first-of-type &:first-of-type {
|
|
||||||
border-top-left-radius: ${borderRadiusCss};
|
|
||||||
}
|
|
||||||
thead tr:first-of-type &:last-of-type {
|
|
||||||
border-top-right-radius: ${borderRadiusCss};
|
|
||||||
}
|
|
||||||
tbody tr:last-of-type &:first-of-type {
|
|
||||||
border-bottom-left-radius: ${borderRadiusCss};
|
|
||||||
}
|
|
||||||
tbody tr:last-of-type &:last-of-type {
|
|
||||||
border-bottom-right-radius: ${borderRadiusCss};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</ClassNames>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
Loading…
Reference in a new issue