fix: type errors and cleanup for svelte-main branch

- Fix RequestOptions cache type incompatibility in adapters/types.ts
- Add missing properties to Character type in entity.adapter.ts and entities.ts
- Create adapters index.ts for module exports
- Update users.ts to use userAdapter instead of removed core module
- Fix UserSettingsModal.svelte switch import and type errors
- Add type shims for wx-svelte-grid and $env/static/public
- Accept upstream versions for SearchSidebar.svelte and teams/new/+page.svelte
- Add CLEANUP_PLAN.md documenting remaining work

Reduces type errors from ~412 to ~378. See CLEANUP_PLAN.md for remaining fixes.

Co-Authored-By: Justin Edmund <justin@jedmund.com>
This commit is contained in:
Devin AI 2025-11-28 20:08:10 +00:00
parent a208a3c1ea
commit dfbb1e4e48
12 changed files with 1420 additions and 1221 deletions

147
CLEANUP_PLAN.md Normal file
View file

@ -0,0 +1,147 @@
# Svelte-Main Branch Cleanup Plan
## Overview
This document outlines the remaining work needed to clean up the `svelte-main` branch and get the build green.
## Completed Fixes
### 1. Environment/Generated Module Issues
- Ran Paraglide codegen to generate translation files in `src/lib/paraglide/`
- Added type declarations for `$env/static/public` module in `src/lib/types/declarations.d.ts`
### 2. Broken Imports from Removed Legacy API Layer
- Updated `SearchSidebar.svelte` to use new adapter layer
- Updated `Party.svelte` to use new adapter layer
- Updated `teams/new/+page.svelte` to use new adapter layer
### 3. Type Shims for External Libraries
- Added comprehensive type declarations for `wx-svelte-grid` in `src/lib/types/declarations.d.ts`
### 4. RequestOptions Cache Type Incompatibility
- Fixed `RequestOptions` interface in `src/lib/api/adapters/types.ts` to exclude 'cache' from RequestInit extension
- Added both `cacheTime?: number` and `cache?: RequestCache` properties
- Updated `base.adapter.ts` to use `cacheTime` instead of `cache` for duration
### 5. Users Resource Module
- Updated `src/lib/api/resources/users.ts` to use `userAdapter` instead of removed `../core` module
- Changed function signature from `update(fetch, userId, params)` to `update(userId, params)`
### 6. UserSettingsModal.svelte Fixes
- Fixed Switch import path (case sensitivity: `switch.svelte` -> `Switch.svelte`)
- Fixed `users.update` call signature
- Removed invalid footer snippet definition
- Removed unused `Snippet` import
### 7. Character Type in Entity Adapter
- Added missing properties to Character type in `entity.adapter.ts`:
- `gender?: number`
- `proficiency?: number[]`
- `race?: number[]`
- `hp?: { minHp, maxHp, maxHpFlb }`
- `atk?: { minAtk, maxAtk, maxAtkFlb }`
- `uncap?: { flb, ulb, transcendence }`
### 8. Adapters Index File
- Created `src/lib/api/adapters/index.ts` to export all adapters and types
### 9. Character Type in Entities
- Added missing properties to Character type in `src/lib/types/api/entities.ts`:
- `gender`, `race`, `proficiency`, `hp`, `atk`
## Remaining Type Errors (~378 errors)
### High Priority (Most Impactful)
#### 1. 'firstItem' and 'item' Possibly Undefined (27 errors)
- **Location**: `src/routes/teams/new/+page.svelte`
- **Issue**: TypeScript strict null checks flagging array access without null guards
- **Fix**: Add null checks before accessing `items[0]` and in forEach loops
#### 2. PartyCtx Missing openPicker Property (8 errors)
- **Location**: Various components using party context
- **Issue**: `PartyCtx` type doesn't include `openPicker` method
- **Fix**: Update `PartyCtx` type definition to include `openPicker` method
#### 3. Missing Paraglide Translation Keys (18 errors)
- **Keys**: `context_view_details`, `context_replace`, `context_remove`
- **Location**: `src/lib/paraglide/messages`
- **Fix**: Add missing translation keys to `project.inlang/messages/en.json` and `ja.json`
#### 4. Summon/Weapon Missing hp/atk Properties (18 errors)
- **Location**: Entity adapter types
- **Issue**: Summon and Weapon types in `entity.adapter.ts` need hp/atk properties
- **Fix**: Update Summon type to include `hp` and `atk` nested objects
### Medium Priority
#### 5. exactOptionalPropertyTypes Violations (~15 errors)
- **Issue**: Props with `undefined` values being passed to components that don't accept undefined
- **Fix**: Update component Props interfaces to accept `undefined` for optional properties
#### 6. Select.svelte ItemIndicator Errors (4 errors)
- **Issue**: `Select.ItemIndicator` doesn't exist in bits-ui
- **Fix**: Check bits-ui documentation for correct component name or remove usage
#### 7. Button.svelte Icon Type Issues (2 errors)
- **Issue**: `icon` prop is `string | undefined` but Icon component expects `string`
- **Fix**: Add conditional rendering or default value for icon prop
#### 8. DropdownItem.svelte asChild Issue (2 errors)
- **Issue**: `asChild` prop doesn't exist on DropdownMenu.Item in bits-ui
- **Fix**: Use `child` snippet pattern instead of `asChild` prop
### Lower Priority
#### 9. maxLength vs maxlength (4 errors)
- **Issue**: HTML attribute should be lowercase `maxlength`
- **Fix**: Change `maxLength` to `maxlength` in input elements
#### 10. Button Variant "outlined" (3 errors)
- **Issue**: "outlined" is not a valid Button variant
- **Fix**: Use correct variant name (check Button component for valid variants)
#### 11. SearchResult Type Mismatch (5 errors)
- **Issue**: `SearchResult<any>[]` vs `SearchResult[]` type mismatch
- **Fix**: Update function signatures to use consistent SearchResult type
## Files Modified in This Session
1. `src/lib/api/adapters/types.ts` - RequestOptions cache fix
2. `src/lib/api/adapters/base.adapter.ts` - cacheTime usage
3. `src/lib/api/adapters/entity.adapter.ts` - Character type properties
4. `src/lib/api/adapters/index.ts` - New file for exports
5. `src/lib/api/resources/users.ts` - Updated to use userAdapter
6. `src/lib/types/declarations.d.ts` - wx-svelte-grid and $env type shims
7. `src/lib/types/api/entities.ts` - Character type properties
8. `src/lib/components/UserSettingsModal.svelte` - Multiple fixes
9. `src/lib/components/panels/SearchSidebar.svelte` - Accepted upstream version
10. `src/lib/components/party/Party.svelte` - granblueId fix
11. `src/routes/teams/new/+page.svelte` - Accepted upstream version
## Commands to Verify Progress
```bash
# Count remaining errors
pnpm check 2>&1 | grep -c "Error:"
# Analyze error patterns
pnpm check 2>&1 | grep "Error:" | sort | uniq -c | sort -rn | head -20
# Run lint
pnpm lint
# Run build
pnpm build
```
## Next Steps
1. Fix the 'firstItem'/'item' possibly undefined errors in teams/new/+page.svelte
2. Add missing Paraglide translation keys
3. Update PartyCtx type to include openPicker
4. Update Summon type in entity.adapter.ts to include hp/atk
5. Fix exactOptionalPropertyTypes violations
6. Fix bits-ui component usage (Select.ItemIndicator, DropdownItem asChild)
7. Run `pnpm check` to verify all errors are resolved
8. Run `pnpm lint` and `pnpm build`
9. Create PR with all fixes

