refactor(frontend): flowbite-svelte v1 APIs, bindings-aligned store types

flowbite-svelte v1 turns components into runes: on:click on
components becomes onclick props (native elements keep on:), Modal
footers become {#snippet footer()}, Button["color"] indexing becomes
the exported ButtonProps type, and slate is gone from the color
union so the datepicker greys map to gray.

Stores now use the generated bindings models (what the Go backend
actually returns) instead of hand-written shapes that drifted from
them - nullability of genres/tags/mediaList included. That single
move clears most of the newly surfaced type errors, including the
optional episodes/nextAiringEpisode cluster. The rest is strictness
debt the old red check masked: definite-assignment on
subscribe-fed lets, null guards on querySelector/match results and
date parsing (two of which fixed latent throw-on-garbage paths),
aria-labels on icon-only buttons, the completed default-data
literal, and a shared flex centering context for the detail poster
column so poster and stars align by construction at every width.
This commit is contained in:
John O'Keefe
2026-09-16 14:00:17 -04:00
parent 59b81396f9
commit 8d5d8ac2d7
16 changed files with 144 additions and 89 deletions
+36 -22
View File
@@ -11,22 +11,22 @@
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { push } from "svelte-spa-router";
import WebsiteLink from "./WebsiteLink.svelte";
import type { AniListGetSingleAnime } from "../anilist/types/AniListCurrentUserWatchListType";
import type {
AniListGetSingleAnime,
AniListUpdateVariables,
MALAnime,
MalListStatus,
MALUploadStatus,
SimklAnime,
} from "../../bindings/AniTrack/models";
import Rating from "./Rating.svelte";
import {
convertAniListDateToString,
convertAniListDateToDate,
} from "../helperFunctions/convertAniListDateIn";
import AnimeTable from "./AnimeTable.svelte";
import type {
MALAnime,
MalListStatus,
MALUploadStatus,
} from "../mal/types/MALTypes";
import type { SimklAnime } from "../simkl/types/simklTypes";
import { writable } from "svelte/store";
import type { StatusOption, StatusOptions } from "../helperTypes/StatusTypes";
import type { AniListUpdateVariables } from "../anilist/types/AniListTypes";
import { convertDateToAniList } from "../helperFunctions/convertDateToAniList";
import {App} from "../../bindings/AniTrack";
import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte";
@@ -35,12 +35,12 @@
import { Badge, Tooltip } from "flowbite-svelte";
const re = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/;
let isAniListLoggedIn: boolean;
let isMalLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let currentAniListAnime: AniListGetSingleAnime;
let currentMalAnime: MALAnime;
let currentSimklAnime: SimklAnime;
let isAniListLoggedIn!: boolean;
let isMalLoggedIn!: boolean;
let isSimklLoggedIn!: boolean;
let currentAniListAnime!: AniListGetSingleAnime;
let currentMalAnime!: MALAnime;
let currentSimklAnime!: SimklAnime;
let submitting = writable(false);
let isSubmitting: boolean;
let submitSuccess = writable(false);
@@ -103,11 +103,15 @@
let finishDate = "";
if (currentMalAnime.my_list_status.start_date !== "") {
const startArray = re.exec(currentMalAnime.my_list_status.start_date);
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
if (startArray) {
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
}
}
if (currentMalAnime.my_list_status.finish_date !== "") {
const finishArray = re.exec(currentMalAnime.my_list_status.finish_date);
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
if (finishArray) {
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
}
}
AddAnimeServiceToTable({
id: `m-${currentMalAnime.id}`,
@@ -180,7 +184,11 @@
submitData.status = startingAnilistStatusOption;
continue;
}
submitData[key] = value;
// The only remaining form field is "notes" (string); anything else
// is ignored rather than assigned into a mistyped slot.
if (key === "notes" && typeof value === "string") {
submitData.notes = value;
}
}
try {
@@ -271,13 +279,17 @@
const startArray = re.exec(
currentMalAnime.my_list_status.start_date,
);
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
if (startArray) {
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
}
}
if (currentMalAnime.my_list_status.finish_date !== "") {
const finishArray = re.exec(
currentMalAnime.my_list_status.finish_date,
);
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
if (finishArray) {
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
}
}
AddAnimeServiceToTable({
id: `m-${currentMalAnime.id}`,
@@ -503,7 +515,7 @@
{title}
</h1>
<div class="grid grid-cols-1 md:grid-cols-10 grid-flow-col gap-4">
<div class="md:col-span-2 space-y-3">
<div class="md:col-span-2 space-y-3 flex flex-col items-center">
<img
class="rounded-lg"
src={currentAniListAnime.data.MediaList.media.coverImage.large}
@@ -525,6 +537,7 @@
<div class="relative flex items-center max-w-[8rem]">
<button
type="button"
aria-label="Decrease episode progress"
id="decrement-button"
data-input-counter-decrement="quantity-input"
on:click={() => {
@@ -583,6 +596,7 @@
/>
<button
type="button"
aria-label="Increase episode progress"
id="increment-button"
data-input-counter-increment="quantity-input"
on:click={() => {
@@ -682,7 +696,7 @@
>
<Datepicker
bind:value={startedAtDate}
color="slate"
color="gray"
dateFormat={{
year: "numeric",
month: "2-digit",
@@ -699,7 +713,7 @@
>
<Datepicker
bind:value={completedAtDate}
color="slate"
color="gray"
dateFormat={{
year: "numeric",
month: "2-digit",
+10 -10
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { Avatar } from "flowbite-svelte";
import type { AniListUser } from "../anilist/types/AniListTypes";
import type { AniListUser, MyAnimeListUser, SimklUser } from "../../bindings/AniTrack/models";
import {
aniListLoggedIn,
aniListUser,
@@ -17,16 +17,14 @@
serviceLoggingIn,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import {Application} from "@wailsio/runtime";
import type { MyAnimeListUser } from "../mal/types/MALTypes";
import type { SimklUser } from "../simkl/types/simklTypes";
import {App} from "../../bindings/AniTrack";
import {App} from "../../bindings/AniTrack";
let currentAniListUser: AniListUser;
let currentMALUser: MyAnimeListUser;
let currentSimklUser: SimklUser;
let isAniListLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMALLoggedIn: boolean;
let isAniListLoggedIn!: boolean;
let isSimklLoggedIn!: boolean;
let isMALLoggedIn!: boolean;
let loggingIn: string[] = [];
aniListUser.subscribe((value) => (currentAniListUser = value));
@@ -38,7 +36,8 @@
serviceLoggingIn.subscribe((value) => (loggingIn = value));
function dropdownUser(): void {
let dropdown = document.querySelector("#userDropdown");
const dropdown = document.querySelector("#userDropdown");
if (!dropdown) return;
dropdown.classList.toggle("hidden");
if (!dropdown.classList.contains("hidden")) {
@@ -47,8 +46,9 @@
}
function clickOutside(event: Event): void {
let dropdown = document.querySelector("#userDropdown");
let toggleBtn = document.querySelector("#userDropdownButton");
const dropdown = document.querySelector("#userDropdown");
const toggleBtn = document.querySelector("#userDropdownButton");
if (!dropdown || !toggleBtn) return;
if (
!dropdown.contains(event.target as Node) &&
+15 -14
View File
@@ -2,6 +2,7 @@
import { createEventDispatcher, onMount } from "svelte";
import { fade } from "svelte/transition";
import { Button } from "flowbite-svelte";
import type { ButtonProps } from "flowbite-svelte";
export let value: Date | null = null;
export let defaultDate: Date | null = null;
@@ -19,7 +20,7 @@
export let disabled: boolean = false;
export let required: boolean = false;
export let inputClass: string = "";
export let color: Button["color"] = "primary";
export let color: ButtonProps["color"] = "primary";
export let inline: boolean = false;
export let autohide: boolean = true;
export let showActionButtons: boolean = false;
@@ -47,7 +48,7 @@
});
// Color handling functions
function getFocusRingClass(color: Button["color"]): string {
function getFocusRingClass(color: ButtonProps["color"]): string {
switch (color) {
case "primary":
return "focus:ring-2 focus:ring-primary-400";
@@ -61,14 +62,14 @@
return "focus:ring-2 focus:ring-yellow-400";
case "purple":
return "focus:ring-2 focus:ring-purple-400";
case "slate":
return "focus:ring-2 focus:ring-slate-400";
case "gray":
return "focus:ring-2 focus:ring-gray-400";
default:
return "";
}
}
function getRangeBackgroundClass(color: Button["color"]): string {
function getRangeBackgroundClass(color: ButtonProps["color"]): string {
switch (color) {
case "primary":
return "bg-primary-900";
@@ -82,8 +83,8 @@
return "bg-yellow-900";
case "purple":
return "bg-purple-900";
case "slate":
return "bg-slate-900";
case "gray":
return "bg-gray-900";
default:
return "";
}
@@ -352,7 +353,7 @@
{/if}
<div class="flex items-center justify-between mb-4">
<Button
on:click={() => changeMonth(-1)}
onclick={() => changeMonth(-1)}
{color}
size="sm"
aria-label="Previous month"
@@ -382,7 +383,7 @@
})}
</h3>
<Button
on:click={() => changeMonth(1)}
onclick={() => changeMonth(1)}
{color}
size="sm"
aria-label="Next month"
@@ -424,8 +425,8 @@
: ''} {isInRange(day)
? getRangeBackgroundClass(color)
: ''}"
on:click={() => handleDaySelect(day)}
on:keydown={handleCalendarKeydown}
onclick={() => handleDaySelect(day)}
onkeydown={handleCalendarKeydown}
aria-label={day.toLocaleDateString(locale, {
weekday: "long",
year: "numeric",
@@ -441,13 +442,13 @@
</div>
{#if showActionButtons}
<div class="mt-4 flex justify-between">
<Button on:click={handleToday} {color} size="sm"
<Button onclick={handleToday} {color} size="sm"
>Today</Button
>
<Button on:click={handleClear} color="red" size="sm"
<Button onclick={handleClear} color="red" size="sm"
>Clear</Button
>
<Button on:click={handleApply} {color} size="sm"
<Button onclick={handleApply} {color} size="sm"
>Apply</Button
>
</div>
@@ -61,13 +61,15 @@
dismiss this message to continue with limited functionality.
</p>
</div>
<div slot="footer" class="flex gap-3 justify-end">
{#if $apiError.canRetry}
<Button on:click={handleRetry} class="bg-blue-600 hover:bg-blue-700">
Retry Connection
</Button>
{/if}
<Button on:click={handleDismiss} color="alternative">Dismiss</Button>
</div>
{#snippet footer()}
<div class="flex gap-3 justify-end">
{#if $apiError.canRetry}
<Button onclick={handleRetry} class="bg-blue-600 hover:bg-blue-700">
Retry Connection
</Button>
{/if}
<Button onclick={handleDismiss} color="alternative">Dismiss</Button>
</div>
{/snippet}
</Modal>
{/if}
+5 -4
View File
@@ -13,9 +13,9 @@
import logo from "../assets/images/AniTrackLogo.svg";
import { link } from "svelte-spa-router";
let isAniListLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMALLoggedIn: boolean;
let isAniListLoggedIn!: boolean;
let isSimklLoggedIn!: boolean;
let isMALLoggedIn!: boolean;
let loggingIn: string[] = [];
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
@@ -42,7 +42,8 @@
<AvatarMenu />
<button
on:click={() => {
let menu = document.querySelector("#navbar-user");
const menu = document.querySelector("#navbar-user");
if (!menu) return;
menu.classList.toggle("hidden");
}}
type="button"
@@ -7,13 +7,13 @@
watchListPage,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType";
import type { AniListCurrentUserWatchList } from "../../bindings/AniTrack/models";
import {App} from "../../bindings/AniTrack";
let aniListWatchListLoaded: AniListCurrentUserWatchList;
let page: number;
let perPage: number;
let sort: string;
let sort!: string;
watchListPage.subscribe((value) => (page = value));
animePerPage.subscribe((value) => (perPage = value));
@@ -151,6 +151,7 @@
<div class="relative flex items-center max-w-[11rem]">
<button
type="button"
aria-label="Previous page"
id="decrement-button"
on:click={() => ChangeWatchListPage(page - 1)}
class={page <= 1
@@ -191,6 +192,7 @@
</div>
<button
type="button"
aria-label="Next page"
id="increment-button"
on:click={() => ChangeWatchListPage(page + 1)}
class={page >= aniListWatchListLoaded.data.Page.pageInfo.lastPage
+1 -1
View File
@@ -50,7 +50,7 @@
{ value: MediaListSort.MediaPopularityDesc, name: "Media Popularity Desc" },
];
let sort: string;
let sort!: string;
aniListSort.subscribe((value) => (sort = value));
console.log(sort);
@@ -6,14 +6,14 @@
loading,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { push } from "svelte-spa-router";
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType";
import type { AniListCurrentUserWatchList } from "../../bindings/AniTrack/models";
import { Rating } from "flowbite-svelte";
import loader from "../helperFunctions/loader";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Sort from "../helperComponents/Sort.svelte";
import RefreshWatchListButton from "./RefreshWatchListButton.svelte";
let isAniListLoggedIn: boolean;
let isAniListLoggedIn!: boolean;
let aniListWatchListLoaded: AniListCurrentUserWatchList;
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
@@ -12,7 +12,7 @@
isAniList = id.includes("a-");
isMAL = id.includes("m-");
isSimkl = id.includes("s-");
if (isAniList || isMAL || isSimkl) newId = id.match(re)[1];
if (isAniList || isMAL || isSimkl) newId = id.match(re)![1];
else newId = id;
}
@@ -1,4 +1,4 @@
import type {AniListGetSingleAnime} from "../anilist/types/AniListCurrentUserWatchListType";
import type {AniListGetSingleAnime} from "../../bindings/AniTrack/models";
export const AniListGetSingleAnimeDefaultData: AniListGetSingleAnime = {
data: {
@@ -10,23 +10,57 @@ export const AniListGetSingleAnimeDefaultData: AniListGetSingleAnime = {
id: 0,
idMal: 0,
title: {
userPreferred: "",
romaji: "",
english: "",
native: "",
},
description: "",
coverImage: {
ExtraLarge: "",
large: "",
Medium: "",
Color: "",
},
BannerImage: "",
Format: "",
season: "",
seasonYear: 0,
status: "",
episodes: 0,
Duration: 0,
CountryOfOrigin: "",
Source: "",
Synonyms: null,
AverageScore: 0,
MeanScore: 0,
Popularity: 0,
Trending: 0,
Favourites: 0,
relations: {
nodes: null,
},
startDate: {
year: 0,
month: 0,
day: 0,
},
endDate: {
year: 0,
month: 0,
day: 0,
},
nextAiringEpisode: {
airingAt: 0,
timeUntilAiring: 0,
episode: 0,
}
},
airingSchedule: {
nodes: null,
},
genres: [],
tags: [],
isAdult: false,
},
status: "",
startedAt: {
@@ -13,7 +13,7 @@ const convertAniListDateToString = (date: {
) {
return "";
}
const newISODate = new Date(date.year, date.month - 1, date.day);
const newISODate = new Date(date.year, date.month! - 1, date.day);
const newMoment = moment(newISODate);
return newMoment.format("MM-DD-YYYY");
};
@@ -31,7 +31,7 @@ const convertAniListDateToDate = (date: {
) {
return null;
}
return new Date(date.year, date.month - 1, date.day);
return new Date(date.year, date.month! - 1, date.day);
};
export { convertAniListDateToString, convertAniListDateToDate };
@@ -14,6 +14,13 @@ const convertDateStringToAniList = (date: string): AnilistDate => {
}
const re = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/;
const newDate = re.exec(date);
if (newDate === null) {
return {
year: 0,
month: 0,
day: 0,
};
}
return {
year: Number(newDate[1]),
month: Number(newDate[2]),
@@ -16,7 +16,7 @@
let isAniListPrimary: boolean;
let page: number;
let perPage: number;
let sort: string;
let sort!: string;
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
watchListPage.subscribe((value) => (page = value));
@@ -1,7 +1,7 @@
<script lang="ts" context="module">
import {App} from "../../bindings/AniTrack";
import {malUser, malPrimary, malWatchList, malLoggedIn, serviceLoggingIn} from "./GlobalVariablesAndHelperFunctions.svelte"
import type { MyAnimeListUser } from "../mal/types/MALTypes";
import type { MyAnimeListUser } from "../../bindings/AniTrack/models";
let isMalPrimary: boolean
malPrimary.subscribe(value => isMalPrimary = value)
@@ -3,22 +3,16 @@
import type {
AniListCurrentUserWatchList,
AniListGetSingleAnime,
} from "../anilist/types/AniListCurrentUserWatchListType.js";
import { writable } from "svelte/store";
import type {
SimklAnime,
SimklUser,
SimklWatchList,
} from "../simkl/types/simklTypes";
import {
type AniListUser,
MediaListSort,
} from "../anilist/types/AniListTypes";
import type {
AniListUser,
MALAnime,
MALWatchlist,
MyAnimeListUser,
} from "../mal/types/MALTypes";
SimklAnime,
SimklUser,
SimklWatchListType,
} from "../../bindings/AniTrack/models";
import { writable } from "svelte/store";
import { MediaListSort } from "../anilist/types/AniListTypes";
import type { TableItems } from "../helperTypes/TableTypes";
import { AniListGetSingleAnimeDefaultData } from "../helperDefaults/AniListGetSingleAnime";
@@ -28,7 +22,7 @@
export let simklLoggedIn = writable(false);
export let malLoggedIn = writable(false);
export const serviceLoggingIn = writable([] as string[]);
export let simklWatchList = writable({} as SimklWatchList);
export let simklWatchList = writable({} as SimklWatchListType);
export let aniListPrimary = writable(true);
export let simklPrimary = writable(false);
export let malPrimary = writable(false);
@@ -50,12 +44,12 @@
let isAniListPrimary: boolean;
let page: number;
let perPage: number;
let sort: string;
let sort!: string;
let aniWatchlist: AniListCurrentUserWatchList;
let currentAniListAnime: AniListGetSingleAnime;
let currentAniListAnime!: AniListGetSingleAnime;
let isMalLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMalLoggedIn!: boolean;
let isSimklLoggedIn!: boolean;
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
watchListPage.subscribe((value) => (page = value));
+1 -1
View File
@@ -12,7 +12,7 @@
import loader from "../helperFunctions/loader";
let isAniListPrimary: boolean;
let isAniListLoggedIn: boolean;
let isAniListLoggedIn!: boolean;
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));