2020-11-23 11:25:23 -08:00
|
|
|
import React from "react";
|
|
|
|
import { css } from "emotion";
|
2020-12-25 09:08:33 -08:00
|
|
|
import { Box, Skeleton, useColorModeValue, useToken } from "@chakra-ui/react";
|
2020-11-23 11:25:23 -08:00
|
|
|
import gql from "graphql-tag";
|
|
|
|
import { useQuery } from "@apollo/client";
|
2020-11-24 15:05:23 -08:00
|
|
|
import { Link, useHistory, useParams } from "react-router-dom";
|
2020-11-23 11:25:23 -08:00
|
|
|
|
|
|
|
import { Heading2, usePageTitle } from "./util";
|
|
|
|
import ItemPageLayout from "./ItemPageLayout";
|
2020-11-25 01:53:42 -08:00
|
|
|
import useCurrentUser from "./components/useCurrentUser";
|
2020-11-23 11:25:23 -08:00
|
|
|
|
|
|
|
export function ItemTradesOfferingPage() {
|
|
|
|
return (
|
|
|
|
<ItemTradesPage
|
|
|
|
title="Trades: Offering"
|
|
|
|
userHeading="Owner"
|
2020-11-25 02:24:15 -08:00
|
|
|
compareColumnLabel="Trade for your…"
|
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-25 01:53:42 -08:00
|
|
|
matchingItems: itemsTheyWantThatCurrentUserOwns {
|
|
|
|
id
|
|
|
|
name
|
|
|
|
}
|
2020-11-24 14:24:34 -08:00
|
|
|
}
|
|
|
|
closetList {
|
|
|
|
id
|
|
|
|
name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`}
|
2020-11-23 11:25:23 -08:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function ItemTradesSeekingPage() {
|
|
|
|
return (
|
|
|
|
<ItemTradesPage
|
|
|
|
title="Trades: Seeking"
|
|
|
|
userHeading="Seeker"
|
2020-11-25 02:24:15 -08:00
|
|
|
compareColumnLabel="Trade for their…"
|
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-25 01:53:42 -08:00
|
|
|
matchingItems: itemsTheyOwnThatCurrentUserWants {
|
|
|
|
id
|
|
|
|
name
|
|
|
|
}
|
2020-11-24 14:24:34 -08:00
|
|
|
}
|
|
|
|
closetList {
|
|
|
|
id
|
|
|
|
name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`}
|
2020-11-23 11:25:23 -08:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-24 14:24:34 -08:00
|
|
|
function ItemTradesPage({
|
|
|
|
title,
|
|
|
|
userHeading,
|
2020-11-25 02:24:15 -08:00
|
|
|
compareColumnLabel,
|
2020-11-24 14:24:34 -08:00
|
|
|
tradesQuery,
|
|
|
|
}) {
|
2020-11-23 11:25:23 -08:00
|
|
|
const { itemId } = useParams();
|
|
|
|
|
2020-11-24 14:24:34 -08:00
|
|
|
const { error, data } = useQuery(
|
2020-11-23 11:25:23 -08:00
|
|
|
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 });
|
2020-11-23 11:25:23 -08:00
|
|
|
|
|
|
|
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}
|
2020-11-25 02:24:15 -08:00
|
|
|
compareColumnLabel={compareColumnLabel}
|
2020-11-24 14:24:34 -08:00
|
|
|
tradesQuery={tradesQuery}
|
2020-11-23 11:25:23 -08:00
|
|
|
/>
|
|
|
|
</ItemPageLayout>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-24 14:24:34 -08:00
|
|
|
function ItemTradesTable({
|
|
|
|
itemId,
|
|
|
|
userHeading,
|
2020-11-25 02:24:15 -08:00
|
|
|
compareColumnLabel,
|
2020-11-24 14:24:34 -08:00
|
|
|
tradesQuery,
|
|
|
|
}) {
|
2020-11-25 01:53:42 -08:00
|
|
|
const { isLoggedIn } = useCurrentUser();
|
2020-11-24 14:24:34 -08:00
|
|
|
const { loading, error, data } = useQuery(tradesQuery, {
|
|
|
|
variables: { itemId },
|
|
|
|
});
|
|
|
|
|
2020-11-25 01:53:42 -08:00
|
|
|
const shouldShowCompareColumn = isLoggedIn;
|
2020-11-24 14:49:20 -08:00
|
|
|
|
2020-11-25 00:33:00 -08:00
|
|
|
// 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,
|
2020-11-25 01:53:42 -08:00
|
|
|
getVaguelyRandomizedTradeSortKey(
|
|
|
|
trade.user.lastTradeActivity,
|
|
|
|
trade.user.matchingItems.length
|
|
|
|
)
|
2020-11-25 00:33:00 -08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
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>;
|
|
|
|
}
|
2020-11-24 14:58:11 -08:00
|
|
|
|
2020-11-25 01:53:42 -08:00
|
|
|
const minorColumnWidth = {
|
|
|
|
base: shouldShowCompareColumn ? "23%" : "30%",
|
|
|
|
md: "20ex",
|
|
|
|
};
|
|
|
|
|
2020-11-23 11:25:23 -08:00
|
|
|
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-23 11:25:23 -08:00
|
|
|
`}
|
|
|
|
>
|
2020-11-24 14:24:34 -08:00
|
|
|
<Box as="thead" fontSize={{ base: "xs", sm: "sm" }}>
|
2020-11-23 11:25:23 -08:00
|
|
|
<Box as="tr">
|
2020-11-24 14:49:20 -08:00
|
|
|
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
2020-11-23 11:25:23 -08:00
|
|
|
{/* 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>
|
2020-11-23 11:25:23 -08:00
|
|
|
</ItemTradesTableCell>
|
2020-11-24 14:49:20 -08:00
|
|
|
{shouldShowCompareColumn && (
|
|
|
|
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
2020-11-25 01:53:42 -08:00
|
|
|
<Box display={{ base: "none", sm: "block" }}>
|
2020-11-25 02:24:15 -08:00
|
|
|
{compareColumnLabel}
|
2020-11-25 01:53:42 -08:00
|
|
|
</Box>
|
|
|
|
<Box display={{ base: "block", sm: "none" }}>Matches</Box>
|
2020-11-24 14:49:20 -08:00
|
|
|
</ItemTradesTableCell>
|
|
|
|
)}
|
2020-11-25 01:53:42 -08:00
|
|
|
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
|
|
|
{userHeading}
|
|
|
|
</ItemTradesTableCell>
|
2020-11-24 14:58:11 -08:00
|
|
|
<ItemTradesTableCell as="th">List</ItemTradesTableCell>
|
2020-11-23 11:25:23 -08:00
|
|
|
</Box>
|
|
|
|
</Box>
|
|
|
|
<Box as="tbody">
|
2020-11-24 14:24:34 -08:00
|
|
|
{loading && (
|
|
|
|
<>
|
2020-11-24 14:58:11 -08:00
|
|
|
<ItemTradesTableRowSkeleton
|
|
|
|
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
|
|
/>
|
|
|
|
<ItemTradesTableRowSkeleton
|
|
|
|
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
|
|
/>
|
|
|
|
<ItemTradesTableRowSkeleton
|
|
|
|
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
|
|
/>
|
|
|
|
<ItemTradesTableRowSkeleton
|
|
|
|
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
|
|
/>
|
|
|
|
<ItemTradesTableRowSkeleton
|
|
|
|
shouldShowCompareColumn={shouldShowCompareColumn}
|
|
|
|
/>
|
2020-11-24 14:24:34 -08:00
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{!loading &&
|
2020-11-24 14:58:11 -08:00
|
|
|
trades.length > 0 &&
|
|
|
|
trades.map((trade) => (
|
2020-11-24 14:24:34 -08:00
|
|
|
<ItemTradesTableRow
|
|
|
|
key={trade.id}
|
|
|
|
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-25 01:53:42 -08:00
|
|
|
matchingItems={trade.user.matchingItems}
|
2020-11-24 14:49:20 -08:00
|
|
|
shouldShowCompareColumn={shouldShowCompareColumn}
|
2020-11-24 14:24:34 -08:00
|
|
|
/>
|
|
|
|
))}
|
2020-11-24 14:58:11 -08:00
|
|
|
{!loading && trades.length === 0 && (
|
2020-11-24 14:24:34 -08:00
|
|
|
<Box as="tr">
|
|
|
|
<ItemTradesTableCell
|
2020-11-24 15:02:03 -08:00
|
|
|
colSpan={shouldShowCompareColumn ? 4 : 3}
|
2020-11-24 14:24:34 -08:00
|
|
|
textAlign="center"
|
|
|
|
fontStyle="italic"
|
|
|
|
>
|
|
|
|
No trades yet!
|
|
|
|
</ItemTradesTableCell>
|
|
|
|
</Box>
|
|
|
|
)}
|
2020-11-23 11:25:23 -08:00
|
|
|
</Box>
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-24 14:43:43 -08:00
|
|
|
function ItemTradesTableRow({
|
|
|
|
href,
|
|
|
|
username,
|
|
|
|
listName,
|
|
|
|
lastTradeActivity,
|
2020-11-25 01:53:42 -08:00
|
|
|
matchingItems,
|
2020-11-24 14:49:20 -08:00
|
|
|
shouldShowCompareColumn,
|
2020-11-24 14:43:43 -08:00
|
|
|
}) {
|
2020-11-23 11:25:23 -08:00
|
|
|
const history = useHistory();
|
|
|
|
const onClick = React.useCallback(() => history.push(href), [history, href]);
|
|
|
|
const focusBackground = useColorModeValue("gray.100", "gray.600");
|
|
|
|
|
2020-11-25 01:53:42 -08:00
|
|
|
const sortedMatchingItems = [...matchingItems].sort((a, b) =>
|
|
|
|
a.name.localeCompare(b.name)
|
|
|
|
);
|
|
|
|
|
2020-11-23 11:25:23 -08:00
|
|
|
return (
|
|
|
|
<Box
|
|
|
|
as="tr"
|
2020-11-24 14:24:34 -08:00
|
|
|
cursor="pointer"
|
2020-11-23 11:25:23 -08:00
|
|
|
_hover={{ background: focusBackground }}
|
|
|
|
_focusWithin={{ background: focusBackground }}
|
|
|
|
onClick={onClick}
|
|
|
|
>
|
2020-11-24 14:24:34 -08:00
|
|
|
<ItemTradesTableCell fontSize="xs">
|
2020-11-25 00:33:00 -08:00
|
|
|
{formatVagueDate(lastTradeActivity)}
|
2020-11-23 13:30:34 -08:00
|
|
|
</ItemTradesTableCell>
|
2020-11-24 14:49:20 -08:00
|
|
|
{shouldShowCompareColumn && (
|
|
|
|
<ItemTradesTableCell fontSize="xs">
|
2020-11-25 01:53:42 -08:00
|
|
|
{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}
|
2020-11-24 14:49:20 -08:00
|
|
|
</Box>
|
2020-11-23 11:25:23 -08:00
|
|
|
</Box>
|
2020-11-25 01:53:42 -08:00
|
|
|
))}
|
|
|
|
{matchingItems.length > 4 && (
|
|
|
|
<Box as="li">+ {matchingItems.length - 4} more</Box>
|
|
|
|
)}
|
2020-11-24 14:49:20 -08:00
|
|
|
</Box>
|
2020-11-25 01:53:42 -08:00
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
<Box display={{ base: "none", sm: "block" }}>No matches</Box>
|
|
|
|
<Box display={{ base: "block", sm: "none" }}>None</Box>
|
|
|
|
</>
|
|
|
|
)}
|
2020-11-24 14:49:20 -08:00
|
|
|
</ItemTradesTableCell>
|
|
|
|
)}
|
2020-11-25 01:53:42 -08:00
|
|
|
<ItemTradesTableCell fontSize="xs">{username}</ItemTradesTableCell>
|
|
|
|
<ItemTradesTableCell fontSize="sm">
|
2020-11-24 14:58:11 -08:00
|
|
|
<Box
|
2020-11-24 15:05:23 -08:00
|
|
|
as={Link}
|
|
|
|
to={href}
|
2020-11-24 14:58:11 -08:00
|
|
|
className={css`
|
|
|
|
&:hover,
|
|
|
|
&:focus,
|
|
|
|
tr:hover &,
|
|
|
|
tr:focus-within & {
|
|
|
|
text-decoration: underline;
|
|
|
|
}
|
|
|
|
`}
|
|
|
|
>
|
|
|
|
{listName}
|
|
|
|
</Box>
|
|
|
|
</ItemTradesTableCell>
|
2020-11-23 11:25:23 -08:00
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-24 14:58:11 -08:00
|
|
|
function ItemTradesTableRowSkeleton({ shouldShowCompareColumn }) {
|
2020-11-24 14:24:34 -08:00
|
|
|
return (
|
|
|
|
<Box as="tr">
|
|
|
|
<ItemTradesTableCell>
|
2020-11-24 14:29:00 -08:00
|
|
|
<Skeleton width="100%">X</Skeleton>
|
2020-11-24 14:24:34 -08:00
|
|
|
</ItemTradesTableCell>
|
|
|
|
<ItemTradesTableCell>
|
2020-11-24 14:29:00 -08:00
|
|
|
<Skeleton width="100%">X</Skeleton>
|
2020-11-24 14:24:34 -08:00
|
|
|
</ItemTradesTableCell>
|
|
|
|
<ItemTradesTableCell>
|
2020-11-24 14:29:00 -08:00
|
|
|
<Skeleton width="100%">X</Skeleton>
|
2020-11-24 14:24:34 -08:00
|
|
|
</ItemTradesTableCell>
|
2020-11-24 14:58:11 -08:00
|
|
|
{shouldShowCompareColumn && (
|
|
|
|
<ItemTradesTableCell>
|
|
|
|
<Skeleton width="100%">X</Skeleton>
|
|
|
|
</ItemTradesTableCell>
|
|
|
|
)}
|
2020-11-24 14:24:34 -08:00
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-23 11:25:23 -08:00
|
|
|
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>
|
|
|
|
);
|
|
|
|
}
|
2020-11-25 00:33:00 -08:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-11-25 01:53:42 -08:00
|
|
|
function getVaguelyRandomizedTradeSortKey(dateString, numMatchingItems) {
|
2020-11-25 00:33:00 -08:00
|
|
|
const date = new Date(dateString);
|
2020-11-25 01:53:42 -08:00
|
|
|
const hasMatchingItems = numMatchingItems >= 1;
|
2020-11-25 00:33:00 -08:00
|
|
|
|
|
|
|
// "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)
|
2020-11-25 01:53:42 -08:00
|
|
|
//
|
|
|
|
// 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.
|
2020-11-25 00:33:00 -08:00
|
|
|
if (isThisWeek(date)) {
|
2020-11-25 01:53:42 -08:00
|
|
|
const matchingItemsKey = hasMatchingItems
|
|
|
|
? "ZZmatchingZZ"
|
|
|
|
: "AAnotmatchingAA";
|
|
|
|
return `ZZZthisweekZZZ-${matchingItemsKey}-${Math.random()}`;
|
2020-11-25 00:33:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return dateString;
|
|
|
|
}
|