diff --git a/src/lib/components/ui/DetailItem.svelte b/src/lib/components/ui/DetailItem.svelte index 4e087ce6..d323f781 100644 --- a/src/lib/components/ui/DetailItem.svelte +++ b/src/lib/components/ui/DetailItem.svelte @@ -7,6 +7,7 @@ import Checkbox from './checkbox/Checkbox.svelte' import CheckboxGroup from './checkbox/CheckboxGroup.svelte' import DatePicker from './DatePicker.svelte' + import Icon from '../Icon.svelte' interface SelectOption { value: string | number @@ -25,7 +26,9 @@ placeholder, element, onchange, - width + width, + linkUrl, + hasLinkButton = false }: { label: string /** Secondary label displayed below the main label */ @@ -41,17 +44,32 @@ onchange?: (checked: boolean) => void /** Custom width for the input field (e.g., '320px') */ width?: string + /** URL to open when link button is clicked */ + linkUrl?: string | null + /** Whether to show the link button (disabled when linkUrl is empty) */ + hasLinkButton?: boolean } = $props() // For checkbox type, derive the checked state from value // This ensures external changes to value are reflected in the checkbox const checkboxValue = $derived(type === 'checkbox' ? Boolean(value) : false) + // Show link button when hasLinkButton is true or linkUrl is provided + const showLinkButton = $derived(hasLinkButton || !!linkUrl) + const linkDisabled = $derived(!linkUrl) + // Handle checkbox change and call onchange if provided function handleCheckboxChange(checked: boolean) { value = checked as any onchange?.(checked) } + + // Open URL in new tab + function openLink() { + if (linkUrl) { + window.open(linkUrl, '_blank', 'noopener,noreferrer') + } + }
@@ -95,8 +113,21 @@ /> {:else if type === 'date'} + {:else if children} + {@render children()} {:else} + {#if showLinkButton} + + {/if} {/if}
{:else if children} @@ -133,11 +164,6 @@ background: colors.$grey-90; } - &.editable:focus-within, - &.hasChildren:focus-within { - background: var(--input-bg-hover); - } - &.editable, &.hasChildren { background: var(--input-bg); @@ -173,15 +199,48 @@ display: flex; flex-grow: 0; justify-content: flex-end; + align-items: center; + gap: spacing.$unit-half; :global(.input), - :global(.select) { + :global(.select), + :global(.multi-select) { width: var(--custom-width, 240px); } :global(.input.number) { width: var(--custom-width, 120px); } + + .link-button { + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + padding: 0; + border: none; + border-radius: layout.$item-corner; + background: transparent; + color: colors.$grey-50; + cursor: pointer; + flex-shrink: 0; + @include effects.smooth-transition(effects.$duration-quick, background-color, color, opacity); + + &:hover:not(:disabled) { + background: colors.$grey-90; + color: colors.$grey-30; + } + + &:active:not(:disabled) { + background: colors.$grey-80; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.4; + } + } } } diff --git a/src/lib/components/ui/SuggestionDetailItem.svelte b/src/lib/components/ui/SuggestionDetailItem.svelte index e7f3467e..58157cd3 100644 --- a/src/lib/components/ui/SuggestionDetailItem.svelte +++ b/src/lib/components/ui/SuggestionDetailItem.svelte @@ -7,6 +7,7 @@ import Checkbox from './checkbox/Checkbox.svelte' import DatePicker from './DatePicker.svelte' import SuggestionBadge from './SuggestionBadge.svelte' + import Icon from '../Icon.svelte' interface SelectOption { value: string | number @@ -26,6 +27,8 @@ element, onchange, width, + linkUrl, + hasLinkButton = false, // Suggestion props suggestion, suggestionLabel, @@ -47,6 +50,10 @@ onchange?: (checked: boolean) => void /** Custom width for the input field (e.g., '320px') */ width?: string + /** URL to open when link button is clicked */ + linkUrl?: string | null + /** Whether to show the link button (disabled when linkUrl is empty) */ + hasLinkButton?: boolean // Suggestion props /** The suggested value from wiki */ suggestion?: string | number | boolean | null | undefined @@ -63,12 +70,23 @@ // For checkbox type, derive the checked state from value const checkboxValue = $derived(type === 'checkbox' ? Boolean(value) : false) + // Show link button when hasLinkButton is true or linkUrl is provided + const showLinkButton = $derived(hasLinkButton || !!linkUrl) + const linkDisabled = $derived(!linkUrl) + // Handle checkbox change and call onchange if provided function handleCheckboxChange(checked: boolean) { value = checked as any onchange?.(checked) } + // Open URL in new tab + function openLink() { + if (linkUrl) { + window.open(linkUrl, '_blank', 'noopener,noreferrer') + } + } + // Show suggestion badge when: // 1. We have a suggestion // 2. The suggestion hasn't been dismissed @@ -138,6 +156,17 @@ {:else} + {#if showLinkButton} + + {/if} {/if} {:else if children} @@ -154,6 +183,7 @@ @use '$src/themes/layout' as layout; @use '$src/themes/spacing' as spacing; @use '$src/themes/typography' as typography; + @use '$src/themes/effects' as effects; .detail-item { display: flex; @@ -218,6 +248,8 @@ display: flex; flex-grow: 0; justify-content: flex-end; + align-items: center; + gap: spacing.$unit-half; :global(.input), :global(.select) { @@ -227,6 +259,36 @@ :global(.input.number) { width: var(--custom-width, 120px); } + + .link-button { + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + padding: 0; + border: none; + border-radius: layout.$item-corner; + background: transparent; + color: colors.$grey-50; + cursor: pointer; + flex-shrink: 0; + @include effects.smooth-transition(effects.$duration-quick, background-color, color, opacity); + + &:hover:not(:disabled) { + background: colors.$grey-90; + color: colors.$grey-30; + } + + &:active:not(:disabled) { + background: colors.$grey-80; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.4; + } + } } } diff --git a/src/lib/utils/external-links.ts b/src/lib/utils/external-links.ts new file mode 100644 index 00000000..ce6354ff --- /dev/null +++ b/src/lib/utils/external-links.ts @@ -0,0 +1,112 @@ +/** + * URL builders for external game resource sites + * These convert stored database values into full URLs + */ + +// Base URLs +const WIKI_EN_BASE = 'https://gbf.wiki' +const WIKI_JA_BASE = 'https://gbf-wiki.com' +const GAMEWITH_BASE = 'https://xn--bck3aza1a2if6kra4ee0hf.gamewith.jp/article/show' +const KAMIGAME_BASE = 'https://kamigame.jp' + +// Kamigame paths by entity type +const KAMIGAME_PATHS = { + character: '/グラブル/キャラクター', + weapon: '/グラブル/武器', + summon: '/グラブル/召喚石' +} as const + +export type EntityType = 'character' | 'weapon' | 'summon' + +/** + * Build English wiki URL from page name + * Input: "Florence (Dark)" + * Output: "https://gbf.wiki/Florence_(Dark)" + */ +export function buildWikiEnUrl(pageName: string | undefined | null): string | null { + if (!pageName?.trim()) return null + // Wiki URLs use underscores for spaces + const encoded = encodeURIComponent(pageName.trim().replace(/ /g, '_')) + return `${WIKI_EN_BASE}/${encoded}` +} + +/** + * Build Japanese wiki URL from page name + * Input: "フロレンス (SSR)闇属性バージョン" + * Output: "https://gbf-wiki.com/?フロレンス+(SSR)闇属性バージョン" + */ +export function buildWikiJaUrl(pageName: string | undefined | null): string | null { + if (!pageName?.trim()) return null + // Japanese wiki uses query string with + for spaces + const encoded = encodeURIComponent(pageName.trim()).replace(/%20/g, '+') + return `${WIKI_JA_BASE}/?${encoded}` +} + +/** + * Build Gamewith URL from article ID + * Input: "519325" + * Output: "https://xn--bck3aza1a2if6kra4ee0hf.gamewith.jp/article/show/519325" + */ +export function buildGamewithUrl(articleId: string | number | undefined | null): string | null { + if (!articleId) return null + const id = String(articleId).trim() + if (!id) return null + return `${GAMEWITH_BASE}/${id}` +} + +/** + * Build Kamigame URL from slug and entity type + * + * Character input: "SSR水着リッチ" + * Character output: "https://kamigame.jp/グラブル/キャラクター/SSR水着リッチ.html" + * + * Weapon input: "ブラインド・アンド・ストレイン", rarity: 3 (SSR) + * Weapon output: "https://kamigame.jp/グラブル/武器/SSR/ブラインド・アンド・ストレイン.html" + * + * Summon input: "SSR/アグニス" + * Summon output: "https://kamigame.jp/グラブル/召喚石/SSR/アグニス.html" + */ +export function buildKamigameUrl( + slug: string | undefined | null, + entityType: EntityType, + rarity?: number +): string | null { + if (!slug?.trim()) return null + + const basePath = KAMIGAME_PATHS[entityType] + + if (entityType === 'weapon' && rarity !== undefined) { + // Weapons: rarity is a separate path segment + const rarityPrefix = getRarityPrefix(rarity) + const encodedPath = encodeURIPath(basePath) + const encodedSlug = encodeURIComponent(slug.trim()) + return `${KAMIGAME_BASE}${encodedPath}/${rarityPrefix}/${encodedSlug}.html` + } + + // Characters and summons: value includes rarity info + const encodedPath = encodeURIPath(basePath) + const encodedSlug = encodeURIComponent(slug.trim()) + return `${KAMIGAME_BASE}${encodedPath}/${encodedSlug}.html` +} + +/** + * Map rarity enum to string prefix + */ +function getRarityPrefix(rarity: number): string { + const rarityMap: Record = { + 3: 'SSR', + 2: 'SR', + 1: 'R' + } + return rarityMap[rarity] || 'SSR' +} + +/** + * Encode a path while preserving slashes + */ +function encodeURIPath(path: string): string { + return path + .split('/') + .map((segment) => encodeURIComponent(segment)) + .join('/') +} diff --git a/src/routes/(app)/database/characters/import/+page.svelte b/src/routes/(app)/database/characters/import/+page.svelte index 934aa038..00555966 100644 --- a/src/routes/(app)/database/characters/import/+page.svelte +++ b/src/routes/(app)/database/characters/import/+page.svelte @@ -4,7 +4,13 @@ import { goto } from '$app/navigation' import { entityAdapter, type CharacterSuggestions } from '$lib/api/adapters/entity.adapter' import { fetchWikiPages, buildWikiDataMap } from '$lib/api/wiki' - import { getCharacterImage, getPlaceholderImage } from '$lib/utils/images' + import { getGameCdnCharacterImage, getPlaceholderImage } from '$lib/utils/images' + import { + buildWikiEnUrl, + buildWikiJaUrl, + buildGamewithUrl, + buildKamigameUrl + } from '$lib/utils/external-links' // Components import CharacterUncapSection from '$lib/features/database/characters/sections/CharacterUncapSection.svelte' @@ -45,9 +51,9 @@ let entities = $state>(new Map()) let selectedWikiPage = $state(null) - // Form data per entity (keyed by wikiPage) - let formDataMap = $state>(new Map()) - let dismissedSuggestionsMap = $state>>(new Map()) + // Form data per entity (keyed by wikiPage) - using Record for proper reactivity + let formDataByPage = $state>({}) + let dismissedByPage = $state>>({}) let savedEntities = $state>(new Set()) // Saving state @@ -73,10 +79,6 @@ // Get selected entity data const selectedEntity = $derived(selectedWikiPage ? entities.get(selectedWikiPage) : null) - const selectedFormData = $derived(selectedWikiPage ? formDataMap.get(selectedWikiPage) : null) - const selectedDismissed = $derived>( - selectedWikiPage ? dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set() : new Set() - ) // Entity tabs for TabbedEntitySelector const entityTabs = $derived( @@ -85,7 +87,7 @@ granblueId: entity.granblueId, status: entity.status, imageUrl: entity.granblueId - ? getCharacterImage(entity.granblueId, 'square') + ? getGameCdnCharacterImage(entity.granblueId) : getPlaceholderImage('character', 'square'), error: entity.error, saved: savedEntities.has(wikiPage) @@ -107,7 +109,8 @@ proficiency1: suggestions?.proficiency1 ?? 0, proficiency2: suggestions?.proficiency2 ?? 0, season: null as number | null, - series: [] as number[], + series: suggestions?.series ?? ([] as number[]), + promotions: [] as number[], gacha_available: true, minHp: suggestions?.minHp ?? 0, maxHp: suggestions?.maxHp ?? 0, @@ -128,7 +131,7 @@ releaseDate: suggestions?.releaseDate ?? '', flbDate: suggestions?.flbDate ?? '', ulbDate: suggestions?.ulbDate ?? '', - wikiEn: wikiPage ? `https://gbf.wiki/${wikiPage.replace(/ /g, '_')}` : '', + wikiEn: wikiPage ? wikiPage.replace(/ /g, '_') : '', wikiJa: '', gamewith: suggestions?.gamewith ?? '', kamigame: suggestions?.kamigame ?? '', @@ -199,16 +202,15 @@ // Create form data for successful results if (result.status === 'success') { - const formData = createEmptyFormData(result.wikiPage, result.suggestions) - formDataMap.set(result.wikiPage, formData) - dismissedSuggestionsMap.set(result.wikiPage, new Set()) + formDataByPage[result.wikiPage] = createEmptyFormData(result.wikiPage, result.suggestions) + dismissedByPage[result.wikiPage] = new Set() } }) entities = updatedEntities - // Update formDataMap and dismissedSuggestionsMap to trigger reactivity - formDataMap = new Map(formDataMap) - dismissedSuggestionsMap = new Map(dismissedSuggestionsMap) + // Trigger reactivity by reassigning + formDataByPage = { ...formDataByPage } + dismissedByPage = { ...dismissedByPage } } catch (error) { console.error('Batch preview error:', error) fetchError = 'Failed to fetch wiki data. Please try again.' @@ -224,28 +226,26 @@ // Accept a suggestion function handleAcceptSuggestion(field: string, value: any) { - if (!selectedWikiPage || !selectedFormData) return + if (!selectedWikiPage || !formDataByPage[selectedWikiPage]) return - const formData = formDataMap.get(selectedWikiPage) - if (formData) { - formData[field] = value - formDataMap = new Map(formDataMap) - } + formDataByPage[selectedWikiPage][field] = value + formDataByPage = { ...formDataByPage } } // Dismiss a suggestion function handleDismissSuggestion(field: string) { if (!selectedWikiPage) return - const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set() + const dismissed = dismissedByPage[selectedWikiPage] ?? new Set() dismissed.add(field) - dismissedSuggestionsMap = new Map(dismissedSuggestionsMap) + dismissedByPage[selectedWikiPage] = dismissed + dismissedByPage = { ...dismissedByPage } } // Save current entity async function saveCurrentEntity() { if (!selectedWikiPage) return - const formData = formDataMap.get(selectedWikiPage) + const formData = formDataByPage[selectedWikiPage] if (!formData) return const entity = entities.get(selectedWikiPage) @@ -273,6 +273,7 @@ proficiency2: formData.proficiency2, season: formData.season === 0 ? null : formData.season, series: formData.series, + promotions: formData.promotions, gacha_available: formData.gacha_available, min_hp: formData.minHp, max_hp: formData.maxHp, @@ -306,7 +307,9 @@ savedEntities = new Set(savedEntities) // Select next unsaved entity - const unsaved = entityTabs.find((e) => !savedEntities.has(e.wikiPage) && e.status === 'success') + const unsaved = entityTabs.find( + (e) => !savedEntities.has(e.wikiPage) && e.status === 'success' + ) if (unsaved) { selectedWikiPage = unsaved.wikiPage } @@ -325,7 +328,7 @@ // Can save current entity const canSave = $derived.by(() => { if (!selectedWikiPage) return false - const formData = formDataMap.get(selectedWikiPage) + const formData = formDataByPage[selectedWikiPage] if (!formData) return false return ( formData.name.trim() !== '' && @@ -368,7 +371,7 @@ {#if entities.size === 0} -
+
{ e.preventDefault(); fetchWikiData(); }}>

