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,
|
||||
useSwapWeapons,
|
||||
useSwapCharacters,
|
||||
useSwapSummons
|
||||
useSwapSummons,
|
||||
useSyncAllPartyItems
|
||||
} from '$lib/api/mutations/grid.mutations'
|
||||
|
||||
// TanStack Query mutations - Party
|
||||
|
|
@ -132,6 +133,7 @@
|
|||
const swapWeapons = useSwapWeapons()
|
||||
const swapCharacters = useSwapCharacters()
|
||||
const swapSummons = useSwapSummons()
|
||||
const syncAllItems = useSyncAllPartyItems()
|
||||
|
||||
// TanStack Query mutations - Party
|
||||
const updatePartyMutation = useUpdateParty()
|
||||
|
|
@ -269,6 +271,17 @@
|
|||
const mainWeaponElement = $derived(mainWeapon?.element ?? mainWeapon?.weapon?.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) {
|
||||
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 deleting = $state(false)
|
||||
|
||||
|
|
@ -846,6 +878,14 @@
|
|||
<DropdownItem>
|
||||
<button onclick={openEditDialog} disabled={loading}>Edit</button>
|
||||
</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 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
|
||||
// but they all share the same structure with different content types
|
||||
const collectionQueryResult = createInfiniteQuery(() => {
|
||||
|
|
@ -163,26 +164,27 @@
|
|||
} 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,
|
||||
rarity: rarityFilters.length > 0 ? rarityFilters : undefined
|
||||
}
|
||||
} : {}
|
||||
|
||||
switch (type) {
|
||||
case 'weapon':
|
||||
return {
|
||||
...collectionQueries.weapons(authUserId, currentFilters),
|
||||
enabled: searchMode === 'collection'
|
||||
enabled: true // Always enabled when authUserId exists
|
||||
} as unknown as ReturnType<typeof collectionQueries.characters>
|
||||
case 'character':
|
||||
return {
|
||||
...collectionQueries.characters(authUserId, currentFilters),
|
||||
enabled: searchMode === 'collection'
|
||||
enabled: true
|
||||
}
|
||||
case 'summon':
|
||||
return {
|
||||
...collectionQueries.summons(authUserId, currentFilters),
|
||||
enabled: searchMode === 'collection'
|
||||
enabled: true
|
||||
} as unknown as ReturnType<typeof collectionQueries.characters>
|
||||
}
|
||||
})
|
||||
|
|
@ -199,6 +201,25 @@
|
|||
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
|
||||
// (e.g., due to items being added/removed between page fetches)
|
||||
const searchResults = $derived.by<AddItemResult[]>(() => {
|
||||
|
|
@ -404,11 +425,13 @@
|
|||
{:else if searchResults.length > 0}
|
||||
<ul class="results-list">
|
||||
{#each searchResults as item (item.id)}
|
||||
{@const owned = searchMode === 'all' && authUserId && isOwned(item)}
|
||||
<li class="result-item">
|
||||
<button
|
||||
class="result-button"
|
||||
class:disabled={!canAddMore}
|
||||
class:from-collection={item.collectionId}
|
||||
class:owned={owned}
|
||||
onclick={() => handleItemClick(item)}
|
||||
aria-label="{canAddMore ? 'Add' : 'Grid full - cannot add'} {getItemName(item)}"
|
||||
disabled={!canAddMore}
|
||||
|
|
@ -422,6 +445,8 @@
|
|||
<span class="result-name">{getItemName(item)}</span>
|
||||
{#if item.collectionId}
|
||||
<Icon name="bookmark" size={14} class="collection-indicator" />
|
||||
{:else if owned}
|
||||
<Icon name="check" size={14} class="owned-indicator" />
|
||||
{/if}
|
||||
{#if item.element !== undefined}
|
||||
<span
|
||||
|
|
@ -680,6 +705,21 @@
|
|||
color: var(--accent-blue);
|
||||
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…
Reference in a new issue