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, }, }; }