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:
parent
a208a3c1ea
commit
dfbb1e4e48
12 changed files with 1420 additions and 1221 deletions
147
CLEANUP_PLAN.md
Normal file
147
CLEANUP_PLAN.md
Normal 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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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<{
|
||||||
|
|
|
||||||
12
src/lib/api/adapters/index.ts
Normal file
12
src/lib/api/adapters/index.ts
Normal 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'
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
50
src/lib/types/declarations.d.ts
vendored
50
src/lib/types/declarations.d.ts
vendored
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue