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:
parent
a19c2facbf
commit
f2259d6487
4 changed files with 171 additions and 82 deletions
|
@ -379,6 +379,10 @@ function NewItemsSectionContent() {
|
|||
thumbnailUrl
|
||||
isNc
|
||||
isPb
|
||||
speciesThatNeedModels {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
@ -402,29 +406,30 @@ function NewItemsSectionContent() {
|
|||
);
|
||||
|
||||
if (loading) {
|
||||
const footer = <Box fontSize="xs" height="1em" />;
|
||||
return (
|
||||
<Delay>
|
||||
<ItemCardHStack>
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
<SquareItemCardSkeleton footer={footer} />
|
||||
</ItemCardHStack>
|
||||
</Delay>
|
||||
);
|
||||
|
@ -448,7 +453,26 @@ function NewItemsSectionContent() {
|
|||
return (
|
||||
<ItemCardHStack>
|
||||
{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>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
Skeleton,
|
||||
useColorModeValue,
|
||||
useTheme,
|
||||
|
@ -11,7 +12,12 @@ import { Link } from "react-router-dom";
|
|||
import { safeImageUrl, useCommonStyles } from "../util";
|
||||
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 tradeMatchOwnShadowColor = useColorModeValue("green.500", "green.200");
|
||||
|
@ -64,6 +70,7 @@ function SquareItemCard({ item, tradeMatchingMode = null, ...props }) {
|
|||
/>
|
||||
}
|
||||
boxShadow={tradeMatchShadow}
|
||||
footer={footer}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
|
@ -74,6 +81,7 @@ function SquareItemCard({ item, tradeMatchingMode = null, ...props }) {
|
|||
function SquareItemCardLayout({
|
||||
name,
|
||||
thumbnailImage,
|
||||
footer,
|
||||
minHeightNumLines = 2,
|
||||
boxShadow = null,
|
||||
}) {
|
||||
|
@ -118,6 +126,7 @@ function SquareItemCardLayout({
|
|||
>
|
||||
{name}
|
||||
</div>
|
||||
{footer && <Box marginTop="2">{footer}</Box>}
|
||||
</div>
|
||||
)}
|
||||
</ClassNames>
|
||||
|
@ -350,7 +359,7 @@ function ItemThumbnailKindBadge({ colorScheme, children }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function SquareItemCardSkeleton({ minHeightNumLines }) {
|
||||
export function SquareItemCardSkeleton({ minHeightNumLines, footer = null }) {
|
||||
return (
|
||||
<SquareItemCardLayout
|
||||
name={
|
||||
|
@ -363,6 +372,7 @@ export function SquareItemCardSkeleton({ minHeightNumLines }) {
|
|||
}
|
||||
thumbnailImage={<Skeleton width="80px" height="80px" />}
|
||||
minHeightNumLines={minHeightNumLines}
|
||||
footer={footer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -480,16 +480,21 @@ const buildNewestItemsLoader = (db, loaders) =>
|
|||
return [entities];
|
||||
});
|
||||
|
||||
const buildItemsThatNeedModelsLoader = (db) =>
|
||||
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"`);
|
||||
async function runItemModelingQuery(db, filterToItemIds) {
|
||||
let itemIdsCondition;
|
||||
let itemIdsValues;
|
||||
if (filterToItemIds === "all") {
|
||||
// For all items, we use the condition `1`, which matches everything.
|
||||
itemIdsCondition = "1";
|
||||
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,
|
||||
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 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%"
|
||||
AND ${itemIdsCondition}
|
||||
ORDER BY item_id
|
||||
) T_ITEMS
|
||||
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")
|
||||
)
|
||||
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 result = new Map();
|
||||
for (const { colorId, itemId, ...entity } of entities) {
|
||||
loaders.speciesThatNeedModelsForItemLoader.prime(
|
||||
{ colorId, itemId },
|
||||
entity
|
||||
);
|
||||
|
||||
if (!result.has(colorId)) {
|
||||
result.set(colorId, new Map());
|
||||
}
|
||||
|
@ -1309,7 +1357,13 @@ function buildLoaders(db) {
|
|||
);
|
||||
loaders.itemSearchItemsLoader = buildItemSearchItemsLoader(db, loaders);
|
||||
loaders.newestItemsLoader = buildNewestItemsLoader(db, loaders);
|
||||
loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(db);
|
||||
loaders.speciesThatNeedModelsForItemLoader = buildSpeciesThatNeedModelsForItemLoader(
|
||||
db
|
||||
);
|
||||
loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(
|
||||
db,
|
||||
loaders
|
||||
);
|
||||
loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader(
|
||||
db
|
||||
);
|
||||
|
|
|
@ -457,13 +457,14 @@ const resolvers = {
|
|||
speciesThatNeedModels: async (
|
||||
{ id },
|
||||
{ colorId = "8" }, // Blue
|
||||
{ itemsThatNeedModelsLoader }
|
||||
{ speciesThatNeedModelsForItemLoader }
|
||||
) => {
|
||||
const speciesIdsByColorIdAndItemId = await itemsThatNeedModelsLoader.load(
|
||||
"all"
|
||||
);
|
||||
const speciesIdsByItemId = speciesIdsByColorIdAndItemId.get(colorId);
|
||||
const row = speciesIdsByItemId && speciesIdsByItemId.get(id);
|
||||
// NOTE: If we're running this in the context of `itemsThatNeedModels`,
|
||||
// this loader should already be primed, no extra query!
|
||||
const row = await speciesThatNeedModelsForItemLoader.load({
|
||||
itemId: id,
|
||||
colorId,
|
||||
});
|
||||
if (!row) {
|
||||
return [];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue