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
|
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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 [];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue