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