1
0
Fork 0
forked from OpenNeo/impress

Compare commits

..

No commits in common. "2ca349d5b09fe640fcefe59e6489f7178dd65644" and "eb915ae851c2b80bcb532a4b100bb1e6e04fe97e" have entirely different histories.

15 changed files with 1197 additions and 212 deletions

View file

@ -14,4 +14,3 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIu5a+mp2KKSGkOGWQPrARCrsqJS4g2vK7TmRIbj/YBh Matchu's Desktop (Leviathan 2023)
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKFwWryq6slOQqkrJ7HIig7BvEQVQeH19hFwb+9VpXgz Matchu's Laptop (Ebon Hawk)
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINq0HDYIUwRnrlKBWyGWJbJsx3M8nLg4nRxaA+9lJp+o Matchu's Laptop (Death Star)

View file

@ -29,17 +29,6 @@ module.exports = {
destination: "/user/:userId/lists",
permanent: true,
},
{
source: "/items/:itemId/trades/offering",
destination:
"https://impress.openneo.net/items/:itemId/trades/offering",
permanent: true,
},
{
source: "/items/:itemId/trades/seeking",
destination: "https://impress.openneo.net/items/:itemId/trades/seeking",
permanent: true,
},
];
},
};

View file

@ -16,6 +16,9 @@
"@sendgrid/mail": "^7.2.6",
"@sentry/react": "^5.30.0",
"@sentry/tracing": "^5.30.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/node": "^14.14.22",
"@types/react": "^18.2.34",
"@types/react-dom": "^17.0.0",

View file

@ -46,6 +46,12 @@ class MyDocument extends Document {
`,
}}
/>
<script
defer
data-domain="impress-2020.openneo.net"
src="https://analytics.openneo.net/js/script.js"
dangerouslySetInnerHTML={{ __html: `` }}
/>
<link
rel="preload"
href="/fonts/Delicious-Heavy.otf"
@ -67,12 +73,6 @@ class MyDocument extends Document {
<body>
<Main />
<NextScript />
<script
defer
data-domain="impress-2020.openneo.net"
src="https://analytics.openneo.net/js/script.js"
dangerouslySetInnerHTML={{ __html: `` }}
/>
</body>
</Html>
);

View file

@ -17,8 +17,11 @@ export async function getValidPetPoses() {
const largestColorIdPromise = getLargestColorId(db);
const distinctPetStatesPromise = getDistinctPetStates(db);
const [largestSpeciesId, largestColorId, distinctPetStates] =
await Promise.all([
const [
largestSpeciesId,
largestColorId,
distinctPetStates,
] = await Promise.all([
largestSpeciesIdPromise,
largestColorIdPromise,
distinctPetStatesPromise,
@ -87,7 +90,7 @@ async function getLargestSpeciesId(db) {
}
async function getLargestColorId(db) {
const [rows] = await db.query(`SELECT max(id) FROM colors`);
const [rows] = await db.query(`SELECT max(id) FROM colors WHERE prank = 0`);
return rows[0]["max(id)"];
}
@ -121,7 +124,7 @@ async function handle(req, res) {
async function handleWithBeeline(req, res) {
beeline.withTrace(
{ name: "api/validPetPoses", operation_name: "api/validPetPoses" },
() => handle(req, res),
() => handle(req, res)
);
}

View file

@ -0,0 +1,54 @@
import { GetServerSideProps } from "next";
import { ItemTradesOfferingPage } from "../../../../src/app/ItemTradesPage";
import { gql, loadGraphqlQuery } from "../../../../src/server/ssr-graphql";
// @ts-ignore doesn't understand module.exports
import { oneDay, oneWeek } from "../../../../src/server/util";
export default function ItemTradesOfferingPageWrapper() {
return <ItemTradesOfferingPage />;
}
export const getServerSideProps: GetServerSideProps = async ({
params,
res,
}) => {
if (params?.itemId == null) {
throw new Error(`assertion error: itemId param is missing`);
}
// Load the most important, most stable item data to get onto the page ASAP.
// We'll cache it real hard, to help it load extra-fast for popular items!
const { errors, graphqlState } = await loadGraphqlQuery({
query: gql`
query ItemsTradesOffering_GetServerSideProps($itemId: ID!) {
item(id: $itemId) {
id
name
thumbnailUrl
description
isNc
isPb
createdAt
}
}
`,
variables: { itemId: params.itemId },
});
if (errors) {
console.warn(
`[SSR: /items/[itemId]/trades/offering] Skipping GraphQL preloading, got errors:`
);
for (const error of errors) {
console.warn(`[SSR: /items/[itemId]/trades/offering]`, error);
}
return { props: { graphqlState: {} } };
}
// Cache this very aggressively, because it's such stable data!
res.setHeader(
"Cache-Control",
`public, s-maxage=${oneDay}, stale-while-revalidate=${oneWeek}`
);
return { props: { graphqlState } };
};

View file

@ -0,0 +1,54 @@
import { GetServerSideProps } from "next";
import { ItemTradesSeekingPage } from "../../../../src/app/ItemTradesPage";
import { gql, loadGraphqlQuery } from "../../../../src/server/ssr-graphql";
// @ts-ignore doesn't understand module.exports
import { oneDay, oneWeek } from "../../../../src/server/util";
export default function ItemTradesSeekingPageWrapper() {
return <ItemTradesSeekingPage />;
}
export const getServerSideProps: GetServerSideProps = async ({
params,
res,
}) => {
if (params?.itemId == null) {
throw new Error(`assertion error: itemId param is missing`);
}
// Load the most important, most stable item data to get onto the page ASAP.
// We'll cache it real hard, to help it load extra-fast for popular items!
const { errors, graphqlState } = await loadGraphqlQuery({
query: gql`
query ItemsTradesSeeking_GetServerSideProps($itemId: ID!) {
item(id: $itemId) {
id
name
thumbnailUrl
description
isNc
isPb
createdAt
}
}
`,
variables: { itemId: params.itemId },
});
if (errors) {
console.warn(
`[SSR: /items/[itemId]/trades/seeking] Skipping GraphQL preloading, got errors:`
);
for (const error of errors) {
console.warn(`[SSR: /items/[itemId]/trades/seeking]`, error);
}
return { props: { graphqlState: {} } };
}
// Cache this very aggressively, because it's such stable data!
res.setHeader(
"Cache-Control",
`public, s-maxage=${oneDay}, stale-while-revalidate=${oneWeek}`
);
return { props: { graphqlState } };
};

View file

@ -92,7 +92,7 @@ export function ItemPageContent({ itemId, isEmbedded = false }) {
}
}
`,
{ variables: { itemId }, returnPartialData: true },
{ variables: { itemId }, returnPartialData: true }
);
if (error) {
@ -302,7 +302,7 @@ const ItemPageOwnWantListsDropdownButton = React.forwardRef(
</Flex>
</Flex>
);
},
}
);
function ItemPageOwnWantListsDropdownContent({ closetLists, item }) {
@ -336,7 +336,7 @@ function ItemPageOwnWantsListsDropdownRow({ closetList, item }) {
}
}
`,
{ context: { sendAuth: true } },
{ context: { sendAuth: true } }
);
const [sendRemoveFromListMutation] = useMutation(
@ -352,7 +352,7 @@ function ItemPageOwnWantsListsDropdownRow({ closetList, item }) {
}
}
`,
{ context: { sendAuth: true } },
{ context: { sendAuth: true } }
);
const onChange = React.useCallback(
@ -397,13 +397,7 @@ function ItemPageOwnWantsListsDropdownRow({ closetList, item }) {
});
}
},
[
closetList,
item,
sendAddToListMutation,
sendRemoveFromListMutation,
toast,
],
[closetList, item, sendAddToListMutation, sendRemoveFromListMutation, toast]
);
return (
@ -451,7 +445,7 @@ function ItemPageOwnButton({ itemId, isChecked }) {
context: { sendAuth: true },
},
],
},
}
);
const [sendRemoveMutation] = useMutation(
@ -482,7 +476,7 @@ function ItemPageOwnButton({ itemId, isChecked }) {
context: { sendAuth: true },
},
],
},
}
);
return (
@ -577,7 +571,7 @@ function ItemPageWantButton({ itemId, isChecked }) {
context: { sendAuth: true },
},
],
},
}
);
const [sendRemoveMutation] = useMutation(
@ -608,7 +602,7 @@ function ItemPageWantButton({ itemId, isChecked }) {
context: { sendAuth: true },
},
],
},
}
);
return (
@ -682,7 +676,7 @@ function ItemPageTradeLinks({ itemId, isEmbedded }) {
}
}
`,
{ variables: { itemId } },
{ variables: { itemId } }
);
if (error) {
@ -696,7 +690,7 @@ function ItemPageTradeLinks({ itemId, isEmbedded }) {
</Box>
<SubtleSkeleton isLoaded={!loading}>
<ItemPageTradeLink
href={`https://impress.openneo.net/items/${itemId}/trades/offering`}
href={`/items/${itemId}/trades/offering`}
count={data?.item?.numUsersOfferingThis || 0}
label="offering"
colorScheme="green"
@ -705,7 +699,7 @@ function ItemPageTradeLinks({ itemId, isEmbedded }) {
</SubtleSkeleton>
<SubtleSkeleton isLoaded={!loading}>
<ItemPageTradeLink
href={`https://impress.openneo.net/items/${itemId}/trades/seeking`}
href={`/items/${itemId}/trades/seeking`}
count={data?.item?.numUsersSeekingThis || 0}
label="seeking"
colorScheme="blue"
@ -775,7 +769,7 @@ function IconCheckbox({ icon, isChecked, ...props }) {
function ItemPageOutfitPreview({ itemId }) {
const idealPose = React.useMemo(
() => (Math.random() > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC"),
[],
[]
);
const [petState, setPetState] = React.useState({
// We'll fill these in once the canonical appearance data arrives.
@ -793,11 +787,11 @@ function ItemPageOutfitPreview({ itemId }) {
});
const [preferredSpeciesId, setPreferredSpeciesId] = useLocalStorage(
"DTIItemPreviewPreferredSpeciesId",
null,
null
);
const [preferredColorId, setPreferredColorId] = useLocalStorage(
"DTIItemPreviewPreferredColorId",
null,
null
);
const setPetStateFromUserAction = React.useCallback(
@ -832,7 +826,7 @@ function ItemPageOutfitPreview({ itemId }) {
return newPetState;
}),
[setPreferredColorId, setPreferredSpeciesId],
[setPreferredColorId, setPreferredSpeciesId]
);
// We don't need to reload this query when preferred species/color change, so
@ -851,11 +845,7 @@ function ItemPageOutfitPreview({ itemId }) {
// query after this loads, because our Apollo cache can't detect the
// shared item appearance. (For standard colors though, our logic to
// cover standard-color switches works for this preloading too.)
const {
loading: loadingGQL,
error: errorGQL,
data,
} = useQuery(
const { loading: loadingGQL, error: errorGQL, data } = useQuery(
gql`
query ItemPageOutfitPreview(
$itemId: ID!
@ -930,7 +920,7 @@ function ItemPageOutfitPreview({ itemId }) {
appearanceId: canonicalPetAppearance?.id,
});
},
},
}
);
const compatibleBodies =
@ -946,7 +936,7 @@ function ItemPageOutfitPreview({ itemId }) {
compatibleBodies.length === 1 &&
!compatibleBodies[0].representsAllBodies &&
(data?.item?.name || "").includes(
data?.item?.canonicalAppearance?.body?.canonicalAppearance?.species?.name,
data?.item?.canonicalAppearance?.body?.canonicalAppearance?.species?.name
);
const couldProbablyModelMoreData = !isProbablySpeciesSpecific;
@ -992,7 +982,7 @@ function ItemPageOutfitPreview({ itemId }) {
appearanceId: null,
});
},
[valids, idealPose, setPetStateFromUserAction],
[valids, idealPose, setPetStateFromUserAction]
);
const borderColor = useColorModeValue("green.700", "green.400");
@ -1211,8 +1201,8 @@ function ExpandOnGroupHover({ children, ...props }) {
// I don't think this is possible, but I'd like to know if it happens!
logAndCapture(
new Error(
`Measurer node not ready during effect. Transition won't be smooth.`,
),
`Measurer node not ready during effect. Transition won't be smooth.`
)
);
return;
}
@ -1293,8 +1283,8 @@ export function ItemZonesInfo({
const sortedZonesAndTheirBodies = [...zoneLabelsAndTheirBodies].sort((a, b) =>
buildSortKeyForZoneLabelsAndTheirBodies(a).localeCompare(
buildSortKeyForZoneLabelsAndTheirBodies(b),
),
buildSortKeyForZoneLabelsAndTheirBodies(b)
)
);
const restrictedZoneLabels = [
@ -1306,8 +1296,8 @@ export function ItemZonesInfo({
// preview available in the list has the zones listed here.
const bodyGroups = new Set(
zoneLabelsAndTheirBodies.map(({ bodies }) =>
bodies.map((b) => b.id).join(","),
),
bodies.map((b) => b.id).join(",")
)
);
const showBodyInfo = bodyGroups.size > 1;

View file

@ -30,11 +30,7 @@ function SpeciesFacesPicker({
// the query all the time, and have Apollo happen to satisfy it fast?
// The semantics of returning our colorful random set could be weird…
const selectedColorIsBasic = colorIsBasic(selectedColorId);
const {
loading: loadingGQL,
error,
data,
} = useQuery(
const { loading: loadingGQL, error, data } = useQuery(
gql`
query SpeciesFacesPicker($selectedColorId: ID!) {
color(id: $selectedColorId) {
@ -56,11 +52,11 @@ function SpeciesFacesPicker({
variables: { selectedColorId },
skip: selectedColorId == null || selectedColorIsBasic,
onError: (e) => console.error(e),
},
}
);
const allBodiesAreCompatible = compatibleBodies.some(
(body) => body.representsAllBodies,
(body) => body.representsAllBodies
);
const compatibleBodyIds = compatibleBodies.map((body) => body.id);
@ -68,7 +64,7 @@ function SpeciesFacesPicker({
const allSpeciesFaces = DEFAULT_SPECIES_FACES.map((defaultSpeciesFace) => {
const providedSpeciesFace = speciesFacesFromData.find(
(f) => f.species.id === defaultSpeciesFace.speciesId,
(f) => f.species.id === defaultSpeciesFace.speciesId
);
if (providedSpeciesFace) {
return {
@ -263,10 +259,10 @@ const SpeciesFaceOption = React.memo(
`}
>
<CrossFadeImage
src={`https://pets.neopets.com/cp/${neopetsImageHash}/${emotionId}/1.png`}
src={`https://pets.neopets-asset-proxy.openneo.net/cp/${neopetsImageHash}/${emotionId}/1.png`}
srcSet={
`https://pets.neopets.com/cp/${neopetsImageHash}/${emotionId}/1.png 1x, ` +
`https://pets.neopets.com/cp/${neopetsImageHash}/${emotionId}/6.png 2x`
`https://pets.neopets-asset-proxy.openneo.net/cp/${neopetsImageHash}/${emotionId}/1.png 1x, ` +
`https://pets.neopets-asset-proxy.openneo.net/cp/${neopetsImageHash}/${emotionId}/6.png 2x`
}
alt={speciesName}
width={55}
@ -324,7 +320,7 @@ const SpeciesFaceOption = React.memo(
)}
</ClassNames>
);
},
}
);
/**

517
src/app/ItemTradesPage.js Normal file
View file

@ -0,0 +1,517 @@
import React from "react";
import { ClassNames } from "@emotion/react";
import {
Box,
Button,
Flex,
Skeleton,
useColorModeValue,
useToken,
} from "@chakra-ui/react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
import Link from "next/link";
import { useRouter } from "next/router";
import { Heading2 } from "./util";
import ItemPageLayout from "./ItemPageLayout";
import useCurrentUser from "./components/useCurrentUser";
import { ChevronDownIcon, ChevronUpIcon } from "@chakra-ui/icons";
import Head from "next/head";
export function ItemTradesOfferingPage() {
return (
<ItemTradesPage
title="Trades: Offering"
userHeading="Owner"
compareColumnLabel="Trade for your…"
tradesQuery={gql`
query ItemTradesTableOffering($itemId: ID!) {
item(id: $itemId) {
id
trades: tradesOffering {
id
user {
id
username
lastTradeActivity
matchingItems: itemsTheyWantThatCurrentUserOwns {
id
name
}
}
closetList {
id
name
}
}
}
}
`}
/>
);
}
export function ItemTradesSeekingPage() {
return (
<ItemTradesPage
title="Trades: Seeking"
userHeading="Seeker"
compareColumnLabel="Trade for their…"
tradesQuery={gql`
query ItemTradesTableSeeking($itemId: ID!) {
item(id: $itemId) {
id
trades: tradesSeeking {
id
user {
id
username
lastTradeActivity
matchingItems: itemsTheyOwnThatCurrentUserWants {
id
name
}
}
closetList {
id
name
}
}
}
}
`}
/>
);
}
function ItemTradesPage({
title,
userHeading,
compareColumnLabel,
tradesQuery,
}) {
const { query } = useRouter();
const { itemId } = query;
const { error, data } = useQuery(
gql`
query ItemTradesPage($itemId: ID!) {
item(id: $itemId) {
id
name
isNc
isPb
thumbnailUrl
description
createdAt
ncTradeValueText
}
}
`,
{ variables: { itemId }, returnPartialData: true }
);
if (error) {
return <Box color="red.400">{error.message}</Box>;
}
return (
<>
<Head>
{data?.item?.name && (
<title>
{data?.item?.name} | {title} | Dress to Impress
</title>
)}
</Head>
<ItemPageLayout item={data?.item}>
<Heading2 marginTop="6" marginBottom="4">
{title}
</Heading2>
<ItemTradesTable
itemId={itemId}
userHeading={userHeading}
compareColumnLabel={compareColumnLabel}
tradesQuery={tradesQuery}
/>
</ItemPageLayout>
</>
);
}
function ItemTradesTable({
itemId,
userHeading,
compareColumnLabel,
tradesQuery,
}) {
const { isLoggedIn } = useCurrentUser();
const { loading, error, data } = useQuery(tradesQuery, {
variables: { itemId },
context: { sendAuth: true },
});
const [isShowingInactiveTrades, setIsShowingInactiveTrades] = React.useState(
false
);
const shouldShowCompareColumn = isLoggedIn;
// We partially randomize trade sorting, but we want it to stay stable across
// re-renders. To do this, we can use `getTradeSortKey`, which will either
// build a new sort key for the trade, or return the cached one from the
// `tradeSortKeys` map.
const tradeSortKeys = React.useMemo(() => new Map(), []);
const getTradeSortKey = (trade) => {
if (!tradeSortKeys.has(trade.id)) {
tradeSortKeys.set(
trade.id,
getVaguelyRandomizedTradeSortKey(
trade.user.lastTradeActivity,
trade.user.matchingItems.length
)
);
}
return tradeSortKeys.get(trade.id);
};
const allTrades = [...(data?.item?.trades || [])];
// Only trades from users active within the last 6 months are shown by
// default. The user can toggle to the full view, though!
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
const activeTrades = allTrades.filter(
(t) => new Date(t.user.lastTradeActivity) > sixMonthsAgo
);
const trades = isShowingInactiveTrades ? allTrades : activeTrades;
trades.sort((a, b) => getTradeSortKey(b).localeCompare(getTradeSortKey(a)));
const numInactiveTrades = allTrades.length - activeTrades.length;
if (error) {
return <Box color="red.400">{error.message}</Box>;
}
const minorColumnWidth = {
base: shouldShowCompareColumn ? "23%" : "30%",
md: "20ex",
};
return (
<ClassNames>
{({ css }) => (
<Box>
<Box
as="table"
width="100%"
boxShadow="md"
className={css`
/* Chakra doesn't have props for these! */
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
`}
>
<Box as="thead" fontSize={{ base: "xs", sm: "sm" }}>
<Box as="tr">
<ItemTradesTableCell as="th" width={minorColumnWidth}>
{/* A small wording tweak to fit better on the xsmall screens! */}
<Box display={{ base: "none", sm: "block" }}>Last active</Box>
<Box display={{ base: "block", sm: "none" }}>Last edit</Box>
</ItemTradesTableCell>
{shouldShowCompareColumn && (
<ItemTradesTableCell as="th" width={minorColumnWidth}>
<Box display={{ base: "none", sm: "block" }}>
{compareColumnLabel}
</Box>
<Box display={{ base: "block", sm: "none" }}>Matches</Box>
</ItemTradesTableCell>
)}
<ItemTradesTableCell as="th" width={minorColumnWidth}>
{userHeading}
</ItemTradesTableCell>
<ItemTradesTableCell as="th">List</ItemTradesTableCell>
</Box>
</Box>
<Box as="tbody">
{loading && (
<>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
</>
)}
{!loading &&
trades.length > 0 &&
trades.map((trade) => (
<ItemTradesTableRow
key={trade.id}
href={`/user/${trade.user.id}/lists#list-${trade.closetList.id}`}
username={trade.user.username}
listName={trade.closetList.name}
lastTradeActivity={trade.user.lastTradeActivity}
matchingItems={trade.user.matchingItems}
shouldShowCompareColumn={shouldShowCompareColumn}
/>
))}
{!loading && trades.length === 0 && (
<Box as="tr">
<ItemTradesTableCell
colSpan={shouldShowCompareColumn ? 4 : 3}
textAlign="center"
fontStyle="italic"
>
No trades yet!
</ItemTradesTableCell>
</Box>
)}
</Box>
</Box>
{numInactiveTrades > 0 && (
<Flex justify="center">
<Button
size="sm"
variant="outline"
marginTop="4"
onClick={() => setIsShowingInactiveTrades((s) => !s)}
>
{isShowingInactiveTrades ? (
<>
<ChevronUpIcon marginRight="2" />
Hide {numInactiveTrades} older trades
<ChevronUpIcon marginLeft="2" />
</>
) : (
<>
<ChevronDownIcon marginRight="2" />
Show {numInactiveTrades} more older trades
<ChevronDownIcon marginLeft="2" />
</>
)}
</Button>
</Flex>
)}
</Box>
)}
</ClassNames>
);
}
function ItemTradesTableRow({
href,
username,
listName,
lastTradeActivity,
matchingItems,
shouldShowCompareColumn,
}) {
const { push: pushHistory } = useRouter();
const onClick = React.useCallback(() => pushHistory(href), [
pushHistory,
href,
]);
const focusBackground = useColorModeValue("gray.100", "gray.600");
const sortedMatchingItems = [...matchingItems].sort((a, b) =>
a.name.localeCompare(b.name)
);
return (
<ClassNames>
{({ css }) => (
<Box
as="tr"
cursor="pointer"
_hover={{ background: focusBackground }}
_focusWithin={{ background: focusBackground }}
onClick={onClick}
>
<ItemTradesTableCell fontSize="xs">
{formatVagueDate(lastTradeActivity)}
</ItemTradesTableCell>
{shouldShowCompareColumn && (
<ItemTradesTableCell fontSize="xs">
{matchingItems.length > 0 ? (
<Box as="ul">
{sortedMatchingItems.slice(0, 4).map((item) => (
<Box key={item.id} as="li">
<Box
lineHeight="1.5"
maxHeight="1.5em"
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
{item.name}
</Box>
</Box>
))}
{matchingItems.length > 4 && (
<Box as="li">+ {matchingItems.length - 4} more</Box>
)}
</Box>
) : (
<>
<Box display={{ base: "none", sm: "block" }}>No matches</Box>
<Box display={{ base: "block", sm: "none" }}>None</Box>
</>
)}
</ItemTradesTableCell>
)}
<ItemTradesTableCell fontSize="xs">{username}</ItemTradesTableCell>
<ItemTradesTableCell fontSize="sm">
<Link href={href} passHref>
<Box
as="a"
className={css`
&:hover,
&:focus,
tr:hover &,
tr:focus-within & {
text-decoration: underline;
}
`}
>
{listName}
</Box>
</Link>
</ItemTradesTableCell>
</Box>
)}
</ClassNames>
);
}
function ItemTradesTableRowSkeleton({ shouldShowCompareColumn }) {
return (
<Box as="tr">
<ItemTradesTableCell>
<Skeleton width="100%">X</Skeleton>
</ItemTradesTableCell>
<ItemTradesTableCell>
<Skeleton width="100%">X</Skeleton>
</ItemTradesTableCell>
<ItemTradesTableCell>
<Skeleton width="100%">X</Skeleton>
</ItemTradesTableCell>
{shouldShowCompareColumn && (
<ItemTradesTableCell>
<Skeleton width="100%">X</Skeleton>
</ItemTradesTableCell>
)}
</Box>
);
}
function ItemTradesTableCell({ children, as = "td", ...props }) {
const borderColor = useColorModeValue("gray.300", "gray.400");
const borderColorCss = useToken("colors", borderColor);
const borderRadiusCss = useToken("radii", "md");
return (
<ClassNames>
{({ css }) => (
<Box
as={as}
paddingX="4"
paddingY="2"
textAlign="left"
className={css`
/* Lol sigh, getting this right is way more involved than I wish it
* were. What I really want is border-collapse and a simple 1px border,
* but that disables border-radius. So, we homebrew it by giving all
* cells bottom and right borders, but only the cells on the edges a
* top or left border; and then target the exact 4 corner cells to
* round them. Pretty old-school tbh 🙃 */
border-bottom: 1px solid ${borderColorCss};
border-right: 1px solid ${borderColorCss};
thead tr:first-of-type & {
border-top: 1px solid ${borderColorCss};
}
&:first-of-type {
border-left: 1px solid ${borderColorCss};
}
thead tr:first-of-type &:first-of-type {
border-top-left-radius: ${borderRadiusCss};
}
thead tr:first-of-type &:last-of-type {
border-top-right-radius: ${borderRadiusCss};
}
tbody tr:last-of-type &:first-of-type {
border-bottom-left-radius: ${borderRadiusCss};
}
tbody tr:last-of-type &:last-of-type {
border-bottom-right-radius: ${borderRadiusCss};
}
`}
{...props}
>
{children}
</Box>
)}
</ClassNames>
);
}
function isThisWeek(date) {
const startOfThisWeek = new Date();
startOfThisWeek.setDate(startOfThisWeek.getDate() - 7);
return date > startOfThisWeek;
}
const shortMonthYearFormatter = new Intl.DateTimeFormat("en", {
month: "short",
year: "numeric",
});
function formatVagueDate(dateString) {
const date = new Date(dateString);
if (isThisWeek(date)) {
return "This week";
}
return shortMonthYearFormatter.format(date);
}
function getVaguelyRandomizedTradeSortKey(dateString, numMatchingItems) {
const date = new Date(dateString);
const hasMatchingItems = numMatchingItems >= 1;
// "This week" sorts after all other dates, but with a random factor! I don't
// want people worrying about gaming themselves up to the very top, just be
// active and trust the system 😅 (I figure that, if you care enough to "game"
// the system by faking activity every week, you probably also care enough to
// be... making real trades every week lmao)
//
// We also prioritize having matches, but we don't bother to sort _how many_
// matches, to decrease the power of gaming with large honeypot lists, and
// because it's hard to judge how good matches are anyway.
if (isThisWeek(date)) {
const matchingItemsKey = hasMatchingItems
? "ZZmatchingZZ"
: "AAnotmatchingAA";
return `ZZZthisweekZZZ-${matchingItemsKey}-${Math.random()}`;
}
return dateString;
}

