impress/app/javascript/wardrobe-2020/loaders/items.js
Emi Matchu ece7a02902 Remove our testing-escape-hatch for item search from the console
Did this before we had the ability to trigger searches from the app
itself, to allow me to open up the browser JS console and call this
function directly. Now we can just do it in app, goodbye!
2024-02-27 16:25:17 -08:00

225 lines
5.4 KiB
JavaScript

import gql from "graphql-tag";
import { useQuery } from "@tanstack/react-query";
import apolloClient from "../apolloClient";
import { normalizeSwfAssetToLayer, normalizeZone } from "./shared-types";
export function useItemAppearances(id, options = {}) {
return useQuery({
...options,
queryKey: ["items", String(id)],
queryFn: () => loadItemAppearancesData(id),
});
}
async function loadItemAppearancesData(id) {
const res = await fetch(
`/items/${encodeURIComponent(id)}/appearances.json`,
);
if (!res.ok) {
throw new Error(
`loading item appearances failed: ${res.status} ${res.statusText}`,
);
}
return res.json().then(normalizeItemAppearancesData);
}
export function useItemSearch(searchOptions, queryOptions = {}) {
// Item searches are considered fresh for an hour, unless the search
// includes user-specific filters, in which case React Query will pretty
// aggressively reload it!
const includesUserSpecificFilters = searchOptions.filters.some(
(f) => f.key === "user_closet_hanger_ownership",
);
const staleTime = includesUserSpecificFilters ? 0 : 1000 * 60 * 5;
return useQuery({
...queryOptions,
queryKey: ["itemSearch", buildItemSearchParams(searchOptions)],
queryFn: () => loadItemSearch(searchOptions),
staleTime,
});
}
function buildItemSearchParams({
filters = [],
withAppearancesFor = null,
page = 1,
perPage = 30,
}) {
const params = new URLSearchParams();
for (const [i, { key, value, isPositive }] of filters.entries()) {
params.append(`q[${i}][key]`, key);
if (key === "fits") {
params.append(`q[${i}][value][species_id]`, value.speciesId);
params.append(`q[${i}][value][color_id]`, value.colorId);
if (value.altStyleId != null) {
params.append(`q[${i}][value][alt_style_id]`, value.altStyleId);
}
} else {
params.append(`q[${i}][value]`, value);
}
if (isPositive == false) {
params.append(`q[${i}][is_positive]`, "false");
}
}
if (withAppearancesFor != null) {
const { speciesId, colorId, altStyleId } = withAppearancesFor;
params.append(`with_appearances_for[species_id]`, speciesId);
params.append(`with_appearances_for[color_id]`, colorId);
if (altStyleId != null) {
params.append(`with_appearances_for[alt_style_id]`, altStyleId);
}
}
params.append("page", page);
params.append("per_page", perPage);
return params.toString();
}
async function loadItemSearch(searchOptions) {
const params = buildItemSearchParams(searchOptions);
const res = await fetch(`/items.json?${params}`);
if (!res.ok) {
throw new Error(
`loading item search failed: ${res.status} ${res.statusText}`,
);
}
const data = await res.json();
const result = normalizeItemSearchData(data, searchOptions);
for (const item of result.items) {
writeItemToApolloCache(item, searchOptions.withAppearancesFor);
}
return result;
}
/**
* writeItemToApolloCache is one last important bridge between our loaders and
* GQL! In `useOutfitState`, we consult the GraphQL cache to look up basic item
* info like zones, to decide when wearing an item would trigger a conflict
* with another.
*/
function writeItemToApolloCache(item, { speciesId, colorId, altStyleId }) {
apolloClient.writeQuery({
query: gql`
query WriteItemFromLoader(
$itemId: ID!
$speciesId: ID!
$colorId: ID!
$altStyleId: ID
) {
item(id: $itemId) {
id
name
thumbnailUrl
isNc
isPb
currentUserOwnsThis
currentUserWantsThis
appearanceOn(
speciesId: $speciesId
colorId: $colorId
altStyleId: $altStyleId
) {
id
layers {
id
remoteId
bodyId
knownGlitches
svgUrl
canvasMovieLibraryUrl
imageUrl: imageUrlV2(idealSize: SIZE_600)
swfUrl
zone {
id
}
}
restrictedZones {
id
}
}
}
}
`,
variables: {
itemId: item.id,
speciesId,
colorId,
altStyleId,
},
data: { item },
});
}
function normalizeItemAppearancesData(data) {
return {
name: data.name,
appearances: data.appearances.map((appearance) => ({
body: normalizeBody(appearance.body),
swfAssets: appearance.swf_assets.map(normalizeSwfAssetToLayer),
})),
restrictedZones: data.restricted_zones.map((z) => normalizeZone(z)),
};
}
function normalizeItemSearchData(data, searchOptions) {
return {
__typename: "ItemSearchResultV2",
id: buildItemSearchParams(searchOptions),
numTotalPages: data.total_pages,
items: data.items.map((item) => ({
__typename: "Item",
id: String(item.id),
name: item.name,
thumbnailUrl: item.thumbnail_url,
isNc: item["nc?"],
isPb: item["pb?"],
currentUserOwnsThis: item["owned?"],
currentUserWantsThis: item["wanted?"],
appearanceOn: normalizeItemSearchAppearance(
data.appearances[item.id],
item,
),
})),
};
}
function normalizeItemSearchAppearance(data, item) {
if (data == null) {
return null;
}
return {
__typename: "ItemAppearance",
id: `item-${item.id}-body-${data.body.id}`,
layers: data.swf_assets.map(normalizeSwfAssetToLayer),
restrictedZones: data.swf_assets
.map((a) => a.restricted_zones)
.flat()
.map(normalizeZone),
};
}
function normalizeBody(body) {
if (String(body.id) === "0") {
return { id: "0" };
}
return {
__typename: "Body",
id: String(body.id),
species: {
id: String(body.species.id),
name: body.species.name,
humanName: body.species.human_name,
},
};
}