Add modeling info to homepage

To make this fast, I had to tweak the GraphQL resolver a bit to run a filtered version of the query for `newestItems` instead of scanning the full database! But yeah, looking good!

I think I'm gonna want to swap out "Fully modeled" for some insight about who it fits
This commit is contained in:
Emi Matchu 2021-07-11 18:09:29 -07:00
parent a19c2facbf
commit f2259d6487
4 changed files with 171 additions and 82 deletions

View file

@ -379,6 +379,10 @@ function NewItemsSectionContent() {
thumbnailUrl thumbnailUrl
isNc isNc
isPb isPb
speciesThatNeedModels {
id
name
}
} }
} }
` `
@ -402,29 +406,30 @@ function NewItemsSectionContent() {
); );
if (loading) { if (loading) {
const footer = <Box fontSize="xs" height="1em" />;
return ( return (
<Delay> <Delay>
<ItemCardHStack> <ItemCardHStack>
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
</ItemCardHStack> </ItemCardHStack>
</Delay> </Delay>
); );
@ -448,7 +453,26 @@ function NewItemsSectionContent() {
return ( return (
<ItemCardHStack> <ItemCardHStack>
{newestItems.map((item) => ( {newestItems.map((item) => (
<SquareItemCard key={item.id} item={item} /> <SquareItemCard
key={item.id}
item={item}
footer={
item.speciesThatNeedModels.length > 0 ? (
<Box
fontSize="xs"
fontStyle="italic"
fontWeight="600"
opacity="0.8"
>
Need {item.speciesThatNeedModels.length}1 models
</Box>
) : (
<Box fontSize="xs" fontStyle="italic" opacity="0.8">
Fully modeled!
</Box>
)
}
/>
))} ))}
</ItemCardHStack> </ItemCardHStack>
); );

View file

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { import {
Box,
Skeleton, Skeleton,
useColorModeValue, useColorModeValue,
useTheme, useTheme,
@ -11,7 +12,12 @@ import { Link } from "react-router-dom";
import { safeImageUrl, useCommonStyles } from "../util"; import { safeImageUrl, useCommonStyles } from "../util";
import { CheckIcon, StarIcon } from "@chakra-ui/icons"; import { CheckIcon, StarIcon } from "@chakra-ui/icons";
function SquareItemCard({ item, tradeMatchingMode = null, ...props }) { function SquareItemCard({
item,
tradeMatchingMode = null,
footer = null,
...props
}) {
const outlineShadowValue = useToken("shadows", "outline"); const outlineShadowValue = useToken("shadows", "outline");
const tradeMatchOwnShadowColor = useColorModeValue("green.500", "green.200"); const tradeMatchOwnShadowColor = useColorModeValue("green.500", "green.200");
@ -64,6 +70,7 @@ function SquareItemCard({ item, tradeMatchingMode = null, ...props }) {
/> />
} }
boxShadow={tradeMatchShadow} boxShadow={tradeMatchShadow}
footer={footer}
/> />
</Link> </Link>
)} )}
@ -74,6 +81,7 @@ function SquareItemCard({ item, tradeMatchingMode = null, ...props }) {
function SquareItemCardLayout({ function SquareItemCardLayout({
name, name,
thumbnailImage, thumbnailImage,
footer,
minHeightNumLines = 2, minHeightNumLines = 2,
boxShadow = null, boxShadow = null,
}) { }) {
@ -118,6 +126,7 @@ function SquareItemCardLayout({
> >
{name} {name}
</div> </div>
{footer && <Box marginTop="2">{footer}</Box>}
</div> </div>
)} )}
</ClassNames> </ClassNames>
@ -350,7 +359,7 @@ function ItemThumbnailKindBadge({ colorScheme, children }) {
); );
} }
export function SquareItemCardSkeleton({ minHeightNumLines }) { export function SquareItemCardSkeleton({ minHeightNumLines, footer = null }) {
return ( return (
<SquareItemCardLayout <SquareItemCardLayout
name={ name={
@ -363,6 +372,7 @@ export function SquareItemCardSkeleton({ minHeightNumLines }) {
} }
thumbnailImage={<Skeleton width="80px" height="80px" />} thumbnailImage={<Skeleton width="80px" height="80px" />}
minHeightNumLines={minHeightNumLines} minHeightNumLines={minHeightNumLines}
footer={footer}
/> />
); );
} }

View file

@ -480,16 +480,21 @@ const buildNewestItemsLoader = (db, loaders) =>
return [entities]; return [entities];
}); });
const buildItemsThatNeedModelsLoader = (db) => async function runItemModelingQuery(db, filterToItemIds) {
new DataLoader(async (keys) => { let itemIdsCondition;
// Essentially, I want to take easy advantage of DataLoader's caching, for let itemIdsValues;
// this query that can only run one way ^_^` There might be a better way to if (filterToItemIds === "all") {
// do this! // For all items, we use the condition `1`, which matches everything.
if (keys.length !== 1 && keys[0] !== "all") { itemIdsCondition = "1";
throw new Error(`this loader can only be loaded with the key "all"`); itemIdsValues = [];
} else {
// Or, to filter to certain items, we add their IDs to the WHERE clause.
const qs = filterToItemIds.map((_) => "?").join(", ");
itemIdsCondition = `(item_id IN (${qs}))`;
itemIdsValues = filterToItemIds;
} }
const [rows] = await db.query( return await db.execute(
` `
SELECT T_ITEMS.item_id, SELECT T_ITEMS.item_id,
T_BODIES.color_id, T_BODIES.color_id,
@ -518,6 +523,7 @@ const buildItemsThatNeedModelsLoader = (db) =>
INNER JOIN swf_assets ON swf_assets.id = psa.swf_asset_id INNER JOIN swf_assets ON swf_assets.id = psa.swf_asset_id
INNER JOIN item_translations it ON it.item_id = items.id AND it.locale = "en" INNER JOIN item_translations it ON it.item_id = items.id AND it.locale = "en"
WHERE items.modeling_status_hint IS NULL AND it.name NOT LIKE "%MME%" WHERE items.modeling_status_hint IS NULL AND it.name NOT LIKE "%MME%"
AND ${itemIdsCondition}
ORDER BY item_id ORDER BY item_id
) T_ITEMS ) T_ITEMS
INNER JOIN ( INNER JOIN (
@ -538,13 +544,55 @@ const buildItemsThatNeedModelsLoader = (db) =>
OR (NOT T_ITEMS.supports_vandagyre AND modeled_species_count = 54 AND modeled_species_ids = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54") OR (NOT T_ITEMS.supports_vandagyre AND modeled_species_count = 54 AND modeled_species_ids = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54")
) )
ORDER BY T_ITEMS.item_id; ORDER BY T_ITEMS.item_id;
` `,
[...itemIdsValues]
); );
}
const buildSpeciesThatNeedModelsForItemLoader = (db) =>
new DataLoader(
async (colorIdAndItemIdPairs) => {
// Get the requested item IDs, ignoring color for now. Remove duplicates.
let itemIds = colorIdAndItemIdPairs.map(({ itemId }) => itemId);
itemIds = [...new Set(itemIds)];
// Run the big modeling query, but filtered to specifically these items.
// The filter happens very early in the query, so it runs way faster than
// the full modeling query.
const [rows] = await runItemModelingQuery(db, itemIds);
const entities = rows.map(normalizeRow);
// Finally, the query returned a row for each item combined with each
// color built into the query (well, no row when no models needed!). So,
// find the right row for each color/item pair, or possibly null!
return colorIdAndItemIdPairs.map(({ colorId, itemId }) =>
entities.find((e) => e.itemId === itemId && e.colorId === colorId)
);
},
{ cacheKeyFn: ({ colorId, itemId }) => `${colorId}-${itemId}` }
);
const buildItemsThatNeedModelsLoader = (db, loaders) =>
new DataLoader(async (keys) => {
// Essentially, I want to take easy advantage of DataLoader's caching, for
// this query that can only run one way ^_^` There might be a better way to
// do this!
if (keys.length !== 1 && keys[0] !== "all") {
throw new Error(`this loader can only be loaded with the key "all"`);
}
const [rows] = await runItemModelingQuery(db, "all");
const entities = rows.map(normalizeRow); const entities = rows.map(normalizeRow);
const result = new Map(); const result = new Map();
for (const { colorId, itemId, ...entity } of entities) { for (const { colorId, itemId, ...entity } of entities) {
loaders.speciesThatNeedModelsForItemLoader.prime(
{ colorId, itemId },
entity
);
if (!result.has(colorId)) { if (!result.has(colorId)) {
result.set(colorId, new Map()); result.set(colorId, new Map());
} }
@ -1309,7 +1357,13 @@ function buildLoaders(db) {
); );
loaders.itemSearchItemsLoader = buildItemSearchItemsLoader(db, loaders); loaders.itemSearchItemsLoader = buildItemSearchItemsLoader(db, loaders);
loaders.newestItemsLoader = buildNewestItemsLoader(db, loaders); loaders.newestItemsLoader = buildNewestItemsLoader(db, loaders);
loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(db); loaders.speciesThatNeedModelsForItemLoader = buildSpeciesThatNeedModelsForItemLoader(
db
);
loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(
db,
loaders
);
loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader( loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader(
db db
); );

View file

@ -457,13 +457,14 @@ const resolvers = {
speciesThatNeedModels: async ( speciesThatNeedModels: async (
{ id }, { id },
{ colorId = "8" }, // Blue { colorId = "8" }, // Blue
{ itemsThatNeedModelsLoader } { speciesThatNeedModelsForItemLoader }
) => { ) => {
const speciesIdsByColorIdAndItemId = await itemsThatNeedModelsLoader.load( // NOTE: If we're running this in the context of `itemsThatNeedModels`,
"all" // this loader should already be primed, no extra query!
); const row = await speciesThatNeedModelsForItemLoader.load({
const speciesIdsByItemId = speciesIdsByColorIdAndItemId.get(colorId); itemId: id,
const row = speciesIdsByItemId && speciesIdsByItemId.get(id); colorId,
});
if (!row) { if (!row) {
return []; return [];
} }