View file

@ -96,8 +96,8 @@ export abstract class BaseAdapter {
// Generate a unique ID for this request (used for cancellation and caching) // Generate a unique ID for this request (used for cancellation and caching)
const requestId = this.generateRequestId(path, options.method, options.body as string) const requestId = this.generateRequestId(path, options.method, options.body as string)
// Check cache first if caching is enabled (support both cache and cacheTTL) // Check cache first if caching is enabled (support both cacheTime and cacheTTL)
const cacheTime = options.cacheTTL ?? options.cache ?? this.options.cacheTime const cacheTime = options.cacheTTL ?? options.cacheTime ?? this.options.cacheTime
// Allow caching for any method if explicitly set (unless cache is disabled) // Allow caching for any method if explicitly set (unless cache is disabled)
if (!this.disableCache && cacheTime > 0) { if (!this.disableCache && cacheTime > 0) {
const cached = this.getFromCache(requestId) const cached = this.getFromCache(requestId)

View file

@ -56,19 +56,27 @@ export interface Character {
} }
rarity: number rarity: number
element: number element: number
gender?: number
proficiency?: number[]
proficiency1?: number proficiency1?: number
proficiency2?: number proficiency2?: number
series?: number series?: number
race?: number[]
hp?: {
minHp?: number minHp?: number
maxHp?: number maxHp?: number
minAttack?: number maxHpFlb?: number
maxAttack?: number }
flbHp?: number atk?: {
flbAttack?: number minAtk?: number
ulbHp?: number maxAtk?: number
ulbAttack?: number maxAtkFlb?: number
transcendenceHp?: number }
transcendenceAttack?: number uncap?: {
flb?: boolean
ulb?: boolean
transcendence?: boolean
}
special?: boolean special?: boolean
seasonalId?: string seasonalId?: string
awakenings?: Array<{ awakenings?: Array<{

View file

@ -0,0 +1,12 @@
// Re-export all adapters and types
export { BaseAdapter } from './base.adapter'
export { EntityAdapter, entityAdapter } from './entity.adapter'
export type { Character, Weapon, Summon } from './entity.adapter'
export { GridAdapter, gridAdapter } from './grid.adapter'
export { JobAdapter, jobAdapter } from './job.adapter'
export { PartyAdapter, partyAdapter } from './party.adapter'
export { SearchAdapter, searchAdapter } from './search.adapter'
export { UserAdapter, userAdapter } from './user.adapter'
export { DEFAULT_ADAPTER_CONFIG } from './config'
export * from './types'
export * from './errors'

View file

@ -32,7 +32,7 @@ export interface AdapterOptions {
* Options for individual HTTP requests * Options for individual HTTP requests
* Extends the standard RequestInit interface with additional features * Extends the standard RequestInit interface with additional features
*/ */
export interface RequestOptions extends Omit<RequestInit, 'body'> { export interface RequestOptions extends Omit<RequestInit, 'body' | 'cache'> {
/** Query parameters to append to the URL */ /** Query parameters to append to the URL */
params?: Record<string, any> params?: Record<string, any>

View file

@ -1,5 +1,4 @@
import type { FetchLike } from '../core' import { userAdapter } from '../adapters/user.adapter'
import { put } from '../core'
export interface UserUpdateParams { export interface UserUpdateParams {
picture?: string picture?: string
@ -26,6 +25,16 @@ export const users = {
/** /**
* Update user settings * Update user settings
*/ */
update: (fetch: FetchLike, userId: string, params: UserUpdateParams) => update: async (userId: string, params: UserUpdateParams): Promise<UserResponse> => {
put<UserResponse>(fetch, `/users/${userId}`, { user: params }) const result = await userAdapter.updateProfile(params)
return {
id: result.id,
username: result.username,
avatar: result.avatar,
gender: result.gender,
language: result.language,
theme: result.theme,
role: result.role
}
}
} }

View file

@ -3,14 +3,13 @@
<script lang="ts"> <script lang="ts">
import Dialog from './ui/Dialog.svelte' import Dialog from './ui/Dialog.svelte'
import Select from './ui/Select.svelte' import Select from './ui/Select.svelte'
import Switch from './ui/switch/switch.svelte' import Switch from './ui/switch/Switch.svelte'
import Button from './ui/Button.svelte' import Button from './ui/Button.svelte'
import { pictureData, type Picture } from '$lib/utils/pictureData' import { pictureData, type Picture } from '$lib/utils/pictureData'
import { users } from '$lib/api/resources/users' import { users } from '$lib/api/resources/users'
import type { UserCookie } from '$lib/types/UserCookie' import type { UserCookie } from '$lib/types/UserCookie'
import { setUserCookie } from '$lib/auth/cookies' import { setUserCookie } from '$lib/auth/cookies'
import { invalidateAll } from '$app/navigation' import { invalidateAll } from '$app/navigation'
import type { Snippet } from 'svelte'
interface Props { interface Props {
open: boolean open: boolean
@ -83,7 +82,7 @@
} }
// Call API to update user settings // Call API to update user settings
const response = await users.update(fetch, userId, updateData) const response = await users.update(userId, updateData)
// Update the user cookie // Update the user cookie
const updatedUser: UserCookie = { const updatedUser: UserCookie = {
@ -130,10 +129,6 @@
onOpenChange?.(false) onOpenChange?.(false)
} }
// Footer snippet for the dialog
const footer: Snippet = {
render: () => ({})
}
</script> </script>
<Dialog bind:open {onOpenChange} title="@{username}" description="Account Settings"> <Dialog bind:open {onOpenChange} title="@{username}" description="Account Settings">

View file

@ -1,10 +1,8 @@
<svelte:options runes={true} /> <svelte:options runes={true} />
<script lang="ts"> <script lang="ts">
import type { SearchResult } from '$lib/api/resources/search' import { onMount } from 'svelte'
import { createInfiniteQuery } from '@tanstack/svelte-query' import { searchAdapter, type SearchResult } from '$lib/api/adapters'
import { searchQueries, type SearchFilters } from '$lib/api/queries/search.queries'
import InfiniteScrollQuery from '$lib/components/InfiniteScrollQuery.svelte'
interface Props { interface Props {
open?: boolean open?: boolean
@ -22,34 +20,13 @@
canAddMore = true canAddMore = true
}: Props = $props() }: Props = $props()
// Search state - simple reactive values with Svelte 5 runes // Search state
let searchQuery = $state('') let searchQuery = $state('')
let debouncedQuery = $state('') let searchResults = $state<SearchResult[]>([])
let debounceTimer: ReturnType<typeof setTimeout> | undefined let isLoading = $state(false)
let currentPage = $state(1)
// Debounce the search query using $effect let totalPages = $state(1)
$effect(() => { let hasInitialLoad = $state(false)
const query = searchQuery
if (debounceTimer) {
clearTimeout(debounceTimer)
}
// Skip single character searches
if (query.length === 1) {
return
}
debounceTimer = setTimeout(() => {
debouncedQuery = query
}, 300)
return () => {
if (debounceTimer) {
clearTimeout(debounceTimer)
}
}
})
// Filter state // Filter state
let elementFilters = $state<number[]>([]) let elementFilters = $state<number[]>([])
@ -58,6 +35,7 @@
// Refs // Refs
let searchInput: HTMLInputElement let searchInput: HTMLInputElement
let resultsContainer: HTMLDivElement
// Constants // Constants
const elements = [ const elements = [
@ -89,44 +67,74 @@
{ value: 10, label: 'Katana' } { value: 10, label: 'Katana' }
] ]
// Build filters object reactively using $derived // Focus search input and load recent items when opened
const currentFilters = $derived<SearchFilters>({
element: elementFilters.length > 0 ? elementFilters : undefined,
rarity: rarityFilters.length > 0 ? rarityFilters : undefined,
proficiency: type === 'weapon' && proficiencyFilters.length > 0 ? proficiencyFilters : undefined,
proficiency2: type === 'character' && proficiencyFilters.length > 0 ? proficiencyFilters : undefined
})
// TanStack Query v6 - Use query options pattern for type safety and reusability
// The thunk (function) wrapper is required for Svelte 5 runes reactivity
const searchQueryResult = createInfiniteQuery(() => {
// Select the appropriate query options based on type
const baseOptions = (() => {
switch (type) {
case 'weapon':
return searchQueries.weapons(debouncedQuery, currentFilters)
case 'character':
return searchQueries.characters(debouncedQuery, currentFilters)
case 'summon':
return searchQueries.summons(debouncedQuery, currentFilters)
}
})()
// Merge with component-specific options (like enabled)
return {
...baseOptions,
enabled: open // Only fetch when sidebar is open
}
})
// Focus search input when opened
$effect(() => { $effect(() => {
if (open && searchInput) { if (open && searchInput) {
searchInput.focus() searchInput.focus()
} }
// Load recent items when opening if we haven't already
if (open && !hasInitialLoad) {
hasInitialLoad = true
performSearch()
}
}) })
// Search when query or filters change
$effect(() => {
// Always search if we have filters or a search query
// If no query and no filters, still search to show recent items
if (searchQuery.length >= 2 || elementFilters.length > 0 || rarityFilters.length > 0 || proficiencyFilters.length > 0) {
performSearch()
} else if (searchQuery.length === 1) {
// Don't search with just 1 character
return
} else if (searchQuery.length === 0 && elementFilters.length === 0 && rarityFilters.length === 0 && proficiencyFilters.length === 0) {
// Load recent items when no search criteria
if (hasInitialLoad) {
performSearch()
}
}
})
async function performSearch() {
isLoading = true
try {
const params = {
query: searchQuery || undefined, // Don't send empty string
page: currentPage,
filters: {
element: elementFilters.length > 0 ? elementFilters : undefined,
rarity: rarityFilters.length > 0 ? rarityFilters : undefined,
proficiency1: type === 'weapon' && proficiencyFilters.length > 0 ? proficiencyFilters : undefined
}
}
let response
switch (type) {
case 'weapon':
response = await searchAdapter.searchWeapons(params)
break
case 'character':
response = await searchAdapter.searchCharacters(params)
break
case 'summon':
response = await searchAdapter.searchSummons(params)
break
}
searchResults = response?.results ?? []
totalPages = response?.totalPages ?? 1
} catch (error) {
console.error('Search failed:', error)
searchResults = []
} finally {
isLoading = false
}
}
function handleItemClick(item: SearchResult) { function handleItemClick(item: SearchResult) {
// Only add if we can add more items
if (canAddMore) { if (canAddMore) {
onAddItems([item]) onAddItems([item])
} }
@ -134,7 +142,7 @@
function toggleElementFilter(element: number) { function toggleElementFilter(element: number) {
if (elementFilters.includes(element)) { if (elementFilters.includes(element)) {
elementFilters = elementFilters.filter((e) => e !== element) elementFilters = elementFilters.filter(e => e !== element)
} else { } else {
elementFilters = [...elementFilters, element] elementFilters = [...elementFilters, element]
} }
@ -142,7 +150,7 @@
function toggleRarityFilter(rarity: number) { function toggleRarityFilter(rarity: number) {
if (rarityFilters.includes(rarity)) { if (rarityFilters.includes(rarity)) {
rarityFilters = rarityFilters.filter((r) => r !== rarity) rarityFilters = rarityFilters.filter(r => r !== rarity)
} else { } else {
rarityFilters = [...rarityFilters, rarity] rarityFilters = [...rarityFilters, rarity]
} }
@ -150,7 +158,7 @@
function toggleProficiencyFilter(prof: number) { function toggleProficiencyFilter(prof: number) {
if (proficiencyFilters.includes(prof)) { if (proficiencyFilters.includes(prof)) {
proficiencyFilters = proficiencyFilters.filter((p) => p !== prof) proficiencyFilters = proficiencyFilters.filter(p => p !== prof)
} else { } else {
proficiencyFilters = [...proficiencyFilters, prof] proficiencyFilters = [...proficiencyFilters, prof]
} }
@ -178,14 +186,14 @@
<aside <aside
class="sidebar" class="sidebar"
class:open class:open={open}
aria-hidden={!open} aria-hidden={!open}
aria-label="Search {type}s" aria-label="Search {type}s"
onkeydown={handleKeyDown} on:keydown={handleKeyDown}
> >
<header class="sidebar-header"> <header class="sidebar-header">
<h2>Search {type}s</h2> <h2>Search {type}s</h2>
<button class="close-btn" onclick={onClose} aria-label="Close">×</button> <button class="close-btn" on:click={onClose} aria-label="Close">×</button>
</header> </header>
<div class="search-section"> <div class="search-section">
@ -209,7 +217,7 @@
class="filter-btn element-btn" class="filter-btn element-btn"
class:active={elementFilters.includes(element.value)} class:active={elementFilters.includes(element.value)}
style="--element-color: {element.color}" style="--element-color: {element.color}"
onclick={() => toggleElementFilter(element.value)} on:click={() => toggleElementFilter(element.value)}
aria-pressed={elementFilters.includes(element.value)} aria-pressed={elementFilters.includes(element.value)}
> >
{element.label} {element.label}
@ -226,7 +234,7 @@
<button <button
class="filter-btn rarity-btn" class="filter-btn rarity-btn"
class:active={rarityFilters.includes(rarity.value)} class:active={rarityFilters.includes(rarity.value)}
onclick={() => toggleRarityFilter(rarity.value)} on:click={() => toggleRarityFilter(rarity.value)}
aria-pressed={rarityFilters.includes(rarity.value)} aria-pressed={rarityFilters.includes(rarity.value)}
> >
{rarity.label} {rarity.label}
@ -244,7 +252,7 @@
<button <button
class="filter-btn prof-btn" class="filter-btn prof-btn"
class:active={proficiencyFilters.includes(prof.value)} class:active={proficiencyFilters.includes(prof.value)}
onclick={() => toggleProficiencyFilter(prof.value)} on:click={() => toggleProficiencyFilter(prof.value)}
aria-pressed={proficiencyFilters.includes(prof.value)} aria-pressed={proficiencyFilters.includes(prof.value)}
> >
{prof.label} {prof.label}
@ -256,53 +264,63 @@
</div> </div>
<!-- Results --> <!-- Results -->
<div class="results-section"> <div class="results-section" bind:this={resultsContainer}>
<InfiniteScrollQuery query={searchQueryResult} threshold={300}> {#if isLoading}
{#snippet children(resultItems)} <div class="loading">Searching...</div>
{#if resultItems.length > 0} {:else if searchResults.length > 0}
<ul class="results-list"> <ul class="results-list">
{#each resultItems as item (item.id)} {#each searchResults as item (item.id)}
<li class="result-item"> <li class="result-item">
<button <button
class="result-button" class="result-button"
class:disabled={!canAddMore} class:disabled={!canAddMore}
onclick={() => handleItemClick(item)} on:click={() => handleItemClick(item)}
aria-label="{canAddMore ? 'Add' : 'Grid full - cannot add'} {getItemName(item)}" aria-label="{canAddMore ? 'Add' : 'Grid full - cannot add'} {getItemName(item)}"
disabled={!canAddMore} disabled={!canAddMore}
> >
<img src={getImageUrl(item)} alt={getItemName(item)} class="result-image" /> <img
src={getImageUrl(item)}
alt={getItemName(item)}
class="result-image"
/>
<span class="result-name">{getItemName(item)}</span> <span class="result-name">{getItemName(item)}</span>
{#if item.element !== undefined} {#if item.element !== undefined}
<span <span
class="result-element" class="result-element"
style="color: {elements.find((e) => e.value === item.element)?.color}" style="color: {elements.find(e => e.value === item.element)?.color}"
> >
{elements.find((e) => e.value === item.element)?.label} {elements.find(e => e.value === item.element)?.label}
</span> </span>
{/if} {/if}
</button> </button>
</li> </li>
{/each} {/each}
</ul> </ul>
{#if totalPages > 1}
<div class="pagination">
<button
on:click={() => currentPage = Math.max(1, currentPage - 1)}
disabled={currentPage === 1}
>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button
on:click={() => currentPage = Math.min(totalPages, currentPage + 1)}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
{/if} {/if}
{/snippet} {:else if searchQuery.length > 0}
{#snippet loadingSnippet()}
<div class="loading">Searching...</div>
{/snippet}
{#snippet emptySnippet()}
<div class="no-results">No results found</div> <div class="no-results">No results found</div>
{/snippet} {:else if !hasInitialLoad}
<div class="empty-state">Loading recent items...</div>
{#snippet loadingMoreSnippet()} {:else}
<div class="loading-more">Loading more...</div> <div class="no-results">No items found</div>
{/snippet} {/if}
{#snippet endSnippet()}
<div class="end-of-results">All results loaded</div>
{/snippet}
</InfiniteScrollQuery>
</div> </div>
</aside> </aside>
@ -313,7 +331,7 @@
background: var(--app-bg, #fff); background: var(--app-bg, #fff);
display: none; display: none;
flex-direction: column; flex-direction: column;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); box-shadow: -2px 0 8px rgba(0,0,0,0.1);
border-left: 1px solid #e0e0e0; border-left: 1px solid #e0e0e0;
position: sticky; position: sticky;
top: 0; top: 0;
@ -352,7 +370,7 @@
border-radius: 4px; border-radius: 4px;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.05); background: rgba(0,0,0,0.05);
} }
} }
} }
@ -437,14 +455,13 @@
} }
} }
.results-section { .results-section {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 12px; padding: 12px;
.loading, .loading, .no-results, .empty-state {
.no-results,
.empty-state {
text-align: center; text-align: center;
padding: 24px; padding: 24px;
color: #666; color: #666;
@ -475,7 +492,7 @@
&:hover { &:hover {
background: #f5f5f5; background: #f5f5f5;
border-color: #3366ff; border-color: #3366ff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
} }
&:active:not(:disabled) { &:active:not(:disabled) {
@ -519,18 +536,37 @@
} }
} }
.loading-more { .pagination {
text-align: center; display: flex;
padding: 16px; justify-content: center;
color: #666; align-items: center;
font-size: 14px; gap: 12px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #e0e0e0;
button {
padding: 4px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
&:hover:not(:disabled) {
background: #f0f0f0;
} }
.end-of-results { &:disabled {
text-align: center; opacity: 0.5;
padding: 16px; cursor: not-allowed;
color: #999; }
}
span {
font-size: 13px; font-size: 13px;
color: #666;
}
} }
} }
</style> </style>

View file

@ -10,7 +10,7 @@
import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte' import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte'
import { openSearchSidebar } from '$lib/features/search/openSearchSidebar.svelte' import { openSearchSidebar } from '$lib/features/search/openSearchSidebar.svelte'
import PartySegmentedControl from '$lib/components/party/PartySegmentedControl.svelte' import PartySegmentedControl from '$lib/components/party/PartySegmentedControl.svelte'
import type { SearchResult } from '$lib/api/resources/search' import type { SearchResult } from '$lib/api/adapters'
import { GridType } from '$lib/types/enums' import { GridType } from '$lib/types/enums'
import Dialog from '$lib/components/ui/Dialog.svelte' import Dialog from '$lib/components/ui/Dialog.svelte'
import Button from '$lib/components/ui/Button.svelte' import Button from '$lib/components/ui/Button.svelte'
@ -562,8 +562,8 @@
let targetSlot = selectedSlot let targetSlot = selectedSlot
// Call appropriate grid service method based on current tab // Call appropriate grid service method based on current tab
// Use granblue_id (snake_case) as that's what the search API returns // Use granblueId (camelCase) as that's what the SearchResult type uses
const itemId = item.granblue_id || item.granblueId const itemId = item.granblueId
if (activeTab === GridType.Weapon) { if (activeTab === GridType.Weapon) {
await gridService.addWeapon(party.id, itemId, targetSlot, editKey || undefined, { await gridService.addWeapon(party.id, itemId, targetSlot, editKey || undefined, {
mainhand: targetSlot === -1, mainhand: targetSlot === -1,

View file

@ -53,6 +53,22 @@ export interface Character {
} }
special: boolean special: boolean
recruits: string | null recruits: string | null
gender: number
race: {
race1: number
race2: number
}
proficiency: number[]
hp: {
minHp: number
maxHp: number
maxHpFlb: number
}
atk: {
minAtk: number
maxAtk: number
maxAtkFlb: number
}
} }
// Summon entity from SummonBlueprint // Summon entity from SummonBlueprint

View file

@ -5,3 +5,53 @@ declare module '*.svg' {
const SVG: React.FunctionComponent<React.SVGProps<SVGSVGElement>> const SVG: React.FunctionComponent<React.SVGProps<SVGSVGElement>>
export default SVG export default SVG
} }
// SvelteKit environment variables type declarations
// These are populated from .env files at build time
declare module '$env/static/public' {
export const PUBLIC_SIERO_API_URL: string
}
// wx-svelte-grid type declarations
declare module 'wx-svelte-grid' {
import type { SvelteComponent } from 'svelte'
export interface IColumn {
id: string
header?: string
width?: number
flexgrow?: number
sort?: boolean
cell?: any
template?: any
[key: string]: any
}
export interface IRow {
id: string | number
[key: string]: any
}
export interface ICellProps {
row: IRow
col: IColumn
value: any
}
export interface IDataConfig {
data: IRow[]
columns: IColumn[]
}
export class Grid extends SvelteComponent<{
data?: IRow[]
columns?: IColumn[]
[key: string]: any
}> {}
export class RestDataProvider<T = any> {
constructor(url: string, options?: any)
getData(): Promise<T[]>
[key: string]: any
}
}

View file

@ -4,19 +4,20 @@
import WeaponGrid from '$lib/components/grids/WeaponGrid.svelte' import WeaponGrid from '$lib/components/grids/WeaponGrid.svelte'
import SummonGrid from '$lib/components/grids/SummonGrid.svelte' import SummonGrid from '$lib/components/grids/SummonGrid.svelte'
import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte' import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte'
import { import { openSearchSidebar, closeSearchSidebar } from '$lib/features/search/openSearchSidebar.svelte'
openSearchSidebar,
closeSearchSidebar
} from '$lib/features/search/openSearchSidebar.svelte'
import PartySegmentedControl from '$lib/components/party/PartySegmentedControl.svelte' import PartySegmentedControl from '$lib/components/party/PartySegmentedControl.svelte'
import { GridType } from '$lib/types/enums' import { GridType } from '$lib/types/enums'
import { setContext } from 'svelte' import { setContext } from 'svelte'
import type { SearchResult } from '$lib/api/resources/search' import type { SearchResult } from '$lib/api/adapters'
import { apiClient } from '$lib/api/client' import { partyAdapter, gridAdapter } from '$lib/api/adapters'
import { PartyService } from '$lib/services/party.service'
import { Dialog } from 'bits-ui' import { Dialog } from 'bits-ui'
import { replaceState } from '$app/navigation' import { replaceState } from '$app/navigation'
import { page } from '$app/stores' import { page } from '$app/stores'
// Initialize party service for local ID management
const partyService = new PartyService()
// Get authentication status from page store // Get authentication status from page store
const isAuthenticated = $derived($page.data?.isAuthenticated ?? false) const isAuthenticated = $derived($page.data?.isAuthenticated ?? false)
const currentUser = $derived($page.data?.currentUser) const currentUser = $derived($page.data?.currentUser)
@ -44,12 +45,9 @@
activeTab = gridType activeTab = gridType
// Open sidebar when switching tabs // Open sidebar when switching tabs
openSearchSidebar({ openSearchSidebar({
type: type: gridType === GridType.Weapon ? 'weapon' :
gridType === GridType.Weapon gridType === GridType.Summon ? 'summon' :
? 'weapon' 'character',
: gridType === GridType.Summon
? 'summon'
: 'character',
onAddItems: handleAddItems, onAddItems: handleAddItems,
canAddMore: !isGridFull(gridType) canAddMore: !isGridFull(gridType)
}) })
@ -80,17 +78,16 @@
let errorMessage = $state('') let errorMessage = $state('')
let errorDetails = $state<string[]>([]) let errorDetails = $state<string[]>([])
// Calculate if grids are full // Calculate if grids are full
let isWeaponGridFull = $derived(weapons.length >= 10) // 1 mainhand + 9 grid slots let isWeaponGridFull = $derived(weapons.length >= 10) // 1 mainhand + 9 grid slots
let isSummonGridFull = $derived(summons.length >= 6) // 6 summon slots (main + 4 grid + friend) let isSummonGridFull = $derived(summons.length >= 6) // 6 summon slots (main + 4 grid + friend)
let isCharacterGridFull = $derived(characters.length >= 5) // 5 character slots let isCharacterGridFull = $derived(characters.length >= 5) // 5 character slots
let canAddMore = $derived( let canAddMore = $derived(
activeTab === GridType.Weapon activeTab === GridType.Weapon ? !isWeaponGridFull :
? !isWeaponGridFull activeTab === GridType.Summon ? !isSummonGridFull :
: activeTab === GridType.Summon !isCharacterGridFull
? !isSummonGridFull
: !isCharacterGridFull
) )
// Handle adding items from search // Handle adding items from search
@ -112,66 +109,58 @@
// Only include localId for anonymous users // Only include localId for anonymous users
if (!isAuthenticated) { if (!isAuthenticated) {
const localId = apiClient.getLocalId() const localId = partyService.getLocalId()
partyPayload.localId = localId partyPayload.localId = localId
} }
// Create party using the API client // Create party using the party adapter
const result = await apiClient.createParty(partyPayload) const createdParty = await partyAdapter.create(partyPayload)
console.log('Party created:', result) console.log('Party created:', createdParty)
// The response has { party, editKey? } // The adapter returns the party directly
const response = result.party partyId = createdParty.id
partyId = response.id shortcode = createdParty.shortcode
shortcode = response.shortcode
if (!partyId || !shortcode) { if (!partyId || !shortcode) {
throw new Error('Party creation did not return ID or shortcode') throw new Error('Party creation did not return ID or shortcode')
} }
// Store edit key if present (handled by API client)
if (result.editKey) {
editKey = result.editKey
}
// Step 2: Add the first item to the party // Step 2: Add the first item to the party
let position = selectedSlot !== null ? selectedSlot : -1 // Use selectedSlot if available let position = selectedSlot !== null ? selectedSlot : -1 // Use selectedSlot if available
let itemAdded = false let itemAdded = false
try { try {
console.log('Adding item to party:', { console.log('Adding item to party:', { partyId, itemId: firstItem.id, type: activeTab, position })
partyId,
itemId: firstItem.id,
type: activeTab,
position
})
if (activeTab === GridType.Weapon) { if (activeTab === GridType.Weapon) {
// Use selectedSlot if available, otherwise default to mainhand // Use selectedSlot if available, otherwise default to mainhand
if (selectedSlot === null) position = -1 if (selectedSlot === null) position = -1
const addResult = await apiClient.addWeapon(partyId, firstItem.granblue_id, position, { const addResult = await gridAdapter.createWeapon({
partyId,
weaponId: firstItem.granblueId,
position,
mainhand: position === -1 mainhand: position === -1
}) })
console.log('Weapon added:', addResult) console.log('Weapon added:', addResult)
itemAdded = true itemAdded = true
// Update local state with the added weapon // Update local state with the added weapon
weapons = [ weapons = [...weapons, {
...weapons, id: addResult.id || `temp-${Date.now()}`,
{
id: addResult.grid_weapon?.id || `temp-${Date.now()}`,
position, position,
object: { object: {
granblueId: firstItem.granblue_id, granblueId: firstItem.granblueId,
name: firstItem.name, name: firstItem.name,
element: firstItem.element element: firstItem.element
}, },
mainhand: position === -1 mainhand: position === -1
} }]
]
} else if (activeTab === GridType.Summon) { } else if (activeTab === GridType.Summon) {
// Use selectedSlot if available, otherwise default to main summon // Use selectedSlot if available, otherwise default to main summon
if (selectedSlot === null) position = -1 if (selectedSlot === null) position = -1
const addResult = await apiClient.addSummon(partyId, firstItem.granblue_id, position, { const addResult = await gridAdapter.createSummon({
partyId,
summonId: firstItem.granblueId,
position,
main: position === -1, main: position === -1,
friend: position === 6 friend: position === 6
}) })
@ -179,45 +168,38 @@
itemAdded = true itemAdded = true
// Update local state with the added summon // Update local state with the added summon
summons = [ summons = [...summons, {
...summons, id: addResult.id || `temp-${Date.now()}`,
{
id: addResult.grid_summon?.id || `temp-${Date.now()}`,
position, position,
object: { object: {
granblueId: firstItem.granblue_id, granblueId: firstItem.granblueId,
name: firstItem.name, name: firstItem.name,
element: firstItem.element element: firstItem.element
}, },
main: position === -1, main: position === -1,
friend: position === 6 friend: position === 6
} }]
]
} else if (activeTab === GridType.Character) { } else if (activeTab === GridType.Character) {
// Use selectedSlot if available, otherwise default to first slot // Use selectedSlot if available, otherwise default to first slot
if (selectedSlot === null) position = 0 if (selectedSlot === null) position = 0
const addResult = await apiClient.addCharacter( const addResult = await gridAdapter.createCharacter({
partyId, partyId,
firstItem.granblue_id, characterId: firstItem.granblueId,
position, position
{} })
)
console.log('Character added:', addResult) console.log('Character added:', addResult)
itemAdded = true itemAdded = true
// Update local state with the added character // Update local state with the added character
characters = [ characters = [...characters, {
...characters, id: addResult.id || `temp-${Date.now()}`,
{
id: addResult.grid_character?.id || `temp-${Date.now()}`,
position, position,
object: { object: {
granblueId: firstItem.granblue_id, granblueId: firstItem.granblueId,
name: firstItem.name, name: firstItem.name,
element: firstItem.element element: firstItem.element
} }
} }]
]
} }
selectedSlot = null // Reset after using selectedSlot = null // Reset after using
@ -264,8 +246,7 @@
} else if (error.errors && typeof error.errors === 'object') { } else if (error.errors && typeof error.errors === 'object') {
// Rails-style validation errors // Rails-style validation errors
errorDetails = Object.entries(error.errors).map( errorDetails = Object.entries(error.errors).map(
([field, messages]) => ([field, messages]) => `${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`
`${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`
) )
} else { } else {
errorDetails = [] errorDetails = []
@ -285,123 +266,100 @@
if (activeTab === GridType.Weapon) { if (activeTab === GridType.Weapon) {
// Use selectedSlot for first item if available // Use selectedSlot for first item if available
if ( if (i === 0 && selectedSlot !== null && !weapons.find(w => w.position === selectedSlot)) {
i === 0 &&
selectedSlot !== null &&
!weapons.find((w) => w.position === selectedSlot)
) {
position = selectedSlot position = selectedSlot
selectedSlot = null // Reset after using selectedSlot = null // Reset after using
} else { } else {
// Find next empty weapon slot // Find next empty weapon slot
const emptySlots = Array.from({ length: 10 }, (_, i) => i - 1).filter( const emptySlots = Array.from({ length: 10 }, (_, i) => i - 1)
(i) => !weapons.find((w) => w.position === i) .filter(i => !weapons.find(w => w.position === i))
)
if (emptySlots.length === 0) return // Grid full if (emptySlots.length === 0) return // Grid full
position = emptySlots[0] position = emptySlots[0]
} }
// Add weapon via API // Add weapon via API
const response = await apiClient.addWeapon( const response = await gridAdapter.createWeapon({
partyId, partyId,
item.granblue_id, // Use granblue_id weaponId: item.granblueId,
position, position,
{ mainhand: position === -1 } mainhand: position === -1
) })
// Add to local state // Add to local state
weapons = [ weapons = [...weapons, {
...weapons, id: response.id || `temp-${Date.now()}`,
{
id: response.grid_weapon?.id || `temp-${Date.now()}`,
position, position,
object: { object: {
granblueId: item.granblue_id, granblueId: item.granblueId,
name: item.name, name: item.name,
element: item.element element: item.element
}, },
mainhand: position === -1 mainhand: position === -1
} }]
]
} else if (activeTab === GridType.Summon) { } else if (activeTab === GridType.Summon) {
// Use selectedSlot for first item if available // Use selectedSlot for first item if available
if ( if (i === 0 && selectedSlot !== null && !summons.find(s => s.position === selectedSlot)) {
i === 0 &&
selectedSlot !== null &&
!summons.find((s) => s.position === selectedSlot)
) {
position = selectedSlot position = selectedSlot
selectedSlot = null // Reset after using selectedSlot = null // Reset after using
} else { } else {
// Find next empty summon slot // Find next empty summon slot
const emptySlots = [-1, 0, 1, 2, 3, 6] // main, 4 grid slots, friend const emptySlots = [-1, 0, 1, 2, 3, 6] // main, 4 grid slots, friend
.filter((i) => !summons.find((s) => s.position === i)) .filter(i => !summons.find(s => s.position === i))
if (emptySlots.length === 0) return // Grid full if (emptySlots.length === 0) return // Grid full
position = emptySlots[0] position = emptySlots[0]
} }
// Add summon via API // Add summon via API
const response = await apiClient.addSummon( const response = await gridAdapter.createSummon({
partyId, partyId,
item.granblue_id, // Use granblue_id summonId: item.granblueId,
position, position,
{ main: position === -1, friend: position === 6 } main: position === -1,
) friend: position === 6
})
// Add to local state // Add to local state
summons = [ summons = [...summons, {
...summons, id: response.id || `temp-${Date.now()}`,
{
id: response.grid_summon?.id || `temp-${Date.now()}`,
position, position,
object: { object: {
granblueId: item.granblue_id, granblueId: item.granblueId,
name: item.name, name: item.name,
element: item.element element: item.element
}, },
main: position === -1, main: position === -1,
friend: position === 6 friend: position === 6
} }]
]
} else if (activeTab === GridType.Character) { } else if (activeTab === GridType.Character) {
// Use selectedSlot for first item if available // Use selectedSlot for first item if available
if ( if (i === 0 && selectedSlot !== null && !characters.find(c => c.position === selectedSlot)) {
i === 0 &&
selectedSlot !== null &&
!characters.find((c) => c.position === selectedSlot)
) {
position = selectedSlot position = selectedSlot
selectedSlot = null // Reset after using selectedSlot = null // Reset after using
} else { } else {
// Find next empty character slot // Find next empty character slot
const emptySlots = Array.from({ length: 5 }, (_, i) => i).filter( const emptySlots = Array.from({ length: 5 }, (_, i) => i)
(i) => !characters.find((c) => c.position === i) .filter(i => !characters.find(c => c.position === i))
)
if (emptySlots.length === 0) return // Grid full if (emptySlots.length === 0) return // Grid full
position = emptySlots[0] position = emptySlots[0]
} }
// Add character via API // Add character via API
const response = await apiClient.addCharacter( const response = await gridAdapter.createCharacter({
partyId, partyId,
item.granblue_id, // Use granblue_id characterId: item.granblueId,
position, position
{} })
)
// Add to local state // Add to local state
characters = [ characters = [...characters, {
...characters, id: response.id || `temp-${Date.now()}`,
{
id: response.grid_character?.id || `temp-${Date.now()}`,
position, position,
object: { object: {
granblueId: item.granblue_id, granblueId: item.granblueId,
name: item.name, name: item.name,
element: item.element element: item.element
} }
} }]
]
} }
} }
} catch (error: any) { } catch (error: any) {
@ -417,21 +375,17 @@
if (activeTab === GridType.Weapon) { if (activeTab === GridType.Weapon) {
// Add weapons to empty slots // Add weapons to empty slots
const emptySlots = Array.from({ length: 10 }, (_, i) => i - 1) // -1 for mainhand, 0-8 for grid const emptySlots = Array.from({ length: 10 }, (_, i) => i - 1) // -1 for mainhand, 0-8 for grid
.filter((i) => !weapons.find((w) => w.position === i)) .filter(i => !weapons.find(w => w.position === i))
items.forEach((item, index) => { items.forEach((item, index) => {
let position: number let position: number
// Use selectedSlot for first item if available // Use selectedSlot for first item if available
if ( if (index === 0 && selectedSlot !== null && !weapons.find(w => w.position === selectedSlot)) {
index === 0 &&
selectedSlot !== null &&
!weapons.find((w) => w.position === selectedSlot)
) {
position = selectedSlot position = selectedSlot
selectedSlot = null // Reset after using selectedSlot = null // Reset after using
} else { } else {
// Find next empty slot // Find next empty slot
const availableSlots = emptySlots.filter((s) => !weapons.find((w) => w.position === s)) const availableSlots = emptySlots.filter(s => !weapons.find(w => w.position === s))
if (availableSlots.length === 0) return if (availableSlots.length === 0) return
position = availableSlots[0] position = availableSlots[0]
} }
@ -440,7 +394,7 @@
id: `temp-${Date.now()}-${index}`, id: `temp-${Date.now()}-${index}`,
position, position,
object: { object: {
granblueId: item.granblue_id, granblueId: item.granblueId,
name: item.name, name: item.name,
element: item.element element: item.element
}, },
@ -453,77 +407,60 @@
} else if (activeTab === GridType.Summon) { } else if (activeTab === GridType.Summon) {
// Add summons to empty slots // Add summons to empty slots
const emptySlots = [-1, 0, 1, 2, 3, 6] // main, 4 grid slots, friend const emptySlots = [-1, 0, 1, 2, 3, 6] // main, 4 grid slots, friend
.filter((i) => !summons.find((s) => s.position === i)) .filter(i => !summons.find(s => s.position === i))
items.forEach((item, index) => { items.forEach((item, index) => {
let position: number let position: number
// Use selectedSlot for first item if available // Use selectedSlot for first item if available
if ( if (index === 0 && selectedSlot !== null && !summons.find(s => s.position === selectedSlot)) {
index === 0 &&
selectedSlot !== null &&
!summons.find((s) => s.position === selectedSlot)
) {
position = selectedSlot position = selectedSlot
selectedSlot = null // Reset after using selectedSlot = null // Reset after using
} else { } else {
// Find next empty slot // Find next empty slot
const availableSlots = emptySlots.filter( const availableSlots = emptySlots.filter(s => !summons.find(sum => sum.position === s))
(s) => !summons.find((sum) => sum.position === s)
)
if (availableSlots.length === 0) return if (availableSlots.length === 0) return
position = availableSlots[0] position = availableSlots[0]
} }
summons = [ summons = [...summons, {
...summons,
{
id: `temp-${Date.now()}-${index}`, id: `temp-${Date.now()}-${index}`,
position, position,
object: { object: {
granblueId: item.granblue_id, granblueId: item.granblueId,
name: item.name, name: item.name,
element: item.element element: item.element
}, },
main: position === -1, main: position === -1,
friend: position === 6 friend: position === 6
} }]
]
}) })
} else if (activeTab === GridType.Character) { } else if (activeTab === GridType.Character) {
// Add characters to empty slots // Add characters to empty slots
const emptySlots = Array.from({ length: 5 }, (_, i) => i).filter( const emptySlots = Array.from({ length: 5 }, (_, i) => i)
(i) => !characters.find((c) => c.position === i) .filter(i => !characters.find(c => c.position === i))
)
items.forEach((item, index) => { items.forEach((item, index) => {
let position: number let position: number
// Use selectedSlot for first item if available // Use selectedSlot for first item if available
if ( if (index === 0 && selectedSlot !== null && !characters.find(c => c.position === selectedSlot)) {
index === 0 &&
selectedSlot !== null &&
!characters.find((c) => c.position === selectedSlot)
) {
position = selectedSlot position = selectedSlot
selectedSlot = null // Reset after using selectedSlot = null // Reset after using
} else { } else {
// Find next empty slot // Find next empty slot
const availableSlots = emptySlots.filter((s) => !characters.find((c) => c.position === s)) const availableSlots = emptySlots.filter(s => !characters.find(c => c.position === s))
if (availableSlots.length === 0) return if (availableSlots.length === 0) return
position = availableSlots[0] position = availableSlots[0]
} }
characters = [ characters = [...characters, {
...characters,
{
id: `temp-${Date.now()}-${index}`, id: `temp-${Date.now()}-${index}`,
position, position,
object: { object: {
granblueId: item.granblue_id, granblueId: item.granblueId,
name: item.name, name: item.name,
element: item.element element: item.element
} }
} }]
]
}) })
} }
} }
@ -531,19 +468,19 @@
// Remove functions // Remove functions
function removeWeapon(itemId: string) { function removeWeapon(itemId: string) {
console.log('Removing weapon:', itemId) console.log('Removing weapon:', itemId)
weapons = weapons.filter((w) => w.id !== itemId) weapons = weapons.filter(w => w.id !== itemId)
return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters }) return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters })
} }
function removeSummon(itemId: string) { function removeSummon(itemId: string) {
console.log('Removing summon:', itemId) console.log('Removing summon:', itemId)
summons = summons.filter((s) => s.id !== itemId) summons = summons.filter(s => s.id !== itemId)
return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters }) return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters })
} }
function removeCharacter(itemId: string) { function removeCharacter(itemId: string) {
console.log('Removing character:', itemId) console.log('Removing character:', itemId)
characters = characters.filter((c) => c.id !== itemId) characters = characters.filter(c => c.id !== itemId)
return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters }) return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters })
} }
@ -562,36 +499,24 @@
removeWeapon: (partyId: string, itemId: string) => removeWeapon(itemId), removeWeapon: (partyId: string, itemId: string) => removeWeapon(itemId),
removeSummon: (partyId: string, itemId: string) => removeSummon(itemId), removeSummon: (partyId: string, itemId: string) => removeSummon(itemId),
removeCharacter: (partyId: string, itemId: string) => removeCharacter(itemId), removeCharacter: (partyId: string, itemId: string) => removeCharacter(itemId),
addWeapon: () => addWeapon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }), addSummon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
addSummon: () => addCharacter: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }), replaceWeapon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
addCharacter: () => replaceSummon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }), replaceCharacter: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } })
replaceWeapon: () =>
Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
replaceSummon: () =>
Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
replaceCharacter: () =>
Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } })
}, },
partyService: { getEditKey: () => null } partyService: { getEditKey: () => null }
}, },
openPicker: (opts: { openPicker: (opts: { type: 'weapon' | 'summon' | 'character'; position: number; item?: any }) => {
type: 'weapon' | 'summon' | 'character'
position: number
item?: any
}) => {
selectedSlot = opts.position selectedSlot = opts.position
openSearchSidebar({ openSearchSidebar({
type: opts.type, type: opts.type,
onAddItems: handleAddItems, onAddItems: handleAddItems,
canAddMore: !isGridFull( canAddMore: !isGridFull(
opts.type === 'weapon' opts.type === 'weapon' ? GridType.Weapon :
? GridType.Weapon opts.type === 'summon' ? GridType.Summon :
: opts.type === 'summon' GridType.Character
? GridType.Summon
: GridType.Character
) )
}) })
} }
@ -606,20 +531,13 @@
<h1>Create a new team</h1> <h1>Create a new team</h1>
<p class="description">Search and click items to add them to your grid</p> <p class="description">Search and click items to add them to your grid</p>
</div> </div>
<button <button class="toggle-sidebar" on:click={() => openSearchSidebar({
class="toggle-sidebar" type: activeTab === GridType.Weapon ? 'weapon' :
on:click={() => activeTab === GridType.Summon ? 'summon' :
openSearchSidebar({ 'character',
type:
activeTab === GridType.Weapon
? 'weapon'
: activeTab === GridType.Summon
? 'summon'
: 'character',
onAddItems: handleAddItems, onAddItems: handleAddItems,
canAddMore: !isGridFull(activeTab) canAddMore: !isGridFull(activeTab)
})} })}>
>
Open Search Open Search
</button> </button>
</header> </header>
@ -672,13 +590,20 @@
{/if} {/if}
<div class="dialog-actions"> <div class="dialog-actions">
<Dialog.Close class="dialog-button">OK</Dialog.Close> <Dialog.Close class="dialog-button">
OK
</Dialog.Close>
</div> </div>
</Dialog.Content> </Dialog.Content>
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>
<style> <style>
/* Override the main element's padding for this page */
:global(main) {
padding: 0 !important;
}
.page-container { .page-container {
display: flex; display: flex;
gap: 0; gap: 0;
@ -695,6 +620,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: start; align-items: start;
margin-bottom: 1rem;
} }
.party-info h1 { .party-info h1 {