impress-2020/src/app/ItemTradesPage.js

432 lines
12 KiB
JavaScript
Raw Normal View History

import React from "react";
import { css } from "emotion";
2020-11-24 14:24:34 -08:00
import {
Box,
Skeleton,
Tooltip,
useColorModeValue,
useToken,
} from "@chakra-ui/core";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
import { Link, useHistory, useParams } from "react-router-dom";
import { Heading2, usePageTitle } from "./util";
import ItemPageLayout from "./ItemPageLayout";
export function ItemTradesOfferingPage() {
return (
<ItemTradesPage
title="Trades: Offering"
userHeading="Owner"
compareListHeading="They're seeking"
2020-11-24 14:24:34 -08:00
tradesQuery={gql`
query ItemTradesTableOffering($itemId: ID!) {
item(id: $itemId) {
id
trades: tradesOffering {
id
user {
id
username
2020-11-24 14:43:43 -08:00
lastTradeActivity
2020-11-24 14:24:34 -08:00
}
closetList {
id
name
}
}
}
}
`}
/>
);
}
export function ItemTradesSeekingPage() {
return (
<ItemTradesPage
title="Trades: Seeking"
userHeading="Seeker"
compareListHeading="They're offering"
2020-11-24 14:24:34 -08:00
tradesQuery={gql`
query ItemTradesTableSeeking($itemId: ID!) {
item(id: $itemId) {
id
trades: tradesSeeking {
id
user {
id
username
2020-11-24 14:43:43 -08:00
lastTradeActivity
2020-11-24 14:24:34 -08:00
}
closetList {
id
name
}
}
}
}
`}
/>
);
}
2020-11-24 14:24:34 -08:00
function ItemTradesPage({
title,
userHeading,
compareListHeading,
tradesQuery,
}) {
const { itemId } = useParams();
2020-11-24 14:24:34 -08:00
const { error, data } = useQuery(
gql`
query ItemTradesPage($itemId: ID!) {
item(id: $itemId) {
id
name
isNc
isPb
thumbnailUrl
description
createdAt
}
}
`,
{ variables: { itemId }, returnPartialData: true }
);
2020-11-24 14:24:34 -08:00
usePageTitle(`${data?.item?.name} | ${title}`, { skip: !data?.item?.name });
if (error) {
return <Box color="red.400">{error.message}</Box>;
}
return (
<ItemPageLayout item={data?.item}>
<Heading2 marginTop="6" marginBottom="4">
{title}
</Heading2>
<ItemTradesTable
itemId={itemId}
userHeading={userHeading}
compareListHeading={compareListHeading}
2020-11-24 14:24:34 -08:00
tradesQuery={tradesQuery}
/>
</ItemPageLayout>
);
}
2020-11-24 14:24:34 -08:00
function ItemTradesTable({
itemId,
userHeading,
compareListHeading,
tradesQuery,
}) {
const { loading, error, data } = useQuery(tradesQuery, {
variables: { itemId },
});
2020-11-24 14:49:20 -08:00
// HACK: I'm pretty much hiding this for now, because it's not ready. But
/// it's visible at #show-compare-column!
const shouldShowCompareColumn = window.location.href.includes(
"show-compare-column"
);
const minorColumnWidth = {
base: shouldShowCompareColumn ? "23%" : "30%",
md: "18ex",
};
// 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,
getVaguelyRandomizedSortKeyForDate(trade.user.lastTradeActivity)
);
}
return tradeSortKeys.get(trade.id);
};
const trades = [...(data?.item?.trades || [])];
trades.sort((a, b) => getTradeSortKey(b).localeCompare(getTradeSortKey(a)));
if (error) {
return <Box color="red.400">{error.message}</Box>;
}
return (
<Box
as="table"
width="100%"
boxShadow="md"
className={css`
/* Chakra doesn't have props for these! */
border-collapse: separate;
border-spacing: 0;
2020-11-24 14:24:34 -08:00
table-layout: fixed;
`}
>
2020-11-24 14:24:34 -08:00
<Box as="thead" fontSize={{ base: "xs", sm: "sm" }}>
<Box as="tr">
2020-11-24 14:49:20 -08:00
<ItemTradesTableCell as="th" width={minorColumnWidth}>
{/* A small wording tweak to fit better on the xsmall screens! */}
2020-11-24 14:24:34 -08:00
<Box display={{ base: "none", sm: "block" }}>Last active</Box>
2020-11-24 23:59:44 -08:00
<Box display={{ base: "block", sm: "none" }}>Last edit</Box>
</ItemTradesTableCell>
<ItemTradesTableCell as="th" width={minorColumnWidth}>
{userHeading}
</ItemTradesTableCell>
2020-11-24 14:49:20 -08:00
{shouldShowCompareColumn && (
<ItemTradesTableCell as="th" width={minorColumnWidth}>
Compare
</ItemTradesTableCell>
)}
<ItemTradesTableCell as="th">List</ItemTradesTableCell>
</Box>
</Box>
<Box as="tbody">
2020-11-24 14:24:34 -08:00
{loading && (
<>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
2020-11-24 14:24:34 -08:00
</>
)}
{!loading &&
trades.length > 0 &&
trades.map((trade) => (
2020-11-24 14:24:34 -08:00
<ItemTradesTableRow
key={trade.id}
compareListHeading={compareListHeading}
href={`/user/${trade.user.id}/items#list-${trade.closetList.id}`}
username={trade.user.username}
listName={trade.closetList.name}
2020-11-24 14:43:43 -08:00
lastTradeActivity={trade.user.lastTradeActivity}
2020-11-24 14:49:20 -08:00
shouldShowCompareColumn={shouldShowCompareColumn}
2020-11-24 14:24:34 -08:00
/>
))}
{!loading && trades.length === 0 && (
2020-11-24 14:24:34 -08:00
<Box as="tr">
<ItemTradesTableCell
colSpan={shouldShowCompareColumn ? 4 : 3}
2020-11-24 14:24:34 -08:00
textAlign="center"
fontStyle="italic"
>
No trades yet!
</ItemTradesTableCell>
</Box>
)}
</Box>
</Box>
);
}
2020-11-24 14:43:43 -08:00
function ItemTradesTableRow({
compareListHeading,
href,
username,
listName,
lastTradeActivity,
2020-11-24 14:49:20 -08:00
shouldShowCompareColumn,
2020-11-24 14:43:43 -08:00
}) {
const history = useHistory();
const onClick = React.useCallback(() => history.push(href), [history, href]);
const focusBackground = useColorModeValue("gray.100", "gray.600");
return (
<Box
as="tr"
2020-11-24 14:24:34 -08:00
cursor="pointer"
_hover={{ background: focusBackground }}
_focusWithin={{ background: focusBackground }}
onClick={onClick}
>
2020-11-24 14:24:34 -08:00
<ItemTradesTableCell fontSize="xs">
{formatVagueDate(lastTradeActivity)}
</ItemTradesTableCell>
<ItemTradesTableCell overflowWrap="break-word" fontSize="xs">
{username}
</ItemTradesTableCell>
2020-11-24 14:49:20 -08:00
{shouldShowCompareColumn && (
<ItemTradesTableCell fontSize="xs">
<Tooltip
placement="bottom"
label={
<Box>
{compareListHeading}:
<Box as="ul" listStyle="disc">
<Box as="li" marginLeft="1em">
Adorable Freckles
</Box>
<Box as="li" marginLeft="1em">
Constellation Dress
</Box>
</Box>
2020-11-24 14:49:20 -08:00
<Box>(WIP: This is placeholder data!)</Box>
</Box>
2020-11-24 14:49:20 -08:00
}
>
2020-11-24 14:49:20 -08:00
<Box
tabIndex="0"
width="100%"
className={css`
&:hover,
&:focus,
tr:hover &,
tr:focus-within & {
text-decoration: underline dashed;
}
`}
>
<Box display={{ base: "block", md: "none" }}>2 match</Box>
<Box display={{ base: "none", md: "block" }}>2 matches</Box>
</Box>
</Tooltip>
</ItemTradesTableCell>
)}
<ItemTradesTableCell overflowWrap="break-word" fontSize="sm">
<Box
as={Link}
to={href}
className={css`
&:hover,
&:focus,
tr:hover &,
tr:focus-within & {
text-decoration: underline;
}
`}
>
{listName}
</Box>
</ItemTradesTableCell>
</Box>
);
}
function ItemTradesTableRowSkeleton({ shouldShowCompareColumn }) {
2020-11-24 14:24:34 -08:00
return (
<Box as="tr">
<ItemTradesTableCell>
<Skeleton width="100%">X</Skeleton>
2020-11-24 14:24:34 -08:00
</ItemTradesTableCell>
<ItemTradesTableCell>
<Skeleton width="100%">X</Skeleton>
2020-11-24 14:24:34 -08:00
</ItemTradesTableCell>
<ItemTradesTableCell>
<Skeleton width="100%">X</Skeleton>
2020-11-24 14:24:34 -08:00
</ItemTradesTableCell>
{shouldShowCompareColumn && (
<ItemTradesTableCell>
<Skeleton width="100%">X</Skeleton>
</ItemTradesTableCell>
)}
2020-11-24 14:24:34 -08:00
</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 (
<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>
);
}
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 getVaguelyRandomizedSortKeyForDate(dateString) {
const date = new Date(dateString);
// "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)
if (isThisWeek(date)) {
return `ZZZthisweekZZZ-${Math.random()}`;
}
return dateString;
}