add real offering/seeking counts to item page

This commit is contained in:
Emi Matchu 2020-11-09 06:26:29 -08:00
parent 90233c0a03
commit 68e6e9e243
3 changed files with 112 additions and 10 deletions

View file

@ -552,22 +552,54 @@ function ItemPageWantButton({ itemId, isChecked }) {
} }
function ItemPageTradeLinks({ itemId }) { function ItemPageTradeLinks({ itemId }) {
const { data, loading, error } = useQuery(
gql`
query ItemPageTradeLinks($itemId: ID!) {
item(id: $itemId) {
id
numUsersOfferingThis
numUsersSeekingThis
}
}
`,
{ variables: { itemId } }
);
if (error) {
return <Box color="red.400">{error.message}</Box>;
}
return ( return (
<Tooltip label="These are placeholders—real trade lists coming soon!"> <HStack spacing="2">
<HStack spacing="2"> <Box as="header" fontSize="sm" fontWeight="bold">
<Box as="header" fontSize="sm" fontWeight="bold"> Trading:
Trading: </Box>
</Box> <SubtleSkeleton isLoaded={!loading}>
<ItemPageTradeLink count={8} label="offering" colorScheme="green" /> <ItemPageTradeLink
<ItemPageTradeLink count={12} label="seeking" colorScheme="blue" /> itemId={itemId}
</HStack> count={data?.item?.numUsersOfferingThis || 0}
</Tooltip> label="offering"
colorScheme="green"
/>
</SubtleSkeleton>
<SubtleSkeleton isLoaded={!loading}>
<ItemPageTradeLink
itemId={itemId}
count={data?.item?.numUsersSeekingThis || 0}
label="seeking"
colorScheme="blue"
/>
</SubtleSkeleton>
</HStack>
); );
} }
function ItemPageTradeLink({ count, label, colorScheme }) { function ItemPageTradeLink({ itemId, count, label, colorScheme }) {
return ( return (
<Button <Button
as="a"
// TODO: Link to a new Impress 2020 page instead!
href={`https://impress.openneo.net/items/${itemId}`}
size="xs" size="xs"
variant="outline" variant="outline"
colorScheme={colorScheme} colorScheme={colorScheme}

View file

@ -388,6 +388,56 @@ const buildItemAllOccupiedZonesLoader = (db) =>
); );
}); });
const buildItemTradeCountsLoader = (db) =>
new DataLoader(
async (itemIdOwnedPairs) => {
const qs = itemIdOwnedPairs
.map((_) => "(closet_hangers.item_id = ? AND closet_hangers.owned = ?)")
.join(" OR ");
const values = itemIdOwnedPairs
.map(({ itemId, isOwned }) => [itemId, isOwned])
.flat();
const [rows, _] = await db.execute(
`
SELECT
items.id AS item_id, closet_hangers.owned AS is_owned,
count(DISTINCT closet_hangers.user_id) AS users_count
FROM items
INNER JOIN closet_hangers ON closet_hangers.item_id = items.id
INNER JOIN users ON users.id = closet_hangers.user_id
LEFT JOIN closet_lists ON closet_lists.id = closet_hangers.list_id
WHERE (
(${qs})
AND (
(closet_hangers.list_id IS NOT NULL AND closet_lists.visibility >= 2)
OR (
closet_hangers.list_id IS NULL AND closet_hangers.owned = 1
AND users.owned_closet_hangers_visibility >= 2
)
OR (
closet_hangers.list_id IS NULL AND closet_hangers.owned = 0
AND users.wanted_closet_hangers_visibility >= 2
)
)
)
GROUP BY items.id, closet_hangers.owned;
`,
values
);
const entities = rows.map(normalizeRow);
return itemIdOwnedPairs.map(({ itemId, isOwned }) => {
// NOTE: There may be no matching row, if there are 0 such trades.
const entity = entities.find(
(e) => e.itemId === itemId && Boolean(e.isOwned) === isOwned
);
return entity ? entity.usersCount : 0;
});
},
{ cacheKeyFn: ({ itemId, isOwned }) => `${itemId}-${isOwned}` }
);
const buildPetTypeLoader = (db, loaders) => const buildPetTypeLoader = (db, loaders) =>
new DataLoader(async (petTypeIds) => { new DataLoader(async (petTypeIds) => {
const qs = petTypeIds.map((_) => "?").join(","); const qs = petTypeIds.map((_) => "?").join(",");
@ -811,6 +861,7 @@ function buildLoaders(db) {
db db
); );
loaders.itemAllOccupiedZonesLoader = buildItemAllOccupiedZonesLoader(db); loaders.itemAllOccupiedZonesLoader = buildItemAllOccupiedZonesLoader(db);
loaders.itemTradeCountsLoader = buildItemTradeCountsLoader(db);
loaders.petTypeLoader = buildPetTypeLoader(db, loaders); loaders.petTypeLoader = buildPetTypeLoader(db, loaders);
loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader( loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader(
db, db,

View file

@ -22,6 +22,10 @@ const typeDefs = gql`
currentUserOwnsThis: Boolean! currentUserOwnsThis: Boolean!
currentUserWantsThis: Boolean! currentUserWantsThis: Boolean!
# How many users are offering/seeking this in their public trade lists.
numUsersOfferingThis: Int!
numUsersSeekingThis: Int!
# How this item appears on the given species/color combo. If it does not # How this item appears on the given species/color combo. If it does not
# fit the pet, we'll return an empty ItemAppearance with no layers. # fit the pet, we'll return an empty ItemAppearance with no layers.
appearanceOn(speciesId: ID!, colorId: ID!): ItemAppearance! appearanceOn(speciesId: ID!, colorId: ID!): ItemAppearance!
@ -185,6 +189,21 @@ const resolvers = {
return closetHangers.some((h) => h.itemId === id && !h.owned); return closetHangers.some((h) => h.itemId === id && !h.owned);
}, },
numUsersOfferingThis: async ({ id }, _, { itemTradeCountsLoader }) => {
const count = await itemTradeCountsLoader.load({
itemId: id,
isOwned: true,
});
return count;
},
numUsersSeekingThis: async ({ id }, _, { itemTradeCountsLoader }) => {
const count = await itemTradeCountsLoader.load({
itemId: id,
isOwned: false,
});
return count;
},
appearanceOn: async ( appearanceOn: async (
{ id }, { id },
{ speciesId, colorId }, { speciesId, colorId },