impress/app/javascript/wardrobe-2020/loaders/outfits.js
Emi Matchu dc44b4dbb3 Oops, fix bug with saving outfits of pets loaded from Neopets.com
Okay right, the wardrobe-2020 app treats `state` as a bit of an
override thing, and `pose` is the main canonical field for how a pet
looks. We were missing a few pieces here:

1. After loading a pet, we weren't including the `pose` field in the
   initial query string for the wardrobe URL, but we _were_ including
   the `state` field, so the outfit would get set up with a conflicting
   pet state ID vs pose.
2. When saving an outfit, we weren't taking the `state` field into
   account at all. This could cause the saved outfit to not quite match
   how it actually looked in-app, because the default pet state for
   that species/color/pose trio could be different; and regardless, the
   outfit state would come back with `appearanceId` set to `null`,
   which wouldn't match the local outfit state, which would trigger an
   infinite loop.

Here, we complete the round-trip of the `state` field, from pet loading
to outfit saving to the outfit data that comes back after saving!
2024-02-08 09:51:31 -08:00

146 lines
3.2 KiB
JavaScript

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
export function useSavedOutfit(id, options = {}) {
return useQuery({
...options,
queryKey: ["outfits", String(id)],
queryFn: () => loadSavedOutfit(id),
});
}
export function useSaveOutfitMutation(options = {}) {
const queryClient = useQueryClient();
return useMutation({
...options,
mutationFn: saveOutfit,
onSuccess: (outfit) => {
queryClient.setQueryData(["outfits", outfit.id], outfit);
if (options.onSuccess) {
options.onSuccess(outfit);
}
},
});
}
export function useDeleteOutfitMutation(options = {}) {
const queryClient = useQueryClient();
return useMutation({
...options,
mutationFn: deleteOutfit,
onSuccess: (emptyData, id, context) => {
queryClient.invalidateQueries({
queryKey: ["outfits", String(id)],
});
if (options.onSuccess) {
options.onSuccess(emptyData, id, context);
}
},
});
}
async function loadSavedOutfit(id) {
const res = await fetch(`/outfits/${encodeURIComponent(id)}.json`);
if (!res.ok) {
throw new Error(
`loading outfit failed: ${res.status} ${res.statusText}`,
);
}
return res.json().then(normalizeOutfit);
}
async function saveOutfit({
id, // optional, null when creating a new outfit
name, // optional, server may fill in a placeholder
speciesId,
colorId,
pose,
appearanceId,
altStyleId,
wornItemIds,
closetedItemIds,
}) {
const params = {
outfit: {
name: name,
biology: {
species_id: speciesId,
color_id: colorId,
pose: pose,
pet_state_id: appearanceId,
},
alt_style_id: altStyleId,
item_ids: { worn: wornItemIds, closeted: closetedItemIds },
},
};
let res;
if (id == null) {
res = await fetch(`/outfits.json`, {
method: "POST",
body: JSON.stringify(params),
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": getCSRFToken(),
},
});
} else {
res = await fetch(`/outfits/${encodeURIComponent(id)}.json`, {
method: "PUT",
body: JSON.stringify(params),
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": getCSRFToken(),
},
});
}
if (!res.ok) {
throw new Error(
`saving outfit failed: ${res.status} ${res.statusText}`,
);
}
return res.json().then(normalizeOutfit);
}
async function deleteOutfit(id) {
const res = await fetch(`/outfits/${encodeURIComponent(id)}.json`, {
method: "DELETE",
headers: {
"X-CSRF-Token": getCSRFToken(),
},
});
if (!res.ok) {
throw new Error(
`deleting outfit failed: ${res.status} ${res.statusText}`,
);
}
}
function normalizeOutfit(outfit) {
return {
id: String(outfit.id),
name: outfit.name,
speciesId: String(outfit.species_id),
colorId: String(outfit.color_id),
pose: outfit.pose,
appearanceId: outfit.pet_state_id,
altStyleId: outfit.alt_style_id ? String(outfit.alt_style_id) : null,
wornItemIds: (outfit.item_ids?.worn || []).map((id) => String(id)),
closetedItemIds: (outfit.item_ids?.closeted || []).map((id) =>
String(id),
),
creator: outfit.user ? { id: String(outfit.user.id) } : null,
createdAt: outfit.created_at,
updatedAt: outfit.updated_at,
};
}
function getCSRFToken() {
return document.querySelector("meta[name=csrf-token]")?.content;
}