refactor: restructure party component layout

This commit is contained in:
Justin Edmund 2025-09-17 10:47:59 -07:00
parent e4c59e14f6
commit 0bab6e0d7e

View file

@ -14,6 +14,7 @@
import type { SearchResult } from '$lib/api/resources/search'
import { GridType } from '$lib/types/enums'
import Dialog from '$lib/components/ui/Dialog.svelte'
import Button from '$lib/components/ui/button/Button.svelte'
interface Props {
party?: Party
@ -36,9 +37,7 @@
// Initialize party state with proper validation
let party = $state<Party>(
initial?.id && initial?.id !== 'new' && Array.isArray(initial?.weapons)
? initial
: defaultParty
initial?.id && initial?.id !== 'new' && Array.isArray(initial?.weapons) ? initial : defaultParty
)
let activeTab = $state<GridType>(GridType.Weapon)
let loading = $state(false)
@ -74,7 +73,8 @@
if (target.type === 'weapon' && target.position === -1) return false
// 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
}
@ -205,17 +205,14 @@
if (canEditServer) return true
// Re-compute on client with localStorage values
const result = partyService.computeEditability(
party,
authUserId,
localId,
editKey
)
const result = partyService.computeEditability(party, authUserId, localId, editKey)
return result.canEdit
})
// 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 partyElement = $derived(() => party?.element)
@ -296,11 +293,7 @@
error = null
try {
const result = await partyService.remix(
party.shortcode,
localId,
editKey || undefined
)
const result = await partyService.remix(party.shortcode, localId, editKey || undefined)
// Store new edit key if returned
if (result.editKey) {
@ -351,12 +344,12 @@
if (activeTab === GridType.Weapon) {
// 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
} else {
// Check grid slots 0-8
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
break
}
@ -364,25 +357,25 @@
}
} else if (activeTab === GridType.Summon) {
// 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
} else {
// Check grid slots 0-5
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
break
}
}
// 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
}
}
} else if (activeTab === GridType.Character) {
// Check character slots 0-4
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
break
}
@ -449,13 +442,121 @@
console.error('Failed to remove character:', 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
setContext('party', {
getParty: () => party,
updateParty: (p: Party) => party = p,
updateParty: (p: Party) => (party = p),
canEdit: () => canEdit(),
getEditKey: () => editKey,
services: {
@ -463,11 +564,19 @@
gridService: clientGridService, // Use client-side wrapper
conflictService
},
openPicker: (opts: { type: 'weapon' | 'summon' | 'character'; position: number; item?: any }) => {
openPicker: (opts: {
type: 'weapon' | 'summon' | 'character'
position: number
item?: any
}) => {
if (!canEdit()) return
selectedSlot = opts.position
activeTab = opts.type === 'weapon' ? GridType.Weapon :
opts.type === 'summon' ? GridType.Summon : GridType.Character
activeTab =
opts.type === 'weapon'
? GridType.Weapon
: opts.type === 'summon'
? GridType.Summon
: GridType.Character
pickerTitle = `Search ${opts.type}s`
pickerOpen = true
}
@ -490,36 +599,35 @@
<div class="party-actions">
{#if canEdit()}
<button
class="edit-btn"
<Button
variant="secondary"
onclick={openEditDialog}
disabled={loading}
aria-label="Edit party details"
>
Edit
</button>
</Button>
{/if}
{#if authUserId}
<button
class="favorite-btn"
class:favorited={party.favorited}
<Button
variant="secondary"
onclick={toggleFavorite}
disabled={loading}
aria-label={party.favorited ? 'Remove from favorites' : 'Add to favorites'}
>
{party.favorited ? '★' : '☆'}
</button>
</Button>
{/if}
<button
class="remix-btn"
<Button
variant="secondary"
onclick={remixParty}
disabled={loading}
aria-label="Remix this party"
>
Remix
</button>
</Button>
</div>
</header>
@ -528,15 +636,15 @@
✏️ You can edit this party - Click on any slot to add or replace items
</div>
{:else}
<div class="edit-status readonly">
🔒 Read-only
</div>
<div class="edit-status readonly">🔒 Read-only</div>
{/if}
{#if party.raid}
<div class="raid-info">
<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>
{#if party.raid.group}
<span class="raid-difficulty">Difficulty: {party.raid.group.difficulty}</span>
@ -547,7 +655,7 @@
<PartySegmentedControl
selectedTab={activeTab}
onTabChange={handleTabChange}
party={party}
{party}
class="party-tabs"
/>
@ -568,16 +676,17 @@
{:else if activeTab === GridType.Summon}
<SummonGrid summons={party.summons} />
{:else}
<CharacterGrid characters={party.characters} mainWeaponElement={mainWeaponElement} partyElement={partyElement} />
<CharacterGrid characters={party.characters} {mainWeaponElement} {partyElement} />
{/if}
</div>
</section>
<SearchSidebar
open={pickerOpen}
type={activeTab === GridType.Weapon ? 'weapon' :
activeTab === GridType.Summon ? 'summon' : 'character'}
type={activeTab === GridType.Weapon
? 'weapon'
: activeTab === GridType.Summon
? 'summon'
: 'character'}
onClose={() => (pickerOpen = false)}
onAddItems={handleAddItems}
canAddMore={true}
@ -601,27 +710,31 @@
{/snippet}
{#snippet footer()}
<button
class="btn-secondary"
onclick={() => (editDialogOpen = false)}
disabled={loading}
>
<button class="btn-secondary" onclick={() => (editDialogOpen = false)} disabled={loading}>
Cancel
</button>
<button
class="btn-primary"
onclick={savePartyTitle}
disabled={loading || !editingTitle.trim()}
>
<button class="btn-primary" onclick={savePartyTitle} disabled={loading || !editingTitle.trim()}>
{loading ? 'Saving...' : 'Save'}
</button>
{/snippet}
</Dialog>
<style>
.page-wrap { position: relative; --panel-w: 380px; overflow-x: auto; }
.track { display: flex; gap: 0; align-items: flex-start; }
.party-container { width: 1200px; margin: 0 auto; padding: 1rem; }
.page-wrap {
position: relative;
--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 {
display: flex;