add link buttons to import form fields
- add URL builders for wiki/gamewith/kamigame - add hasLinkButton prop to DetailItem components - show disabled link button when value is empty - store page names instead of full URLs - fix DetailItem to render children when editable - remove focus background styling
This commit is contained in:
parent
e1ba34048a
commit
64725bd4e8
6 changed files with 462 additions and 175 deletions
|
|
@ -7,6 +7,7 @@
|
||||||
import Checkbox from './checkbox/Checkbox.svelte'
|
import Checkbox from './checkbox/Checkbox.svelte'
|
||||||
import CheckboxGroup from './checkbox/CheckboxGroup.svelte'
|
import CheckboxGroup from './checkbox/CheckboxGroup.svelte'
|
||||||
import DatePicker from './DatePicker.svelte'
|
import DatePicker from './DatePicker.svelte'
|
||||||
|
import Icon from '../Icon.svelte'
|
||||||
|
|
||||||
interface SelectOption {
|
interface SelectOption {
|
||||||
value: string | number
|
value: string | number
|
||||||
|
|
@ -25,7 +26,9 @@
|
||||||
placeholder,
|
placeholder,
|
||||||
element,
|
element,
|
||||||
onchange,
|
onchange,
|
||||||
width
|
width,
|
||||||
|
linkUrl,
|
||||||
|
hasLinkButton = false
|
||||||
}: {
|
}: {
|
||||||
label: string
|
label: string
|
||||||
/** Secondary label displayed below the main label */
|
/** Secondary label displayed below the main label */
|
||||||
|
|
@ -41,17 +44,32 @@
|
||||||
onchange?: (checked: boolean) => void
|
onchange?: (checked: boolean) => void
|
||||||
/** Custom width for the input field (e.g., '320px') */
|
/** Custom width for the input field (e.g., '320px') */
|
||||||
width?: string
|
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()
|
} = $props()
|
||||||
|
|
||||||
// For checkbox type, derive the checked state from value
|
// For checkbox type, derive the checked state from value
|
||||||
// This ensures external changes to value are reflected in the checkbox
|
// This ensures external changes to value are reflected in the checkbox
|
||||||
const checkboxValue = $derived(type === 'checkbox' ? Boolean(value) : false)
|
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
|
// Handle checkbox change and call onchange if provided
|
||||||
function handleCheckboxChange(checked: boolean) {
|
function handleCheckboxChange(checked: boolean) {
|
||||||
value = checked as any
|
value = checked as any
|
||||||
onchange?.(checked)
|
onchange?.(checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open URL in new tab
|
||||||
|
function openLink() {
|
||||||
|
if (linkUrl) {
|
||||||
|
window.open(linkUrl, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="detail-item" class:editable class:hasChildren={!!children}>
|
<div class="detail-item" class:editable class:hasChildren={!!children}>
|
||||||
|
|
@ -95,8 +113,21 @@
|
||||||
/>
|
/>
|
||||||
{:else if type === 'date'}
|
{:else if type === 'date'}
|
||||||
<DatePicker bind:value={value as string | null} contained={true} {placeholder} />
|
<DatePicker bind:value={value as string | null} contained={true} {placeholder} />
|
||||||
|
{:else if children}
|
||||||
|
{@render children()}
|
||||||
{:else}
|
{:else}
|
||||||
<Input bind:value type="text" contained={true} {placeholder} alignRight={false} />
|
<Input bind:value type="text" contained={true} {placeholder} alignRight={false} />
|
||||||
|
{#if showLinkButton}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="link-button"
|
||||||
|
onclick={openLink}
|
||||||
|
disabled={linkDisabled}
|
||||||
|
title={linkDisabled ? 'No link available' : 'Open link'}
|
||||||
|
>
|
||||||
|
<Icon name="link" size={16} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if children}
|
{:else if children}
|
||||||
|
|
@ -133,11 +164,6 @@
|
||||||
background: colors.$grey-90;
|
background: colors.$grey-90;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.editable:focus-within,
|
|
||||||
&.hasChildren:focus-within {
|
|
||||||
background: var(--input-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.editable,
|
&.editable,
|
||||||
&.hasChildren {
|
&.hasChildren {
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
|
|
@ -173,15 +199,48 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit-half;
|
||||||
|
|
||||||
:global(.input),
|
:global(.input),
|
||||||
:global(.select) {
|
:global(.select),
|
||||||
|
:global(.multi-select) {
|
||||||
width: var(--custom-width, 240px);
|
width: var(--custom-width, 240px);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.input.number) {
|
:global(.input.number) {
|
||||||
width: var(--custom-width, 120px);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import Checkbox from './checkbox/Checkbox.svelte'
|
import Checkbox from './checkbox/Checkbox.svelte'
|
||||||
import DatePicker from './DatePicker.svelte'
|
import DatePicker from './DatePicker.svelte'
|
||||||
import SuggestionBadge from './SuggestionBadge.svelte'
|
import SuggestionBadge from './SuggestionBadge.svelte'
|
||||||
|
import Icon from '../Icon.svelte'
|
||||||
|
|
||||||
interface SelectOption {
|
interface SelectOption {
|
||||||
value: string | number
|
value: string | number
|
||||||
|
|
@ -26,6 +27,8 @@
|
||||||
element,
|
element,
|
||||||
onchange,
|
onchange,
|
||||||
width,
|
width,
|
||||||
|
linkUrl,
|
||||||
|
hasLinkButton = false,
|
||||||
// Suggestion props
|
// Suggestion props
|
||||||
suggestion,
|
suggestion,
|
||||||
suggestionLabel,
|
suggestionLabel,
|
||||||
|
|
@ -47,6 +50,10 @@
|
||||||
onchange?: (checked: boolean) => void
|
onchange?: (checked: boolean) => void
|
||||||
/** Custom width for the input field (e.g., '320px') */
|
/** Custom width for the input field (e.g., '320px') */
|
||||||
width?: string
|
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
|
// Suggestion props
|
||||||
/** The suggested value from wiki */
|
/** The suggested value from wiki */
|
||||||
suggestion?: string | number | boolean | null | undefined
|
suggestion?: string | number | boolean | null | undefined
|
||||||
|
|
@ -63,12 +70,23 @@
|
||||||
// For checkbox type, derive the checked state from value
|
// For checkbox type, derive the checked state from value
|
||||||
const checkboxValue = $derived(type === 'checkbox' ? Boolean(value) : false)
|
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
|
// Handle checkbox change and call onchange if provided
|
||||||
function handleCheckboxChange(checked: boolean) {
|
function handleCheckboxChange(checked: boolean) {
|
||||||
value = checked as any
|
value = checked as any
|
||||||
onchange?.(checked)
|
onchange?.(checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open URL in new tab
|
||||||
|
function openLink() {
|
||||||
|
if (linkUrl) {
|
||||||
|
window.open(linkUrl, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show suggestion badge when:
|
// Show suggestion badge when:
|
||||||
// 1. We have a suggestion
|
// 1. We have a suggestion
|
||||||
// 2. The suggestion hasn't been dismissed
|
// 2. The suggestion hasn't been dismissed
|
||||||
|
|
@ -138,6 +156,17 @@
|
||||||
<DatePicker bind:value={value as string | null} contained={true} {placeholder} />
|
<DatePicker bind:value={value as string | null} contained={true} {placeholder} />
|
||||||
{:else}
|
{:else}
|
||||||
<Input bind:value type="text" contained={true} {placeholder} alignRight={false} />
|
<Input bind:value type="text" contained={true} {placeholder} alignRight={false} />
|
||||||
|
{#if showLinkButton}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="link-button"
|
||||||
|
onclick={openLink}
|
||||||
|
disabled={linkDisabled}
|
||||||
|
title={linkDisabled ? 'No link available' : 'Open link'}
|
||||||
|
>
|
||||||
|
<Icon name="link" size={16} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if children}
|
{:else if children}
|
||||||
|
|
@ -154,6 +183,7 @@
|
||||||
@use '$src/themes/layout' as layout;
|
@use '$src/themes/layout' as layout;
|
||||||
@use '$src/themes/spacing' as spacing;
|
@use '$src/themes/spacing' as spacing;
|
||||||
@use '$src/themes/typography' as typography;
|
@use '$src/themes/typography' as typography;
|
||||||
|
@use '$src/themes/effects' as effects;
|
||||||
|
|
||||||
.detail-item {
|
.detail-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -218,6 +248,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit-half;
|
||||||
|
|
||||||
:global(.input),
|
:global(.input),
|
||||||
:global(.select) {
|
:global(.select) {
|
||||||
|
|
@ -227,6 +259,36 @@
|
||||||
:global(.input.number) {
|
:global(.input.number) {
|
||||||
width: var(--custom-width, 120px);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
112
src/lib/utils/external-links.ts
Normal file
112
src/lib/utils/external-links.ts
Normal file
|
|
@ -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<number, string> = {
|
||||||
|
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('/')
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,13 @@
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { entityAdapter, type CharacterSuggestions } from '$lib/api/adapters/entity.adapter'
|
import { entityAdapter, type CharacterSuggestions } from '$lib/api/adapters/entity.adapter'
|
||||||
import { fetchWikiPages, buildWikiDataMap } from '$lib/api/wiki'
|
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
|
// Components
|
||||||
import CharacterUncapSection from '$lib/features/database/characters/sections/CharacterUncapSection.svelte'
|
import CharacterUncapSection from '$lib/features/database/characters/sections/CharacterUncapSection.svelte'
|
||||||
|
|
@ -45,9 +51,9 @@
|
||||||
let entities = $state<Map<string, EntityState>>(new Map())
|
let entities = $state<Map<string, EntityState>>(new Map())
|
||||||
let selectedWikiPage = $state<string | null>(null)
|
let selectedWikiPage = $state<string | null>(null)
|
||||||
|
|
||||||
// Form data per entity (keyed by wikiPage)
|
// Form data per entity (keyed by wikiPage) - using Record for proper reactivity
|
||||||
let formDataMap = $state<Map<string, any>>(new Map())
|
let formDataByPage = $state<Record<string, any>>({})
|
||||||
let dismissedSuggestionsMap = $state<Map<string, Set<string>>>(new Map())
|
let dismissedByPage = $state<Record<string, Set<string>>>({})
|
||||||
let savedEntities = $state<Set<string>>(new Set())
|
let savedEntities = $state<Set<string>>(new Set())
|
||||||
|
|
||||||
// Saving state
|
// Saving state
|
||||||
|
|
@ -73,10 +79,6 @@
|
||||||
|
|
||||||
// Get selected entity data
|
// Get selected entity data
|
||||||
const selectedEntity = $derived(selectedWikiPage ? entities.get(selectedWikiPage) : null)
|
const selectedEntity = $derived(selectedWikiPage ? entities.get(selectedWikiPage) : null)
|
||||||
const selectedFormData = $derived(selectedWikiPage ? formDataMap.get(selectedWikiPage) : null)
|
|
||||||
const selectedDismissed = $derived<Set<string>>(
|
|
||||||
selectedWikiPage ? dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set<string>() : new Set<string>()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Entity tabs for TabbedEntitySelector
|
// Entity tabs for TabbedEntitySelector
|
||||||
const entityTabs = $derived<EntityTab[]>(
|
const entityTabs = $derived<EntityTab[]>(
|
||||||
|
|
@ -85,7 +87,7 @@
|
||||||
granblueId: entity.granblueId,
|
granblueId: entity.granblueId,
|
||||||
status: entity.status,
|
status: entity.status,
|
||||||
imageUrl: entity.granblueId
|
imageUrl: entity.granblueId
|
||||||
? getCharacterImage(entity.granblueId, 'square')
|
? getGameCdnCharacterImage(entity.granblueId)
|
||||||
: getPlaceholderImage('character', 'square'),
|
: getPlaceholderImage('character', 'square'),
|
||||||
error: entity.error,
|
error: entity.error,
|
||||||
saved: savedEntities.has(wikiPage)
|
saved: savedEntities.has(wikiPage)
|
||||||
|
|
@ -107,7 +109,8 @@
|
||||||
proficiency1: suggestions?.proficiency1 ?? 0,
|
proficiency1: suggestions?.proficiency1 ?? 0,
|
||||||
proficiency2: suggestions?.proficiency2 ?? 0,
|
proficiency2: suggestions?.proficiency2 ?? 0,
|
||||||
season: null as number | null,
|
season: null as number | null,
|
||||||
series: [] as number[],
|
series: suggestions?.series ?? ([] as number[]),
|
||||||
|
promotions: [] as number[],
|
||||||
gacha_available: true,
|
gacha_available: true,
|
||||||
minHp: suggestions?.minHp ?? 0,
|
minHp: suggestions?.minHp ?? 0,
|
||||||
maxHp: suggestions?.maxHp ?? 0,
|
maxHp: suggestions?.maxHp ?? 0,
|
||||||
|
|
@ -128,7 +131,7 @@
|
||||||
releaseDate: suggestions?.releaseDate ?? '',
|
releaseDate: suggestions?.releaseDate ?? '',
|
||||||
flbDate: suggestions?.flbDate ?? '',
|
flbDate: suggestions?.flbDate ?? '',
|
||||||
ulbDate: suggestions?.ulbDate ?? '',
|
ulbDate: suggestions?.ulbDate ?? '',
|
||||||
wikiEn: wikiPage ? `https://gbf.wiki/${wikiPage.replace(/ /g, '_')}` : '',
|
wikiEn: wikiPage ? wikiPage.replace(/ /g, '_') : '',
|
||||||
wikiJa: '',
|
wikiJa: '',
|
||||||
gamewith: suggestions?.gamewith ?? '',
|
gamewith: suggestions?.gamewith ?? '',
|
||||||
kamigame: suggestions?.kamigame ?? '',
|
kamigame: suggestions?.kamigame ?? '',
|
||||||
|
|
@ -199,16 +202,15 @@
|
||||||
|
|
||||||
// Create form data for successful results
|
// Create form data for successful results
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
const formData = createEmptyFormData(result.wikiPage, result.suggestions)
|
formDataByPage[result.wikiPage] = createEmptyFormData(result.wikiPage, result.suggestions)
|
||||||
formDataMap.set(result.wikiPage, formData)
|
dismissedByPage[result.wikiPage] = new Set<string>()
|
||||||
dismissedSuggestionsMap.set(result.wikiPage, new Set<string>())
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
entities = updatedEntities
|
entities = updatedEntities
|
||||||
|
|
||||||
// Update formDataMap and dismissedSuggestionsMap to trigger reactivity
|
// Trigger reactivity by reassigning
|
||||||
formDataMap = new Map(formDataMap)
|
formDataByPage = { ...formDataByPage }
|
||||||
dismissedSuggestionsMap = new Map(dismissedSuggestionsMap)
|
dismissedByPage = { ...dismissedByPage }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Batch preview error:', error)
|
console.error('Batch preview error:', error)
|
||||||
fetchError = 'Failed to fetch wiki data. Please try again.'
|
fetchError = 'Failed to fetch wiki data. Please try again.'
|
||||||
|
|
@ -224,28 +226,26 @@
|
||||||
|
|
||||||
// Accept a suggestion
|
// Accept a suggestion
|
||||||
function handleAcceptSuggestion(field: string, value: any) {
|
function handleAcceptSuggestion(field: string, value: any) {
|
||||||
if (!selectedWikiPage || !selectedFormData) return
|
if (!selectedWikiPage || !formDataByPage[selectedWikiPage]) return
|
||||||
|
|
||||||
const formData = formDataMap.get(selectedWikiPage)
|
formDataByPage[selectedWikiPage][field] = value
|
||||||
if (formData) {
|
formDataByPage = { ...formDataByPage }
|
||||||
formData[field] = value
|
|
||||||
formDataMap = new Map(formDataMap)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dismiss a suggestion
|
// Dismiss a suggestion
|
||||||
function handleDismissSuggestion(field: string) {
|
function handleDismissSuggestion(field: string) {
|
||||||
if (!selectedWikiPage) return
|
if (!selectedWikiPage) return
|
||||||
|
|
||||||
const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set()
|
const dismissed = dismissedByPage[selectedWikiPage] ?? new Set<string>()
|
||||||
dismissed.add(field)
|
dismissed.add(field)
|
||||||
dismissedSuggestionsMap = new Map(dismissedSuggestionsMap)
|
dismissedByPage[selectedWikiPage] = dismissed
|
||||||
|
dismissedByPage = { ...dismissedByPage }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current entity
|
// Save current entity
|
||||||
async function saveCurrentEntity() {
|
async function saveCurrentEntity() {
|
||||||
if (!selectedWikiPage) return
|
if (!selectedWikiPage) return
|
||||||
const formData = formDataMap.get(selectedWikiPage)
|
const formData = formDataByPage[selectedWikiPage]
|
||||||
if (!formData) return
|
if (!formData) return
|
||||||
const entity = entities.get(selectedWikiPage)
|
const entity = entities.get(selectedWikiPage)
|
||||||
|
|
||||||
|
|
@ -273,6 +273,7 @@
|
||||||
proficiency2: formData.proficiency2,
|
proficiency2: formData.proficiency2,
|
||||||
season: formData.season === 0 ? null : formData.season,
|
season: formData.season === 0 ? null : formData.season,
|
||||||
series: formData.series,
|
series: formData.series,
|
||||||
|
promotions: formData.promotions,
|
||||||
gacha_available: formData.gacha_available,
|
gacha_available: formData.gacha_available,
|
||||||
min_hp: formData.minHp,
|
min_hp: formData.minHp,
|
||||||
max_hp: formData.maxHp,
|
max_hp: formData.maxHp,
|
||||||
|
|
@ -306,7 +307,9 @@
|
||||||
savedEntities = new Set(savedEntities)
|
savedEntities = new Set(savedEntities)
|
||||||
|
|
||||||
// Select next unsaved entity
|
// 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) {
|
if (unsaved) {
|
||||||
selectedWikiPage = unsaved.wikiPage
|
selectedWikiPage = unsaved.wikiPage
|
||||||
}
|
}
|
||||||
|
|
@ -325,7 +328,7 @@
|
||||||
// Can save current entity
|
// Can save current entity
|
||||||
const canSave = $derived.by(() => {
|
const canSave = $derived.by(() => {
|
||||||
if (!selectedWikiPage) return false
|
if (!selectedWikiPage) return false
|
||||||
const formData = formDataMap.get(selectedWikiPage)
|
const formData = formDataByPage[selectedWikiPage]
|
||||||
if (!formData) return false
|
if (!formData) return false
|
||||||
return (
|
return (
|
||||||
formData.name.trim() !== '' &&
|
formData.name.trim() !== '' &&
|
||||||
|
|
@ -368,7 +371,7 @@
|
||||||
|
|
||||||
<!-- Input phase -->
|
<!-- Input phase -->
|
||||||
{#if entities.size === 0}
|
{#if entities.size === 0}
|
||||||
<div class="input-phase">
|
<form class="input-phase" onsubmit={(e) => { e.preventDefault(); fetchWikiData(); }}>
|
||||||
<p class="hint">Enter up to 10 wiki page names to import data</p>
|
<p class="hint">Enter up to 10 wiki page names to import data</p>
|
||||||
<div class="wiki-inputs">
|
<div class="wiki-inputs">
|
||||||
{#each wikiPagesInputs as _, index}
|
{#each wikiPagesInputs as _, index}
|
||||||
|
|
@ -391,8 +394,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<Button variant="ghost" onclick={addInput}>
|
<Button
|
||||||
<Icon name="plus" size={16} />
|
variant="ghost"
|
||||||
|
class="add-input-button"
|
||||||
|
leftIcon="plus"
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
onclick={addInput}
|
||||||
|
>
|
||||||
Add another
|
Add another
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -400,11 +409,11 @@
|
||||||
<p class="error">{fetchError}</p>
|
<p class="error">{fetchError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="fetch-button">
|
<div class="fetch-button">
|
||||||
<Button variant="primary" onclick={fetchWikiData} disabled={isFetching}>
|
<Button variant="primary" type="submit" disabled={isFetching}>
|
||||||
{isFetching ? 'Fetching...' : 'Fetch data'}
|
{isFetching ? 'Fetching...' : 'Fetch data'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Entity selector -->
|
<!-- Entity selector -->
|
||||||
<div class="entity-selector-container">
|
<div class="entity-selector-container">
|
||||||
|
|
@ -427,15 +436,14 @@
|
||||||
<div class="entity-loading">
|
<div class="entity-loading">
|
||||||
<p>Loading wiki data...</p>
|
<p>Loading wiki data...</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if selectedWikiPage && formDataMap.has(selectedWikiPage)}
|
{:else if selectedWikiPage && formDataByPage[selectedWikiPage]}
|
||||||
{@const formData = formDataMap.get(selectedWikiPage)!}
|
|
||||||
{@const suggestions = selectedEntity.suggestions}
|
{@const suggestions = selectedEntity.suggestions}
|
||||||
{@const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set<string>()}
|
{@const dismissed = dismissedByPage[selectedWikiPage] ?? new Set<string>()}
|
||||||
<section class="details">
|
<section class="details">
|
||||||
<DetailsContainer title="Basic Info">
|
<DetailsContainer title="Basic Info">
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Name (EN)"
|
label="Name (EN)"
|
||||||
bind:value={formData.name}
|
bind:value={formDataByPage[selectedWikiPage].name}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Character name"
|
placeholder="Character name"
|
||||||
|
|
@ -446,7 +454,7 @@
|
||||||
/>
|
/>
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Name (JP)"
|
label="Name (JP)"
|
||||||
bind:value={formData.nameJp}
|
bind:value={formDataByPage[selectedWikiPage].nameJp}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="キャラクター名"
|
placeholder="キャラクター名"
|
||||||
|
|
@ -458,7 +466,7 @@
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Character ID"
|
label="Character ID"
|
||||||
sublabel="Separate multiple IDs with commas"
|
sublabel="Separate multiple IDs with commas"
|
||||||
bind:value={formData.characterId}
|
bind:value={formDataByPage[selectedWikiPage].characterId}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Character IDs"
|
placeholder="Character IDs"
|
||||||
|
|
@ -468,7 +476,7 @@
|
||||||
<CharacterMetadataSection
|
<CharacterMetadataSection
|
||||||
character={emptyCharacter}
|
character={emptyCharacter}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -478,7 +486,7 @@
|
||||||
<CharacterUncapSection
|
<CharacterUncapSection
|
||||||
character={emptyCharacter}
|
character={emptyCharacter}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -488,7 +496,7 @@
|
||||||
<CharacterTaxonomySection
|
<CharacterTaxonomySection
|
||||||
character={emptyCharacter}
|
character={emptyCharacter}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -498,7 +506,7 @@
|
||||||
<CharacterStatsSection
|
<CharacterStatsSection
|
||||||
character={emptyCharacter}
|
character={emptyCharacter}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -507,29 +515,30 @@
|
||||||
|
|
||||||
<DetailsContainer title="Nicknames">
|
<DetailsContainer title="Nicknames">
|
||||||
<DetailItem label="Nicknames (EN)">
|
<DetailItem label="Nicknames (EN)">
|
||||||
<TagInput bind:value={formData.nicknamesEn} placeholder="Add nickname..." contained />
|
<TagInput bind:value={formDataByPage[selectedWikiPage].nicknamesEn} placeholder="Add nickname..." contained />
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
<DetailItem label="Nicknames (JP)">
|
<DetailItem label="Nicknames (JP)">
|
||||||
<TagInput bind:value={formData.nicknamesJp} placeholder="ニックネーム..." contained />
|
<TagInput bind:value={formDataByPage[selectedWikiPage].nicknamesJp} placeholder="ニックネーム..." contained />
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
<DetailsContainer title="Dates">
|
<DetailsContainer title="Dates">
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Release Date"
|
label="Release Date"
|
||||||
bind:value={formData.releaseDate}
|
bind:value={formDataByPage[selectedWikiPage].releaseDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
suggestion={suggestions?.releaseDate}
|
suggestion={suggestions?.releaseDate}
|
||||||
dismissedSuggestion={dismissed.has('releaseDate')}
|
dismissedSuggestion={dismissed.has('releaseDate')}
|
||||||
onAcceptSuggestion={() => handleAcceptSuggestion('releaseDate', suggestions?.releaseDate)}
|
onAcceptSuggestion={() =>
|
||||||
|
handleAcceptSuggestion('releaseDate', suggestions?.releaseDate)}
|
||||||
onDismissSuggestion={() => handleDismissSuggestion('releaseDate')}
|
onDismissSuggestion={() => handleDismissSuggestion('releaseDate')}
|
||||||
/>
|
/>
|
||||||
{#if formData.flb}
|
{#if formDataByPage[selectedWikiPage].flb}
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="FLB Date"
|
label="FLB Date"
|
||||||
bind:value={formData.flbDate}
|
bind:value={formDataByPage[selectedWikiPage].flbDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
|
|
@ -539,10 +548,10 @@
|
||||||
onDismissSuggestion={() => handleDismissSuggestion('flbDate')}
|
onDismissSuggestion={() => handleDismissSuggestion('flbDate')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if formData.ulb}
|
{#if formDataByPage[selectedWikiPage].ulb}
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="ULB Date"
|
label="ULB Date"
|
||||||
bind:value={formData.ulbDate}
|
bind:value={formDataByPage[selectedWikiPage].ulbDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
|
|
@ -557,27 +566,33 @@
|
||||||
<DetailsContainer title="Links">
|
<DetailsContainer title="Links">
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Wiki (EN)"
|
label="Wiki (EN)"
|
||||||
bind:value={formData.wikiEn}
|
bind:value={formDataByPage[selectedWikiPage].wikiEn}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://gbf.wiki/..."
|
placeholder="Page name (e.g., Narmaya)"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildWikiEnUrl(formDataByPage[selectedWikiPage].wikiEn)}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Wiki (JP)"
|
label="Wiki (JP)"
|
||||||
bind:value={formData.wikiJa}
|
bind:value={formDataByPage[selectedWikiPage].wikiJa}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://gbf-wiki.com/..."
|
placeholder="Japanese page name"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildWikiJaUrl(formDataByPage[selectedWikiPage].wikiJa)}
|
||||||
/>
|
/>
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Gamewith"
|
label="Gamewith"
|
||||||
bind:value={formData.gamewith}
|
bind:value={formDataByPage[selectedWikiPage].gamewith}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://..."
|
placeholder="Article ID (e.g., 519325)"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildGamewithUrl(formDataByPage[selectedWikiPage].gamewith)}
|
||||||
suggestion={suggestions?.gamewith}
|
suggestion={suggestions?.gamewith}
|
||||||
dismissedSuggestion={dismissed.has('gamewith')}
|
dismissedSuggestion={dismissed.has('gamewith')}
|
||||||
onAcceptSuggestion={() => handleAcceptSuggestion('gamewith', suggestions?.gamewith)}
|
onAcceptSuggestion={() => handleAcceptSuggestion('gamewith', suggestions?.gamewith)}
|
||||||
|
|
@ -585,11 +600,13 @@
|
||||||
/>
|
/>
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Kamigame"
|
label="Kamigame"
|
||||||
bind:value={formData.kamigame}
|
bind:value={formDataByPage[selectedWikiPage].kamigame}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://..."
|
placeholder="Slug (e.g., SSR闇フロレンス)"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildKamigameUrl(formDataByPage[selectedWikiPage].kamigame, 'character')}
|
||||||
suggestion={suggestions?.kamigame}
|
suggestion={suggestions?.kamigame}
|
||||||
dismissedSuggestion={dismissed.has('kamigame')}
|
dismissedSuggestion={dismissed.has('kamigame')}
|
||||||
onAcceptSuggestion={() => handleAcceptSuggestion('kamigame', suggestions?.kamigame)}
|
onAcceptSuggestion={() => handleAcceptSuggestion('kamigame', suggestions?.kamigame)}
|
||||||
|
|
@ -641,6 +658,10 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.wiki-inputs .add-input-button) {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
.remove-button {
|
.remove-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@
|
||||||
import { entityAdapter, type SummonSuggestions } from '$lib/api/adapters/entity.adapter'
|
import { entityAdapter, type SummonSuggestions } from '$lib/api/adapters/entity.adapter'
|
||||||
import { fetchWikiPages, buildWikiDataMap } from '$lib/api/wiki'
|
import { fetchWikiPages, buildWikiDataMap } from '$lib/api/wiki'
|
||||||
import { getGameCdnSummonImage, getPlaceholderImage } from '$lib/utils/images'
|
import { getGameCdnSummonImage, getPlaceholderImage } from '$lib/utils/images'
|
||||||
|
import {
|
||||||
|
buildWikiEnUrl,
|
||||||
|
buildWikiJaUrl,
|
||||||
|
buildGamewithUrl,
|
||||||
|
buildKamigameUrl
|
||||||
|
} from '$lib/utils/external-links'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import SummonUncapSection from '$lib/features/database/summons/sections/SummonUncapSection.svelte'
|
import SummonUncapSection from '$lib/features/database/summons/sections/SummonUncapSection.svelte'
|
||||||
|
|
@ -44,9 +50,9 @@
|
||||||
let entities = $state<Map<string, EntityState>>(new Map())
|
let entities = $state<Map<string, EntityState>>(new Map())
|
||||||
let selectedWikiPage = $state<string | null>(null)
|
let selectedWikiPage = $state<string | null>(null)
|
||||||
|
|
||||||
// Form data per entity (keyed by wikiPage)
|
// Form data per entity (keyed by wikiPage) - using Record for proper reactivity
|
||||||
let formDataMap = $state<Map<string, any>>(new Map())
|
let formDataByPage = $state<Record<string, any>>({})
|
||||||
let dismissedSuggestionsMap = $state<Map<string, Set<string>>>(new Map())
|
let dismissedByPage = $state<Record<string, Set<string>>>({})
|
||||||
let savedEntities = $state<Set<string>>(new Set())
|
let savedEntities = $state<Set<string>>(new Set())
|
||||||
|
|
||||||
// Saving state
|
// Saving state
|
||||||
|
|
@ -118,7 +124,7 @@
|
||||||
flbDate: suggestions?.flbDate ?? '',
|
flbDate: suggestions?.flbDate ?? '',
|
||||||
ulbDate: suggestions?.ulbDate ?? '',
|
ulbDate: suggestions?.ulbDate ?? '',
|
||||||
transcendenceDate: '',
|
transcendenceDate: '',
|
||||||
wikiEn: wikiPage ? `https://gbf.wiki/${wikiPage.replace(/ /g, '_')}` : '',
|
wikiEn: wikiPage ? wikiPage.replace(/ /g, '_') : '',
|
||||||
wikiJa: '',
|
wikiJa: '',
|
||||||
gamewith: suggestions?.gamewith ?? '',
|
gamewith: suggestions?.gamewith ?? '',
|
||||||
kamigame: suggestions?.kamigame ?? '',
|
kamigame: suggestions?.kamigame ?? '',
|
||||||
|
|
@ -188,16 +194,15 @@
|
||||||
|
|
||||||
// Create form data for successful results
|
// Create form data for successful results
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
const formData = createEmptyFormData(result.wikiPage, result.suggestions)
|
formDataByPage[result.wikiPage] = createEmptyFormData(result.wikiPage, result.suggestions)
|
||||||
formDataMap.set(result.wikiPage, formData)
|
dismissedByPage[result.wikiPage] = new Set<string>()
|
||||||
dismissedSuggestionsMap.set(result.wikiPage, new Set<string>())
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
entities = updatedEntities
|
entities = updatedEntities
|
||||||
|
|
||||||
// Update formDataMap and dismissedSuggestionsMap to trigger reactivity
|
// Trigger reactivity by reassigning
|
||||||
formDataMap = new Map(formDataMap)
|
formDataByPage = { ...formDataByPage }
|
||||||
dismissedSuggestionsMap = new Map(dismissedSuggestionsMap)
|
dismissedByPage = { ...dismissedByPage }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Batch preview error:', error)
|
console.error('Batch preview error:', error)
|
||||||
fetchError = 'Failed to fetch wiki data. Please try again.'
|
fetchError = 'Failed to fetch wiki data. Please try again.'
|
||||||
|
|
@ -213,28 +218,26 @@
|
||||||
|
|
||||||
// Accept a suggestion
|
// Accept a suggestion
|
||||||
function handleAcceptSuggestion(field: string, value: any) {
|
function handleAcceptSuggestion(field: string, value: any) {
|
||||||
if (!selectedWikiPage) return
|
if (!selectedWikiPage || !formDataByPage[selectedWikiPage]) return
|
||||||
|
|
||||||
const formData = formDataMap.get(selectedWikiPage)
|
formDataByPage[selectedWikiPage][field] = value
|
||||||
if (formData) {
|
formDataByPage = { ...formDataByPage }
|
||||||
formData[field] = value
|
|
||||||
formDataMap = new Map(formDataMap)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dismiss a suggestion
|
// Dismiss a suggestion
|
||||||
function handleDismissSuggestion(field: string) {
|
function handleDismissSuggestion(field: string) {
|
||||||
if (!selectedWikiPage) return
|
if (!selectedWikiPage) return
|
||||||
|
|
||||||
const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set()
|
const dismissed = dismissedByPage[selectedWikiPage] ?? new Set<string>()
|
||||||
dismissed.add(field)
|
dismissed.add(field)
|
||||||
dismissedSuggestionsMap = new Map(dismissedSuggestionsMap)
|
dismissedByPage[selectedWikiPage] = dismissed
|
||||||
|
dismissedByPage = { ...dismissedByPage }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current entity
|
// Save current entity
|
||||||
async function saveCurrentEntity() {
|
async function saveCurrentEntity() {
|
||||||
if (!selectedWikiPage) return
|
if (!selectedWikiPage) return
|
||||||
const formData = formDataMap.get(selectedWikiPage)
|
const formData = formDataByPage[selectedWikiPage]
|
||||||
if (!formData) return
|
if (!formData) return
|
||||||
|
|
||||||
isSaving = true
|
isSaving = true
|
||||||
|
|
@ -304,7 +307,7 @@
|
||||||
// Can save current entity
|
// Can save current entity
|
||||||
const canSave = $derived.by(() => {
|
const canSave = $derived.by(() => {
|
||||||
if (!selectedWikiPage) return false
|
if (!selectedWikiPage) return false
|
||||||
const formData = formDataMap.get(selectedWikiPage)
|
const formData = formDataByPage[selectedWikiPage]
|
||||||
if (!formData) return false
|
if (!formData) return false
|
||||||
return (
|
return (
|
||||||
formData.name.trim() !== '' &&
|
formData.name.trim() !== '' &&
|
||||||
|
|
@ -318,6 +321,7 @@
|
||||||
entityTabs.length > 0 &&
|
entityTabs.length > 0 &&
|
||||||
entityTabs.filter((e) => e.status === 'success').every((e) => savedEntities.has(e.wikiPage))
|
entityTabs.filter((e) => e.status === 'success').every((e) => savedEntities.has(e.wikiPage))
|
||||||
)
|
)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
|
@ -405,15 +409,14 @@
|
||||||
<div class="entity-loading">
|
<div class="entity-loading">
|
||||||
<p>Loading wiki data...</p>
|
<p>Loading wiki data...</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if selectedWikiPage && formDataMap.has(selectedWikiPage)}
|
{:else if selectedWikiPage && formDataByPage[selectedWikiPage]}
|
||||||
{@const formData = formDataMap.get(selectedWikiPage)!}
|
|
||||||
{@const suggestions = selectedEntity.suggestions}
|
{@const suggestions = selectedEntity.suggestions}
|
||||||
{@const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set<string>()}
|
{@const dismissed = dismissedByPage[selectedWikiPage] ?? new Set<string>()}
|
||||||
<section class="details">
|
<section class="details">
|
||||||
<DetailsContainer title="Basic Info">
|
<DetailsContainer title="Basic Info">
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Name (EN)"
|
label="Name (EN)"
|
||||||
bind:value={formData.name}
|
bind:value={formDataByPage[selectedWikiPage].name}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Summon name"
|
placeholder="Summon name"
|
||||||
|
|
@ -424,7 +427,7 @@
|
||||||
/>
|
/>
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Name (JP)"
|
label="Name (JP)"
|
||||||
bind:value={formData.nameJp}
|
bind:value={formDataByPage[selectedWikiPage].nameJp}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="召喚石名"
|
placeholder="召喚石名"
|
||||||
|
|
@ -436,7 +439,7 @@
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Summon ID"
|
label="Summon ID"
|
||||||
sublabel="Internal game identifier (if known)"
|
sublabel="Internal game identifier (if known)"
|
||||||
bind:value={formData.summonId}
|
bind:value={formDataByPage[selectedWikiPage].summonId}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Optional"
|
placeholder="Optional"
|
||||||
|
|
@ -446,7 +449,7 @@
|
||||||
<SummonMetadataSection
|
<SummonMetadataSection
|
||||||
summon={emptySummon}
|
summon={emptySummon}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -456,7 +459,7 @@
|
||||||
<SummonUncapSection
|
<SummonUncapSection
|
||||||
summon={emptySummon}
|
summon={emptySummon}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -466,7 +469,7 @@
|
||||||
<SummonTaxonomySection
|
<SummonTaxonomySection
|
||||||
summon={emptySummon}
|
summon={emptySummon}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -476,7 +479,7 @@
|
||||||
<SummonStatsSection
|
<SummonStatsSection
|
||||||
summon={emptySummon}
|
summon={emptySummon}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -485,17 +488,17 @@
|
||||||
|
|
||||||
<DetailsContainer title="Nicknames">
|
<DetailsContainer title="Nicknames">
|
||||||
<DetailItem label="Nicknames (EN)">
|
<DetailItem label="Nicknames (EN)">
|
||||||
<TagInput bind:value={formData.nicknamesEn} placeholder="Add nickname..." contained />
|
<TagInput bind:value={formDataByPage[selectedWikiPage].nicknamesEn} placeholder="Add nickname..." contained />
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
<DetailItem label="Nicknames (JP)">
|
<DetailItem label="Nicknames (JP)">
|
||||||
<TagInput bind:value={formData.nicknamesJp} placeholder="ニックネーム..." contained />
|
<TagInput bind:value={formDataByPage[selectedWikiPage].nicknamesJp} placeholder="ニックネーム..." contained />
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
<DetailsContainer title="Dates">
|
<DetailsContainer title="Dates">
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Release Date"
|
label="Release Date"
|
||||||
bind:value={formData.releaseDate}
|
bind:value={formDataByPage[selectedWikiPage].releaseDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
|
|
@ -505,10 +508,10 @@
|
||||||
handleAcceptSuggestion('releaseDate', suggestions?.releaseDate)}
|
handleAcceptSuggestion('releaseDate', suggestions?.releaseDate)}
|
||||||
onDismissSuggestion={() => handleDismissSuggestion('releaseDate')}
|
onDismissSuggestion={() => handleDismissSuggestion('releaseDate')}
|
||||||
/>
|
/>
|
||||||
{#if formData.flb}
|
{#if formDataByPage[selectedWikiPage].flb}
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="FLB Date"
|
label="FLB Date"
|
||||||
bind:value={formData.flbDate}
|
bind:value={formDataByPage[selectedWikiPage].flbDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
|
|
@ -518,10 +521,10 @@
|
||||||
onDismissSuggestion={() => handleDismissSuggestion('flbDate')}
|
onDismissSuggestion={() => handleDismissSuggestion('flbDate')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if formData.ulb}
|
{#if formDataByPage[selectedWikiPage].ulb}
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="ULB Date"
|
label="ULB Date"
|
||||||
bind:value={formData.ulbDate}
|
bind:value={formDataByPage[selectedWikiPage].ulbDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
|
|
@ -531,10 +534,10 @@
|
||||||
onDismissSuggestion={() => handleDismissSuggestion('ulbDate')}
|
onDismissSuggestion={() => handleDismissSuggestion('ulbDate')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if formData.transcendence}
|
{#if formDataByPage[selectedWikiPage].transcendence}
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Transcendence Date"
|
label="Transcendence Date"
|
||||||
bind:value={formData.transcendenceDate}
|
bind:value={formDataByPage[selectedWikiPage].transcendenceDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
|
|
@ -545,27 +548,33 @@
|
||||||
<DetailsContainer title="Links">
|
<DetailsContainer title="Links">
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Wiki (EN)"
|
label="Wiki (EN)"
|
||||||
bind:value={formData.wikiEn}
|
bind:value={formDataByPage[selectedWikiPage].wikiEn}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://gbf.wiki/..."
|
placeholder="Page name (e.g., Bahamut)"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildWikiEnUrl(formDataByPage[selectedWikiPage].wikiEn)}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Wiki (JP)"
|
label="Wiki (JP)"
|
||||||
bind:value={formData.wikiJa}
|
bind:value={formDataByPage[selectedWikiPage].wikiJa}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://gbf-wiki.com/..."
|
placeholder="Japanese page name"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildWikiJaUrl(formDataByPage[selectedWikiPage].wikiJa)}
|
||||||
/>
|
/>
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Gamewith"
|
label="Gamewith"
|
||||||
bind:value={formData.gamewith}
|
bind:value={formDataByPage[selectedWikiPage].gamewith}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://..."
|
placeholder="Article ID (e.g., 519325)"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildGamewithUrl(formDataByPage[selectedWikiPage].gamewith)}
|
||||||
suggestion={suggestions?.gamewith}
|
suggestion={suggestions?.gamewith}
|
||||||
dismissedSuggestion={dismissed.has('gamewith')}
|
dismissedSuggestion={dismissed.has('gamewith')}
|
||||||
onAcceptSuggestion={() => handleAcceptSuggestion('gamewith', suggestions?.gamewith)}
|
onAcceptSuggestion={() => handleAcceptSuggestion('gamewith', suggestions?.gamewith)}
|
||||||
|
|
@ -573,11 +582,13 @@
|
||||||
/>
|
/>
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Kamigame"
|
label="Kamigame"
|
||||||
bind:value={formData.kamigame}
|
bind:value={formDataByPage[selectedWikiPage].kamigame}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://..."
|
placeholder="Slug (e.g., SSR/アグニス)"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildKamigameUrl(formDataByPage[selectedWikiPage].kamigame, 'summon')}
|
||||||
suggestion={suggestions?.kamigame}
|
suggestion={suggestions?.kamigame}
|
||||||
dismissedSuggestion={dismissed.has('kamigame')}
|
dismissedSuggestion={dismissed.has('kamigame')}
|
||||||
onAcceptSuggestion={() => handleAcceptSuggestion('kamigame', suggestions?.kamigame)}
|
onAcceptSuggestion={() => handleAcceptSuggestion('kamigame', suggestions?.kamigame)}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { entityAdapter, type WeaponSuggestions } from '$lib/api/adapters/entity.adapter'
|
import { entityAdapter, type WeaponSuggestions } from '$lib/api/adapters/entity.adapter'
|
||||||
import { fetchWikiPages, buildWikiDataMap } from '$lib/api/wiki'
|
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
|
// Components
|
||||||
import WeaponUncapSection from '$lib/features/database/weapons/sections/WeaponUncapSection.svelte'
|
import WeaponUncapSection from '$lib/features/database/weapons/sections/WeaponUncapSection.svelte'
|
||||||
|
|
@ -45,9 +51,9 @@
|
||||||
let entities = $state<Map<string, EntityState>>(new Map())
|
let entities = $state<Map<string, EntityState>>(new Map())
|
||||||
let selectedWikiPage = $state<string | null>(null)
|
let selectedWikiPage = $state<string | null>(null)
|
||||||
|
|
||||||
// Form data per entity (keyed by wikiPage)
|
// Form data per entity (keyed by wikiPage) - using Record for proper reactivity
|
||||||
let formDataMap = $state<Map<string, any>>(new Map())
|
let formDataByPage = $state<Record<string, any>>({})
|
||||||
let dismissedSuggestionsMap = $state<Map<string, Set<string>>>(new Map())
|
let dismissedByPage = $state<Record<string, Set<string>>>({})
|
||||||
let savedEntities = $state<Set<string>>(new Set())
|
let savedEntities = $state<Set<string>>(new Set())
|
||||||
|
|
||||||
// Saving state
|
// Saving state
|
||||||
|
|
@ -79,7 +85,7 @@
|
||||||
granblueId: entity.granblueId,
|
granblueId: entity.granblueId,
|
||||||
status: entity.status,
|
status: entity.status,
|
||||||
imageUrl: entity.granblueId
|
imageUrl: entity.granblueId
|
||||||
? getWeaponImage(entity.granblueId, 'square')
|
? getGameCdnWeaponImage(entity.granblueId)
|
||||||
: getPlaceholderImage('weapon', 'square'),
|
: getPlaceholderImage('weapon', 'square'),
|
||||||
error: entity.error,
|
error: entity.error,
|
||||||
saved: savedEntities.has(wikiPage)
|
saved: savedEntities.has(wikiPage)
|
||||||
|
|
@ -119,7 +125,7 @@
|
||||||
flbDate: suggestions?.flbDate ?? '',
|
flbDate: suggestions?.flbDate ?? '',
|
||||||
ulbDate: suggestions?.ulbDate ?? '',
|
ulbDate: suggestions?.ulbDate ?? '',
|
||||||
transcendenceDate: '',
|
transcendenceDate: '',
|
||||||
wikiEn: wikiPage ? `https://gbf.wiki/${wikiPage.replace(/ /g, '_')}` : '',
|
wikiEn: wikiPage ? wikiPage.replace(/ /g, '_') : '',
|
||||||
wikiJa: '',
|
wikiJa: '',
|
||||||
gamewith: suggestions?.gamewith ?? '',
|
gamewith: suggestions?.gamewith ?? '',
|
||||||
kamigame: suggestions?.kamigame ?? '',
|
kamigame: suggestions?.kamigame ?? '',
|
||||||
|
|
@ -190,16 +196,15 @@
|
||||||
|
|
||||||
// Create form data for successful results
|
// Create form data for successful results
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
const formData = createEmptyFormData(result.wikiPage, result.suggestions)
|
formDataByPage[result.wikiPage] = createEmptyFormData(result.wikiPage, result.suggestions)
|
||||||
formDataMap.set(result.wikiPage, formData)
|
dismissedByPage[result.wikiPage] = new Set<string>()
|
||||||
dismissedSuggestionsMap.set(result.wikiPage, new Set<string>())
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
entities = updatedEntities
|
entities = updatedEntities
|
||||||
|
|
||||||
// Update formDataMap and dismissedSuggestionsMap to trigger reactivity
|
// Trigger reactivity by reassigning
|
||||||
formDataMap = new Map(formDataMap)
|
formDataByPage = { ...formDataByPage }
|
||||||
dismissedSuggestionsMap = new Map(dismissedSuggestionsMap)
|
dismissedByPage = { ...dismissedByPage }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Batch preview error:', error)
|
console.error('Batch preview error:', error)
|
||||||
fetchError = 'Failed to fetch wiki data. Please try again.'
|
fetchError = 'Failed to fetch wiki data. Please try again.'
|
||||||
|
|
@ -215,28 +220,26 @@
|
||||||
|
|
||||||
// Accept a suggestion
|
// Accept a suggestion
|
||||||
function handleAcceptSuggestion(field: string, value: any) {
|
function handleAcceptSuggestion(field: string, value: any) {
|
||||||
if (!selectedWikiPage) return
|
if (!selectedWikiPage || !formDataByPage[selectedWikiPage]) return
|
||||||
|
|
||||||
const formData = formDataMap.get(selectedWikiPage)
|
formDataByPage[selectedWikiPage][field] = value
|
||||||
if (formData) {
|
formDataByPage = { ...formDataByPage }
|
||||||
formData[field] = value
|
|
||||||
formDataMap = new Map(formDataMap)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dismiss a suggestion
|
// Dismiss a suggestion
|
||||||
function handleDismissSuggestion(field: string) {
|
function handleDismissSuggestion(field: string) {
|
||||||
if (!selectedWikiPage) return
|
if (!selectedWikiPage) return
|
||||||
|
|
||||||
const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set()
|
const dismissed = dismissedByPage[selectedWikiPage] ?? new Set<string>()
|
||||||
dismissed.add(field)
|
dismissed.add(field)
|
||||||
dismissedSuggestionsMap = new Map(dismissedSuggestionsMap)
|
dismissedByPage[selectedWikiPage] = dismissed
|
||||||
|
dismissedByPage = { ...dismissedByPage }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current entity
|
// Save current entity
|
||||||
async function saveCurrentEntity() {
|
async function saveCurrentEntity() {
|
||||||
if (!selectedWikiPage) return
|
if (!selectedWikiPage) return
|
||||||
const formData = formDataMap.get(selectedWikiPage)
|
const formData = formDataByPage[selectedWikiPage]
|
||||||
if (!formData) return
|
if (!formData) return
|
||||||
|
|
||||||
isSaving = true
|
isSaving = true
|
||||||
|
|
@ -288,7 +291,9 @@
|
||||||
savedEntities = new Set(savedEntities)
|
savedEntities = new Set(savedEntities)
|
||||||
|
|
||||||
// Select next unsaved entity
|
// 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) {
|
if (unsaved) {
|
||||||
selectedWikiPage = unsaved.wikiPage
|
selectedWikiPage = unsaved.wikiPage
|
||||||
}
|
}
|
||||||
|
|
@ -307,7 +312,7 @@
|
||||||
// Can save current entity
|
// Can save current entity
|
||||||
const canSave = $derived.by(() => {
|
const canSave = $derived.by(() => {
|
||||||
if (!selectedWikiPage) return false
|
if (!selectedWikiPage) return false
|
||||||
const formData = formDataMap.get(selectedWikiPage)
|
const formData = formDataByPage[selectedWikiPage]
|
||||||
if (!formData) return false
|
if (!formData) return false
|
||||||
return (
|
return (
|
||||||
formData.name.trim() !== '' &&
|
formData.name.trim() !== '' &&
|
||||||
|
|
@ -321,6 +326,7 @@
|
||||||
entityTabs.length > 0 &&
|
entityTabs.length > 0 &&
|
||||||
entityTabs.filter((e) => e.status === 'success').every((e) => savedEntities.has(e.wikiPage))
|
entityTabs.filter((e) => e.status === 'success').every((e) => savedEntities.has(e.wikiPage))
|
||||||
)
|
)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
|
@ -350,17 +356,12 @@
|
||||||
|
|
||||||
<!-- Input phase -->
|
<!-- Input phase -->
|
||||||
{#if entities.size === 0}
|
{#if entities.size === 0}
|
||||||
<div class="input-phase">
|
<form class="input-phase" onsubmit={(e) => { e.preventDefault(); fetchWikiData(); }}>
|
||||||
<p class="hint">Enter up to 10 wiki page names to import data</p>
|
<p class="hint">Enter up to 10 wiki page names to import data</p>
|
||||||
<div class="wiki-inputs">
|
<div class="wiki-inputs">
|
||||||
{#each wikiPagesInputs as _, index}
|
{#each wikiPagesInputs as _, index}
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<Input
|
<Input bind:value={wikiPagesInputs[index]} placeholder="Ixaba" contained fullWidth />
|
||||||
bind:value={wikiPagesInputs[index]}
|
|
||||||
placeholder="Ixaba"
|
|
||||||
contained
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
{#if wikiPagesInputs.length > 1}
|
{#if wikiPagesInputs.length > 1}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -373,8 +374,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<Button variant="ghost" onclick={addInput}>
|
<Button
|
||||||
<Icon name="plus" size={16} />
|
variant="ghost"
|
||||||
|
class="add-input-button"
|
||||||
|
leftIcon="plus"
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
onclick={addInput}
|
||||||
|
>
|
||||||
Add another
|
Add another
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -382,11 +389,11 @@
|
||||||
<p class="error">{fetchError}</p>
|
<p class="error">{fetchError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="fetch-button">
|
<div class="fetch-button">
|
||||||
<Button variant="primary" onclick={fetchWikiData} disabled={isFetching}>
|
<Button variant="primary" type="submit" disabled={isFetching}>
|
||||||
{isFetching ? 'Fetching...' : 'Fetch data'}
|
{isFetching ? 'Fetching...' : 'Fetch data'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Entity selector -->
|
<!-- Entity selector -->
|
||||||
<div class="entity-selector-container">
|
<div class="entity-selector-container">
|
||||||
|
|
@ -409,15 +416,14 @@
|
||||||
<div class="entity-loading">
|
<div class="entity-loading">
|
||||||
<p>Loading wiki data...</p>
|
<p>Loading wiki data...</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if selectedWikiPage && formDataMap.has(selectedWikiPage)}
|
{:else if selectedWikiPage && formDataByPage[selectedWikiPage]}
|
||||||
{@const formData = formDataMap.get(selectedWikiPage)!}
|
|
||||||
{@const suggestions = selectedEntity.suggestions}
|
{@const suggestions = selectedEntity.suggestions}
|
||||||
{@const dismissed = dismissedSuggestionsMap.get(selectedWikiPage) ?? new Set<string>()}
|
{@const dismissed = dismissedByPage[selectedWikiPage] ?? new Set<string>()}
|
||||||
<section class="details">
|
<section class="details">
|
||||||
<DetailsContainer title="Basic Info">
|
<DetailsContainer title="Basic Info">
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Name (EN)"
|
label="Name (EN)"
|
||||||
bind:value={formData.name}
|
bind:value={formDataByPage[selectedWikiPage].name}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Weapon name"
|
placeholder="Weapon name"
|
||||||
|
|
@ -428,7 +434,7 @@
|
||||||
/>
|
/>
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Name (JP)"
|
label="Name (JP)"
|
||||||
bind:value={formData.nameJp}
|
bind:value={formDataByPage[selectedWikiPage].nameJp}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="武器名"
|
placeholder="武器名"
|
||||||
|
|
@ -442,7 +448,7 @@
|
||||||
<WeaponMetadataSection
|
<WeaponMetadataSection
|
||||||
weapon={emptyWeapon}
|
weapon={emptyWeapon}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -452,7 +458,7 @@
|
||||||
<WeaponUncapSection
|
<WeaponUncapSection
|
||||||
weapon={emptyWeapon}
|
weapon={emptyWeapon}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -462,7 +468,7 @@
|
||||||
<WeaponTaxonomySection
|
<WeaponTaxonomySection
|
||||||
weapon={emptyWeapon}
|
weapon={emptyWeapon}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -472,7 +478,7 @@
|
||||||
<WeaponStatsSection
|
<WeaponStatsSection
|
||||||
weapon={emptyWeapon}
|
weapon={emptyWeapon}
|
||||||
editMode={true}
|
editMode={true}
|
||||||
editData={formData}
|
bind:editData={formDataByPage[selectedWikiPage]}
|
||||||
{suggestions}
|
{suggestions}
|
||||||
dismissedSuggestions={dismissed}
|
dismissedSuggestions={dismissed}
|
||||||
onAcceptSuggestion={handleAcceptSuggestion}
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
|
@ -481,35 +487,39 @@
|
||||||
|
|
||||||
<DetailsContainer title="Nicknames">
|
<DetailsContainer title="Nicknames">
|
||||||
<DetailItem label="Nicknames (EN)">
|
<DetailItem label="Nicknames (EN)">
|
||||||
<TagInput bind:value={formData.nicknamesEn} placeholder="Add nickname..." contained />
|
<TagInput bind:value={formDataByPage[selectedWikiPage].nicknamesEn} placeholder="Add nickname..." contained />
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
<DetailItem label="Nicknames (JP)">
|
<DetailItem label="Nicknames (JP)">
|
||||||
<TagInput bind:value={formData.nicknamesJp} placeholder="ニックネーム..." contained />
|
<TagInput bind:value={formDataByPage[selectedWikiPage].nicknamesJp} placeholder="ニックネーム..." contained />
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
<DetailsContainer title="Recruits">
|
<DetailsContainer title="Recruits">
|
||||||
<DetailItem label="Recruits Character" sublabel="Character recruited by this weapon">
|
<DetailItem label="Recruits Character" sublabel="Character recruited by this weapon">
|
||||||
<CharacterTypeahead bind:value={formData.recruits} placeholder="Search for character..." />
|
<CharacterTypeahead
|
||||||
|
bind:value={formDataByPage[selectedWikiPage].recruits}
|
||||||
|
placeholder="Search for character..."
|
||||||
|
/>
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
</DetailsContainer>
|
</DetailsContainer>
|
||||||
|
|
||||||
<DetailsContainer title="Dates">
|
<DetailsContainer title="Dates">
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Release Date"
|
label="Release Date"
|
||||||
bind:value={formData.releaseDate}
|
bind:value={formDataByPage[selectedWikiPage].releaseDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
suggestion={suggestions?.releaseDate}
|
suggestion={suggestions?.releaseDate}
|
||||||
dismissedSuggestion={dismissed.has('releaseDate')}
|
dismissedSuggestion={dismissed.has('releaseDate')}
|
||||||
onAcceptSuggestion={() => handleAcceptSuggestion('releaseDate', suggestions?.releaseDate)}
|
onAcceptSuggestion={() =>
|
||||||
|
handleAcceptSuggestion('releaseDate', suggestions?.releaseDate)}
|
||||||
onDismissSuggestion={() => handleDismissSuggestion('releaseDate')}
|
onDismissSuggestion={() => handleDismissSuggestion('releaseDate')}
|
||||||
/>
|
/>
|
||||||
{#if formData.flb}
|
{#if formDataByPage[selectedWikiPage].flb}
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="FLB Date"
|
label="FLB Date"
|
||||||
bind:value={formData.flbDate}
|
bind:value={formDataByPage[selectedWikiPage].flbDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
|
|
@ -519,10 +529,10 @@
|
||||||
onDismissSuggestion={() => handleDismissSuggestion('flbDate')}
|
onDismissSuggestion={() => handleDismissSuggestion('flbDate')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if formData.ulb}
|
{#if formDataByPage[selectedWikiPage].ulb}
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="ULB Date"
|
label="ULB Date"
|
||||||
bind:value={formData.ulbDate}
|
bind:value={formDataByPage[selectedWikiPage].ulbDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
|
|
@ -532,10 +542,10 @@
|
||||||
onDismissSuggestion={() => handleDismissSuggestion('ulbDate')}
|
onDismissSuggestion={() => handleDismissSuggestion('ulbDate')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if formData.transcendence}
|
{#if formDataByPage[selectedWikiPage].transcendence}
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Transcendence Date"
|
label="Transcendence Date"
|
||||||
bind:value={formData.transcendenceDate}
|
bind:value={formDataByPage[selectedWikiPage].transcendenceDate}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
|
|
@ -546,27 +556,33 @@
|
||||||
<DetailsContainer title="Links">
|
<DetailsContainer title="Links">
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Wiki (EN)"
|
label="Wiki (EN)"
|
||||||
bind:value={formData.wikiEn}
|
bind:value={formDataByPage[selectedWikiPage].wikiEn}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://gbf.wiki/..."
|
placeholder="Page name (e.g., Cosmic_Sword)"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildWikiEnUrl(formDataByPage[selectedWikiPage].wikiEn)}
|
||||||
/>
|
/>
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Wiki (JP)"
|
label="Wiki (JP)"
|
||||||
bind:value={formData.wikiJa}
|
bind:value={formDataByPage[selectedWikiPage].wikiJa}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://gbf-wiki.com/..."
|
placeholder="Japanese page name"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildWikiJaUrl(formDataByPage[selectedWikiPage].wikiJa)}
|
||||||
/>
|
/>
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Gamewith"
|
label="Gamewith"
|
||||||
bind:value={formData.gamewith}
|
bind:value={formDataByPage[selectedWikiPage].gamewith}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://..."
|
placeholder="Article ID (e.g., 519325)"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildGamewithUrl(formDataByPage[selectedWikiPage].gamewith)}
|
||||||
suggestion={suggestions?.gamewith}
|
suggestion={suggestions?.gamewith}
|
||||||
dismissedSuggestion={dismissed.has('gamewith')}
|
dismissedSuggestion={dismissed.has('gamewith')}
|
||||||
onAcceptSuggestion={() => handleAcceptSuggestion('gamewith', suggestions?.gamewith)}
|
onAcceptSuggestion={() => handleAcceptSuggestion('gamewith', suggestions?.gamewith)}
|
||||||
|
|
@ -574,11 +590,13 @@
|
||||||
/>
|
/>
|
||||||
<SuggestionDetailItem
|
<SuggestionDetailItem
|
||||||
label="Kamigame"
|
label="Kamigame"
|
||||||
bind:value={formData.kamigame}
|
bind:value={formDataByPage[selectedWikiPage].kamigame}
|
||||||
editable={true}
|
editable={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://..."
|
placeholder="Japanese name (e.g., 神刃エクス・アシャワン)"
|
||||||
width="480px"
|
width="480px"
|
||||||
|
hasLinkButton={true}
|
||||||
|
linkUrl={buildKamigameUrl(formDataByPage[selectedWikiPage].kamigame, 'weapon', formDataByPage[selectedWikiPage].rarity)}
|
||||||
suggestion={suggestions?.kamigame}
|
suggestion={suggestions?.kamigame}
|
||||||
dismissedSuggestion={dismissed.has('kamigame')}
|
dismissedSuggestion={dismissed.has('kamigame')}
|
||||||
onAcceptSuggestion={() => handleAcceptSuggestion('kamigame', suggestions?.kamigame)}
|
onAcceptSuggestion={() => handleAcceptSuggestion('kamigame', suggestions?.kamigame)}
|
||||||
|
|
@ -622,6 +640,10 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.wiki-inputs .add-input-button) {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
.remove-button {
|
.remove-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue