2023-11-02 13:50:33 -07:00
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
2023-11-11 08:15:10 -08:00
|
|
|
export function useSavedOutfit(id, options = {}) {
|
2023-11-02 13:50:33 -07:00
|
|
|
return useQuery({
|
|
|
|
...options,
|
|
|
|
queryKey: ["outfits", String(id)],
|
|
|
|
queryFn: () => loadSavedOutfit(id),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-02 17:39:26 -07:00
|
|
|
export function useSaveOutfitMutation(options = {}) {
|
2023-11-02 13:50:33 -07:00
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
return useMutation({
|
|
|
|
...options,
|
|
|
|
mutationFn: saveOutfit,
|
|
|
|
onSuccess: (outfit) => {
|
2023-11-02 17:12:59 -07:00
|
|
|
queryClient.setQueryData(["outfits", outfit.id], outfit);
|
2023-11-02 17:39:26 -07:00
|
|
|
if (options.onSuccess) {
|
|
|
|
options.onSuccess(outfit);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useDeleteOutfitMutation(options = {}) {
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
return useMutation({
|
|
|
|
...options,
|
|
|
|
mutationFn: deleteOutfit,
|
|
|
|
onSuccess: (emptyData, id, context) => {
|
2024-02-01 05:55:19 -08:00
|
|
|
queryClient.invalidateQueries({
|
|
|
|
queryKey: ["outfits", String(id)],
|
|
|
|
});
|
2023-11-02 17:39:26 -07:00
|
|
|
if (options.onSuccess) {
|
|
|
|
options.onSuccess(emptyData, id, context);
|
|
|
|
}
|
2023-11-02 13:50:33 -07:00
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function loadSavedOutfit(id) {
|
|
|
|
const res = await fetch(`/outfits/${encodeURIComponent(id)}.json`);
|
|
|
|
|
|
|
|
if (!res.ok) {
|
2024-02-01 05:55:19 -08:00
|
|
|
throw new Error(
|
|
|
|
`loading outfit failed: ${res.status} ${res.statusText}`,
|
|
|
|
);
|
2023-11-02 13:50:33 -07:00
|
|
|
}
|
|
|
|
|
2023-11-02 17:12:59 -07:00
|
|
|
return res.json().then(normalizeOutfit);
|
2023-11-02 13:50:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async function saveOutfit({
|
|
|
|
id, // optional, null when creating a new outfit
|
|
|
|
name, // optional, server may fill in a placeholder
|
|
|
|
speciesId,
|
|
|
|
colorId,
|
|
|
|
pose,
|
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
|
|
|
appearanceId,
|
2024-02-01 05:55:19 -08:00
|
|
|
altStyleId,
|
2023-11-02 13:50:33 -07:00
|
|
|
wornItemIds,
|
|
|
|
closetedItemIds,
|
|
|
|
}) {
|
|
|
|
const params = {
|
|
|
|
outfit: {
|
|
|
|
name: name,
|
|
|
|
biology: {
|
|
|
|
species_id: speciesId,
|
|
|
|
color_id: colorId,
|
|
|
|
pose: pose,
|
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
|
|
|
pet_state_id: appearanceId,
|
2023-11-02 13:50:33 -07:00
|
|
|
},
|
2024-02-01 05:55:19 -08:00
|
|
|
alt_style_id: altStyleId,
|
2023-11-02 13:50:33 -07:00
|
|
|
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) {
|
2024-02-01 05:55:19 -08:00
|
|
|
throw new Error(
|
|
|
|
`saving outfit failed: ${res.status} ${res.statusText}`,
|
|
|
|
);
|
2023-11-02 13:50:33 -07:00
|
|
|
}
|
|
|
|
|
2023-11-02 17:12:59 -07:00
|
|
|
return res.json().then(normalizeOutfit);
|
|
|
|
}
|
|
|
|
|
2023-11-02 17:39:26 -07:00
|
|
|
async function deleteOutfit(id) {
|
|
|
|
const res = await fetch(`/outfits/${encodeURIComponent(id)}.json`, {
|
|
|
|
method: "DELETE",
|
|
|
|
headers: {
|
|
|
|
"X-CSRF-Token": getCSRFToken(),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!res.ok) {
|
2024-02-01 05:55:19 -08:00
|
|
|
throw new Error(
|
|
|
|
`deleting outfit failed: ${res.status} ${res.statusText}`,
|
|
|
|
);
|
2023-11-02 17:39:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-02 17:12:59 -07:00
|
|
|
function normalizeOutfit(outfit) {
|
|
|
|
return {
|
|
|
|
id: String(outfit.id),
|
|
|
|
name: outfit.name,
|
|
|
|
speciesId: String(outfit.species_id),
|
|
|
|
colorId: String(outfit.color_id),
|
|
|
|
pose: outfit.pose,
|
2024-02-08 10:15:31 -08:00
|
|
|
appearanceId: String(outfit.pet_state_id),
|
2024-02-01 05:55:19 -08:00
|
|
|
altStyleId: outfit.alt_style_id ? String(outfit.alt_style_id) : null,
|
2023-11-02 17:12:59 -07:00
|
|
|
wornItemIds: (outfit.item_ids?.worn || []).map((id) => String(id)),
|
2024-02-01 05:55:19 -08:00
|
|
|
closetedItemIds: (outfit.item_ids?.closeted || []).map((id) =>
|
|
|
|
String(id),
|
|
|
|
),
|
2023-11-02 17:12:59 -07:00
|
|
|
creator: outfit.user ? { id: String(outfit.user.id) } : null,
|
|
|
|
createdAt: outfit.created_at,
|
|
|
|
updatedAt: outfit.updated_at,
|
|
|
|
};
|
2023-11-02 13:50:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function getCSRFToken() {
|
|
|
|
return document.querySelector("meta[name=csrf-token]")?.content;
|
|
|
|
}
|