add context menus to units and party
This commit is contained in:
parent
da4c3d09f9
commit
e26b5c2e20
4 changed files with 459 additions and 112 deletions
|
|
@ -10,6 +10,7 @@
|
||||||
import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte'
|
import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte'
|
||||||
import SearchSidebar from '$lib/components/panels/SearchSidebar.svelte'
|
import SearchSidebar from '$lib/components/panels/SearchSidebar.svelte'
|
||||||
import type { SearchResult } from '$lib/api/resources/search'
|
import type { SearchResult } from '$lib/api/resources/search'
|
||||||
|
import Dialog from '$lib/components/ui/Dialog.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
party?: Party
|
party?: Party
|
||||||
|
|
@ -42,6 +43,8 @@
|
||||||
let pickerOpen = $state(false)
|
let pickerOpen = $state(false)
|
||||||
let pickerTitle = $state('Search')
|
let pickerTitle = $state('Search')
|
||||||
let selectedSlot = $state<number>(0)
|
let selectedSlot = $state<number>(0)
|
||||||
|
let editDialogOpen = $state(false)
|
||||||
|
let editingTitle = $state('')
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
const partyService = new PartyService(fetch)
|
const partyService = new PartyService(fetch)
|
||||||
|
|
@ -93,23 +96,49 @@
|
||||||
function selectTab(key: typeof tabs[number]['key']) {
|
function selectTab(key: typeof tabs[number]['key']) {
|
||||||
activeTab = key
|
activeTab = key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit dialog functions
|
||||||
|
function openEditDialog() {
|
||||||
|
if (!canEdit()) return
|
||||||
|
editingTitle = party.name || ''
|
||||||
|
editDialogOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePartyTitle() {
|
||||||
|
if (!canEdit()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading = true
|
||||||
|
error = null
|
||||||
|
|
||||||
|
// Update party title via API
|
||||||
|
const updated = await updatePartyDetails({ name: editingTitle })
|
||||||
|
if (updated) {
|
||||||
|
party = updated
|
||||||
|
editDialogOpen = false
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
error = err.message || 'Failed to update party title'
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Party operations
|
// Party operations
|
||||||
async function updatePartyDetails(updates: Partial<Party>) {
|
async function updatePartyDetails(updates: Partial<Party>) {
|
||||||
if (!canEdit()) return
|
if (!canEdit()) return null
|
||||||
|
|
||||||
loading = true
|
loading = true
|
||||||
error = null
|
error = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated = await partyService.update(
|
// Use apiClient for client-side updates (handles edit keys automatically)
|
||||||
party.id,
|
const updated = await apiClient.updateParty(party.id, updates)
|
||||||
updates,
|
|
||||||
editKey || undefined
|
|
||||||
)
|
|
||||||
party = updated
|
party = updated
|
||||||
|
return updated
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message || 'Failed to update party'
|
error = err.message || 'Failed to update party'
|
||||||
|
return null
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
@ -263,22 +292,37 @@
|
||||||
// Create client-side wrappers for grid operations using API client
|
// Create client-side wrappers for grid operations using API client
|
||||||
const clientGridService = {
|
const clientGridService = {
|
||||||
async removeWeapon(partyId: string, gridWeaponId: string, _editKey?: string) {
|
async removeWeapon(partyId: string, gridWeaponId: string, _editKey?: string) {
|
||||||
await apiClient.removeWeapon(partyId, gridWeaponId)
|
try {
|
||||||
// Reload party data
|
await apiClient.removeWeapon(partyId, gridWeaponId)
|
||||||
const updated = await partyService.getByShortcode(party.shortcode)
|
// Reload party data
|
||||||
return updated
|
const updated = await partyService.getByShortcode(party.shortcode)
|
||||||
|
return updated
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to remove weapon:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async removeSummon(partyId: string, gridSummonId: string, _editKey?: string) {
|
async removeSummon(partyId: string, gridSummonId: string, _editKey?: string) {
|
||||||
await apiClient.removeSummon(partyId, gridSummonId)
|
try {
|
||||||
// Reload party data
|
await apiClient.removeSummon(partyId, gridSummonId)
|
||||||
const updated = await partyService.getByShortcode(party.shortcode)
|
// Reload party data
|
||||||
return updated
|
const updated = await partyService.getByShortcode(party.shortcode)
|
||||||
|
return updated
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to remove summon:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async removeCharacter(partyId: string, gridCharacterId: string, _editKey?: string) {
|
async removeCharacter(partyId: string, gridCharacterId: string, _editKey?: string) {
|
||||||
await apiClient.removeCharacter(partyId, gridCharacterId)
|
try {
|
||||||
// Reload party data
|
await apiClient.removeCharacter(partyId, gridCharacterId)
|
||||||
const updated = await partyService.getByShortcode(party.shortcode)
|
// Reload party data
|
||||||
return updated
|
const updated = await partyService.getByShortcode(party.shortcode)
|
||||||
|
return updated
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to remove character:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,23 +358,34 @@
|
||||||
<p class="description">{party.description}</p>
|
<p class="description">{party.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="party-actions">
|
<div class="party-actions">
|
||||||
|
{#if canEdit()}
|
||||||
|
<button
|
||||||
|
class="edit-btn"
|
||||||
|
onclick={openEditDialog}
|
||||||
|
disabled={loading}
|
||||||
|
aria-label="Edit party details"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if authUserId}
|
{#if authUserId}
|
||||||
<button
|
<button
|
||||||
class="favorite-btn"
|
class="favorite-btn"
|
||||||
class:favorited={party.favorited}
|
class:favorited={party.favorited}
|
||||||
on:click={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"
|
class="remix-btn"
|
||||||
on:click={remixParty}
|
onclick={remixParty}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
aria-label="Remix this party"
|
aria-label="Remix this party"
|
||||||
>
|
>
|
||||||
|
|
@ -366,7 +421,7 @@
|
||||||
class="tab-btn"
|
class="tab-btn"
|
||||||
aria-pressed={activeTab === t.key}
|
aria-pressed={activeTab === t.key}
|
||||||
class:active={activeTab === t.key}
|
class:active={activeTab === t.key}
|
||||||
on:click={() => selectTab(t.key)}
|
onclick={() => selectTab(t.key)}
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}
|
||||||
{#if t.count > 0}
|
{#if t.count > 0}
|
||||||
|
|
@ -410,6 +465,39 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Dialog -->
|
||||||
|
<Dialog bind:open={editDialogOpen} title="Edit Party Details">
|
||||||
|
{#snippet children()}
|
||||||
|
<div class="edit-form">
|
||||||
|
<label for="party-title">Party Title</label>
|
||||||
|
<input
|
||||||
|
id="party-title"
|
||||||
|
type="text"
|
||||||
|
bind:value={editingTitle}
|
||||||
|
placeholder="Enter party title..."
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet footer()}
|
||||||
|
<button
|
||||||
|
class="btn-secondary"
|
||||||
|
onclick={() => (editDialogOpen = false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
onclick={savePartyTitle}
|
||||||
|
disabled={loading || !editingTitle.trim()}
|
||||||
|
>
|
||||||
|
{loading ? 'Saving...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.page-wrap { position: relative; --panel-w: 380px; overflow-x: auto; }
|
.page-wrap { position: relative; --panel-w: 380px; overflow-x: auto; }
|
||||||
.track { display: flex; gap: 0; align-items: flex-start; }
|
.track { display: flex; gap: 0; align-items: flex-start; }
|
||||||
|
|
@ -437,6 +525,7 @@
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-btn,
|
||||||
.favorite-btn,
|
.favorite-btn,
|
||||||
.remix-btn {
|
.remix-btn {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
|
|
@ -446,6 +535,10 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.favorite-btn {
|
.favorite-btn {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
|
@ -456,11 +549,13 @@
|
||||||
color: gold;
|
color: gold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover,
|
||||||
.favorite-btn:hover,
|
.favorite-btn:hover,
|
||||||
.remix-btn:hover {
|
.remix-btn:hover {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-btn:disabled,
|
||||||
.favorite-btn:disabled,
|
.favorite-btn:disabled,
|
||||||
.remix-btn:disabled {
|
.remix-btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
@ -561,4 +656,75 @@
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Edit form styles */
|
||||||
|
.edit-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-secondary, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form input {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--border-color, #ddd);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--focus-ring, #3366ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form input:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog buttons */
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--button-primary-bg, #3366ff);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: var(--button-primary-hover, #2855cc);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: white;
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
border: 1px solid var(--border-color, #ddd);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
import type { Party } from '$lib/types/api/party'
|
import type { Party } from '$lib/types/api/party'
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
import Icon from '$lib/components/Icon.svelte'
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
|
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
|
||||||
|
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item?: GridCharacter
|
item?: GridCharacter
|
||||||
|
|
@ -55,35 +57,92 @@
|
||||||
|
|
||||||
async function remove() {
|
async function remove() {
|
||||||
if (!item?.id) return
|
if (!item?.id) return
|
||||||
const party = ctx.getParty()
|
try {
|
||||||
const editKey = ctx.getEditKey()
|
const party = ctx.getParty()
|
||||||
const updated = await ctx.services.gridService.removeCharacter(party.id, item.id as any, editKey || undefined)
|
const editKey = ctx.getEditKey()
|
||||||
ctx.updateParty(updated)
|
const updated = await ctx.services.gridService.removeCharacter(party.id, item.id as any, editKey || undefined)
|
||||||
|
if (updated) {
|
||||||
|
ctx.updateParty(updated)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error removing character:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewDetails() {
|
||||||
|
// TODO: Implement view details modal
|
||||||
|
console.log('View details for:', item)
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace() {
|
||||||
|
if (ctx?.openPicker) {
|
||||||
|
ctx.openPicker({ type: 'character', position, item })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="unit" class:empty={!item}>
|
<div class="unit" class:empty={!item}>
|
||||||
{#key (item ? (item as any).id ?? position : `empty-${position}`)}
|
{#if item}
|
||||||
<div
|
<ContextMenu>
|
||||||
class="frame character cell"
|
{#snippet children()}
|
||||||
class:editable={ctx?.canEdit()}
|
{#key (item as any).id ?? position}
|
||||||
on:click={() => ctx?.openPicker && ctx.openPicker({ type: 'character', position, item })}
|
<div
|
||||||
>
|
class="frame character cell"
|
||||||
<img class="image" class:placeholder={!item || !(item?.character?.granblueId || (item as any)?.object?.granblueId)} alt={item ? displayName(item?.character || (item as any)?.object) : ''} src={imageUrl()} />
|
class:editable={ctx?.canEdit()}
|
||||||
{#if !item && ctx?.canEdit()}
|
onclick={() => ctx?.canEdit() && replace()}
|
||||||
<span class="icon">
|
>
|
||||||
<Icon name="plus" size={24} />
|
<img
|
||||||
</span>
|
class="image"
|
||||||
{/if}
|
class:placeholder={!(item?.character?.granblueId || (item as any)?.object?.granblueId)}
|
||||||
{#if ctx.canEdit() && item?.id}
|
alt={displayName(item?.character || (item as any)?.object)}
|
||||||
<div class="actions">
|
src={imageUrl()}
|
||||||
<button class="remove" title="Remove" on:click|stopPropagation={remove}>×</button>
|
/>
|
||||||
</div>
|
{#if ctx?.canEdit() && item?.id}
|
||||||
{/if}
|
<div class="actions">
|
||||||
</div>
|
<button class="remove" title="Remove" onclick={(e) => { e.stopPropagation(); remove() }}>×</button>
|
||||||
{/key}
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet menu()}
|
||||||
|
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}>
|
||||||
|
View Details
|
||||||
|
</ContextMenuBase.Item>
|
||||||
|
{#if ctx?.canEdit()}
|
||||||
|
<ContextMenuBase.Item class="context-menu-item" onclick={replace}>
|
||||||
|
Replace
|
||||||
|
</ContextMenuBase.Item>
|
||||||
|
<ContextMenuBase.Separator class="context-menu-separator" />
|
||||||
|
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}>
|
||||||
|
Remove
|
||||||
|
</ContextMenuBase.Item>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</ContextMenu>
|
||||||
|
{:else}
|
||||||
|
{#key `empty-${position}`}
|
||||||
|
<div
|
||||||
|
class="frame character cell"
|
||||||
|
class:editable={ctx?.canEdit()}
|
||||||
|
onclick={() => ctx?.canEdit() && ctx?.openPicker && ctx.openPicker({ type: 'character', position, item })}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="image placeholder"
|
||||||
|
alt=""
|
||||||
|
src="/images/placeholders/placeholder-weapon-grid.png"
|
||||||
|
/>
|
||||||
|
{#if ctx?.canEdit()}
|
||||||
|
<span class="icon">
|
||||||
|
<Icon name="plus" size={24} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
<div class="name">{item ? displayName(item?.character || (item as any)?.object) : ''}</div>
|
<div class="name">{item ? displayName(item?.character || (item as any)?.object) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
import type { Party } from '$lib/types/api/party'
|
import type { Party } from '$lib/types/api/party'
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
import Icon from '$lib/components/Icon.svelte'
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
|
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
|
||||||
|
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item?: GridSummon
|
item?: GridSummon
|
||||||
|
|
@ -49,44 +51,104 @@
|
||||||
|
|
||||||
async function remove() {
|
async function remove() {
|
||||||
if (!item?.id) return
|
if (!item?.id) return
|
||||||
const party = ctx.getParty()
|
try {
|
||||||
const editKey = ctx.getEditKey()
|
const party = ctx.getParty()
|
||||||
const updated = await ctx.services.gridService.removeSummon(party.id, item.id as any, editKey || undefined)
|
const editKey = ctx.getEditKey()
|
||||||
ctx.updateParty(updated)
|
const updated = await ctx.services.gridService.removeSummon(party.id, item.id as any, editKey || undefined)
|
||||||
|
if (updated) {
|
||||||
|
ctx.updateParty(updated)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error removing summon:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewDetails() {
|
||||||
|
// TODO: Implement view details modal
|
||||||
|
console.log('View details for:', item)
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace() {
|
||||||
|
if (ctx?.openPicker) {
|
||||||
|
ctx.openPicker({ type: 'summon', position, item })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="unit" class:empty={!item}>
|
<div class="unit" class:empty={!item}>
|
||||||
{#key (item ? (item as any).id ?? position : `empty-${position}`)}
|
{#if item}
|
||||||
<div
|
<ContextMenu>
|
||||||
class="frame summon"
|
{#snippet children()}
|
||||||
class:main={item?.main || position === -1}
|
{#key (item as any).id ?? position}
|
||||||
class:friend={item?.friend || position === 6}
|
<div
|
||||||
class:cell={!((item?.main || position === -1) || (item?.friend || position === 6))}
|
class="frame summon"
|
||||||
class:editable={ctx?.canEdit()}
|
class:main={item?.main || position === -1}
|
||||||
on:click={() => ctx?.openPicker && ctx.openPicker({ type: 'summon', position, item })}
|
class:friend={item?.friend || position === 6}
|
||||||
>
|
class:cell={!((item?.main || position === -1) || (item?.friend || position === 6))}
|
||||||
<img class="image" class:placeholder={!item || !(item?.summon?.granblueId || (item as any)?.object?.granblueId)} alt={item ? displayName(item?.summon || (item as any)?.object) : ''} src={imageUrl()} />
|
class:editable={ctx?.canEdit()}
|
||||||
{#if !item && ctx?.canEdit()}
|
onclick={() => ctx?.canEdit() && replace()}
|
||||||
<span class="icon">
|
>
|
||||||
<Icon name="plus" size={24} />
|
<img
|
||||||
</span>
|
class="image"
|
||||||
{/if}
|
class:placeholder={!(item?.summon?.granblueId || (item as any)?.object?.granblueId)}
|
||||||
{#if ctx.canEdit() && item?.id}
|
alt={displayName(item?.summon || (item as any)?.object)}
|
||||||
<div class="actions">
|
src={imageUrl()}
|
||||||
<button class="remove" title="Remove" on:click|stopPropagation={remove}>×</button>
|
/>
|
||||||
</div>
|
{#if ctx?.canEdit() && item?.id}
|
||||||
{/if}
|
<div class="actions">
|
||||||
{#if item?.main || position === -1}
|
<button class="remove" title="Remove" onclick={(e) => { e.stopPropagation(); remove() }}>×</button>
|
||||||
<span class="badge">Main</span>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if item?.friend || position === 6}
|
{#if item?.main || position === -1}
|
||||||
<span class="badge" style="left:auto; right:6px">Friend</span>
|
<span class="badge">Main</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{#if item?.friend || position === 6}
|
||||||
{/key}
|
<span class="badge" style="left:auto; right:6px">Friend</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet menu()}
|
||||||
|
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}>
|
||||||
|
View Details
|
||||||
|
</ContextMenuBase.Item>
|
||||||
|
{#if ctx?.canEdit()}
|
||||||
|
<ContextMenuBase.Item class="context-menu-item" onclick={replace}>
|
||||||
|
Replace
|
||||||
|
</ContextMenuBase.Item>
|
||||||
|
<ContextMenuBase.Separator class="context-menu-separator" />
|
||||||
|
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}>
|
||||||
|
Remove
|
||||||
|
</ContextMenuBase.Item>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</ContextMenu>
|
||||||
|
{:else}
|
||||||
|
{#key `empty-${position}`}
|
||||||
|
<div
|
||||||
|
class="frame summon"
|
||||||
|
class:main={position === -1}
|
||||||
|
class:friend={position === 6}
|
||||||
|
class:cell={!(position === -1 || position === 6)}
|
||||||
|
class:editable={ctx?.canEdit()}
|
||||||
|
onclick={() => ctx?.canEdit() && ctx?.openPicker && ctx.openPicker({ type: 'summon', position, item })}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="image placeholder"
|
||||||
|
alt=""
|
||||||
|
src={position === -1 || position === 6 ? '/images/placeholders/placeholder-summon-main.png' : '/images/placeholders/placeholder-summon-sub.png'}
|
||||||
|
/>
|
||||||
|
{#if ctx?.canEdit()}
|
||||||
|
<span class="icon">
|
||||||
|
<Icon name="plus" size={24} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
<div class="name">{item ? displayName(item?.summon || (item as any)?.object) : ''}</div>
|
<div class="name">{item ? displayName(item?.summon || (item as any)?.object) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
import type { Party } from '$lib/types/api/party'
|
import type { Party } from '$lib/types/api/party'
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
import Icon from '$lib/components/Icon.svelte'
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
|
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
|
||||||
|
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item?: GridWeapon
|
item?: GridWeapon
|
||||||
|
|
@ -57,40 +59,98 @@
|
||||||
|
|
||||||
async function remove() {
|
async function remove() {
|
||||||
if (!item?.id) return
|
if (!item?.id) return
|
||||||
const party = ctx.getParty()
|
try {
|
||||||
const editKey = ctx.getEditKey()
|
const party = ctx.getParty()
|
||||||
const updated = await ctx.services.gridService.removeWeapon(party.id, item.id as any, editKey || undefined)
|
const editKey = ctx.getEditKey()
|
||||||
ctx.updateParty(updated)
|
const updated = await ctx.services.gridService.removeWeapon(party.id, item.id as any, editKey || undefined)
|
||||||
|
if (updated) {
|
||||||
|
ctx.updateParty(updated)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error removing weapon:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewDetails() {
|
||||||
|
// TODO: Implement view details modal
|
||||||
|
console.log('View details for:', item)
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace() {
|
||||||
|
if (ctx?.openPicker) {
|
||||||
|
ctx.openPicker({ type: 'weapon', position, item })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="unit" class:empty={!item}>
|
<div class="unit" class:empty={!item}>
|
||||||
{#key (item ? (item as any).id ?? position : `empty-${position}`)}
|
{#if item}
|
||||||
<div
|
<ContextMenu>
|
||||||
class="frame weapon"
|
{#snippet children()}
|
||||||
class:main={item?.mainhand || position === -1}
|
{#key (item as any).id ?? position}
|
||||||
class:cell={!(item?.mainhand || position === -1)}
|
<div
|
||||||
class:editable={ctx?.canEdit()}
|
class="frame weapon"
|
||||||
on:click={() => ctx?.openPicker && ctx.openPicker({ type: 'weapon', position, item })}
|
class:main={item?.mainhand || position === -1}
|
||||||
>
|
class:cell={!(item?.mainhand || position === -1)}
|
||||||
<img class="image" class:placeholder={!item || !(item?.weapon?.granblueId || (item as any)?.object?.granblueId)} alt={item ? displayName(item?.weapon || (item as any)?.object) : ''} src={imageUrl()} />
|
class:editable={ctx?.canEdit()}
|
||||||
{#if !item && ctx?.canEdit()}
|
>
|
||||||
<span class="icon">
|
<img
|
||||||
<Icon name="plus" size={24} />
|
class="image"
|
||||||
</span>
|
class:placeholder={!(item?.weapon?.granblueId || (item as any)?.object?.granblueId)}
|
||||||
{/if}
|
alt={displayName(item?.weapon || (item as any)?.object)}
|
||||||
{#if ctx.canEdit() && item?.id}
|
src={imageUrl()}
|
||||||
<div class="actions">
|
/>
|
||||||
<button class="remove" title="Remove" on:click|stopPropagation={remove}>×</button>
|
{#if ctx?.canEdit() && item?.id}
|
||||||
</div>
|
<div class="actions">
|
||||||
{/if}
|
<button class="remove" title="Remove" onclick={(e) => { e.stopPropagation(); remove() }}>×</button>
|
||||||
{#if item?.mainhand || position === -1}
|
</div>
|
||||||
<span class="badge">Main</span>
|
{/if}
|
||||||
{/if}
|
{#if item?.mainhand || position === -1}
|
||||||
</div>
|
<span class="badge">Main</span>
|
||||||
{/key}
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet menu()}
|
||||||
|
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}>
|
||||||
|
View Details
|
||||||
|
</ContextMenuBase.Item>
|
||||||
|
{#if ctx?.canEdit()}
|
||||||
|
<ContextMenuBase.Item class="context-menu-item" onclick={replace}>
|
||||||
|
Replace
|
||||||
|
</ContextMenuBase.Item>
|
||||||
|
<ContextMenuBase.Separator class="context-menu-separator" />
|
||||||
|
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}>
|
||||||
|
Remove
|
||||||
|
</ContextMenuBase.Item>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</ContextMenu>
|
||||||
|
{:else}
|
||||||
|
{#key `empty-${position}`}
|
||||||
|
<div
|
||||||
|
class="frame weapon"
|
||||||
|
class:main={position === -1}
|
||||||
|
class:cell={position !== -1}
|
||||||
|
class:editable={ctx?.canEdit()}
|
||||||
|
onclick={() => ctx?.canEdit() && ctx?.openPicker && ctx.openPicker({ type: 'weapon', position, item })}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="image placeholder"
|
||||||
|
alt=""
|
||||||
|
src={position === -1 ? '/images/placeholders/placeholder-weapon-main.png' : '/images/placeholders/placeholder-weapon-grid.png'}
|
||||||
|
/>
|
||||||
|
{#if ctx?.canEdit()}
|
||||||
|
<span class="icon">
|
||||||
|
<Icon name="plus" size={24} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
<div class="name">{item ? displayName(item?.weapon || (item as any)?.object) : ''}</div>
|
<div class="name">{item ? displayName(item?.weapon || (item as any)?.object) : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue