refactor: restructure party component layout
This commit is contained in:
parent
e4c59e14f6
commit
0bab6e0d7e
1 changed files with 826 additions and 713 deletions
|
|
@ -14,6 +14,7 @@
|
||||||
import type { SearchResult } from '$lib/api/resources/search'
|
import type { SearchResult } from '$lib/api/resources/search'
|
||||||
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/Button.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
party?: Party
|
party?: Party
|
||||||
|
|
@ -36,9 +37,7 @@
|
||||||
|
|
||||||
// Initialize party state with proper validation
|
// Initialize party state with proper validation
|
||||||
let party = $state<Party>(
|
let party = $state<Party>(
|
||||||
initial?.id && initial?.id !== 'new' && Array.isArray(initial?.weapons)
|
initial?.id && initial?.id !== 'new' && Array.isArray(initial?.weapons) ? initial : defaultParty
|
||||||
? initial
|
|
||||||
: defaultParty
|
|
||||||
)
|
)
|
||||||
let activeTab = $state<GridType>(GridType.Weapon)
|
let activeTab = $state<GridType>(GridType.Weapon)
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
|
|
@ -74,7 +73,8 @@
|
||||||
if (target.type === 'weapon' && target.position === -1) return false
|
if (target.type === 'weapon' && target.position === -1) return false
|
||||||
|
|
||||||
// Summons: Main/Friend not draggable
|
// Summons: Main/Friend not draggable
|
||||||
if (target.type === 'summon' && (target.position === -1 || target.position === 6)) return false
|
if (target.type === 'summon' && (target.position === -1 || target.position === 6))
|
||||||
|
return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -205,17 +205,14 @@
|
||||||
if (canEditServer) return true
|
if (canEditServer) return true
|
||||||
|
|
||||||
// Re-compute on client with localStorage values
|
// Re-compute on client with localStorage values
|
||||||
const result = partyService.computeEditability(
|
const result = partyService.computeEditability(party, authUserId, localId, editKey)
|
||||||
party,
|
|
||||||
authUserId,
|
|
||||||
localId,
|
|
||||||
editKey
|
|
||||||
)
|
|
||||||
return result.canEdit
|
return result.canEdit
|
||||||
})
|
})
|
||||||
|
|
||||||
// Derived elements for character image logic
|
// Derived elements for character image logic
|
||||||
const mainWeapon = $derived(() => (party?.weapons ?? []).find(w => w?.mainhand || w?.position === -1))
|
const mainWeapon = $derived(() =>
|
||||||
|
(party?.weapons ?? []).find((w) => w?.mainhand || w?.position === -1)
|
||||||
|
)
|
||||||
const mainWeaponElement = $derived(() => mainWeapon?.element ?? mainWeapon?.weapon?.element)
|
const mainWeaponElement = $derived(() => mainWeapon?.element ?? mainWeapon?.weapon?.element)
|
||||||
const partyElement = $derived(() => party?.element)
|
const partyElement = $derived(() => party?.element)
|
||||||
|
|
||||||
|
|
@ -296,11 +293,7 @@
|
||||||
error = null
|
error = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await partyService.remix(
|
const result = await partyService.remix(party.shortcode, localId, editKey || undefined)
|
||||||
party.shortcode,
|
|
||||||
localId,
|
|
||||||
editKey || undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
// Store new edit key if returned
|
// Store new edit key if returned
|
||||||
if (result.editKey) {
|
if (result.editKey) {
|
||||||
|
|
@ -351,12 +344,12 @@
|
||||||
|
|
||||||
if (activeTab === GridType.Weapon) {
|
if (activeTab === GridType.Weapon) {
|
||||||
// Check mainhand first (position -1)
|
// Check mainhand first (position -1)
|
||||||
if (!party.weapons.find(w => w.position === -1 || w.mainhand)) {
|
if (!party.weapons.find((w) => w.position === -1 || w.mainhand)) {
|
||||||
nextEmptySlot = -1
|
nextEmptySlot = -1
|
||||||
} else {
|
} else {
|
||||||
// Check grid slots 0-8
|
// Check grid slots 0-8
|
||||||
for (let i = 0; i < 9; i++) {
|
for (let i = 0; i < 9; i++) {
|
||||||
if (!party.weapons.find(w => w.position === i)) {
|
if (!party.weapons.find((w) => w.position === i)) {
|
||||||
nextEmptySlot = i
|
nextEmptySlot = i
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -364,25 +357,25 @@
|
||||||
}
|
}
|
||||||
} else if (activeTab === GridType.Summon) {
|
} else if (activeTab === GridType.Summon) {
|
||||||
// Check main summon first (position -1)
|
// Check main summon first (position -1)
|
||||||
if (!party.summons.find(s => s.position === -1 || s.main)) {
|
if (!party.summons.find((s) => s.position === -1 || s.main)) {
|
||||||
nextEmptySlot = -1
|
nextEmptySlot = -1
|
||||||
} else {
|
} else {
|
||||||
// Check grid slots 0-5
|
// Check grid slots 0-5
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
if (!party.summons.find(s => s.position === i)) {
|
if (!party.summons.find((s) => s.position === i)) {
|
||||||
nextEmptySlot = i
|
nextEmptySlot = i
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check friend summon (position 6)
|
// Check friend summon (position 6)
|
||||||
if (nextEmptySlot === -999 && !party.summons.find(s => s.position === 6 || s.friend)) {
|
if (nextEmptySlot === -999 && !party.summons.find((s) => s.position === 6 || s.friend)) {
|
||||||
nextEmptySlot = 6
|
nextEmptySlot = 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (activeTab === GridType.Character) {
|
} else if (activeTab === GridType.Character) {
|
||||||
// Check character slots 0-4
|
// Check character slots 0-4
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
if (!party.characters.find(c => c.position === i)) {
|
if (!party.characters.find((c) => c.position === i)) {
|
||||||
nextEmptySlot = i
|
nextEmptySlot = i
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -449,13 +442,121 @@
|
||||||
console.error('Failed to remove character:', err)
|
console.error('Failed to remove character:', err)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async updateWeapon(partyId: string, gridWeaponId: string, updates: any, _editKey?: string) {
|
||||||
|
try {
|
||||||
|
// Use the grid service to update weapon
|
||||||
|
const updated = await gridService.updateWeapon(partyId, gridWeaponId, updates, editKey || undefined)
|
||||||
|
return updated
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update weapon:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateSummon(partyId: string, gridSummonId: string, updates: any, _editKey?: string) {
|
||||||
|
try {
|
||||||
|
// Use the grid service to update summon
|
||||||
|
const updated = await gridService.updateSummon(partyId, gridSummonId, updates, editKey || undefined)
|
||||||
|
return updated
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update summon:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateCharacter(partyId: string, gridCharacterId: string, updates: any, _editKey?: string) {
|
||||||
|
try {
|
||||||
|
// Use the grid service to update character
|
||||||
|
const updated = await gridService.updateCharacter(partyId, gridCharacterId, updates, editKey || undefined)
|
||||||
|
return updated
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update character:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateCharacterUncap(gridCharacterId: string, uncapLevel?: number, transcendenceStep?: number, _editKey?: string) {
|
||||||
|
try {
|
||||||
|
const response = await gridService.updateCharacterUncap(gridCharacterId, uncapLevel, transcendenceStep, editKey || undefined)
|
||||||
|
// The API returns {grid_character: {...}} with the updated item only
|
||||||
|
// We need to update just that character in the current party state
|
||||||
|
if (response.grid_character) {
|
||||||
|
const updatedParty = { ...party }
|
||||||
|
if (updatedParty.characters) {
|
||||||
|
const charIndex = updatedParty.characters.findIndex((c: any) => c.id === gridCharacterId)
|
||||||
|
if (charIndex !== -1) {
|
||||||
|
// Preserve the character object reference but update uncap fields
|
||||||
|
updatedParty.characters[charIndex] = {
|
||||||
|
...updatedParty.characters[charIndex],
|
||||||
|
uncapLevel: response.grid_character.uncap_level,
|
||||||
|
transcendenceStep: response.grid_character.transcendence_step
|
||||||
|
}
|
||||||
|
return updatedParty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return party // Return unchanged party if update failed
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update character uncap:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateWeaponUncap(gridWeaponId: string, uncapLevel?: number, transcendenceStep?: number, _editKey?: string) {
|
||||||
|
try {
|
||||||
|
const response = await gridService.updateWeaponUncap(gridWeaponId, uncapLevel, transcendenceStep, editKey || undefined)
|
||||||
|
// The API returns {grid_weapon: {...}} with the updated item only
|
||||||
|
// We need to update just that weapon in the current party state
|
||||||
|
if (response.grid_weapon) {
|
||||||
|
const updatedParty = { ...party }
|
||||||
|
if (updatedParty.weapons) {
|
||||||
|
const weaponIndex = updatedParty.weapons.findIndex((w: any) => w.id === gridWeaponId)
|
||||||
|
if (weaponIndex !== -1) {
|
||||||
|
// Preserve the weapon object reference but update uncap fields
|
||||||
|
updatedParty.weapons[weaponIndex] = {
|
||||||
|
...updatedParty.weapons[weaponIndex],
|
||||||
|
uncapLevel: response.grid_weapon.uncap_level,
|
||||||
|
transcendenceStep: response.grid_weapon.transcendence_step
|
||||||
|
}
|
||||||
|
return updatedParty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return party // Return unchanged party if update failed
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update weapon uncap:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateSummonUncap(gridSummonId: string, uncapLevel?: number, transcendenceStep?: number, _editKey?: string) {
|
||||||
|
try {
|
||||||
|
const response = await gridService.updateSummonUncap(gridSummonId, uncapLevel, transcendenceStep, editKey || undefined)
|
||||||
|
// The API returns {grid_summon: {...}} with the updated item only
|
||||||
|
// We need to update just that summon in the current party state
|
||||||
|
if (response.grid_summon) {
|
||||||
|
const updatedParty = { ...party }
|
||||||
|
if (updatedParty.summons) {
|
||||||
|
const summonIndex = updatedParty.summons.findIndex((s: any) => s.id === gridSummonId)
|
||||||
|
if (summonIndex !== -1) {
|
||||||
|
// Preserve the summon object reference but update uncap fields
|
||||||
|
updatedParty.summons[summonIndex] = {
|
||||||
|
...updatedParty.summons[summonIndex],
|
||||||
|
uncapLevel: response.grid_summon.uncap_level,
|
||||||
|
transcendenceStep: response.grid_summon.transcendence_step
|
||||||
|
}
|
||||||
|
return updatedParty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return party // Return unchanged party if update failed
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update summon uncap:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide services to child components via context
|
// Provide services to child components via context
|
||||||
setContext('party', {
|
setContext('party', {
|
||||||
getParty: () => party,
|
getParty: () => party,
|
||||||
updateParty: (p: Party) => party = p,
|
updateParty: (p: Party) => (party = p),
|
||||||
canEdit: () => canEdit(),
|
canEdit: () => canEdit(),
|
||||||
getEditKey: () => editKey,
|
getEditKey: () => editKey,
|
||||||
services: {
|
services: {
|
||||||
|
|
@ -463,11 +564,19 @@
|
||||||
gridService: clientGridService, // Use client-side wrapper
|
gridService: clientGridService, // Use client-side wrapper
|
||||||
conflictService
|
conflictService
|
||||||
},
|
},
|
||||||
openPicker: (opts: { type: 'weapon' | 'summon' | 'character'; position: number; item?: any }) => {
|
openPicker: (opts: {
|
||||||
|
type: 'weapon' | 'summon' | 'character'
|
||||||
|
position: number
|
||||||
|
item?: any
|
||||||
|
}) => {
|
||||||
if (!canEdit()) return
|
if (!canEdit()) return
|
||||||
selectedSlot = opts.position
|
selectedSlot = opts.position
|
||||||
activeTab = opts.type === 'weapon' ? GridType.Weapon :
|
activeTab =
|
||||||
opts.type === 'summon' ? GridType.Summon : GridType.Character
|
opts.type === 'weapon'
|
||||||
|
? GridType.Weapon
|
||||||
|
: opts.type === 'summon'
|
||||||
|
? GridType.Summon
|
||||||
|
: GridType.Character
|
||||||
pickerTitle = `Search ${opts.type}s`
|
pickerTitle = `Search ${opts.type}s`
|
||||||
pickerOpen = true
|
pickerOpen = true
|
||||||
}
|
}
|
||||||
|
|
@ -490,36 +599,35 @@
|
||||||
|
|
||||||
<div class="party-actions">
|
<div class="party-actions">
|
||||||
{#if canEdit()}
|
{#if canEdit()}
|
||||||
<button
|
<Button
|
||||||
class="edit-btn"
|
variant="secondary"
|
||||||
onclick={openEditDialog}
|
onclick={openEditDialog}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
aria-label="Edit party details"
|
aria-label="Edit party details"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if authUserId}
|
{#if authUserId}
|
||||||
<button
|
<Button
|
||||||
class="favorite-btn"
|
variant="secondary"
|
||||||
class:favorited={party.favorited}
|
|
||||||
onclick={toggleFavorite}
|
onclick={toggleFavorite}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
aria-label={party.favorited ? 'Remove from favorites' : 'Add to favorites'}
|
aria-label={party.favorited ? 'Remove from favorites' : 'Add to favorites'}
|
||||||
>
|
>
|
||||||
{party.favorited ? '★' : '☆'}
|
{party.favorited ? '★' : '☆'}
|
||||||
</button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
class="remix-btn"
|
variant="secondary"
|
||||||
onclick={remixParty}
|
onclick={remixParty}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
aria-label="Remix this party"
|
aria-label="Remix this party"
|
||||||
>
|
>
|
||||||
Remix
|
Remix
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -528,15 +636,15 @@
|
||||||
✏️ You can edit this party - Click on any slot to add or replace items
|
✏️ You can edit this party - Click on any slot to add or replace items
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="edit-status readonly">
|
<div class="edit-status readonly">🔒 Read-only</div>
|
||||||
🔒 Read-only
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if party.raid}
|
{#if party.raid}
|
||||||
<div class="raid-info">
|
<div class="raid-info">
|
||||||
<span class="raid-name">
|
<span class="raid-name">
|
||||||
{typeof party.raid.name === 'string' ? party.raid.name : party.raid.name?.en || party.raid.name?.ja || 'Unknown Raid'}
|
{typeof party.raid.name === 'string'
|
||||||
|
? party.raid.name
|
||||||
|
: party.raid.name?.en || party.raid.name?.ja || 'Unknown Raid'}
|
||||||
</span>
|
</span>
|
||||||
{#if party.raid.group}
|
{#if party.raid.group}
|
||||||
<span class="raid-difficulty">Difficulty: {party.raid.group.difficulty}</span>
|
<span class="raid-difficulty">Difficulty: {party.raid.group.difficulty}</span>
|
||||||
|
|
@ -547,7 +655,7 @@
|
||||||
<PartySegmentedControl
|
<PartySegmentedControl
|
||||||
selectedTab={activeTab}
|
selectedTab={activeTab}
|
||||||
onTabChange={handleTabChange}
|
onTabChange={handleTabChange}
|
||||||
party={party}
|
{party}
|
||||||
class="party-tabs"
|
class="party-tabs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -568,16 +676,17 @@
|
||||||
{:else if activeTab === GridType.Summon}
|
{:else if activeTab === GridType.Summon}
|
||||||
<SummonGrid summons={party.summons} />
|
<SummonGrid summons={party.summons} />
|
||||||
{:else}
|
{:else}
|
||||||
<CharacterGrid characters={party.characters} mainWeaponElement={mainWeaponElement} partyElement={partyElement} />
|
<CharacterGrid characters={party.characters} {mainWeaponElement} {partyElement} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<SearchSidebar
|
<SearchSidebar
|
||||||
open={pickerOpen}
|
open={pickerOpen}
|
||||||
type={activeTab === GridType.Weapon ? 'weapon' :
|
type={activeTab === GridType.Weapon
|
||||||
activeTab === GridType.Summon ? 'summon' : 'character'}
|
? 'weapon'
|
||||||
|
: activeTab === GridType.Summon
|
||||||
|
? 'summon'
|
||||||
|
: 'character'}
|
||||||
onClose={() => (pickerOpen = false)}
|
onClose={() => (pickerOpen = false)}
|
||||||
onAddItems={handleAddItems}
|
onAddItems={handleAddItems}
|
||||||
canAddMore={true}
|
canAddMore={true}
|
||||||
|
|
@ -601,27 +710,31 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
<button
|
<button class="btn-secondary" onclick={() => (editDialogOpen = false)} disabled={loading}>
|
||||||
class="btn-secondary"
|
|
||||||
onclick={() => (editDialogOpen = false)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="btn-primary" onclick={savePartyTitle} disabled={loading || !editingTitle.trim()}>
|
||||||
class="btn-primary"
|
|
||||||
onclick={savePartyTitle}
|
|
||||||
disabled={loading || !editingTitle.trim()}
|
|
||||||
>
|
|
||||||
{loading ? 'Saving...' : 'Save'}
|
{loading ? 'Saving...' : 'Save'}
|
||||||
</button>
|
</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page-wrap { position: relative; --panel-w: 380px; overflow-x: auto; }
|
.page-wrap {
|
||||||
.track { display: flex; gap: 0; align-items: flex-start; }
|
position: relative;
|
||||||
.party-container { width: 1200px; margin: 0 auto; padding: 1rem; }
|
--panel-w: 380px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.track {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.party-container {
|
||||||
|
width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.party-header {
|
.party-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue