Add sync from collection menu and highlight owned items in search
- Party.svelte: Add 'Sync from collection' to overflow menu for parties with linked collection items, with proper menu grouping - SearchContent.svelte: Highlight items user owns in 'All Items' mode with subtle green background and checkmark indicator
This commit is contained in:
parent
f5d0bbe7da
commit
0a2a3894bf
2 changed files with 87 additions and 7 deletions
|
|
@ -20,7 +20,8 @@
|
||||||
useUpdateSummonUncap,
|
useUpdateSummonUncap,
|
||||||
useSwapWeapons,
|
useSwapWeapons,
|
||||||
useSwapCharacters,
|
useSwapCharacters,
|
||||||
useSwapSummons
|
useSwapSummons,
|
||||||
|
useSyncAllPartyItems
|
||||||
} from '$lib/api/mutations/grid.mutations'
|
} from '$lib/api/mutations/grid.mutations'
|
||||||
|
|
||||||
// TanStack Query mutations - Party
|
// TanStack Query mutations - Party
|
||||||
|
|
@ -132,6 +133,7 @@
|
||||||
const swapWeapons = useSwapWeapons()
|
const swapWeapons = useSwapWeapons()
|
||||||
const swapCharacters = useSwapCharacters()
|
const swapCharacters = useSwapCharacters()
|
||||||
const swapSummons = useSwapSummons()
|
const swapSummons = useSwapSummons()
|
||||||
|
const syncAllItems = useSyncAllPartyItems()
|
||||||
|
|
||||||
// TanStack Query mutations - Party
|
// TanStack Query mutations - Party
|
||||||
const updatePartyMutation = useUpdateParty()
|
const updatePartyMutation = useUpdateParty()
|
||||||
|
|
@ -269,6 +271,17 @@
|
||||||
const mainWeaponElement = $derived(mainWeapon?.element ?? mainWeapon?.weapon?.element)
|
const mainWeaponElement = $derived(mainWeapon?.element ?? mainWeapon?.weapon?.element)
|
||||||
const partyElement = $derived((party as any)?.element)
|
const partyElement = $derived((party as any)?.element)
|
||||||
|
|
||||||
|
// Check if any items in the party are linked to collection (for sync menu option)
|
||||||
|
const hasCollectionLinks = $derived.by(() => {
|
||||||
|
const hasLinkedWeapons = (party?.weapons ?? []).some((w) => w?.collectionWeaponId)
|
||||||
|
const hasLinkedCharacters = (party?.characters ?? []).some((c) => c?.collectionCharacterId)
|
||||||
|
const hasLinkedSummons = (party?.summons ?? []).some((s) => s?.collectionSummonId)
|
||||||
|
return hasLinkedWeapons || hasLinkedCharacters || hasLinkedSummons
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if syncing is in progress
|
||||||
|
const isSyncingAll = $derived(syncAllItems.isPending)
|
||||||
|
|
||||||
function handleTabChange(tab: GridType) {
|
function handleTabChange(tab: GridType) {
|
||||||
activeTab = tab // Instant UI update
|
activeTab = tab // Instant UI update
|
||||||
|
|
||||||
|
|
@ -368,6 +381,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncFromCollection() {
|
||||||
|
if (!canEdit() || !hasCollectionLinks) return
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
await syncAllItems.mutateAsync({
|
||||||
|
partyId: party.id,
|
||||||
|
partyShortcode: party.shortcode
|
||||||
|
})
|
||||||
|
// Party will be updated via cache invalidation
|
||||||
|
} catch (err: any) {
|
||||||
|
error = err.message || 'Failed to sync from collection'
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let deleteDialogOpen = $state(false)
|
let deleteDialogOpen = $state(false)
|
||||||
let deleting = $state(false)
|
let deleting = $state(false)
|
||||||
|
|
||||||
|
|
@ -846,6 +878,14 @@
|
||||||
<DropdownItem>
|
<DropdownItem>
|
||||||
<button onclick={openEditDialog} disabled={loading}>Edit</button>
|
<button onclick={openEditDialog} disabled={loading}>Edit</button>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
{#if hasCollectionLinks}
|
||||||
|
<DropdownItem>
|
||||||
|
<button onclick={syncFromCollection} disabled={loading || isSyncingAll}>
|
||||||
|
{isSyncingAll ? 'Syncing...' : 'Sync from collection'}
|
||||||
|
</button>
|
||||||
|
</DropdownItem>
|
||||||
|
{/if}
|
||||||
|
<DropdownMenu.Separator class="dropdown-separator" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if authUserId}
|
{#if authUserId}
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,8 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Collection query - only enabled when in collection mode and authUserId is provided
|
// Collection query - enabled when authUserId is provided
|
||||||
|
// Used both for collection mode AND for highlighting owned items in "all" mode
|
||||||
// Type assertion needed because different types have different query result types
|
// Type assertion needed because different types have different query result types
|
||||||
// but they all share the same structure with different content types
|
// but they all share the same structure with different content types
|
||||||
const collectionQueryResult = createInfiniteQuery(() => {
|
const collectionQueryResult = createInfiniteQuery(() => {
|
||||||
|
|
@ -163,26 +164,27 @@
|
||||||
} as ReturnType<typeof collectionQueries.characters>
|
} as ReturnType<typeof collectionQueries.characters>
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentFilters = {
|
// For collection mode, apply filters; for "all" mode, fetch all to build owned set
|
||||||
|
const currentFilters = searchMode === 'collection' ? {
|
||||||
element: elementFilters.length > 0 ? elementFilters : undefined,
|
element: elementFilters.length > 0 ? elementFilters : undefined,
|
||||||
rarity: rarityFilters.length > 0 ? rarityFilters : undefined
|
rarity: rarityFilters.length > 0 ? rarityFilters : undefined
|
||||||
}
|
} : {}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'weapon':
|
case 'weapon':
|
||||||
return {
|
return {
|
||||||
...collectionQueries.weapons(authUserId, currentFilters),
|
...collectionQueries.weapons(authUserId, currentFilters),
|
||||||
enabled: searchMode === 'collection'
|
enabled: true // Always enabled when authUserId exists
|
||||||
} as unknown as ReturnType<typeof collectionQueries.characters>
|
} as unknown as ReturnType<typeof collectionQueries.characters>
|
||||||
case 'character':
|
case 'character':
|
||||||
return {
|
return {
|
||||||
...collectionQueries.characters(authUserId, currentFilters),
|
...collectionQueries.characters(authUserId, currentFilters),
|
||||||
enabled: searchMode === 'collection'
|
enabled: true
|
||||||
}
|
}
|
||||||
case 'summon':
|
case 'summon':
|
||||||
return {
|
return {
|
||||||
...collectionQueries.summons(authUserId, currentFilters),
|
...collectionQueries.summons(authUserId, currentFilters),
|
||||||
enabled: searchMode === 'collection'
|
enabled: true
|
||||||
} as unknown as ReturnType<typeof collectionQueries.characters>
|
} as unknown as ReturnType<typeof collectionQueries.characters>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -199,6 +201,25 @@
|
||||||
return filterCollectionByQuery(allItems, debouncedSearchQuery)
|
return filterCollectionByQuery(allItems, debouncedSearchQuery)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Set of owned item granblue IDs for fast lookup (used in "all" mode to highlight owned items)
|
||||||
|
const ownedItemIds = $derived.by(() => {
|
||||||
|
const pages = collectionQueryResult.data?.pages ?? []
|
||||||
|
const allItems = pages.flatMap((page) => page.results)
|
||||||
|
const ids = new Set<string>()
|
||||||
|
for (const item of allItems) {
|
||||||
|
const entity = 'character' in item ? item.character : 'weapon' in item ? item.weapon : item.summon
|
||||||
|
if (entity.granblueId) {
|
||||||
|
ids.add(String(entity.granblueId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
})
|
||||||
|
|
||||||
|
// Helper to check if an item is owned (in user's collection)
|
||||||
|
function isOwned(item: AddItemResult): boolean {
|
||||||
|
return ownedItemIds.has(String(item.granblueId))
|
||||||
|
}
|
||||||
|
|
||||||
// Deduplicate by id - needed because the API may return the same item across pages
|
// Deduplicate by id - needed because the API may return the same item across pages
|
||||||
// (e.g., due to items being added/removed between page fetches)
|
// (e.g., due to items being added/removed between page fetches)
|
||||||
const searchResults = $derived.by<AddItemResult[]>(() => {
|
const searchResults = $derived.by<AddItemResult[]>(() => {
|
||||||
|
|
@ -404,11 +425,13 @@
|
||||||
{:else if searchResults.length > 0}
|
{:else if searchResults.length > 0}
|
||||||
<ul class="results-list">
|
<ul class="results-list">
|
||||||
{#each searchResults as item (item.id)}
|
{#each searchResults as item (item.id)}
|
||||||
|
{@const owned = searchMode === 'all' && authUserId && isOwned(item)}
|
||||||
<li class="result-item">
|
<li class="result-item">
|
||||||
<button
|
<button
|
||||||
class="result-button"
|
class="result-button"
|
||||||
class:disabled={!canAddMore}
|
class:disabled={!canAddMore}
|
||||||
class:from-collection={item.collectionId}
|
class:from-collection={item.collectionId}
|
||||||
|
class:owned={owned}
|
||||||
onclick={() => handleItemClick(item)}
|
onclick={() => 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}
|
||||||
|
|
@ -422,6 +445,8 @@
|
||||||
<span class="result-name">{getItemName(item)}</span>
|
<span class="result-name">{getItemName(item)}</span>
|
||||||
{#if item.collectionId}
|
{#if item.collectionId}
|
||||||
<Icon name="bookmark" size={14} class="collection-indicator" />
|
<Icon name="bookmark" size={14} class="collection-indicator" />
|
||||||
|
{:else if owned}
|
||||||
|
<Icon name="check" size={14} class="owned-indicator" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.element !== undefined}
|
{#if item.element !== undefined}
|
||||||
<span
|
<span
|
||||||
|
|
@ -680,6 +705,21 @@
|
||||||
color: var(--accent-blue);
|
color: var(--accent-blue);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.owned-indicator) {
|
||||||
|
color: var(--success, #4caf50);
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtle highlight for owned items in "all" mode
|
||||||
|
.result-button.owned {
|
||||||
|
background: var(--owned-bg, rgba(76, 175, 80, 0.08));
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--owned-bg-hover, rgba(76, 175, 80, 0.15));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue