fix(frontend): resolve submit spinner hang and data loss issues

- Add try-catch-finally error handling to handleSubmit and deleteEntries
  functions to ensure submitting state is always reset, even when API calls
  fail or timeout. This fixes the infinite loading spinner bug.

- Preserve genres field after AniList updates, matching the existing tags
  preservation pattern. Prevents genres array from being lost after form
  submission, which was causing "{#each} only works with iterable values"
  error when the page re-rendered.

- Add fallback (|| []) to genres each block to prevent rendering errors
  when genres is undefined or null for entries without genre data.

These fixes ensure robust error handling and data consistency during anime
list updates across AniList, MAL, and Simkl services.

Fixes: submit button spinner never stopping after form submission
Fixes: "{#each} only works with iterable values" error on genres display
This commit is contained in:
2026-03-20 15:51:55 -04:00
parent b0ca864dfe
commit 3e7f7d1c95

View File

@@ -23,10 +23,7 @@
} from "../mal/types/MALTypes"; } from "../mal/types/MALTypes";
import type { SimklAnime } from "../simkl/types/simklTypes"; import type { SimklAnime } from "../simkl/types/simklTypes";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { import type { StatusOption, StatusOptions } from "../helperTypes/StatusTypes";
StatusOption,
StatusOptions,
} from "../helperTypes/StatusTypes";
import type { AniListUpdateVariables } from "../anilist/types/AniListTypes"; import type { AniListUpdateVariables } from "../anilist/types/AniListTypes";
import { convertDateToAniList } from "../helperFunctions/convertDateToAniList"; import { convertDateToAniList } from "../helperFunctions/convertDateToAniList";
import { import {
@@ -81,8 +78,7 @@
{ id: 5, aniList: "REPEATING", mal: "rewatching", simkl: "watching" }, { id: 5, aniList: "REPEATING", mal: "rewatching", simkl: "watching" },
]; ];
let startingAnilistStatusOption: StatusOption = statusOptions.filter( let startingAnilistStatusOption: StatusOption = statusOptions.filter(
(option) => (option) => currentAniListAnime.data.MediaList.status === option.aniList,
currentAniListAnime.data.MediaList.status === option.aniList,
)[0]; )[0];
let startedAtDate: Date | null = convertAniListDateToDate( let startedAtDate: Date | null = convertAniListDateToDate(
currentAniListAnime.data.MediaList.startedAt, currentAniListAnime.data.MediaList.startedAt,
@@ -113,15 +109,11 @@
let startDate = ""; let startDate = "";
let finishDate = ""; let finishDate = "";
if (currentMalAnime.my_list_status.start_date !== "") { if (currentMalAnime.my_list_status.start_date !== "") {
const startArray = re.exec( const startArray = re.exec(currentMalAnime.my_list_status.start_date);
currentMalAnime.my_list_status.start_date,
);
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`; startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
} }
if (currentMalAnime.my_list_status.finish_date !== "") { if (currentMalAnime.my_list_status.finish_date !== "") {
const finishArray = re.exec( const finishArray = re.exec(currentMalAnime.my_list_status.finish_date);
currentMalAnime.my_list_status.finish_date,
);
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`; finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
} }
AddAnimeServiceToTable({ AddAnimeServiceToTable({
@@ -198,6 +190,7 @@
submitData[key] = value; submitData[key] = value;
} }
try {
if ( if (
isAniListLoggedIn && isAniListLoggedIn &&
currentAniListAnime.data.MediaList.mediaId !== 0 currentAniListAnime.data.MediaList.mediaId !== 0
@@ -212,10 +205,11 @@
startedAt: convertDateToAniList(startedAtDate), startedAt: convertDateToAniList(startedAtDate),
completedAt: convertDateToAniList(completedAtDate), completedAt: convertDateToAniList(completedAtDate),
}; };
await AniListUpdateEntry(body).then( await AniListUpdateEntry(body).then((value: AniListGetSingleAnime) => {
(value: AniListGetSingleAnime) => {
value.data.MediaList.media.tags = value.data.MediaList.media.tags =
currentAniListAnime.data.MediaList.media.tags; currentAniListAnime.data.MediaList.media.tags;
value.data.MediaList.media.genres =
currentAniListAnime.data.MediaList.media.genres;
aniListAnime.update((newValue) => { aniListAnime.update((newValue) => {
newValue = value; newValue = value;
return newValue; return newValue;
@@ -236,8 +230,7 @@
repeat: currentAniListAnime.data.MediaList.repeat, repeat: currentAniListAnime.data.MediaList.repeat,
notes: currentAniListAnime.data.MediaList.notes, notes: currentAniListAnime.data.MediaList.notes,
}); });
}, });
);
} }
if (malLoggedIn && currentMalAnime.id !== 0) { if (malLoggedIn && currentMalAnime.id !== 0) {
@@ -254,8 +247,7 @@
(malAnimeReturn: MalListStatus) => { (malAnimeReturn: MalListStatus) => {
malAnime.update((value) => { malAnime.update((value) => {
value.my_list_status.status = malAnimeReturn.status; value.my_list_status.status = malAnimeReturn.status;
value.my_list_status.is_rewatching = value.my_list_status.is_rewatching = malAnimeReturn.is_rewatching;
malAnimeReturn.is_rewatching;
value.my_list_status.score = malAnimeReturn.score; value.my_list_status.score = malAnimeReturn.score;
value.my_list_status.num_episodes_watched = value.my_list_status.num_episodes_watched =
malAnimeReturn.num_episodes_watched; malAnimeReturn.num_episodes_watched;
@@ -282,14 +274,12 @@
id: `m-${currentMalAnime.id}`, id: `m-${currentMalAnime.id}`,
title: currentMalAnime.title, title: currentMalAnime.title,
service: "MyAnimeList", service: "MyAnimeList",
progress: progress: currentMalAnime.my_list_status.num_episodes_watched,
currentMalAnime.my_list_status.num_episodes_watched,
status: currentMalAnime.my_list_status.status, status: currentMalAnime.my_list_status.status,
startedAt: startDate, startedAt: startDate,
completedAt: finishDate, completedAt: finishDate,
score: currentMalAnime.my_list_status.score, score: currentMalAnime.my_list_status.score,
repeat: currentMalAnime.my_list_status repeat: currentMalAnime.my_list_status.num_times_rewatched,
.num_times_rewatched,
notes: currentMalAnime.my_list_status.comments, notes: currentMalAnime.my_list_status.comments,
}); });
}, },
@@ -297,13 +287,9 @@
} }
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) { if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
if ( if (currentSimklAnime.watched_episodes_count !== submitData.episodes) {
currentSimklAnime.watched_episodes_count !== submitData.episodes await SimklSyncEpisodes(currentSimklAnime, submitData.episodes).then(
) { (value: SimklAnime) => {
await SimklSyncEpisodes(
currentSimklAnime,
submitData.episodes,
).then((value: SimklAnime) => {
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`, id: `s-${value.show.ids.simkl}`,
title: value.show.title, title: value.show.title,
@@ -320,14 +306,13 @@
newValue = value; newValue = value;
return newValue; return newValue;
}); });
}); },
);
} }
if (currentSimklAnime.user_rating !== submitData.rating) { if (currentSimklAnime.user_rating !== submitData.rating) {
await SimklSyncRating( await SimklSyncRating(currentSimklAnime, submitData.rating).then(
currentSimklAnime, (value) => {
submitData.rating,
).then((value) => {
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`, id: `s-${value.show.ids.simkl}`,
title: value.show.title, title: value.show.title,
@@ -344,7 +329,8 @@
newValue = value; newValue = value;
return newValue; return newValue;
}); });
}); },
);
} }
if (currentSimklAnime.status !== submitData.status.simkl) { if (currentSimklAnime.status !== submitData.status.simkl) {
@@ -371,14 +357,18 @@
}); });
} }
} }
} catch (error) {
console.error("Error submitting changes:", error);
} finally {
submitting.set(false); submitting.set(false);
submitSuccess.set(true); submitSuccess.set(true);
setTimeout(() => submitSuccess.set(false), 2000); setTimeout(() => submitSuccess.set(false), 2000);
}
}; };
const deleteEntries = async () => { const deleteEntries = async () => {
submitting.set(true); submitting.set(true);
try {
if ( if (
isAniListLoggedIn && isAniListLoggedIn &&
currentAniListAnime.data.MediaList.mediaId !== 0 currentAniListAnime.data.MediaList.mediaId !== 0
@@ -427,9 +417,13 @@
notes: "", notes: "",
}); });
} }
} catch (error) {
console.error("Error deleting entries:", error);
} finally {
submitting.set(false); submitting.set(false);
submitSuccess.set(true); submitSuccess.set(true);
setTimeout(() => submitSuccess.set(false), 2000); setTimeout(() => submitSuccess.set(false), 2000);
}
}; };
let max = 999; let max = 999;
@@ -442,8 +436,7 @@
currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0 currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0
) { ) {
max = max =
currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode - currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode - 1;
1;
} }
</script> </script>
@@ -479,17 +472,11 @@
on:click={() => { on:click={() => {
currentAniListAnime.data.MediaList.progress -= 1; currentAniListAnime.data.MediaList.progress -= 1;
if ( if (
currentAniListAnime.data.MediaList currentAniListAnime.data.MediaList.progress <
.progress < currentAniListAnime.data.MediaList.media.episodes
currentAniListAnime.data.MediaList.media
.episodes
) { ) {
startingAnilistStatusOption = startingAnilistStatusOption = statusOptions[0];
statusOptions[0]; if (currentAniListAnime.data.MediaList.repeat === 0)
if (
currentAniListAnime.data.MediaList
.repeat === 0
)
completedAtDate = null; completedAtDate = null;
} }
}} }}
@@ -519,22 +506,18 @@
id="episodes" id="episodes"
class="border border-x-0 p-2.5 h-11 text-center text-sm block w-full placeholder-gray-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none class="border border-x-0 p-2.5 h-11 text-center text-sm block w-full placeholder-gray-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none
{currentAniListAnime.data.MediaList.progress < 0 || {currentAniListAnime.data.MediaList.progress < 0 ||
(currentAniListAnime.data.MediaList.media.episodes > (currentAniListAnime.data.MediaList.media.episodes > 0 &&
0 &&
currentAniListAnime.data.MediaList.progress > currentAniListAnime.data.MediaList.progress >
currentAniListAnime.data.MediaList.media currentAniListAnime.data.MediaList.media.episodes) ||
.episodes) || (currentAniListAnime.data.MediaList.media.nextAiringEpisode
(currentAniListAnime.data.MediaList.media .episode > 0 &&
.nextAiringEpisode.episode > 0 &&
currentAniListAnime.data.MediaList.progress > currentAniListAnime.data.MediaList.progress >
currentAniListAnime.data.MediaList.media currentAniListAnime.data.MediaList.media.nextAiringEpisode
.nextAiringEpisode.episode - .episode -
1) 1)
? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500' ? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500'
: 'bg-gray-700 hover:bg-gray-600 border-gray-600 text-white focus:ring-blue-500 focus:border-blue-500'} w-24" : 'bg-gray-700 hover:bg-gray-600 border-gray-600 text-white focus:ring-blue-500 focus:border-blue-500'} w-24"
bind:value={ bind:value={currentAniListAnime.data.MediaList.progress}
currentAniListAnime.data.MediaList.progress
}
required required
/> />
<button <button
@@ -544,24 +527,15 @@
on:click={() => { on:click={() => {
currentAniListAnime.data.MediaList.progress += 1; currentAniListAnime.data.MediaList.progress += 1;
if ( if (
currentAniListAnime.data.MediaList.media currentAniListAnime.data.MediaList.media.episodes ===
.episodes ===
currentAniListAnime.data.MediaList.progress currentAniListAnime.data.MediaList.progress
) { ) {
startingAnilistStatusOption = startingAnilistStatusOption = statusOptions[2];
statusOptions[2];
completedAtDate = new Date(); completedAtDate = new Date();
} }
if ( if (currentAniListAnime.data.MediaList.progress - 1 === 0) {
currentAniListAnime.data.MediaList startingAnilistStatusOption = statusOptions[0];
.progress - if (startedAtDate === null) startedAtDate = new Date();
1 ===
0
) {
startingAnilistStatusOption =
statusOptions[0];
if (startedAtDate === null)
startedAtDate = new Date();
} }
}} }}
class="bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none" class="bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
@@ -584,16 +558,15 @@
</button> </button>
</div> </div>
<div> <div>
/ {currentAniListAnime.data.MediaList.media / {currentAniListAnime.data.MediaList.media.nextAiringEpisode
.nextAiringEpisode.episode !== 0 .episode !== 0
? currentAniListAnime.data.MediaList.media ? currentAniListAnime.data.MediaList.media.nextAiringEpisode
.nextAiringEpisode.episode - 1 .episode - 1
: currentAniListAnime.data.MediaList.media.episodes} : currentAniListAnime.data.MediaList.media.episodes}
</div> </div>
{#if currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0} {#if currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0}
<div> <div>
of {currentAniListAnime.data.MediaList.media of {currentAniListAnime.data.MediaList.media.episodes}
.episodes}
</div> </div>
{/if} {/if}
</div> </div>
@@ -665,8 +638,7 @@
name="repeat" name="repeat"
min="0" min="0"
id="repeat" id="repeat"
class="border {currentAniListAnime.data.MediaList class="border {currentAniListAnime.data.MediaList.repeat < 0
.repeat < 0
? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500' ? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-500 text-white focus:ring-blue-500 focus:border-blue-500'} text-sm rounded-lg block w-24 p-2.5 bg-gray-600 placeholder-gray-400 text-white" : 'border-gray-500 text-white focus:ring-blue-500 focus:border-blue-500'} text-sm rounded-lg block w-24 p-2.5 bg-gray-600 placeholder-gray-400 text-white"
bind:value={currentAniListAnime.data.MediaList.repeat} bind:value={currentAniListAnime.data.MediaList.repeat}
@@ -829,7 +801,7 @@
<div class="flex m-5"> <div class="flex m-5">
<div> <div>
<h3 class="text-2xl">Genres</h3> <h3 class="text-2xl">Genres</h3>
{#each currentAniListAnime.data.MediaList.media.genres as genre} {#each currentAniListAnime.data.MediaList.media.genres || [] as genre}
<div> <div>
<Badge large border color="blue" class="m-1 w-52"> <Badge large border color="blue" class="m-1 w-52">
<div> <div>