use visual segmented control in party views

This commit is contained in:
Justin Edmund 2025-09-16 20:09:36 -07:00
parent acf49c718c
commit cc46a695d5
2 changed files with 58 additions and 153 deletions

View file

@ -10,7 +10,9 @@
import SummonGrid from '$lib/components/grids/SummonGrid.svelte'
import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte'
import SearchSidebar from '$lib/components/panels/SearchSidebar.svelte'
import PartySegmentedControl from '$lib/components/party/PartySegmentedControl.svelte'
import type { SearchResult } from '$lib/api/resources/search'
import { GridType } from '$lib/types/enums'
import Dialog from '$lib/components/ui/Dialog.svelte'
interface Props {
@ -38,7 +40,7 @@
? initial
: defaultParty
)
let activeTab = $state<'weapons' | 'summons' | 'characters'>('weapons')
let activeTab = $state<GridType>(GridType.Weapon)
let loading = $state(false)
let error = $state<string | null>(null)
let pickerOpen = $state(false)
@ -212,20 +214,13 @@
return result.canEdit
})
// Tab configuration - use function to avoid state capture
const tabs = $derived([
{ key: 'weapons' as const, label: 'Weapons', count: (party?.weapons ?? []).length },
{ key: 'summons' as const, label: 'Summons', count: (party?.summons ?? []).length },
{ key: 'characters' as const, label: 'Characters', count: (party?.characters ?? []).length }
])
// Derived elements for character image logic
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)
function selectTab(key: typeof tabs[number]['key']) {
activeTab = key
function handleTabChange(tab: GridType) {
activeTab = tab
}
// Edit dialog functions
@ -334,16 +329,16 @@
let targetSlot = selectedSlot
// Call appropriate API based on current tab
if (activeTab === 'weapons') {
if (activeTab === GridType.Weapon) {
await apiClient.addWeapon(party.id, item.granblue_id, targetSlot, {
mainhand: targetSlot === -1
})
} else if (activeTab === 'summons') {
} else if (activeTab === GridType.Summon) {
await apiClient.addSummon(party.id, item.granblue_id, targetSlot, {
main: targetSlot === -1,
friend: targetSlot === 6
})
} else if (activeTab === 'characters') {
} else if (activeTab === GridType.Character) {
await apiClient.addCharacter(party.id, item.granblue_id, targetSlot)
}
@ -354,7 +349,7 @@
// Find next empty slot for continuous adding
let nextEmptySlot = -999 // sentinel value meaning no empty slot found
if (activeTab === 'weapons') {
if (activeTab === GridType.Weapon) {
// Check mainhand first (position -1)
if (!party.weapons.find(w => w.position === -1 || w.mainhand)) {
nextEmptySlot = -1
@ -367,7 +362,7 @@
}
}
}
} else if (activeTab === 'summons') {
} else if (activeTab === GridType.Summon) {
// Check main summon first (position -1)
if (!party.summons.find(s => s.position === -1 || s.main)) {
nextEmptySlot = -1
@ -384,7 +379,7 @@
nextEmptySlot = 6
}
}
} else if (activeTab === 'characters') {
} 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)) {
@ -471,8 +466,8 @@
openPicker: (opts: { type: 'weapon' | 'summon' | 'character'; position: number; item?: any }) => {
if (!canEdit()) return
selectedSlot = opts.position
activeTab = opts.type === 'weapon' ? 'weapons' :
opts.type === 'summon' ? 'summons' : 'characters'
activeTab = opts.type === 'weapon' ? GridType.Weapon :
opts.type === 'summon' ? GridType.Summon : GridType.Character
pickerTitle = `Search ${opts.type}s`
pickerOpen = true
}
@ -549,21 +544,12 @@
</div>
{/if}
<nav class="party-tabs" aria-label="Party sections">
{#each tabs as t}
<button
class="tab-btn"
aria-pressed={activeTab === t.key}
class:active={activeTab === t.key}
onclick={() => selectTab(t.key)}
>
{t.label}
{#if t.count > 0}
<span class="count">({t.count})</span>
{/if}
</button>
{/each}
</nav>
<PartySegmentedControl
selectedTab={activeTab}
onTabChange={handleTabChange}
party={party}
class="party-tabs"
/>
{#if error}
<div class="error-message" role="alert">
@ -572,14 +558,14 @@
{/if}
<div class="party-content">
{#if activeTab === 'weapons'}
<WeaponGrid
weapons={party.weapons}
{#if activeTab === GridType.Weapon}
<WeaponGrid
weapons={party.weapons}
raidExtra={(party as any)?.raid?.group?.extra}
showGuidebooks={(party as any)?.raid?.group?.guidebooks}
guidebooks={(party as any)?.guidebooks}
/>
{:else if activeTab === 'summons'}
{:else if activeTab === GridType.Summon}
<SummonGrid summons={party.summons} />
{:else}
<CharacterGrid characters={party.characters} mainWeaponElement={mainWeaponElement} partyElement={partyElement} />
@ -590,8 +576,8 @@
</section>
<SearchSidebar
open={pickerOpen}
type={activeTab === 'weapons' ? 'weapon' :
activeTab === 'summons' ? 'summon' : 'character'}
type={activeTab === GridType.Weapon ? 'weapon' :
activeTab === GridType.Summon ? 'summon' : 'character'}
onClose={() => (pickerOpen = false)}
onAddItems={handleAddItems}
canAddMore={true}
@ -729,42 +715,7 @@
.raid-name {
font-weight: 600;
}
.party-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
border-bottom: 2px solid #eee;
}
.tab-btn {
padding: 0.5rem 1rem;
border: none;
background: transparent;
cursor: pointer;
position: relative;
transition: all 0.2s;
}
.tab-btn.active {
color: #3366ff;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: #3366ff;
}
.tab-btn .count {
color: #999;
font-size: 0.9em;
}
.error-message {
padding: 0.75rem;
background: #fee;
@ -773,23 +724,10 @@
color: #c00;
margin-bottom: 1rem;
}
.party-content {
min-height: 400px;
}
.grid-placeholder {
padding: 2rem;
background: #f9f9f9;
border-radius: 8px;
text-align: center;
}
.grid-placeholder ul {
text-align: left;
max-width: 400px;
margin: 1rem auto;
}
/* Edit form styles */
.edit-form {

View file

@ -5,6 +5,8 @@
import SummonGrid from '$lib/components/grids/SummonGrid.svelte'
import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte'
import SearchSidebar from '$lib/components/panels/SearchSidebar.svelte'
import PartySegmentedControl from '$lib/components/party/PartySegmentedControl.svelte'
import { GridType } from '$lib/types/enums'
import { setContext } from 'svelte'
import type { SearchResult } from '$lib/api/resources/search'
import { apiClient } from '$lib/api/client'
@ -17,15 +19,10 @@
const currentUser = $derived($page.data?.currentUser)
// Local, client-only state for tab selection (Svelte 5 runes)
let activeTab = $state<'weapons' | 'summons' | 'characters'>('weapons')
const tabs = [
{ key: 'weapons', label: 'Weapons' },
{ key: 'summons', label: 'Summons' },
{ key: 'characters', label: 'Characters' }
] as const
let activeTab = $state<GridType>(GridType.Weapon)
function selectTab(key: typeof tabs[number]['key']) {
activeTab = key
function selectTab(gridType: GridType) {
activeTab = gridType
sidebarOpen = true // Auto-open sidebar when switching tabs
}
@ -66,8 +63,8 @@
let isCharacterGridFull = $derived(characters.length >= 5) // 5 character slots
let canAddMore = $derived(
activeTab === 'weapons' ? !isWeaponGridFull :
activeTab === 'summons' ? !isSummonGridFull :
activeTab === GridType.Weapon ? !isWeaponGridFull :
activeTab === GridType.Summon ? !isSummonGridFull :
!isCharacterGridFull
)
@ -118,7 +115,7 @@
try {
console.log('Adding item to party:', { partyId, itemId: firstItem.id, type: activeTab, position })
if (activeTab === 'weapons') {
if (activeTab === GridType.Weapon) {
// Use selectedSlot if available, otherwise default to mainhand
if (selectedSlot === null) position = -1
const addResult = await apiClient.addWeapon(
@ -141,7 +138,7 @@
},
mainhand: position === -1
}]
} else if (activeTab === 'summons') {
} else if (activeTab === GridType.Summon) {
// Use selectedSlot if available, otherwise default to main summon
if (selectedSlot === null) position = -1
const addResult = await apiClient.addSummon(
@ -165,7 +162,7 @@
main: position === -1,
friend: position === 6
}]
} else if (activeTab === 'characters') {
} else if (activeTab === GridType.Character) {
// Use selectedSlot if available, otherwise default to first slot
if (selectedSlot === null) position = 0
const addResult = await apiClient.addCharacter(
@ -251,7 +248,7 @@
const item = items[i]
let position = -1 // Default position
if (activeTab === 'weapons') {
if (activeTab === GridType.Weapon) {
// Use selectedSlot for first item if available
if (i === 0 && selectedSlot !== null && !weapons.find(w => w.position === selectedSlot)) {
position = selectedSlot
@ -283,7 +280,7 @@
},
mainhand: position === -1
}]
} else if (activeTab === 'summons') {
} else if (activeTab === GridType.Summon) {
// Use selectedSlot for first item if available
if (i === 0 && selectedSlot !== null && !summons.find(s => s.position === selectedSlot)) {
position = selectedSlot
@ -316,7 +313,7 @@
main: position === -1,
friend: position === 6
}]
} else if (activeTab === 'characters') {
} else if (activeTab === GridType.Character) {
// Use selectedSlot for first item if available
if (i === 0 && selectedSlot !== null && !characters.find(c => c.position === selectedSlot)) {
position = selectedSlot
@ -359,7 +356,7 @@
}
// Original local-only adding logic (before party creation)
if (activeTab === 'weapons') {
if (activeTab === GridType.Weapon) {
// Add weapons to empty slots
const emptySlots = Array.from({ length: 10 }, (_, i) => i - 1) // -1 for mainhand, 0-8 for grid
.filter(i => !weapons.find(w => w.position === i))
@ -391,7 +388,7 @@
weapons = [...weapons, newWeapon]
})
console.log('Updated weapons array:', weapons)
} else if (activeTab === 'summons') {
} else if (activeTab === GridType.Summon) {
// Add summons to empty slots
const emptySlots = [-1, 0, 1, 2, 3, 6] // main, 4 grid slots, friend
.filter(i => !summons.find(s => s.position === i))
@ -421,7 +418,7 @@
friend: position === 6
}]
})
} else if (activeTab === 'characters') {
} else if (activeTab === GridType.Character) {
// Add characters to empty slots
const emptySlots = Array.from({ length: 5 }, (_, i) => i)
.filter(i => !characters.find(c => c.position === i))
@ -515,23 +512,23 @@
</button>
</header>
<nav class="party-tabs" aria-label="Party sections">
{#each tabs as t}
<button
class="tab-btn"
aria-pressed={activeTab === t.key}
class:active={activeTab === t.key}
on:click={() => selectTab(t.key)}
>
{t.label}
</button>
{/each}
</nav>
<PartySegmentedControl
value={activeTab}
onValueChange={selectTab}
party={{
element: 0,
job: undefined,
characters,
weapons,
summons
}}
userGender={currentUser?.gender}
/>
<div class="party-content">
{#if activeTab === 'weapons'}
{#if activeTab === GridType.Weapon}
<WeaponGrid {weapons} />
{:else if activeTab === 'summons'}
{:else if activeTab === GridType.Summon}
<SummonGrid {summons} />
{:else}
<CharacterGrid {characters} />
@ -541,7 +538,7 @@
<SearchSidebar
open={sidebarOpen}
type={activeTab === 'weapons' ? 'weapon' : activeTab === 'summons' ? 'summon' : 'character'}
type={activeTab === GridType.Weapon ? 'weapon' : activeTab === GridType.Summon ? 'summon' : 'character'}
onClose={() => sidebarOpen = false}
onAddItems={handleAddItems}
canAddMore={canAddMore}
@ -629,36 +626,6 @@
background: #2857e0;
}
.party-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
border-bottom: 2px solid #eee;
}
.tab-btn {
padding: 0.5rem 1rem;
border: none;
background: transparent;
cursor: pointer;
position: relative;
transition: all 0.2s;
}
.tab-btn.active {
color: #3366ff;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: #3366ff;
}
.party-content {
min-height: 400px;
}