Merge branch 'waka-ui' into main

This commit is contained in:
Emi Matchu 2021-04-08 18:32:46 -07:00
commit b80149440d
5 changed files with 145 additions and 38 deletions

View file

@ -14,9 +14,9 @@ import connectToDb from "../src/server/db";
async function handle(req, res) {
const allNcItemNamesAndIdsPromise = loadAllNcItemNamesAndIds();
let itemValuesByName;
let itemValuesByIdOrName;
try {
itemValuesByName = await loadWakaValuesByName();
itemValuesByIdOrName = await loadWakaValuesByIdOrName();
} catch (e) {
console.error(e);
res.setHeader("Content-Type", "text/plain");
@ -28,8 +28,10 @@ async function handle(req, res) {
const allNcItemNamesAndIds = await allNcItemNamesAndIdsPromise;
const itemValues = {};
for (const { name, id } of allNcItemNamesAndIds) {
if (name in itemValuesByName) {
itemValues[id] = itemValuesByName[name];
if (id in itemValuesByIdOrName) {
itemValues[id] = itemValuesByIdOrName[id];
} else if (name in itemValuesByIdOrName) {
itemValues[id] = itemValuesByIdOrName[name];
}
}
@ -56,10 +58,15 @@ async function loadAllNcItemNamesAndIds() {
AND item_translations.locale = "en"
`);
return rows;
return rows.map(({ id, name }) => ({ id, name: normalizeItemName(name) }));
}
async function loadWakaValuesByName() {
/**
* Load all Waka values from the spreadsheet. Returns an object keyed by ID or
* name - that is, if the item ID is provided in the sheet, we use that as the
* key; or if not, we use the name as the key.
*/
async function loadWakaValuesByIdOrName() {
if (!process.env["GOOGLE_API_KEY"]) {
throw new Error(`GOOGLE_API_KEY environment variable must be provided`);
}
@ -92,14 +99,35 @@ async function loadWakaValuesByName() {
// the spreadsheet columns that we don't use on DTI, like Notes.
//
// NOTE: The Sheets API only returns the first non-empty cells of the row.
// So, when there's no value specified, it only returns one cell.
// That's why we set `""` as the default `value`.
const itemValuesByName = {};
for (const [itemName, value = ""] of rows) {
itemValuesByName[itemName] = { value };
// That's why we set `""` as the defaults, in case the value/notes/etc
// aren't provided.
const itemValuesByIdOrName = {};
for (const [
itemName,
value = "",
notes = "",
marks = "",
itemId = "",
] of rows) {
const normalizedItemName = normalizeItemName(itemName);
itemValuesByIdOrName[itemId || normalizedItemName] = { value };
}
return itemValuesByName;
return itemValuesByIdOrName;
}
function normalizeItemName(name) {
return (
name
// Remove all spaces, they're a common source of inconsistency
.replace(/\s+/g, "")
// Lower case, because capitalization is another common source
.toLowerCase()
// Remove diacritics: https://stackoverflow.com/a/37511463/107415
// Waka has some stray ones in item names, not sure why!
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
);
}
export default async (req, res) => {

View file

@ -74,6 +74,7 @@ export function ItemPageContent({ itemId, isEmbedded }) {
thumbnailUrl
description
createdAt
wakaValueText
# For Support users.
rarityIndex

View file

@ -5,6 +5,7 @@ import {
Flex,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
Portal,
@ -15,7 +16,11 @@ import {
useToast,
VStack,
} from "@chakra-ui/react";
import { ExternalLinkIcon, ChevronRightIcon } from "@chakra-ui/icons";
import {
ExternalLinkIcon,
ChevronRightIcon,
QuestionIcon,
} from "@chakra-ui/icons";
import { gql, useMutation } from "@apollo/client";
import {
@ -150,6 +155,42 @@ function ItemPageBadges({ item, isEmbedded }) {
Jellyneo
</LinkBadge>
</SubtleSkeleton>
{item.isNc && (
<SubtleSkeleton
isLoaded={
// Distinguish between undefined (still loading) and null (loaded and
// empty).
item.wakaValueText !== undefined
}
>
{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">
Waka: {item.wakaValueText}
</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">
Waka: {item.wakaValueText}
</LinkBadge>
<WakaPopover>
<Flex align="center" fontSize="sm" paddingX="2" tabIndex="0">
<QuestionIcon />
</Flex>
</WakaPopover>
</Flex>
</>
)}
</SubtleSkeleton>
)}
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
{!item?.isNc && !item?.isPb && (
<LinkBadge
@ -317,27 +358,36 @@ function ItemKindBadgeWithSupportTools({ item }) {
return <ItemKindBadge isNc={item.isNc} isPb={item.isPb} />;
}
function LinkBadge({ children, href, isEmbedded }) {
return (
<Badge
as="a"
href={href}
display="flex"
alignItems="center"
// Normally we want to act like a normal webpage, and treat links as
// normal. But when we're on the wardrobe page, we want to avoid
// disrupting the outfit, and open in a new window instead.
target={isEmbedded ? "_blank" : undefined}
>
{children}
{
// We also change the icon to signal whether this will launch in a new
// window or not!
isEmbedded ? <ExternalLinkIcon marginLeft="1" /> : <ChevronRightIcon />
}
</Badge>
);
}
const LinkBadge = React.forwardRef(
({ children, href, isEmbedded, ...props }, ref) => {
return (
<Badge
ref={ref}
as="a"
href={href}
display="flex"
alignItems="center"
// Normally we want to act like a normal webpage, and treat links as
// normal. But when we're on the wardrobe page, we want to avoid
// disrupting the outfit, and open in a new window instead.
target={isEmbedded ? "_blank" : undefined}
_focus={{ outline: "none", boxShadow: "outline" }}
{...props}
>
{children}
{
// We also change the icon to signal whether this will launch in a new
// window or not!
isEmbedded ? (
<ExternalLinkIcon marginLeft="1" />
) : (
<ChevronRightIcon />
)
}
</Badge>
);
}
);
const fullDateFormatter = new Intl.DateTimeFormat("en-US", {
dateStyle: "long",
@ -380,4 +430,30 @@ function ShortTimestamp({ when }) {
);
}
function WakaPopover({ children, ...props }) {
return (
<Popover placement="bottom" {...props}>
<PopoverTrigger>{children}</PopoverTrigger>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverBody fontSize="sm">
<p>
Waka is a community resource that tracks the approximate value of
NC items, based on real-world trades.
</p>
<Box height="1em" />
<p>
The Waka Team aims to maintain the accuracy of the guide as fully
as possible, but please remember values are often changing and
with certain difficult-to-find or popular items it doesn't hurt to
make a value check!
</p>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
}
export default ItemPageLayout;

View file

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

View file

@ -608,10 +608,11 @@ const buildItemWakaValueLoader = () =>
// API would, and avoid putting pressure on our Google Sheets API quotas.
// (Some kind of internal memcache or process-level cache would be a more
// idiomatic solution in a monolith server environment!)
const url =
process.env.NODE_ENV === "production"
? "https://impress-2020.openneo.net/api/allWakaValues"
: "http://localhost:3000/api/allWakaValues";
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/allWakaValues`
: process.env.NODE_ENV === "production"
? "https://impress-2020.openneo.net/api/allWakaValues"
: "http://localhost:3000/api/allWakaValues";
const res = await fetch(url);
if (!res.ok) {
throw new Error(