Enter up to 10 wiki page names to import data

{#each wikiPagesInputs as _, index} @@ -391,8 +394,14 @@ {/if}
{/each} -
@@ -400,11 +409,11 @@

{fetchError}

{/if}
-
- + {:else}
@@ -427,15 +436,14 @@

Loading wiki data...

- {:else if selectedWikiPage && formDataMap.has(selectedWikiPage)} - {@const formData = formDataMap.get(selectedWikiPage)!} + {:else if selectedWikiPage && formDataByPage[selectedWikiPage]} {@const suggestions = selectedEntity.suggestions} - {@const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set()} + {@const dismissed = dismissedByPage[selectedWikiPage] ?? new Set()}
- + - + handleAcceptSuggestion('releaseDate', suggestions?.releaseDate)} + onAcceptSuggestion={() => + handleAcceptSuggestion('releaseDate', suggestions?.releaseDate)} onDismissSuggestion={() => handleDismissSuggestion('releaseDate')} /> - {#if formData.flb} + {#if formDataByPage[selectedWikiPage].flb} handleDismissSuggestion('flbDate')} /> {/if} - {#if formData.ulb} + {#if formDataByPage[selectedWikiPage].ulb} handleAcceptSuggestion('gamewith', suggestions?.gamewith)} @@ -585,11 +600,13 @@ /> handleAcceptSuggestion('kamigame', suggestions?.kamigame)} @@ -641,6 +658,10 @@ align-items: center; } + :global(.wiki-inputs .add-input-button) { + width: fit-content; + } + .remove-button { display: flex; align-items: center; diff --git a/src/routes/(app)/database/summons/import/+page.svelte b/src/routes/(app)/database/summons/import/+page.svelte index 89edd2d5..49d82b5a 100644 --- a/src/routes/(app)/database/summons/import/+page.svelte +++ b/src/routes/(app)/database/summons/import/+page.svelte @@ -5,6 +5,12 @@ import { entityAdapter, type SummonSuggestions } from '$lib/api/adapters/entity.adapter' import { fetchWikiPages, buildWikiDataMap } from '$lib/api/wiki' import { getGameCdnSummonImage, getPlaceholderImage } from '$lib/utils/images' + import { + buildWikiEnUrl, + buildWikiJaUrl, + buildGamewithUrl, + buildKamigameUrl + } from '$lib/utils/external-links' // Components import SummonUncapSection from '$lib/features/database/summons/sections/SummonUncapSection.svelte' @@ -44,9 +50,9 @@ let entities = $state>(new Map()) let selectedWikiPage = $state(null) - // Form data per entity (keyed by wikiPage) - let formDataMap = $state>(new Map()) - let dismissedSuggestionsMap = $state>>(new Map()) + // Form data per entity (keyed by wikiPage) - using Record for proper reactivity + let formDataByPage = $state>({}) + let dismissedByPage = $state>>({}) let savedEntities = $state>(new Set()) // Saving state @@ -118,7 +124,7 @@ flbDate: suggestions?.flbDate ?? '', ulbDate: suggestions?.ulbDate ?? '', transcendenceDate: '', - wikiEn: wikiPage ? `https://gbf.wiki/${wikiPage.replace(/ /g, '_')}` : '', + wikiEn: wikiPage ? wikiPage.replace(/ /g, '_') : '', wikiJa: '', gamewith: suggestions?.gamewith ?? '', kamigame: suggestions?.kamigame ?? '', @@ -188,16 +194,15 @@ // Create form data for successful results if (result.status === 'success') { - const formData = createEmptyFormData(result.wikiPage, result.suggestions) - formDataMap.set(result.wikiPage, formData) - dismissedSuggestionsMap.set(result.wikiPage, new Set()) + formDataByPage[result.wikiPage] = createEmptyFormData(result.wikiPage, result.suggestions) + dismissedByPage[result.wikiPage] = new Set() } }) entities = updatedEntities - // Update formDataMap and dismissedSuggestionsMap to trigger reactivity - formDataMap = new Map(formDataMap) - dismissedSuggestionsMap = new Map(dismissedSuggestionsMap) + // Trigger reactivity by reassigning + formDataByPage = { ...formDataByPage } + dismissedByPage = { ...dismissedByPage } } catch (error) { console.error('Batch preview error:', error) fetchError = 'Failed to fetch wiki data. Please try again.' @@ -213,28 +218,26 @@ // Accept a suggestion function handleAcceptSuggestion(field: string, value: any) { - if (!selectedWikiPage) return + if (!selectedWikiPage || !formDataByPage[selectedWikiPage]) return - const formData = formDataMap.get(selectedWikiPage) - if (formData) { - formData[field] = value - formDataMap = new Map(formDataMap) - } + formDataByPage[selectedWikiPage][field] = value + formDataByPage = { ...formDataByPage } } // Dismiss a suggestion function handleDismissSuggestion(field: string) { if (!selectedWikiPage) return - const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set() + const dismissed = dismissedByPage[selectedWikiPage] ?? new Set() dismissed.add(field) - dismissedSuggestionsMap = new Map(dismissedSuggestionsMap) + dismissedByPage[selectedWikiPage] = dismissed + dismissedByPage = { ...dismissedByPage } } // Save current entity async function saveCurrentEntity() { if (!selectedWikiPage) return - const formData = formDataMap.get(selectedWikiPage) + const formData = formDataByPage[selectedWikiPage] if (!formData) return isSaving = true @@ -304,7 +307,7 @@ // Can save current entity const canSave = $derived.by(() => { if (!selectedWikiPage) return false - const formData = formDataMap.get(selectedWikiPage) + const formData = formDataByPage[selectedWikiPage] if (!formData) return false return ( formData.name.trim() !== '' && @@ -318,6 +321,7 @@ entityTabs.length > 0 && entityTabs.filter((e) => e.status === 'success').every((e) => savedEntities.has(e.wikiPage)) ) +
@@ -405,15 +409,14 @@

Loading wiki data...

- {:else if selectedWikiPage && formDataMap.has(selectedWikiPage)} - {@const formData = formDataMap.get(selectedWikiPage)!} + {:else if selectedWikiPage && formDataByPage[selectedWikiPage]} {@const suggestions = selectedEntity.suggestions} - {@const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set()} + {@const dismissed = dismissedByPage[selectedWikiPage] ?? new Set()}
- + - + handleDismissSuggestion('releaseDate')} /> - {#if formData.flb} + {#if formDataByPage[selectedWikiPage].flb} handleDismissSuggestion('flbDate')} /> {/if} - {#if formData.ulb} + {#if formDataByPage[selectedWikiPage].ulb} handleDismissSuggestion('ulbDate')} /> {/if} - {#if formData.transcendence} + {#if formDataByPage[selectedWikiPage].transcendence} handleAcceptSuggestion('gamewith', suggestions?.gamewith)} @@ -573,11 +582,13 @@ /> handleAcceptSuggestion('kamigame', suggestions?.kamigame)} diff --git a/src/routes/(app)/database/weapons/import/+page.svelte b/src/routes/(app)/database/weapons/import/+page.svelte index 52d89413..63a410e4 100644 --- a/src/routes/(app)/database/weapons/import/+page.svelte +++ b/src/routes/(app)/database/weapons/import/+page.svelte @@ -4,7 +4,13 @@ import { goto } from '$app/navigation' import { entityAdapter, type WeaponSuggestions } from '$lib/api/adapters/entity.adapter' import { fetchWikiPages, buildWikiDataMap } from '$lib/api/wiki' - import { getWeaponImage, getPlaceholderImage } from '$lib/utils/images' + import { getGameCdnWeaponImage, getPlaceholderImage } from '$lib/utils/images' + import { + buildWikiEnUrl, + buildWikiJaUrl, + buildGamewithUrl, + buildKamigameUrl + } from '$lib/utils/external-links' // Components import WeaponUncapSection from '$lib/features/database/weapons/sections/WeaponUncapSection.svelte' @@ -45,9 +51,9 @@ let entities = $state>(new Map()) let selectedWikiPage = $state(null) - // Form data per entity (keyed by wikiPage) - let formDataMap = $state>(new Map()) - let dismissedSuggestionsMap = $state>>(new Map()) + // Form data per entity (keyed by wikiPage) - using Record for proper reactivity + let formDataByPage = $state>({}) + let dismissedByPage = $state>>({}) let savedEntities = $state>(new Set()) // Saving state @@ -79,7 +85,7 @@ granblueId: entity.granblueId, status: entity.status, imageUrl: entity.granblueId - ? getWeaponImage(entity.granblueId, 'square') + ? getGameCdnWeaponImage(entity.granblueId) : getPlaceholderImage('weapon', 'square'), error: entity.error, saved: savedEntities.has(wikiPage) @@ -119,7 +125,7 @@ flbDate: suggestions?.flbDate ?? '', ulbDate: suggestions?.ulbDate ?? '', transcendenceDate: '', - wikiEn: wikiPage ? `https://gbf.wiki/${wikiPage.replace(/ /g, '_')}` : '', + wikiEn: wikiPage ? wikiPage.replace(/ /g, '_') : '', wikiJa: '', gamewith: suggestions?.gamewith ?? '', kamigame: suggestions?.kamigame ?? '', @@ -190,16 +196,15 @@ // Create form data for successful results if (result.status === 'success') { - const formData = createEmptyFormData(result.wikiPage, result.suggestions) - formDataMap.set(result.wikiPage, formData) - dismissedSuggestionsMap.set(result.wikiPage, new Set()) + formDataByPage[result.wikiPage] = createEmptyFormData(result.wikiPage, result.suggestions) + dismissedByPage[result.wikiPage] = new Set() } }) entities = updatedEntities - // Update formDataMap and dismissedSuggestionsMap to trigger reactivity - formDataMap = new Map(formDataMap) - dismissedSuggestionsMap = new Map(dismissedSuggestionsMap) + // Trigger reactivity by reassigning + formDataByPage = { ...formDataByPage } + dismissedByPage = { ...dismissedByPage } } catch (error) { console.error('Batch preview error:', error) fetchError = 'Failed to fetch wiki data. Please try again.' @@ -215,28 +220,26 @@ // Accept a suggestion function handleAcceptSuggestion(field: string, value: any) { - if (!selectedWikiPage) return + if (!selectedWikiPage || !formDataByPage[selectedWikiPage]) return - const formData = formDataMap.get(selectedWikiPage) - if (formData) { - formData[field] = value - formDataMap = new Map(formDataMap) - } + formDataByPage[selectedWikiPage][field] = value + formDataByPage = { ...formDataByPage } } // Dismiss a suggestion function handleDismissSuggestion(field: string) { if (!selectedWikiPage) return - const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set() + const dismissed = dismissedByPage[selectedWikiPage] ?? new Set() dismissed.add(field) - dismissedSuggestionsMap = new Map(dismissedSuggestionsMap) + dismissedByPage[selectedWikiPage] = dismissed + dismissedByPage = { ...dismissedByPage } } // Save current entity async function saveCurrentEntity() { if (!selectedWikiPage) return - const formData = formDataMap.get(selectedWikiPage) + const formData = formDataByPage[selectedWikiPage] if (!formData) return isSaving = true @@ -288,7 +291,9 @@ savedEntities = new Set(savedEntities) // Select next unsaved entity - const unsaved = entityTabs.find((e) => !savedEntities.has(e.wikiPage) && e.status === 'success') + const unsaved = entityTabs.find( + (e) => !savedEntities.has(e.wikiPage) && e.status === 'success' + ) if (unsaved) { selectedWikiPage = unsaved.wikiPage } @@ -307,7 +312,7 @@ // Can save current entity const canSave = $derived.by(() => { if (!selectedWikiPage) return false - const formData = formDataMap.get(selectedWikiPage) + const formData = formDataByPage[selectedWikiPage] if (!formData) return false return ( formData.name.trim() !== '' && @@ -321,6 +326,7 @@ entityTabs.length > 0 && entityTabs.filter((e) => e.status === 'success').every((e) => savedEntities.has(e.wikiPage)) ) +
@@ -350,17 +356,12 @@ {#if entities.size === 0} -
+
{ e.preventDefault(); fetchWikiData(); }}>

Enter up to 10 wiki page names to import data

{#each wikiPagesInputs as _, index}
- + {#if wikiPagesInputs.length > 1}
@@ -382,11 +389,11 @@

{fetchError}

{/if}
-
-
+
{:else}
@@ -409,15 +416,14 @@

Loading wiki data...

- {:else if selectedWikiPage && formDataMap.has(selectedWikiPage)} - {@const formData = formDataMap.get(selectedWikiPage)!} + {:else if selectedWikiPage && formDataByPage[selectedWikiPage]} {@const suggestions = selectedEntity.suggestions} - {@const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set()} + {@const dismissed = dismissedByPage[selectedWikiPage] ?? new Set()}
- + - + - + handleAcceptSuggestion('releaseDate', suggestions?.releaseDate)} + onAcceptSuggestion={() => + handleAcceptSuggestion('releaseDate', suggestions?.releaseDate)} onDismissSuggestion={() => handleDismissSuggestion('releaseDate')} /> - {#if formData.flb} + {#if formDataByPage[selectedWikiPage].flb} handleDismissSuggestion('flbDate')} /> {/if} - {#if formData.ulb} + {#if formDataByPage[selectedWikiPage].ulb} handleDismissSuggestion('ulbDate')} /> {/if} - {#if formData.transcendence} + {#if formDataByPage[selectedWikiPage].transcendence} handleAcceptSuggestion('gamewith', suggestions?.gamewith)} @@ -574,11 +590,13 @@ /> handleAcceptSuggestion('kamigame', suggestions?.kamigame)} @@ -622,6 +640,10 @@ align-items: center; } + :global(.wiki-inputs .add-input-button) { + width: fit-content; + } + .remove-button { display: flex; align-items: center;