From dc4db405d2991d3ec6a114f3b08d3fccd057d3ee Mon Sep 17 00:00:00 2001
From: Wolfgang Müller
Date: Thu, 13 Feb 2025 17:52:16 +0100
Subject: frontend: Migrate to Svelte 5

---
 frontend/src/lib/scraper/ComicScrapeForm.svelte | 125 +++++++++++++-----------
 1 file changed, 67 insertions(+), 58 deletions(-)

(limited to 'frontend/src/lib/scraper/ComicScrapeForm.svelte')

diff --git a/frontend/src/lib/scraper/ComicScrapeForm.svelte b/frontend/src/lib/scraper/ComicScrapeForm.svelte
index 30ad89b..6cc3451 100644
--- a/frontend/src/lib/scraper/ComicScrapeForm.svelte
+++ b/frontend/src/lib/scraper/ComicScrapeForm.svelte
@@ -2,60 +2,69 @@
 	import { upsertComics } from '$gql/Mutations';
 	import { comicScrapersQuery, scrapeComic } from '$gql/Queries';
 	import { isError } from '$gql/Utils';
-	import { OnMissing, type FullComicFragment } from '$gql/graphql';
-	import { ScrapedComicSelector, getScraperContext } from '$lib/Scraper';
+	import { OnMissing, type FullComicFragment, type ScrapeComicQuery } from '$gql/graphql';
 	import { toastError, toastFinally } from '$lib/Toasts';
 	import Select from '$lib/components/Select.svelte';
 	import Spinner from '$lib/components/Spinner.svelte';
-	import { getContextClient } from '@urql/svelte';
+	import { getContextClient, type OperationResult } from '@urql/svelte';
+	import { getScraperContext, ScrapedComicSelector } from './Scraper.svelte';
 	import SelectorGroup from './components/SelectorGroup.svelte';
 	import SelectorItem from './components/SelectorItem.svelte';
 
 	let client = getContextClient();
 	const context = getScraperContext();
 
-	export let comic: FullComicFragment;
-	let createMissing = false;
-	let loading = false;
+	interface Props {
+		comic: FullComicFragment;
+		onupsert: () => void;
+	}
 
-	$: scrapersResult = comicScrapersQuery(client, { id: comic.id });
-	$: scrapers = $scrapersResult.data?.comicScrapers;
+	let { comic, onupsert }: Props = $props();
+	let createMissing = $state(false);
+	let loading = $state(false);
 
-	function scrape() {
-		loading = true;
-		scrapeComic(client, { id: comic.id, scraper: $context.scraper })
-			.then((result) => {
-				if (result.error) {
-					toastError(result.error.message);
-					return;
-				}
+	let scrapersResult = $derived(comicScrapersQuery(client, { id: comic.id }));
+	let scrapers = $derived($scrapersResult.data?.comicScrapers);
 
-				if (result.data) {
-					if (isError(result.data.scrapeComic)) {
-						toastError(result.data.scrapeComic.message);
-						return;
-					}
+	function scrape(event: SubmitEvent) {
+		event.preventDefault();
+		if (!context.scraper) return;
 
-					if (result.data.scrapeComic.__typename === 'ScrapeComicResult') {
-						$context.selector = new ScrapedComicSelector(result.data.scrapeComic.data, comic);
-						$context.warnings = result.data.scrapeComic.warnings;
-					}
-				}
-			})
+		loading = true;
+		scrapeComic(client, { id: comic.id, scraper: context.scraper })
+			.then(handleScrapeResult)
 			.catch(toastFinally)
 			.finally(() => (loading = false));
 	}
 
-	function updateFromScrape(createMissing: boolean) {
-		if (!$context.selector) return;
+	function handleScrapeResult(result: OperationResult<ScrapeComicQuery>) {
+		if (result.error) {
+			toastError(result.error.message);
+			return;
+		}
+
+		if (result.data) {
+			if (isError(result.data.scrapeComic)) {
+				toastError(result.data.scrapeComic.message);
+				return;
+			}
+
+			if (result.data.scrapeComic.__typename === 'ScrapeComicResult') {
+				context.selector = new ScrapedComicSelector(result.data.scrapeComic.data, comic);
+				context.warnings = result.data.scrapeComic.warnings;
+			}
+		}
+	}
+
+	function upsert(event: SubmitEvent) {
+		event.preventDefault();
+		if (!context.selector) return;
 
-		upsertComics(client, {
-			ids: comic.id,
-			input: $context.selector.toInput(createMissing ? OnMissing.Create : OnMissing.Ignore)
-		})
+		const input = context.selector.input(createMissing ? OnMissing.Create : OnMissing.Ignore);
+		upsertComics(client, { ids: comic.id, input })
 			.then(() => {
-				$context.selector = undefined;
-				$context.warnings = [];
+				onupsert();
+				context.reset();
 			})
 			.catch(toastFinally);
 	}
@@ -65,56 +74,56 @@
 	{#if scrapers && scrapers.length === 0}
 		<h2 class="text-base">No scrapers available.</h2>
 	{:else}
-		<form on:submit|preventDefault={scrape}>
+		<form onsubmit={scrape}>
 			<div class="grid grid-cols-6 gap-2">
 				<div class="col-span-5">
 					<Select
 						id="scrapers"
 						options={scrapers}
 						placeholder={'Select scraper...'}
-						bind:value={$context.scraper}
+						bind:value={context.scraper}
 					/>
 				</div>
-				<button type="submit" disabled={!$context.scraper} class="btn-blue">Scrape</button>
+				<button type="submit" disabled={!context.scraper} class="btn-blue">Scrape</button>
 			</div>
 		</form>
 	{/if}
 
 	{#if loading}
 		<Spinner />
-	{:else if $context.selector}
-		{#if $context.warnings.length > 0}
+	{:else if context.selector}
+		{#if context.warnings.length > 0}
 			<div class="flex flex-col gap-2">
 				<h2 class="flex gap-1 border-b border-slate-700 text-base font-medium">Warnings</h2>
 				<ul class="ml-2 list-inside list-disc">
-					{#each $context.warnings as warning}
+					{#each context.warnings as warning}
 						<li>{warning}</li>
 					{/each}
 				</ul>
 			</div>
 		{/if}
-		{#if !$context.selector.hasData()}
+		{#if !context.selector.pending()}
 			<h2 class="text-base">No data to merge.</h2>
 		{:else}
 			<div class="flex flex-col gap-2">
 				<h2 class="border-b border-slate-700 text-base font-medium">Results</h2>
-				<form on:submit|preventDefault={() => updateFromScrape(createMissing)}>
+				<form onsubmit={upsert}>
 					<div class="grid grid-cols-6 gap-4 pb-2">
-						<SelectorItem title="Title" selector={$context.selector.title} />
-						<SelectorItem title="Original Title" selector={$context.selector.originalTitle} />
-						<SelectorItem title="URL" selector={$context.selector.url} />
-						<SelectorItem title="Date" selector={$context.selector.date} --span="2" />
-						<SelectorItem title="Category" selector={$context.selector.category} --span="2" />
-						<SelectorItem title="Language" selector={$context.selector.language} --span="2" />
-						<SelectorItem title="Rating" selector={$context.selector.rating} --span="2" />
-						<SelectorItem title="Censorship" selector={$context.selector.censorship} --span="2" />
-						<SelectorItem title="Direction" selector={$context.selector.direction} --span="2" />
-						<SelectorItem title="Layout" selector={$context.selector.layout} --span="2" />
-						<SelectorGroup title="Artists" selectors={$context.selector.artists} />
-						<SelectorGroup title="Circles" selectors={$context.selector.circles} />
-						<SelectorGroup title="Characters" selectors={$context.selector.characters} />
-						<SelectorGroup title="Worlds" selectors={$context.selector.worlds} />
-						<SelectorGroup title="Tags" selectors={$context.selector.tags} />
+						<SelectorItem title="Title" selector={context.selector.title} />
+						<SelectorItem title="Original Title" selector={context.selector.originalTitle} />
+						<SelectorItem title="URL" selector={context.selector.url} />
+						<SelectorItem title="Date" selector={context.selector.date} --span="2" />
+						<SelectorItem title="Category" selector={context.selector.category} --span="2" />
+						<SelectorItem title="Language" selector={context.selector.language} --span="2" />
+						<SelectorItem title="Rating" selector={context.selector.rating} --span="2" />
+						<SelectorItem title="Censorship" selector={context.selector.censorship} --span="2" />
+						<SelectorItem title="Direction" selector={context.selector.direction} --span="2" />
+						<SelectorItem title="Layout" selector={context.selector.layout} --span="2" />
+						<SelectorGroup title="Artists" selectors={context.selector.artists} />
+						<SelectorGroup title="Circles" selectors={context.selector.circles} />
+						<SelectorGroup title="Characters" selectors={context.selector.characters} />
+						<SelectorGroup title="Worlds" selectors={context.selector.worlds} />
+						<SelectorGroup title="Tags" selectors={context.selector.tags} />
 					</div>
 					<div class="flex flex-col gap-2">
 						<h2 class="border-b border-slate-700 text-base font-medium">Options</h2>
-- 
cgit v1.2.3-2-gb3c3