View file

@ -117,7 +117,7 @@ export function useCommonStyles() {
*/
export function safeImageUrl(
urlString,
{ crossOrigin = null, preferArchive = false } = {},
{ crossOrigin = null, preferArchive = false } = {}
) {
if (urlString == null) {
return urlString;
@ -133,13 +133,13 @@ export function safeImageUrl(
// So, we provide "http://images.neopets.com" as the base URL when
// parsing. Most URLs are absolute and will ignore it, but relative URLs
// will resolve relative to that base.
"http://images.neopets.com",
"http://images.neopets.com"
);
} catch (e) {
logAndCapture(
new Error(
`safeImageUrl could not parse URL: ${urlString}. Returning a placeholder.`,
),
`safeImageUrl could not parse URL: ${urlString}. Returning a placeholder.`
)
);
return "https://impress-2020.openneo.net/__error__URL-was-not-parseable__";
}
@ -154,16 +154,12 @@ export function safeImageUrl(
if (preferArchive) {
const archiveUrl = new URL(
`/api/readFromArchive`,
window.location.origin,
window.location.origin
);
archiveUrl.search = new URLSearchParams({ url: url.toString() });
url = archiveUrl;
} else if (crossOrigin) {
// NOTE: Previously we would rewrite this to our proxy that adds an
// `Access-Control-Allow-Origin` header (images.neopets-asset-proxy.
// openneo.net), but images.neopets.com now includes this header for us!
//
// So, do nothing!
url.host = "images.neopets-asset-proxy.openneo.net";
}
} else if (
url.origin === "http://pets.neopets.com" ||
@ -179,8 +175,8 @@ export function safeImageUrl(
logAndCapture(
new Error(
`safeImageUrl was provided an unsafe URL, but we don't know how to ` +
`upgrade it to HTTPS: ${urlString}. Returning a placeholder.`,
),
`upgrade it to HTTPS: ${urlString}. Returning a placeholder.`
)
);
return "https://impress-2020.openneo.net/__error__URL-was-not-HTTPS__";
}
@ -201,11 +197,11 @@ export function safeImageUrl(
export function useDebounce(
value,
delay,
{ waitForFirstPause = false, initialValue = null, forceReset = null } = {},
{ waitForFirstPause = false, initialValue = null, forceReset = null } = {}
) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = React.useState(
waitForFirstPause ? initialValue : value,
waitForFirstPause ? initialValue : value
);
React.useEffect(
@ -222,7 +218,7 @@ export function useDebounce(
clearTimeout(handler);
};
},
[value, delay], // Only re-call effect if value or delay changes
[value, delay] // Only re-call effect if value or delay changes
);
// The `forceReset` option helps us decide whether to set the value
@ -326,7 +322,7 @@ export function useLocalStorage(key, initialValue) {
console.error(error);
}
},
[key],
[key]
);
const reloadValue = React.useCallback(() => {
@ -353,7 +349,7 @@ export function useLocalStorage(key, initialValue) {
export function loadImage(
rawSrc,
{ crossOrigin = null, preferArchive = false } = {},
{ crossOrigin = null, preferArchive = false } = {}
) {
const src = safeImageUrl(rawSrc, { crossOrigin, preferArchive });
const image = new Image();
@ -407,7 +403,7 @@ export function loadable(load, options) {
// Return a component that renders nothing, while we reload!
return () => null;
}),
options,
options
);
}

View file

@ -33,11 +33,13 @@ export async function loadCustomPetData(petName) {
// prepending "@", which is a special code that can *also* be used in the
// CustomPetService in place of name, to get a pet's appearance from its image
// hash.
if (petName.match(/^[0-9]/)) {
const imageHash = await loadImageHashFromPetName(petName);
console.debug(
`[loadCustomPetData] Converted pet name ${petName} to @${imageHash}`,
);
petName = "@" + imageHash;
}
try {
return neopetsAmfphpCall("CustomPetService.getViewerData", [petName]);

View file

@ -60,7 +60,7 @@ const buildColorLoader = (db) => {
const colorLoader = new DataLoader(async (colorIds) => {
const qs = colorIds.map((_) => "?").join(",");
const [rows] = await db.execute(
`SELECT * FROM colors WHERE id IN (${qs})`,
`SELECT * FROM colors WHERE id IN (${qs}) AND prank = 0`,
colorIds,
);
@ -75,7 +75,7 @@ const buildColorLoader = (db) => {
});
colorLoader.loadAll = async () => {
const [rows] = await db.execute(`SELECT * FROM colors`);
const [rows] = await db.execute(`SELECT * FROM colors WHERE prank = 0`);
const entities = rows.map(normalizeRow);
for (const color of entities) {

View file

@ -1018,45 +1018,12 @@ const resolvers = {
return null;
}
const connection = await db.getConnection();
try {
// Get the relevant lists this is currently in.
await connection.beginTransaction();
const [rows] = await connection.query(
`SELECT DISTINCT list_id FROM closet_hangers
WHERE item_id = ? AND user_id = ? AND owned = ? AND
list_id IS NOT NULL`,
[itemId, currentUserId, true],
);
// Mark all these lists as updated.
const listIds = rows.map((row) => row.list_id);
const qs = listIds.map((_) => "?");
const now = new Date();
await connection.query(
`UPDATE closet_lists SET updated_at = ? WHERE id IN (${qs})`,
[now, ...listIds],
);
// Delete all these hangers. (NOTE: This includes ones not in a list!)
await connection.query(
await db.query(
`DELETE FROM closet_hangers
WHERE item_id = ? AND user_id = ? AND owned = ?;`,
[itemId, currentUserId, true],
);
await connection.commit();
} catch (error) {
try {
await connection.rollback();
} catch (error2) {
console.warn(`Error rolling back transaction`, error2);
}
throw error;
} finally {
await connection.release();
}
return { id: itemId };
},
addToItemsCurrentUserWants: async (
@ -1116,45 +1083,12 @@ const resolvers = {
return null;
}
const connection = await db.getConnection();
try {
// Get the relevant lists this is currently in.
await connection.beginTransaction();
const [rows] = await connection.query(
`SELECT DISTINCT list_id FROM closet_hangers
WHERE item_id = ? AND user_id = ? AND owned = ? AND
list_id IS NOT NULL`,
[itemId, currentUserId, false],
);
// Mark all these lists as updated.
const listIds = rows.map((row) => row.list_id);
const qs = listIds.map((_) => "?");
const now = new Date();
await connection.query(
`UPDATE closet_lists SET updated_at = ? WHERE id IN (${qs})`,
[now, ...listIds],
);
// Delete all these hangers. (NOTE: This includes ones not in a list!)
await connection.query(
await db.query(
`DELETE FROM closet_hangers
WHERE item_id = ? AND user_id = ? AND owned = ?;`,
[itemId, currentUserId, false],
);
await connection.commit();
} catch (error) {
try {
await connection.rollback();
} catch (error2) {
console.warn(`Error rolling back transaction`, error2);
}
throw error;
} finally {
await connection.release();
}
return { id: itemId };
},
addItemToClosetList: async (
@ -1204,14 +1138,6 @@ const resolvers = {
[itemId, userId, ownsOrWantsItems === "OWNS", listId, 1, now, now],
);
// Finally, touch the `updated_at` timestamp for the list.
await connection.query(
`
UPDATE closet_lists SET updated_at = ? WHERE id = ? LIMIT 1;
`,
[now, listId],
);
await connection.commit();
} catch (error) {
try {
@ -1256,9 +1182,6 @@ const resolvers = {
const connection = await db.getConnection();
try {
await connection.beginTransaction();
const now = new Date();
// First, remove the hanger from the list.
await connection.query(
`
DELETE FROM closet_hangers
@ -1267,16 +1190,6 @@ const resolvers = {
[...listMatcherValues, itemId],
);
// Then, touch the `updated_at` timestamp for the list.
if (!closetListRef.isDefaultList) {
await connection.query(
`
UPDATE closet_lists SET updated_at = ? WHERE id = ? LIMIT 1;
`,
[now, closetListRef.id],
);
}
if (ensureInSomeList) {
// If requested, we check whether the item is still in *some* list of
// the same own/want type. If not, we add it to the default list.
@ -1289,6 +1202,7 @@ const resolvers = {
);
if (rows[0].count === 0) {
const now = new Date();
await connection.query(
`
INSERT INTO closet_hangers
@ -1307,8 +1221,6 @@ const resolvers = {
} catch (error) {
console.warn(`Error rolling back transaction`, error);
}
throw error;
} finally {
await connection.release();
}

498
yarn.lock
View file

@ -832,7 +832,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.22.13":
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.22.13":
version: 7.22.13
resolution: "@babel/code-frame@npm:7.22.13"
dependencies:
@ -2211,7 +2211,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.7, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7":
"@babel/runtime-corejs3@npm:^7.10.2":
version: 7.23.2
resolution: "@babel/runtime-corejs3@npm:7.23.2"
dependencies:
core-js-pure: "npm:^3.30.2"
regenerator-runtime: "npm:^0.14.0"
checksum: 1362f04cae16d99175961e4113618e5ae210e17053605d4cd2c7b93b3a0334fcfe6a689601d20c12f8946cd8a472430e25f0bf09b7dcd851f63fd82749fd7503
languageName: node
linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.5.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.7, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7":
version: 7.23.2
resolution: "@babel/runtime@npm:7.23.2"
dependencies:
@ -3360,6 +3370,29 @@ __metadata:
languageName: node
linkType: hard
"@jest/types@npm:^24.9.0":
version: 24.9.0
resolution: "@jest/types@npm:24.9.0"
dependencies:
"@types/istanbul-lib-coverage": "npm:^2.0.0"
"@types/istanbul-reports": "npm:^1.1.1"
"@types/yargs": "npm:^13.0.0"
checksum: 990b03f5e27de292a7fea6b12cd87256dd281263afe37020cad5dceb0b775945a528bafdbc2e41bf8a29c346f94a7aa5580517c5c65a2b33f245f43d3b9b4694
languageName: node
linkType: hard
"@jest/types@npm:^25.5.0":
version: 25.5.0
resolution: "@jest/types@npm:25.5.0"
dependencies:
"@types/istanbul-lib-coverage": "npm:^2.0.0"
"@types/istanbul-reports": "npm:^1.1.1"
"@types/yargs": "npm:^15.0.0"
chalk: "npm:^3.0.0"
checksum: f47c6e98c99d3fd562f2be6c339f41d3c7092e9587b8524fe71411f9c8b8e71f50475278a10e534f56c729ccd3e3b55e3aa20e4b0a2c5c47ded1ba53e0aef286
languageName: node
linkType: hard
"@jimp/bmp@npm:^0.14.0":
version: 0.14.0
resolution: "@jimp/bmp@npm:0.14.0"
@ -4306,6 +4339,13 @@ __metadata:
languageName: node
linkType: hard
"@sheerun/mutationobserver-shim@npm:^0.3.2":
version: 0.3.3
resolution: "@sheerun/mutationobserver-shim@npm:0.3.3"
checksum: 14e7e53805a4320577d88f44fd786b4f226be314e44e97a2889bc01fc05e72ce1faa3f3e27720b9b9a2e0e4bf2e3fe1864df9ac35aac52ca2c29a49c5ac5521e
languageName: node
linkType: hard
"@smithy/abort-controller@npm:^2.0.12":
version: 2.0.12
resolution: "@smithy/abort-controller@npm:2.0.12"
@ -4860,6 +4900,77 @@ __metadata:
languageName: node
linkType: hard
"@testing-library/dom@npm:*":
version: 9.3.3
resolution: "@testing-library/dom@npm:9.3.3"
dependencies:
"@babel/code-frame": "npm:^7.10.4"
"@babel/runtime": "npm:^7.12.5"
"@types/aria-query": "npm:^5.0.1"
aria-query: "npm:5.1.3"
chalk: "npm:^4.1.0"
dom-accessibility-api: "npm:^0.5.9"
lz-string: "npm:^1.5.0"
pretty-format: "npm:^27.0.2"
checksum: c3bbd67503634fd955233dc172531640656701fe35ecb9a83f85e5965874b786452f5e7c26b4f8b3b4fc4379f3a80193c74425b57843ba191f4845e22b0ac483
languageName: node
linkType: hard
"@testing-library/dom@npm:^6.15.0":
version: 6.16.0
resolution: "@testing-library/dom@npm:6.16.0"
dependencies:
"@babel/runtime": "npm:^7.8.4"
"@sheerun/mutationobserver-shim": "npm:^0.3.2"
"@types/testing-library__dom": "npm:^6.12.1"
aria-query: "npm:^4.0.2"
dom-accessibility-api: "npm:^0.3.0"
pretty-format: "npm:^25.1.0"
wait-for-expect: "npm:^3.0.2"
checksum: 9ae7087cd2aa64eb84950512b532bb5cff0797ece26692564910141a879884119c6680cfa5c328a25edb79d38f2b2d98647f3023d897da1abbd71f069b196ed0
languageName: node
linkType: hard
"@testing-library/jest-dom@npm:^4.2.4":
version: 4.2.4
resolution: "@testing-library/jest-dom@npm:4.2.4"
dependencies:
"@babel/runtime": "npm:^7.5.1"
chalk: "npm:^2.4.1"
css: "npm:^2.2.3"
css.escape: "npm:^1.5.1"
jest-diff: "npm:^24.0.0"
jest-matcher-utils: "npm:^24.0.0"
lodash: "npm:^4.17.11"
pretty-format: "npm:^24.0.0"
redent: "npm:^3.0.0"
checksum: b91c91bc736e7d93f062b9597961e8a5a6a3a909ff961db4fcb0dfa03170939f19d2d6bda231c500269ccbe21e910470e7955670e69614621a91bf2e1918daed
languageName: node
linkType: hard
"@testing-library/react@npm:^9.3.2":
version: 9.5.0
resolution: "@testing-library/react@npm:9.5.0"
dependencies:
"@babel/runtime": "npm:^7.8.4"
"@testing-library/dom": "npm:^6.15.0"
"@types/testing-library__react": "npm:^9.1.2"
peerDependencies:
react: "*"
react-dom: "*"
checksum: 37e0ba5e7a9112f5a59d4ebddf84b198ff04ad9bb0d78e8a74604ac87d997fd4092991af6a1871ee2fc07cb40b2b1bccf3a8bcd3d357cbf0355527bd4b80cfc1
languageName: node
linkType: hard
"@testing-library/user-event@npm:^7.1.2":
version: 7.2.1
resolution: "@testing-library/user-event@npm:7.2.1"
peerDependencies:
"@testing-library/dom": ">=5"
checksum: cb02ec4d02beaef21f7e770a3fa93d8385e68d767ccc42ff719de60ccf80a80359d7baa857322071e459bbb8a7515922d03b0464298cec9fc953bf287f0c2760
languageName: node
linkType: hard
"@tootallnate/once@npm:1":
version: 1.1.2
resolution: "@tootallnate/once@npm:1.1.2"
@ -4876,6 +4987,13 @@ __metadata:
languageName: node
linkType: hard
"@types/aria-query@npm:^5.0.1":
version: 5.0.3
resolution: "@types/aria-query@npm:5.0.3"
checksum: 5b82fab31fc6a1d51d36a1f7a91fd78dfbb4c47c6c8da65c712d8d3bf24208e81f26850763ced7e671b54a5c21252fbc15ebb74bed135faa0cfc4ee746375de4
languageName: node
linkType: hard
"@types/body-parser@npm:*":
version: 1.19.4
resolution: "@types/body-parser@npm:1.19.4"
@ -4997,6 +5115,32 @@ __metadata:
languageName: node
linkType: hard
"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0":
version: 2.0.5
resolution: "@types/istanbul-lib-coverage@npm:2.0.5"
checksum: e15cfc01a7ac60062f771314c959011bae7de7ceaef8e294f13427a11f21741cbfac98ad8cd9ecbf0e3d72ab7ddc327bacb3fab32c6b26ab19dbbbc1a69a9d3b
languageName: node
linkType: hard
"@types/istanbul-lib-report@npm:*":
version: 3.0.2
resolution: "@types/istanbul-lib-report@npm:3.0.2"
dependencies:
"@types/istanbul-lib-coverage": "npm:*"
checksum: c168e425c95c167d83c7cbd65ff6b620cc53c5ef199a58428758586bbc28faf5c51291667e4455777b47ada12381e53fce7b92e32f431f85d8ac8025074d1908
languageName: node
linkType: hard
"@types/istanbul-reports@npm:^1.1.1":
version: 1.1.2
resolution: "@types/istanbul-reports@npm:1.1.2"
dependencies:
"@types/istanbul-lib-coverage": "npm:*"
"@types/istanbul-lib-report": "npm:*"
checksum: 80b76715f4ac74a4ddfc82d7942b2faaefbe9fdce8e7dfdfa497b3fb60a3e707b632c6e70e1565cfe30045eaebaf7aad0d6c3d102652d1da8fdb0bf095924eb3
languageName: node
linkType: hard
"@types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9":
version: 7.0.14
resolution: "@types/json-schema@npm:7.0.14"
@ -5152,6 +5296,15 @@ __metadata:
languageName: node
linkType: hard
"@types/react-dom@npm:*":
version: 18.2.14
resolution: "@types/react-dom@npm:18.2.14"
dependencies:
"@types/react": "npm:*"
checksum: 1f79a7708d038cd651bdb21e01a99c594761bc9a40a565abe98958e1d27facfeb6e9824ddf6ae3504e7a56568f0f3da2380fe52ac18477b5864d2d5cf1386a9e
languageName: node
linkType: hard
"@types/react-dom@npm:^17.0.0":
version: 17.0.22
resolution: "@types/react-dom@npm:17.0.22"
@ -5161,7 +5314,7 @@ __metadata:
languageName: node
linkType: hard
"@types/react@npm:>=16.0.0, @types/react@npm:^18.2.34":
"@types/react@npm:*, @types/react@npm:>=16.0.0, @types/react@npm:^18.2.34":
version: 18.2.34
resolution: "@types/react@npm:18.2.34"
dependencies:
@ -5218,6 +5371,35 @@ __metadata:
languageName: node
linkType: hard
"@types/testing-library__dom@npm:*":
version: 7.5.0
resolution: "@types/testing-library__dom@npm:7.5.0"
dependencies:
"@testing-library/dom": "npm:*"
checksum: 24da773a611764958ca2f124d6b8ceea3781ac40c7b71869f36132491fe760fc2731f5ebf06a7052f95e53ea9c5d6f77270732f89de163e0c1423bc590a353eb
languageName: node
linkType: hard
"@types/testing-library__dom@npm:^6.12.1":
version: 6.14.0
resolution: "@types/testing-library__dom@npm:6.14.0"
dependencies:
pretty-format: "npm:^24.3.0"
checksum: 586f74496801eb1ba50f4b2aa357eab3eb97ca91b068c9f6239f302792b7cae984d38166c5c3eee130bfa60cfba3c596b9f0226672346c1bacbe86152e1d91db
languageName: node
linkType: hard
"@types/testing-library__react@npm:^9.1.2":
version: 9.1.3
resolution: "@types/testing-library__react@npm:9.1.3"
dependencies:
"@types/react-dom": "npm:*"
"@types/testing-library__dom": "npm:*"
pretty-format: "npm:^25.1.0"
checksum: 28403de3f09e44fbf34ea4dd0cf6c7ba285f5eb75e00ac8ddaf4460593d1ae6707049b36b500f1eecc80f936b10114745dac623e5dd10fab57ec9a1889184b0d
languageName: node
linkType: hard
"@types/warning@npm:^3.0.0":
version: 3.0.2
resolution: "@types/warning@npm:3.0.2"
@ -5234,6 +5416,31 @@ __metadata:
languageName: node
linkType: hard
"@types/yargs-parser@npm:*":
version: 21.0.2
resolution: "@types/yargs-parser@npm:21.0.2"
checksum: 422b8c59e21d9594e5a94afa45a3692d96c14f8fc7554bb1c1c390276815f09996ce0f8ed11893b6f8b2efc4ced686231dca5be6d76a4c4ceb56534474e95aca
languageName: node
linkType: hard
"@types/yargs@npm:^13.0.0":
version: 13.0.12
resolution: "@types/yargs@npm:13.0.12"
dependencies:
"@types/yargs-parser": "npm:*"
checksum: 81fdac6832d69f2f2a33bb3d77887f571677d5a9ccfd5a171ff3e76252a6c6a9773850a0df6ba9ed0328433a36596488ec4e2ce5d9bc49d713a59bbfef8e12a0
languageName: node
linkType: hard
"@types/yargs@npm:^15.0.0":
version: 15.0.17
resolution: "@types/yargs@npm:15.0.17"
dependencies:
"@types/yargs-parser": "npm:*"
checksum: ddb97bae547f02eefc04353b1df6f2928ca81ae86d5e163965190f7b359342bde5392a0b1866b16ceb96d1c17ebfa7a6bf9faf165744c23635d8858668567b0a
languageName: node
linkType: hard
"@types/yauzl@npm:^2.9.1":
version: 2.10.2
resolution: "@types/yauzl@npm:2.10.2"
@ -5611,7 +5818,14 @@ __metadata:
languageName: node
linkType: hard
"ansi-regex@npm:^5.0.1":
"ansi-regex@npm:^4.0.0":
version: 4.1.1
resolution: "ansi-regex@npm:4.1.1"
checksum: d36d34234d077e8770169d980fed7b2f3724bfa2a01da150ccd75ef9707c80e883d27cdf7a0eac2f145ac1d10a785a8a855cffd05b85f778629a0db62e7033da
languageName: node
linkType: hard
"ansi-regex@npm:^5.0.0, ansi-regex@npm:^5.0.1":
version: 5.0.1
resolution: "ansi-regex@npm:5.0.1"
checksum: 9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737
@ -5632,7 +5846,7 @@ __metadata:
languageName: node
linkType: hard
"ansi-styles@npm:^3.2.1":
"ansi-styles@npm:^3.2.0, ansi-styles@npm:^3.2.1":
version: 3.2.1
resolution: "ansi-styles@npm:3.2.1"
dependencies:
@ -5650,6 +5864,13 @@ __metadata:
languageName: node
linkType: hard
"ansi-styles@npm:^5.0.0":
version: 5.2.0
resolution: "ansi-styles@npm:5.2.0"
checksum: 9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df
languageName: node
linkType: hard
"ansi-styles@npm:^6.1.0":
version: 6.2.1
resolution: "ansi-styles@npm:6.2.1"
@ -5943,6 +6164,25 @@ __metadata:
languageName: node
linkType: hard
"aria-query@npm:5.1.3":
version: 5.1.3
resolution: "aria-query@npm:5.1.3"
dependencies:
deep-equal: "npm:^2.0.5"
checksum: edcbc8044c4663d6f88f785e983e6784f98cb62b4ba1e9dd8d61b725d0203e4cfca38d676aee984c31f354103461102a3d583aa4fbe4fd0a89b679744f4e5faf
languageName: node
linkType: hard
"aria-query@npm:^4.0.2":
version: 4.2.2
resolution: "aria-query@npm:4.2.2"
dependencies:
"@babel/runtime": "npm:^7.10.2"
"@babel/runtime-corejs3": "npm:^7.10.2"
checksum: 7e224fbbb4de8210c5d8cbaf0e1a22caa78f2068bf231f4c75302bd77eeba1c3e3b97912080535140be60174720d2ac817e5d6fec18592951b4b6488d4da7cdc
languageName: node
linkType: hard
"aria-query@npm:^5.3.0":
version: 5.3.0
resolution: "aria-query@npm:5.3.0"
@ -6673,7 +6913,7 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^2.4.2":
"chalk@npm:^2.0.1, chalk@npm:^2.4.1, chalk@npm:^2.4.2":
version: 2.4.2
resolution: "chalk@npm:2.4.2"
dependencies:
@ -6684,6 +6924,16 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^3.0.0":
version: 3.0.0
resolution: "chalk@npm:3.0.0"
dependencies:
ansi-styles: "npm:^4.1.0"
supports-color: "npm:^7.1.0"
checksum: ee650b0a065b3d7a6fda258e75d3a86fc8e4effa55871da730a9e42ccb035bf5fd203525e5a1ef45ec2582ecc4f65b47eb11357c526b84dd29a14fb162c414d2
languageName: node
linkType: hard
"chalk@npm:^4.0.0, chalk@npm:^4.1.0":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
@ -7053,7 +7303,7 @@ __metadata:
languageName: node
linkType: hard
"core-js-pure@npm:^3.10.2":
"core-js-pure@npm:^3.10.2, core-js-pure@npm:^3.30.2":
version: 3.33.2
resolution: "core-js-pure@npm:3.33.2"
checksum: 9de1cc6e64371c1b48d547a75840472a2c39277dbe3dd74adc4c172f05f078218ce69e42e30f663d26a94a181e761325141028c2c0a1d452c8e4a383befa2e25
@ -7131,6 +7381,25 @@ __metadata:
languageName: node
linkType: hard
"css.escape@npm:^1.5.1":
version: 1.5.1
resolution: "css.escape@npm:1.5.1"
checksum: 5e09035e5bf6c2c422b40c6df2eb1529657a17df37fda5d0433d722609527ab98090baf25b13970ca754079a0f3161dd3dfc0e743563ded8cfa0749d861c1525
languageName: node
linkType: hard
"css@npm:^2.2.3":
version: 2.2.4
resolution: "css@npm:2.2.4"
dependencies:
inherits: "npm:^2.0.3"
source-map: "npm:^0.6.1"
source-map-resolve: "npm:^0.5.2"
urix: "npm:^0.1.0"
checksum: 496fa66568ebd9e51b3153817dd36ec004a45780da6f818e13117e3c4e50b774af41fff70a6ff2fa03777b239c4028ff655fe571b20964b90e886441cd141569
languageName: node
linkType: hard
"cssfilter@npm:0.0.10":
version: 0.0.10
resolution: "cssfilter@npm:0.0.10"
@ -7247,6 +7516,32 @@ __metadata:
languageName: node
linkType: hard
"deep-equal@npm:^2.0.5":
version: 2.2.2
resolution: "deep-equal@npm:2.2.2"
dependencies:
array-buffer-byte-length: "npm:^1.0.0"
call-bind: "npm:^1.0.2"
es-get-iterator: "npm:^1.1.3"
get-intrinsic: "npm:^1.2.1"
is-arguments: "npm:^1.1.1"
is-array-buffer: "npm:^3.0.2"
is-date-object: "npm:^1.0.5"
is-regex: "npm:^1.1.4"
is-shared-array-buffer: "npm:^1.0.2"
isarray: "npm:^2.0.5"
object-is: "npm:^1.1.5"
object-keys: "npm:^1.1.1"
object.assign: "npm:^4.1.4"
regexp.prototype.flags: "npm:^1.5.0"
side-channel: "npm:^1.0.4"
which-boxed-primitive: "npm:^1.0.2"
which-collection: "npm:^1.0.1"
which-typed-array: "npm:^1.1.9"
checksum: 07b46a9a848efdab223abc7e3ba612ef9168d88970c3400df185d5840a30ca384749c996ae5d7af844d6b27c42587fb73a4445c63e38aac77c2d0ed9a63faa87
languageName: node
linkType: hard
"deep-extend@npm:^0.6.0":
version: 0.6.0
resolution: "deep-extend@npm:0.6.0"
@ -7442,6 +7737,13 @@ __metadata:
languageName: node
linkType: hard
"diff-sequences@npm:^24.9.0":
version: 24.9.0
resolution: "diff-sequences@npm:24.9.0"
checksum: c7c6cec09502e8266fa499e5b1f359349529b4019135b6a6ae4441a7f48bd518b286d33255376a47e9e970c78527355d0ca3f58d01d6513f6b565283d56600b9
languageName: node
linkType: hard
"diff@npm:^4.0.1":
version: 4.0.2
resolution: "diff@npm:4.0.2"
@ -7476,6 +7778,20 @@ __metadata:
languageName: node
linkType: hard
"dom-accessibility-api@npm:^0.3.0":
version: 0.3.0
resolution: "dom-accessibility-api@npm:0.3.0"
checksum: c6d44a4d0ce2bb24308a57c805bbfaf25ba415bc5a243a18eb6cd2dbb2024fd2dd04b6222c50c24df1a0e1d29d65fca72926325cbde6c08c937766a1965f3ad6
languageName: node
linkType: hard
"dom-accessibility-api@npm:^0.5.9":
version: 0.5.16
resolution: "dom-accessibility-api@npm:0.5.16"
checksum: b2c2eda4fae568977cdac27a9f0c001edf4f95a6a6191dfa611e3721db2478d1badc01db5bb4fa8a848aeee13e442a6c2a4386d65ec65a1436f24715a2f8d053
languageName: node
linkType: hard
"dom-helpers@npm:^5.0.1, dom-helpers@npm:^5.1.3":
version: 5.2.1
resolution: "dom-helpers@npm:5.2.1"
@ -7714,6 +8030,23 @@ __metadata:
languageName: node
linkType: hard
"es-get-iterator@npm:^1.1.3":
version: 1.1.3
resolution: "es-get-iterator@npm:1.1.3"
dependencies:
call-bind: "npm:^1.0.2"
get-intrinsic: "npm:^1.1.3"
has-symbols: "npm:^1.0.3"
is-arguments: "npm:^1.1.1"
is-map: "npm:^2.0.2"
is-set: "npm:^2.0.2"
is-string: "npm:^1.0.7"
isarray: "npm:^2.0.5"
stop-iteration-iterator: "npm:^1.0.0"
checksum: ebd11effa79851ea75d7f079405f9d0dc185559fd65d986c6afea59a0ff2d46c2ed8675f19f03dce7429d7f6c14ff9aede8d121fbab78d75cfda6a263030bac0
languageName: node
linkType: hard
"es-iterator-helpers@npm:^1.0.12, es-iterator-helpers@npm:^1.0.15":
version: 1.0.15
resolution: "es-iterator-helpers@npm:1.0.15"
@ -9459,6 +9792,9 @@ __metadata:
"@sendgrid/mail": "npm:^7.2.6"
"@sentry/react": "npm:^5.30.0"
"@sentry/tracing": "npm:^5.30.0"
"@testing-library/jest-dom": "npm:^4.2.4"
"@testing-library/react": "npm:^9.3.2"
"@testing-library/user-event": "npm:^7.1.2"
"@types/node": "npm:^14.14.22"
"@types/react": "npm:^18.2.34"
"@types/react-dom": "npm:^17.0.0"
@ -9577,7 +9913,7 @@ __metadata:
languageName: node
linkType: hard
"internal-slot@npm:^1.0.5":
"internal-slot@npm:^1.0.4, internal-slot@npm:^1.0.5":
version: 1.0.6
resolution: "internal-slot@npm:1.0.6"
dependencies:
@ -9627,7 +9963,7 @@ __metadata:
languageName: node
linkType: hard
"is-arguments@npm:^1.0.4":
"is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1":
version: 1.1.1
resolution: "is-arguments@npm:1.1.1"
dependencies:
@ -9831,7 +10167,7 @@ __metadata:
languageName: node
linkType: hard
"is-map@npm:^2.0.1":
"is-map@npm:^2.0.1, is-map@npm:^2.0.2":
version: 2.0.2
resolution: "is-map@npm:2.0.2"
checksum: 119ff9137a37fd131a72fab3f4ab8c9d6a24b0a1ee26b4eff14dc625900d8675a97785eea5f4174265e2006ed076cc24e89f6e57ebd080a48338d914ec9168a5
@ -9910,7 +10246,7 @@ __metadata:
languageName: node
linkType: hard
"is-set@npm:^2.0.1":
"is-set@npm:^2.0.1, is-set@npm:^2.0.2":
version: 2.0.2
resolution: "is-set@npm:2.0.2"
checksum: 5f8bd1880df8c0004ce694e315e6e1e47a3452014be792880bb274a3b2cdb952fdb60789636ca6e084c7947ca8b7ae03ccaf54c93a7fcfed228af810559e5432
@ -10093,6 +10429,25 @@ __metadata:
languageName: node
linkType: hard
"jest-diff@npm:^24.0.0, jest-diff@npm:^24.9.0":
version: 24.9.0
resolution: "jest-diff@npm:24.9.0"
dependencies:
chalk: "npm:^2.0.1"
diff-sequences: "npm:^24.9.0"
jest-get-type: "npm:^24.9.0"
pretty-format: "npm:^24.9.0"
checksum: de8f57a6532d95f325478bb963507e055c962fb1255e4c0c3610853c729994a690fe7ec04bf18c5dd922ced6ae0e8e251910909b77d426e6fda96940f10f4f8e
languageName: node
linkType: hard
"jest-get-type@npm:^24.9.0":
version: 24.9.0
resolution: "jest-get-type@npm:24.9.0"
checksum: af1da287a14e5de5888b0114e92cd4042050852d32982d412e1465a8d69cb0a22702c7c491c56eb664e05a1391c1d6eeeb840e249a76d4f6159c402a4dfde56d
languageName: node
linkType: hard
"jest-image-snapshot@npm:^4.3.0":
version: 4.5.1
resolution: "jest-image-snapshot@npm:4.5.1"
@ -10112,6 +10467,18 @@ __metadata:
languageName: node
linkType: hard
"jest-matcher-utils@npm:^24.0.0":
version: 24.9.0
resolution: "jest-matcher-utils@npm:24.9.0"
dependencies:
chalk: "npm:^2.0.1"
jest-diff: "npm:^24.9.0"
jest-get-type: "npm:^24.9.0"
pretty-format: "npm:^24.9.0"
checksum: f5cd624d22d77a105267cf6c50bec0dcf2627ccd385d461e8cf6a0a8a97ca8ecb0a6f2f4282f43a4c55bb5bc9047fa77e0e7a04bfb07a80f153a045bf5b1b57f
languageName: node
linkType: hard
"jimp@npm:^0.14.0":
version: 0.14.0
resolution: "jimp@npm:0.14.0"
@ -10636,7 +11003,7 @@ __metadata:
languageName: node
linkType: hard
"lodash@npm:>=4.17.21, lodash@npm:^4.17.19, lodash@npm:^4.17.4":
"lodash@npm:>=4.17.21, lodash@npm:^4.17.11, lodash@npm:^4.17.19, lodash@npm:^4.17.4":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c
@ -10758,6 +11125,15 @@ __metadata:
languageName: node
linkType: hard
"lz-string@npm:^1.5.0":
version: 1.5.0
resolution: "lz-string@npm:1.5.0"
bin:
lz-string: bin/bin.js
checksum: 36128e4de34791838abe979b19927c26e67201ca5acf00880377af7d765b38d1c60847e01c5ec61b1a260c48029084ab3893a3925fd6e48a04011364b089991b
languageName: node
linkType: hard
"make-dir@npm:^2.0.0, make-dir@npm:^2.1.0":
version: 2.1.0
resolution: "make-dir@npm:2.1.0"
@ -10949,6 +11325,13 @@ __metadata:
languageName: node
linkType: hard
"min-indent@npm:^1.0.0":
version: 1.0.1
resolution: "min-indent@npm:1.0.1"
checksum: 7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c
languageName: node
linkType: hard
"minimalistic-assert@npm:^1.0.1":
version: 1.0.1
resolution: "minimalistic-assert@npm:1.0.1"
@ -11462,6 +11845,16 @@ __metadata:
languageName: node
linkType: hard
"object-is@npm:^1.1.5":
version: 1.1.5
resolution: "object-is@npm:1.1.5"
dependencies:
call-bind: "npm:^1.0.2"
define-properties: "npm:^1.1.3"
checksum: 8c263fb03fc28f1ffb54b44b9147235c5e233dc1ca23768e7d2569740b5d860154d7cc29a30220fe28ed6d8008e2422aefdebfe987c103e1c5d190cf02d9d886
languageName: node
linkType: hard
"object-keys@npm:^1.1.1":
version: 1.1.1
resolution: "object-keys@npm:1.1.1"
@ -12072,6 +12465,41 @@ __metadata:
languageName: node
linkType: hard
"pretty-format@npm:^24.0.0, pretty-format@npm:^24.3.0, pretty-format@npm:^24.9.0":
version: 24.9.0
resolution: "pretty-format@npm:24.9.0"
dependencies:
"@jest/types": "npm:^24.9.0"
ansi-regex: "npm:^4.0.0"
ansi-styles: "npm:^3.2.0"
react-is: "npm:^16.8.4"
checksum: 1e75c0ae55dab8953a5fe8025aab0a6d6090773561b672a7a00108f6cfb7dace198b27143392382dff913cb71f6fbc10ed23beaddf2117c380588a3b575825f0
languageName: node
linkType: hard
"pretty-format@npm:^25.1.0":
version: 25.5.0
resolution: "pretty-format@npm:25.5.0"
dependencies:
"@jest/types": "npm:^25.5.0"
ansi-regex: "npm:^5.0.0"
ansi-styles: "npm:^4.0.0"
react-is: "npm:^16.12.0"
checksum: cbcf79f57a96f5eb9970722614a360539940606a20a924f6202e309433af4ad5b71ba210b6b3efcdcdad178f9aefa74f04a447d86520d721fbe155ff43b33112
languageName: node
linkType: hard
"pretty-format@npm:^27.0.2":
version: 27.5.1
resolution: "pretty-format@npm:27.5.1"
dependencies:
ansi-regex: "npm:^5.0.1"
ansi-styles: "npm:^5.0.0"
react-is: "npm:^17.0.1"
checksum: 0cbda1031aa30c659e10921fa94e0dd3f903ecbbbe7184a729ad66f2b6e7f17891e8c7d7654c458fa4ccb1a411ffb695b4f17bbcd3fe075fabe181027c4040ed
languageName: node
linkType: hard
"private@npm:~0.1.5":
version: 0.1.8
resolution: "private@npm:0.1.8"
@ -12367,13 +12795,20 @@ __metadata:
languageName: node
linkType: hard
"react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.7.0":
"react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.8.4":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
checksum: 33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1
languageName: node
linkType: hard
"react-is@npm:^17.0.1":
version: 17.0.2
resolution: "react-is@npm:17.0.2"
checksum: 2bdb6b93fbb1820b024b496042cce405c57e2f85e777c9aabd55f9b26d145408f9f74f5934676ffdc46f3dcff656d78413a6e43968e7b3f92eea35b3052e9053
languageName: node
linkType: hard
"react-lifecycles-compat@npm:^3.0.4":
version: 3.0.4
resolution: "react-lifecycles-compat@npm:3.0.4"
@ -12544,6 +12979,16 @@ __metadata:
languageName: node
linkType: hard
"redent@npm:^3.0.0":
version: 3.0.0
resolution: "redent@npm:3.0.0"
dependencies:
indent-string: "npm:^4.0.0"
strip-indent: "npm:^3.0.0"
checksum: d64a6b5c0b50eb3ddce3ab770f866658a2b9998c678f797919ceb1b586bab9259b311407280bd80b804e2a7c7539b19238ae6a2a20c843f1a7fcff21d48c2eae
languageName: node
linkType: hard
"reflect.getprototypeof@npm:^1.0.4":
version: 1.0.4
resolution: "reflect.getprototypeof@npm:1.0.4"
@ -13362,7 +13807,7 @@ __metadata:
languageName: node
linkType: hard
"source-map-resolve@npm:^0.5.0":
"source-map-resolve@npm:^0.5.0, source-map-resolve@npm:^0.5.2":
version: 0.5.3
resolution: "source-map-resolve@npm:0.5.3"
dependencies:
@ -13469,6 +13914,15 @@ __metadata:
languageName: node
linkType: hard
"stop-iteration-iterator@npm:^1.0.0":
version: 1.0.0
resolution: "stop-iteration-iterator@npm:1.0.0"
dependencies:
internal-slot: "npm:^1.0.4"
checksum: c4158d6188aac510d9e92925b58709207bd94699e9c31186a040c80932a687f84a51356b5895e6dc72710aad83addb9411c22171832c9ae0e6e11b7d61b0dfb9
languageName: node
linkType: hard
"stoppable@npm:^1.1.0":
version: 1.1.0
resolution: "stoppable@npm:1.1.0"
@ -13647,6 +14101,15 @@ __metadata:
languageName: node
linkType: hard
"strip-indent@npm:^3.0.0":
version: 3.0.0
resolution: "strip-indent@npm:3.0.0"
dependencies:
min-indent: "npm:^1.0.0"
checksum: ae0deaf41c8d1001c5d4fbe16cb553865c1863da4fae036683b474fa926af9fc121e155cb3fc57a68262b2ae7d5b8420aa752c97a6428c315d00efe2a3875679
languageName: node
linkType: hard
"strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1":
version: 3.1.1
resolution: "strip-json-comments@npm:3.1.1"
@ -14576,6 +15039,13 @@ __metadata:
languageName: node
linkType: hard
"wait-for-expect@npm:^3.0.2":
version: 3.0.2
resolution: "wait-for-expect@npm:3.0.2"
checksum: 9616b82381f6571a0f3ed235a62ecdaf529816c9eef04f665ee1b8b515c3b7e1dc90d781111b6284bb777f45c4742f6c058d549a44d9d6e367ffd4d34f5f3b9e
languageName: node
linkType: hard
"warning@npm:^4.0.3":
version: 4.0.3
resolution: "warning@npm:4.0.3"