Finish wiring up ~owls data

Hey nice it looks like it's working! :3 "Bright Speckled Parasol" is a nice test case, it has a long text string! And when the NC value is not included in the ~owls list, we indeed don't show the badge!
This commit is contained in:
Emi Matchu 2022-08-15 23:57:33 -07:00
parent 16b86fc65e
commit 240a683e71
6 changed files with 81 additions and 102 deletions

View file

@ -79,20 +79,26 @@ async function loadOWLSValuesByIdOrName() {
}
const itemValuesByIdOrName = {};
for (const [itemName, value] of Object.entries(json)) {
for (const [itemName, valueText] of Object.entries(json)) {
// OWLS returns an empty string for NC Mall items they don't have a trade
// value for, to allow the script to distinguish between NP items vs
// no-data NC items. We omit it from our data instead, because our UI is
// already aware of whether the item is NP or NC.
if (value.trim() === "") {
if (valueText.trim() === "") {
continue;
}
// TODO: OWLS doesn't currently provide item IDs ever. Add support for it
// if it does! (I'm keeping the rest of the code the same because I
// think that might happen for disambiguation, like Waka did.)
// think that might happen for disambiguation, like Waka did.) Until
// then, we just always key by name.
const normalizedItemName = normalizeItemName(itemName);
itemValuesByIdOrName[normalizedItemName] = value;
// We wrap it in an object with the key `valueText`, just to not break
// potential external consumers of this endpoint if we add more fields.
// (This is kinda silly and unnecessary, but it should get gzipped out and
// shouldn't add substantial time to building or parsing, so like w/e!)
itemValuesByIdOrName[normalizedItemName] = { valueText };
}
return itemValuesByIdOrName;

View file

@ -36,7 +36,13 @@ import { useQuery, useMutation } from "@apollo/client";
import { Link, useParams } from "react-router-dom";
import ItemPageLayout, { SubtleSkeleton } from "./ItemPageLayout";
import { Delay, logAndCapture, useLocalStorage, usePageTitle } from "./util";
import {
Delay,
logAndCapture,
MajorErrorMessage,
useLocalStorage,
usePageTitle,
} from "./util";
import HTML5Badge, { layerUsesHTML5 } from "./components/HTML5Badge";
import {
itemAppearanceFragment,
@ -77,7 +83,7 @@ export function ItemPageContent({ itemId, isEmbedded }) {
thumbnailUrl
description
createdAt
wakaValueText
ncTradeValueText
# For Support users.
rarityIndex
@ -91,7 +97,7 @@ export function ItemPageContent({ itemId, isEmbedded }) {
usePageTitle(data?.item?.name, { skip: isEmbedded });
if (error) {
return <Box color="red.400">{error.message}</Box>;
return <MajorErrorMessage error={error} />;
}
const item = data?.item;

View file

@ -5,7 +5,6 @@ import {
Flex,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
Portal,
@ -16,12 +15,7 @@ import {
useToast,
VStack,
} from "@chakra-ui/react";
import {
ExternalLinkIcon,
ChevronRightIcon,
QuestionIcon,
WarningTwoIcon,
} from "@chakra-ui/icons";
import { ExternalLinkIcon, ChevronRightIcon } from "@chakra-ui/icons";
import { gql, useMutation } from "@apollo/client";
import {
@ -159,44 +153,15 @@ function ItemPageBadges({ item, isEmbedded }) {
{item.isNc && (
<SubtleSkeleton
isLoaded={
// Distinguish between undefined (still loading) and null (loaded and
// empty).
item.wakaValueText !== undefined
// Distinguish between undefined (still loading) and null (loaded
// and empty).
item.ncTradeValueText !== undefined
}
>
{shouldShowWaka() && item.wakaValueText && (
<>
{/* For hover-y devices, use a hover popover over the badge. */}
<Box sx={{ "@media (hover: none)": { display: "none" } }}>
<WakaPopover trigger="hover">
<LinkBadge
href="http://www.neopets.com/~waka"
colorScheme="yellow"
>
<WarningTwoIcon marginRight="1" />
Waka: {item.wakaValueText}
{item.ncTradeValueText && (
<LinkBadge href="http://www.neopets.com/~owls">
OWLS: {item.ncTradeValueText}
</LinkBadge>
</WakaPopover>
</Box>
{/* For touch-y devices, use a tappable help icon. */}
<Flex
sx={{ "@media (hover: hover)": { display: "none" } }}
align="center"
>
<LinkBadge
href="http://www.neopets.com/~waka"
colorScheme="yellow"
>
<WarningTwoIcon marginRight="1" />
Waka: {item.wakaValueText}
</LinkBadge>
<WakaPopover>
<Flex align="center" fontSize="sm" paddingX="2" tabIndex="0">
<QuestionIcon />
</Flex>
</WakaPopover>
</Flex>
</>
)}
</SubtleSkeleton>
)}
@ -439,55 +404,4 @@ function ShortTimestamp({ when }) {
);
}
function WakaPopover({ children, ...props }) {
return (
<Popover placement="bottom" {...props}>
<PopoverTrigger>{children}</PopoverTrigger>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverBody fontSize="sm">
<p>
<strong>
The Waka Guide for NC trade values is closing down!
</strong>{" "}
We're sad to see them go, but excited that the team gets to move
onto the next phase in their life 💖
</p>
<Box height="1em" />
<p>
The Waka guide was last updated August 7, 2021, so this value
might not be accurate anymore. Consider checking in with the
Neoboards!
</p>
<Box height="1em" />
<p>
Thanks again to the Waka team for letting us experiment with
sharing their trade values here. Best wishes for everything to
come! 💜
</p>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
}
// August 21, 2021. (The month uses 0-indexing, but nothing else does! 🙃)
const STOP_SHOWING_WAKA_AFTER = new Date(2021, 7, 21, 0, 0, 0, 0);
/**
* shouldShowWaka returns true if, according to the browser, it's not yet
* August 21, 2021. It starts returning false at midnight on Aug 21.
*
* That way, our Waka deprecation message is on an auto-timer. After Aug 21,
* it's safe to remove all Waka UI code, and the Waka API endpoint and GraphQL
* fields. (It might be kind to return a placeholder string for the GraphQL
* case!)
*/
function shouldShowWaka() {
const now = new Date();
return now < STOP_SHOWING_WAKA_AFTER;
}
export default ItemPageLayout;

View file

@ -102,7 +102,7 @@ function ItemTradesPage({
thumbnailUrl
description
createdAt
wakaValueText
ncTradeValueText
}
}
`,

View file

@ -1,4 +1,5 @@
import DataLoader from "dataloader";
import fetch from "node-fetch";
import { normalizeRow } from "./util";
const buildClosetListLoader = (db) =>
@ -825,6 +826,31 @@ const buildItemTradesLoader = (db, loaders) =>
{ cacheKeyFn: ({ itemId, isOwned }) => `${itemId}-${isOwned}` }
);
const buildItemNCTradeValueLoader = () =>
new DataLoader(async (itemIds) => {
// This loader calls our /api/allNCTradeValues endpoint, to take advantage
// of the CDN caching. This helps us respond a bit faster than calling the
// API directly would, and avoids putting network pressure or caching
// complexity on our ~owls friends! (It would also be pretty reasonable to
// do this as a process-level cache or something instead, but I'm reusing
// Waka code from when we were on a more distributed system where that
// wouldn't have worked out, and I don't think the effort to refactor this
// just for the potential perf win is worthy!)
const url = process.env.NODE_ENV === "production"
? "https://impress-2020.openneo.net/api/allNCTradeValues"
: "http://localhost:3000/api/allNCTradeValues";
const res = await fetch(url);
if (!res.ok) {
throw new Error(
`Error loading /api/allNCTradeValues: ${res.status} ${res.statusText}`
);
}
const allNCTradeValues = await res.json();
return itemIds.map((itemId) => allNCTradeValues[itemId]);
});
const buildPetTypeLoader = (db, loaders) =>
new DataLoader(async (petTypeIds) => {
const qs = petTypeIds.map((_) => "?").join(",");
@ -1495,6 +1521,7 @@ function buildLoaders(db) {
db
);
loaders.itemTradesLoader = buildItemTradesLoader(db, loaders);
loaders.itemNCTradeValueLoader = buildItemNCTradeValueLoader();
loaders.petTypeLoader = buildPetTypeLoader(db, loaders);
loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader(
db,

View file

@ -33,6 +33,18 @@ const typeDefs = gql`
"""
wakaValueText: String @cacheControl(maxAge: ${oneHour})
"""
This item's NC trade value as a human-readable string. Returns null if the
value is not known.
Note that the format of this string is not well-specifiedit's fully
human-curated and may include surprising words or extra notes! We recommend
presenting the text exactly as-is, rather than trying to parse and math it.
This data is currently curated by neopets.com/~owls, thank you!! <3
"""
ncTradeValueText: String @cacheControl(maxAge: ${oneHour})
currentUserOwnsThis: Boolean! @cacheControl(maxAge: 0, scope: PRIVATE)
currentUserWantsThis: Boolean! @cacheControl(maxAge: 0, scope: PRIVATE)
@ -344,6 +356,20 @@ const resolvers = {
// This feature is deprecated, so now we just always return unknown value.
return null;
},
ncTradeValueText: async ({ id }, _, { itemNCTradeValueLoader }) => {
let ncTradeValue;
try {
ncTradeValue = await itemNCTradeValueLoader.load(id);
} catch (e) {
console.error(
`Error loading ncTradeValueText for item ${id}, skipping:`
);
console.error(e);
ncTradeValue = null;
}
return ncTradeValue ? ncTradeValue.valueText : null;
},
currentUserOwnsThis: async (
{ id },