Add secret HTML5 conversion page at /conversion
A lil page for us to keep track of Neopets's HTML5 conversion progress!
This commit is contained in:
parent
6da2ddb453
commit
275d1d62ab
5 changed files with 202 additions and 2 deletions
|
@ -30,6 +30,7 @@ const tryLoadable = (load, options) =>
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ConversionPage = tryLoadable(() => import("./ConversionPage"));
|
||||||
const HomePage = tryLoadable(() => import("./HomePage"));
|
const HomePage = tryLoadable(() => import("./HomePage"));
|
||||||
const ItemSearchPage = tryLoadable(() => import("./ItemSearchPage"));
|
const ItemSearchPage = tryLoadable(() => import("./ItemSearchPage"));
|
||||||
const ItemPage = tryLoadable(() => import("./ItemPage"));
|
const ItemPage = tryLoadable(() => import("./ItemPage"));
|
||||||
|
@ -162,6 +163,11 @@ function App() {
|
||||||
<PrivacyPolicyPage />
|
<PrivacyPolicyPage />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/conversion">
|
||||||
|
<PageLayout>
|
||||||
|
<ConversionPage />
|
||||||
|
</PageLayout>
|
||||||
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<PageLayout hideHomeLink>
|
<PageLayout hideHomeLink>
|
||||||
<HomePage />
|
<HomePage />
|
||||||
|
|
107
src/app/ConversionPage.js
Normal file
107
src/app/ConversionPage.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
CircularProgressLabel,
|
||||||
|
Flex,
|
||||||
|
Stack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import gql from "graphql-tag";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
|
||||||
|
import { ErrorMessage, Heading1 } from "./util";
|
||||||
|
|
||||||
|
function ConversionPage() {
|
||||||
|
const { loading, error, data } = useQuery(
|
||||||
|
gql`
|
||||||
|
query ConversionPage_NoAuthRequired {
|
||||||
|
numAppearanceLayersConverted
|
||||||
|
numAppearanceLayersTotal
|
||||||
|
|
||||||
|
numPetLayersConverted: numAppearanceLayersConverted(type: PET_LAYER)
|
||||||
|
numPetLayersTotal: numAppearanceLayersTotal(type: PET_LAYER)
|
||||||
|
|
||||||
|
numItemLayersConverted: numAppearanceLayersConverted(type: ITEM_LAYER)
|
||||||
|
numItemLayersTotal: numAppearanceLayersTotal(type: ITEM_LAYER)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{ onError: (e) => console.error(e) }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Heading1>HTML5 Conversion Hub</Heading1>
|
||||||
|
<Box height="6" />
|
||||||
|
<Stack direction="row" spacing="12" align="center">
|
||||||
|
<ConversionProgress
|
||||||
|
label="All layers"
|
||||||
|
color="green.500"
|
||||||
|
size="150px"
|
||||||
|
numConverted={data?.numAppearanceLayersConverted}
|
||||||
|
numTotal={data?.numAppearanceLayersTotal}
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
|
<ConversionProgress
|
||||||
|
label="Pet layers"
|
||||||
|
color="blue.500"
|
||||||
|
size="125px"
|
||||||
|
numConverted={data?.numPetLayersConverted}
|
||||||
|
numTotal={data?.numPetLayersTotal}
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
|
<ConversionProgress
|
||||||
|
label="Item layers"
|
||||||
|
color="blue.500"
|
||||||
|
size="125px"
|
||||||
|
numConverted={data?.numItemLayersConverted}
|
||||||
|
numTotal={data?.numItemLayersTotal}
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
{error && (
|
||||||
|
<ErrorMessage marginTop="2">
|
||||||
|
Oops, we couldn't load the latest data: {error.message}
|
||||||
|
</ErrorMessage>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConversionProgress({
|
||||||
|
label,
|
||||||
|
color,
|
||||||
|
size,
|
||||||
|
numConverted,
|
||||||
|
numTotal,
|
||||||
|
isLoading,
|
||||||
|
}) {
|
||||||
|
const convertedPercent = (numConverted / numTotal) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" align="center">
|
||||||
|
<CircularProgress
|
||||||
|
color={color}
|
||||||
|
size={size}
|
||||||
|
value={convertedPercent || 0}
|
||||||
|
isIndeterminate={isLoading}
|
||||||
|
>
|
||||||
|
{numConverted != null && numTotal != null && (
|
||||||
|
<CircularProgressLabel>
|
||||||
|
{Math.floor(convertedPercent)}%
|
||||||
|
</CircularProgressLabel>
|
||||||
|
)}
|
||||||
|
</CircularProgress>
|
||||||
|
<Box height="1" />
|
||||||
|
<Box textAlign="center">
|
||||||
|
<Box fontSize="xl">{label}</Box>
|
||||||
|
{numConverted != null && numTotal != null && (
|
||||||
|
<Box fontSize="xs">
|
||||||
|
{numConverted.toLocaleString()} of {numTotal.toLocaleString()}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConversionPage;
|
|
@ -84,8 +84,12 @@ export function Heading3({ children, ...props }) {
|
||||||
/**
|
/**
|
||||||
* ErrorMessage is a simple error message for simple errors!
|
* ErrorMessage is a simple error message for simple errors!
|
||||||
*/
|
*/
|
||||||
export function ErrorMessage({ children }) {
|
export function ErrorMessage({ children, ...props }) {
|
||||||
return <Box color="red.400">{children}</Box>;
|
return (
|
||||||
|
<Box color="red.400" {...props}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCommonStyles() {
|
export function useCommonStyles() {
|
||||||
|
|
|
@ -709,6 +709,40 @@ const buildSwfAssetLoader = (db) =>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buildSwfAssetCountLoader = (db) =>
|
||||||
|
new DataLoader(
|
||||||
|
async (requests) => {
|
||||||
|
const [rows, _] = await db.execute(
|
||||||
|
`
|
||||||
|
SELECT count(*) AS count, type,
|
||||||
|
(manifest IS NOT NULL AND manifest != "") AS is_converted
|
||||||
|
FROM swf_assets
|
||||||
|
GROUP BY type, is_converted;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
const entities = rows.map(normalizeRow);
|
||||||
|
|
||||||
|
return requests.map(({ type, isConverted }) => {
|
||||||
|
// Find the returned rows that match this count request.
|
||||||
|
let matchingEntities = entities;
|
||||||
|
if (type != null) {
|
||||||
|
matchingEntities = matchingEntities.filter((e) => e.type === type);
|
||||||
|
}
|
||||||
|
if (isConverted != null) {
|
||||||
|
matchingEntities = matchingEntities.filter(
|
||||||
|
(e) => Boolean(e.isConverted) === isConverted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add their counts together, and return the total.
|
||||||
|
return matchingEntities.map((e) => e.count).reduce((a, b) => a + b, 0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cacheKeyFn: ({ type, isConverted }) => `${type},${isConverted}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const buildSwfAssetByRemoteIdLoader = (db) =>
|
const buildSwfAssetByRemoteIdLoader = (db) =>
|
||||||
new DataLoader(
|
new DataLoader(
|
||||||
async (typeAndRemoteIdPairs) => {
|
async (typeAndRemoteIdPairs) => {
|
||||||
|
@ -1190,6 +1224,7 @@ function buildLoaders(db) {
|
||||||
loaders
|
loaders
|
||||||
);
|
);
|
||||||
loaders.swfAssetLoader = buildSwfAssetLoader(db);
|
loaders.swfAssetLoader = buildSwfAssetLoader(db);
|
||||||
|
loaders.swfAssetCountLoader = buildSwfAssetCountLoader(db);
|
||||||
loaders.swfAssetByRemoteIdLoader = buildSwfAssetByRemoteIdLoader(db);
|
loaders.swfAssetByRemoteIdLoader = buildSwfAssetByRemoteIdLoader(db);
|
||||||
loaders.itemSwfAssetLoader = buildItemSwfAssetLoader(db, loaders);
|
loaders.itemSwfAssetLoader = buildItemSwfAssetLoader(db, loaders);
|
||||||
loaders.petSwfAssetLoader = buildPetSwfAssetLoader(db, loaders);
|
loaders.petSwfAssetLoader = buildPetSwfAssetLoader(db, loaders);
|
||||||
|
|
|
@ -8,6 +8,11 @@ const typeDefs = gql`
|
||||||
SIZE_150
|
SIZE_150
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LayerType {
|
||||||
|
PET_LAYER
|
||||||
|
ITEM_LAYER
|
||||||
|
}
|
||||||
|
|
||||||
# Cache for 1 week (unlikely to change)
|
# Cache for 1 week (unlikely to change)
|
||||||
type AppearanceLayer @cacheControl(maxAge: 604800) {
|
type AppearanceLayer @cacheControl(maxAge: 604800) {
|
||||||
# The DTI ID. Guaranteed unique across all layers of all types.
|
# The DTI ID. Guaranteed unique across all layers of all types.
|
||||||
|
@ -67,6 +72,18 @@ const typeDefs = gql`
|
||||||
"""
|
"""
|
||||||
restrictedZones: [Zone!]!
|
restrictedZones: [Zone!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extend type Query {
|
||||||
|
# Return the number of layers that have been converted to HTML5, optionally
|
||||||
|
# filtered by type. Cache for 30 minutes (we re-sync with Neopets every
|
||||||
|
# hour).
|
||||||
|
numAppearanceLayersConverted(type: LayerType): Int!
|
||||||
|
@cacheControl(maxAge: 1800)
|
||||||
|
|
||||||
|
# Return the total number of layers, optionally filtered by type. Cache for
|
||||||
|
# 30 minutes (we re-sync with Neopets every hour).
|
||||||
|
numAppearanceLayersTotal(type: LayerType): Int! @cacheControl(maxAge: 1800)
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
|
@ -200,8 +217,39 @@ const resolvers = {
|
||||||
return { id: String(rows[0].parent_id) };
|
return { id: String(rows[0].parent_id) };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Query: {
|
||||||
|
numAppearanceLayersConverted: async (
|
||||||
|
_,
|
||||||
|
{ type },
|
||||||
|
{ swfAssetCountLoader }
|
||||||
|
) => {
|
||||||
|
const count = await swfAssetCountLoader.load({
|
||||||
|
type: convertLayerTypeToSwfAssetType(type),
|
||||||
|
isConverted: true,
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
numAppearanceLayersTotal: async (_, { type }, { swfAssetCountLoader }) => {
|
||||||
|
const count = await swfAssetCountLoader.load({
|
||||||
|
type: convertLayerTypeToSwfAssetType(type),
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function convertLayerTypeToSwfAssetType(layerType) {
|
||||||
|
switch (layerType) {
|
||||||
|
case "PET_LAYER":
|
||||||
|
return "biology";
|
||||||
|
case "ITEM_LAYER":
|
||||||
|
return "object";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAndCacheAssetManifest(db, layer) {
|
async function loadAndCacheAssetManifest(db, layer) {
|
||||||
let manifest;
|
let manifest;
|
||||||
try {
|
try {
|
||||||
|
|
Loading…
Reference in a new issue