forked from OpenNeo/impress
Emi Matchu
66c1e14dd0
This makes clicking on search results in the new mode actually work! It correctly adds it to the outfit, and removes other items. The thing that's behaving strangely is that, when you add the item, we visually remove all items until we can finish a fresh network request for what they should all look like. This probably means that the cache lookup for `useOutfitAppearance` is not as satisfied with what we cache here as `findItemConflicts` is? Something to investigate!
226 lines
5.4 KiB
JavaScript
226 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;
|
|
}
|
|
window.loadItemSearch = loadItemSearch;
|
|
|
|
/**
|
|
* 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
|
|
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,
|
|
},
|
|
};
|
|
}
|