diff --git a/src/lib/components/party/Party.svelte b/src/lib/components/party/Party.svelte index e4ed5102..1b90f745 100644 --- a/src/lib/components/party/Party.svelte +++ b/src/lib/components/party/Party.svelte @@ -42,7 +42,7 @@ import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte' import { openSearchSidebar } from '$lib/features/search/openSearchSidebar.svelte' import PartySegmentedControl from '$lib/components/party/PartySegmentedControl.svelte' - import type { SearchResult } from '$lib/api/adapters' + import type { AddItemResult } from '$lib/types/api/search' import { GridType } from '$lib/types/enums' import Dialog from '$lib/components/ui/Dialog.svelte' import Button from '$lib/components/ui/Button.svelte' @@ -522,7 +522,7 @@ } // Handle adding items from the search sidebar - async function handleAddItems(items: SearchResult[]) { + async function handleAddItems(items: AddItemResult[]) { if (items.length === 0 || !canEdit()) return const item = items[0] @@ -535,7 +535,7 @@ let targetSlot = selectedSlot // Call appropriate create mutation based on current tab - // Use granblueId (camelCase) as that's what the SearchResult type uses + // Use granblueId (camelCase) as that's what the AddItemResult type uses const itemId = item.granblueId let result: unknown @@ -544,7 +544,9 @@ partyId: party.id, weaponId: itemId, position: targetSlot, - mainhand: targetSlot === -1 + mainhand: targetSlot === -1, + // Link to collection if item was selected from collection + collectionWeaponId: item.collectionId }) // Check if the result is a conflict response @@ -559,13 +561,17 @@ summonId: itemId, position: targetSlot, main: targetSlot === -1, - friend: targetSlot === 6 + friend: targetSlot === 6, + // Link to collection if item was selected from collection + collectionSummonId: item.collectionId }) } else if (activeTab === GridType.Character) { result = await createCharacter.mutateAsync({ partyId: party.id, characterId: itemId, - position: targetSlot + position: targetSlot, + // Link to collection if item was selected from collection + collectionCharacterId: item.collectionId }) // Check if the result is a conflict response @@ -777,10 +783,12 @@ : GridType.Character // Open the search sidebar with the appropriate type + // Pass authUserId to enable collection mode toggle openSearchSidebar({ type: opts.type, onAddItems: handleAddItems, - canAddMore: true + canAddMore: true, + authUserId }) } }) diff --git a/src/lib/components/sidebar/DetailsSidebar.svelte b/src/lib/components/sidebar/DetailsSidebar.svelte index d0669d9d..d3d20f81 100644 --- a/src/lib/components/sidebar/DetailsSidebar.svelte +++ b/src/lib/components/sidebar/DetailsSidebar.svelte @@ -9,6 +9,12 @@ import StatsSection from './details/StatsSection.svelte' import SkillsSection from './details/SkillsSection.svelte' import TeamView from './details/TeamView.svelte' + import Icon from '$lib/components/Icon.svelte' + import { + useSyncGridCharacter, + useSyncGridWeapon, + useSyncGridSummon + } from '$lib/api/mutations/grid.mutations' interface Props { type: 'character' | 'weapon' | 'summon' @@ -17,6 +23,11 @@ let { type, item: initialItem }: Props = $props() + // Sync mutations + const syncCharacterMutation = useSyncGridCharacter() + const syncWeaponMutation = useSyncGridWeapon() + const syncSummonMutation = useSyncGridSummon() + // Derive item from partyStore for reactivity, fall back to prop if not in store // This ensures the sidebar updates when party data changes (e.g., uncap level) let item = $derived.by(() => { @@ -40,14 +51,21 @@ // Track selected view - updated reactively based on modifiability let selectedView = $state<'canonical' | 'user'>('user') - // Update view when switching to items with different modifiability + // Track the item ID to detect when switching to a different item + let currentItemId = $state(undefined) + + // Update view when switching to a different item $effect(() => { - if (!showSegmentedControl) { - // Force canonical view for non-modifiable items - selectedView = 'canonical' - } else if (showSegmentedControl && selectedView === 'canonical') { - // Switch to user view when selecting a modifiable item - selectedView = 'user' + const itemId = item && 'id' in item ? item.id : undefined + if (itemId !== currentItemId) { + currentItemId = itemId + if (!showSegmentedControl) { + // Force canonical view for non-modifiable items + selectedView = 'canonical' + } else { + // Default to user view for modifiable items + selectedView = 'user' + } } }) @@ -81,11 +99,58 @@ ? ((item as GridWeapon).transcendenceStep ?? null) : ((item as GridSummon).transcendenceStep ?? null) ) + + // Sync status - check if linked to collection and out of sync + const isLinkedToCollection = $derived.by(() => { + if (type === 'character') return !!(item as GridCharacter).collectionCharacterId + if (type === 'weapon') return !!(item as GridWeapon).collectionWeaponId + if (type === 'summon') return !!(item as GridSummon).collectionSummonId + return false + }) + + const isOutOfSync = $derived.by(() => { + if (type === 'character') return (item as GridCharacter).outOfSync ?? false + if (type === 'weapon') return (item as GridWeapon).outOfSync ?? false + if (type === 'summon') return (item as GridSummon).outOfSync ?? false + return false + }) + + const isSyncing = $derived( + syncCharacterMutation.isPending || + syncWeaponMutation.isPending || + syncSummonMutation.isPending + ) + + // Handle sync from collection + async function handleSync() { + const itemId = item && 'id' in item ? item.id : undefined + if (!itemId || !isLinkedToCollection) return + + if (type === 'character') { + await syncCharacterMutation.mutateAsync({ id: itemId, partyShortcode: '' }) + } else if (type === 'weapon') { + await syncWeaponMutation.mutateAsync({ id: itemId, partyShortcode: '' }) + } else if (type === 'summon') { + await syncSummonMutation.mutateAsync({ id: itemId, partyShortcode: '' }) + } + }
+ {#if isLinkedToCollection && isOutOfSync} +
+
+ + Out of sync with collection +
+ +
+ {/if} + 0) const canHavePerpetuity = $derived(character.position > 0) + // Sync status + const isLinkedToCollection = $derived(!!character.collectionCharacterId) + const isOutOfSync = $derived(character.outOfSync ?? false) + const isSyncing = $derived(syncMutation.isPending) + + // Handle sync from collection + async function handleSync() { + if (!character.id || !isLinkedToCollection) return + await syncMutation.mutateAsync({ + id: character.id, + partyShortcode: '' // Will be handled by cache invalidation + }) + } + // Convert GridCharacter data to CharacterEditPane format const currentValues = $derived({ uncapLevel: character.uncapLevel ?? 0, @@ -66,17 +85,34 @@ gridTranscendence={character.transcendenceStep} /> + {#if isLinkedToCollection && isOutOfSync} +
+
+ + Out of sync with collection +
+ +
+ {/if} +
diff --git a/src/lib/components/sidebar/EditWeaponSidebar.svelte b/src/lib/components/sidebar/EditWeaponSidebar.svelte index 1b2e30a4..437afaff 100644 --- a/src/lib/components/sidebar/EditWeaponSidebar.svelte +++ b/src/lib/components/sidebar/EditWeaponSidebar.svelte @@ -9,8 +9,10 @@ import AwakeningSelect from './edit/AwakeningSelect.svelte' import AxSkillSelect from './edit/AxSkillSelect.svelte' import Button from '$lib/components/ui/Button.svelte' + import Icon from '$lib/components/Icon.svelte' import { getElementIcon } from '$lib/utils/images' import { seriesHasWeaponKeys, getSeriesSlug } from '$lib/utils/weaponSeries' + import { useSyncGridWeapon } from '$lib/api/mutations/grid.mutations' interface Props { weapon: GridWeapon @@ -20,6 +22,23 @@ let { weapon, onSave, onCancel }: Props = $props() + // Sync mutation + const syncMutation = useSyncGridWeapon() + + // Sync status + const isLinkedToCollection = $derived(!!weapon.collectionWeaponId) + const isOutOfSync = $derived(weapon.outOfSync ?? false) + const isSyncing = $derived(syncMutation.isPending) + + // Handle sync from collection + async function handleSync() { + if (!weapon.id || !isLinkedToCollection) return + await syncMutation.mutateAsync({ + id: weapon.id, + partyShortcode: '' + }) + } + // Local state for edits let element = $state(weapon.element ?? weapon.weapon?.element ?? 0) @@ -210,6 +229,22 @@ gridTranscendence={weapon.transcendenceStep} /> + {#if isLinkedToCollection && isOutOfSync} +
+
+ + Out of sync with collection +
+ +
+ {/if} +
{#if canChangeElement} @@ -312,6 +347,50 @@ gap: spacing.$unit-4x; } + .sync-banner { + display: flex; + align-items: center; + justify-content: space-between; + padding: spacing.$unit spacing.$unit-2x; + background: var(--warning-bg, rgba(255, 193, 7, 0.15)); + border: 1px solid var(--warning-border, rgba(255, 193, 7, 0.3)); + border-radius: spacing.$unit; + gap: spacing.$unit-2x; + } + + .sync-message { + display: flex; + align-items: center; + gap: spacing.$unit-half; + font-size: typography.$font-small; + color: var(--warning-text, #b59100); + + :global(svg) { + color: inherit; + } + } + + .sync-button { + padding: spacing.$unit-half spacing.$unit; + font-size: typography.$font-small; + font-weight: typography.$medium; + color: var(--text-primary); + background: var(--button-bg); + border: 1px solid var(--button-border); + border-radius: spacing.$unit-half; + cursor: pointer; + transition: background 0.15s ease; + + &:hover:not(:disabled) { + background: var(--button-bg-hover); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } + .weapon-title { display: flex; flex-direction: column; diff --git a/src/lib/types/api/party.ts b/src/lib/types/api/party.ts index e2d8f531..e283e5a9 100644 --- a/src/lib/types/api/party.ts +++ b/src/lib/types/api/party.ts @@ -34,6 +34,10 @@ export interface GridWeapon { type?: Awakening level?: number } + /** Reference to the source collection weapon if linked */ + collectionWeaponId?: string + /** Whether the grid item is out of sync with its collection source */ + outOfSync?: boolean } // GridCharacter from GridCharacterBlueprint @@ -52,6 +56,10 @@ export interface GridCharacter { overMastery?: Array<{ modifier: number; strength: number }> /** Equipped artifact (can be grid or collection artifact) */ artifact?: GridArtifact | CollectionArtifact + /** Reference to the source collection character if linked */ + collectionCharacterId?: string + /** Whether the grid item is out of sync with its collection source */ + outOfSync?: boolean } // GridSummon from GridSummonBlueprint @@ -64,6 +72,10 @@ export interface GridSummon { uncapLevel?: number transcendenceStep?: number summon: Summon // Named properly, not "object" + /** Reference to the source collection summon if linked */ + collectionSummonId?: string + /** Whether the grid item is out of sync with its collection source */ + outOfSync?: boolean } // JobSkillList for party job skills