add party edit sidebar with raid selection
This commit is contained in:
parent
b5cc2b7b9f
commit
cf29c00c8d
4 changed files with 598 additions and 132 deletions
|
|
@ -54,12 +54,17 @@
|
||||||
import Icon from '$lib/components/Icon.svelte'
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
import DescriptionRenderer from '$lib/components/DescriptionRenderer.svelte'
|
import DescriptionRenderer from '$lib/components/DescriptionRenderer.svelte'
|
||||||
import { openDescriptionSidebar } from '$lib/features/description/openDescriptionSidebar.svelte'
|
import { openDescriptionSidebar } from '$lib/features/description/openDescriptionSidebar.svelte'
|
||||||
|
import {
|
||||||
|
openPartyEditSidebar,
|
||||||
|
type PartyEditValues
|
||||||
|
} from '$lib/features/party/openPartyEditSidebar.svelte'
|
||||||
|
import PartyInfoGrid from '$lib/components/party/info/PartyInfoGrid.svelte'
|
||||||
import { DropdownMenu } from 'bits-ui'
|
import { DropdownMenu } from 'bits-ui'
|
||||||
import DropdownItem from '$lib/components/ui/dropdown/DropdownItem.svelte'
|
import DropdownItem from '$lib/components/ui/dropdown/DropdownItem.svelte'
|
||||||
import JobSection from '$lib/components/job/JobSection.svelte'
|
import JobSection from '$lib/components/job/JobSection.svelte'
|
||||||
import { Gender } from '$lib/utils/jobUtils'
|
import { Gender } from '$lib/utils/jobUtils'
|
||||||
import { openJobSelectionSidebar, openJobSkillSelectionSidebar } from '$lib/features/job/openJobSidebar.svelte'
|
import { openJobSelectionSidebar, openJobSkillSelectionSidebar } from '$lib/features/job/openJobSidebar.svelte'
|
||||||
import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
import { partyAdapter, type UpdatePartyParams } from '$lib/api/adapters/party.adapter'
|
||||||
import { extractErrorMessage } from '$lib/utils/errors'
|
import { extractErrorMessage } from '$lib/utils/errors'
|
||||||
import { transformSkillsToArray } from '$lib/utils/jobSkills'
|
import { transformSkillsToArray } from '$lib/utils/jobSkills'
|
||||||
import { findNextEmptySlot, SLOT_NOT_FOUND } from '$lib/utils/gridHelpers'
|
import { findNextEmptySlot, SLOT_NOT_FOUND } from '$lib/utils/gridHelpers'
|
||||||
|
|
@ -269,6 +274,16 @@
|
||||||
return result.canEdit
|
return result.canEdit
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Element mapping for theming (used for party element which is numeric)
|
||||||
|
const ELEMENT_MAP: Record<number, 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'> = {
|
||||||
|
1: 'wind',
|
||||||
|
2: 'fire',
|
||||||
|
3: 'water',
|
||||||
|
4: 'earth',
|
||||||
|
5: 'dark',
|
||||||
|
6: 'light'
|
||||||
|
}
|
||||||
|
|
||||||
// Derived elements for character image logic
|
// Derived elements for character image logic
|
||||||
const mainWeapon = $derived(
|
const mainWeapon = $derived(
|
||||||
(party?.weapons ?? []).find((w) => w?.mainhand || w?.position === -1)
|
(party?.weapons ?? []).find((w) => w?.mainhand || w?.position === -1)
|
||||||
|
|
@ -276,6 +291,10 @@
|
||||||
const mainWeaponElement = $derived(mainWeapon?.element ?? mainWeapon?.weapon?.element)
|
const mainWeaponElement = $derived(mainWeapon?.element ?? mainWeapon?.weapon?.element)
|
||||||
const partyElement = $derived((party as any)?.element)
|
const partyElement = $derived((party as any)?.element)
|
||||||
|
|
||||||
|
// User's element preference (string) - used for UI theming
|
||||||
|
type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||||
|
const userElement = $derived(party.user?.avatar?.element as ElementType | undefined)
|
||||||
|
|
||||||
// Check if any items in the party are linked to collection (for sync menu option)
|
// Check if any items in the party are linked to collection (for sync menu option)
|
||||||
const hasCollectionLinks = $derived.by(() => {
|
const hasCollectionLinks = $derived.by(() => {
|
||||||
const hasLinkedWeapons = (party?.weapons ?? []).some((w) => w?.collectionWeaponId)
|
const hasLinkedWeapons = (party?.weapons ?? []).some((w) => w?.collectionWeaponId)
|
||||||
|
|
@ -333,7 +352,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Party operations
|
// Party operations
|
||||||
async function updatePartyDetails(updates: Partial<Party>) {
|
async function updatePartyDetails(updates: Omit<UpdatePartyParams, 'id' | 'shortcode'>) {
|
||||||
if (!canEdit()) return
|
if (!canEdit()) return
|
||||||
|
|
||||||
loading = true
|
loading = true
|
||||||
|
|
@ -341,7 +360,8 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use TanStack Query mutation to update party
|
// Use TanStack Query mutation to update party
|
||||||
await updatePartyMutation.mutateAsync({ shortcode: party.shortcode, ...updates })
|
// Note: API expects UUID (id), shortcode is for cache invalidation
|
||||||
|
await updatePartyMutation.mutateAsync({ id: party.id, shortcode: party.shortcode, ...updates })
|
||||||
// Party will be updated via cache invalidation
|
// Party will be updated via cache invalidation
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message || 'Failed to update party'
|
error = err.message || 'Failed to update party'
|
||||||
|
|
@ -417,6 +437,45 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSettingsPanel() {
|
||||||
|
if (!canEdit()) return
|
||||||
|
|
||||||
|
const initialValues: PartyEditValues = {
|
||||||
|
name: party.name ?? '',
|
||||||
|
fullAuto: party.fullAuto ?? false,
|
||||||
|
autoGuard: party.autoGuard ?? false,
|
||||||
|
autoSummon: party.autoSummon ?? false,
|
||||||
|
chargeAttack: party.chargeAttack ?? true,
|
||||||
|
clearTime: party.clearTime ?? null,
|
||||||
|
buttonCount: party.buttonCount ?? null,
|
||||||
|
chainCount: party.chainCount ?? null,
|
||||||
|
summonCount: party.summonCount ?? null,
|
||||||
|
videoUrl: party.videoUrl ?? null,
|
||||||
|
raid: party.raid ?? null,
|
||||||
|
raidId: party.raid?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
openPartyEditSidebar({
|
||||||
|
initialValues,
|
||||||
|
element: userElement,
|
||||||
|
onSave: async (values) => {
|
||||||
|
await updatePartyDetails({
|
||||||
|
name: values.name,
|
||||||
|
fullAuto: values.fullAuto,
|
||||||
|
autoGuard: values.autoGuard,
|
||||||
|
autoSummon: values.autoSummon,
|
||||||
|
chargeAttack: values.chargeAttack,
|
||||||
|
clearTime: values.clearTime,
|
||||||
|
buttonCount: values.buttonCount,
|
||||||
|
chainCount: values.chainCount,
|
||||||
|
summonCount: values.summonCount,
|
||||||
|
videoUrl: values.videoUrl,
|
||||||
|
raidId: values.raidId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteParty() {
|
async function deleteParty() {
|
||||||
// Only allow deletion if user owns the party
|
// Only allow deletion if user owns the party
|
||||||
if (party.user?.id !== authUserId) return
|
if (party.user?.id !== authUserId) return
|
||||||
|
|
@ -888,7 +947,7 @@
|
||||||
<DropdownMenu.Content class="dropdown-content" sideOffset={6} align="end">
|
<DropdownMenu.Content class="dropdown-content" sideOffset={6} align="end">
|
||||||
{#if canEdit()}
|
{#if canEdit()}
|
||||||
<DropdownItem>
|
<DropdownItem>
|
||||||
<button onclick={openEditDialog} disabled={loading}>Edit</button>
|
<button onclick={openSettingsPanel} disabled={loading}>Edit</button>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
{#if hasCollectionLinks}
|
{#if hasCollectionLinks}
|
||||||
<DropdownItem>
|
<DropdownItem>
|
||||||
|
|
@ -926,41 +985,12 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if party.description || party.raid}
|
<PartyInfoGrid
|
||||||
<div class="cards">
|
{party}
|
||||||
{#if party.description}
|
canEdit={canEdit()}
|
||||||
<div
|
onOpenDescription={openDescriptionPanel}
|
||||||
class="description-card clickable"
|
onOpenEdit={openSettingsPanel}
|
||||||
onclick={openDescriptionPanel}
|
/>
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
onkeydown={(e) => e.key === 'Enter' && openDescriptionPanel()}
|
|
||||||
aria-label="View full description"
|
|
||||||
>
|
|
||||||
<h2 class="card-label">Description</h2>
|
|
||||||
<div class="card-content">
|
|
||||||
<DescriptionRenderer content={party.description} truncate={true} maxLines={4} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if party.raid}
|
|
||||||
<div class="raid-card">
|
|
||||||
<h2 class="card-label">Raid</h2>
|
|
||||||
<div class="raid-content">
|
|
||||||
<span class="raid-name">
|
|
||||||
{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>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<PartySegmentedControl
|
<PartySegmentedControl
|
||||||
selectedTab={activeTab}
|
selectedTab={activeTab}
|
||||||
|
|
@ -1230,99 +1260,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cards container
|
|
||||||
.cards {
|
|
||||||
display: flex;
|
|
||||||
gap: $unit-2x;
|
|
||||||
|
|
||||||
// Individual card styles
|
|
||||||
.description-card,
|
|
||||||
.raid-card {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0; // Allow flexbox to shrink items
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 0.5px solid var(--button-bg);
|
|
||||||
border-radius: $card-corner;
|
|
||||||
padding: $unit-2x;
|
|
||||||
// box-shadow: $card-elevation;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.card-label {
|
|
||||||
margin: 0 0 $unit 0;
|
|
||||||
font-size: $font-small;
|
|
||||||
font-weight: $bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-text {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: $font-regular;
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
// Text truncation after 3 lines
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-content {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-hint {
|
|
||||||
display: none;
|
|
||||||
margin-top: $unit;
|
|
||||||
font-size: $font-small;
|
|
||||||
color: var(--accent-blue);
|
|
||||||
font-weight: $medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
@include smooth-transition($duration-quick, box-shadow);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: $card-elevation-hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Specific styling for raid card
|
|
||||||
.raid-card {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
min-width: 250px;
|
|
||||||
|
|
||||||
.raid-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit-half;
|
|
||||||
}
|
|
||||||
|
|
||||||
.raid-name {
|
|
||||||
font-weight: $bold;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: $font-regular;
|
|
||||||
}
|
|
||||||
|
|
||||||
.raid-difficulty {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: $font-small;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description card takes up more space
|
|
||||||
.description-card {
|
|
||||||
flex: 2;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
padding: $unit-three-quarter;
|
padding: $unit-three-quarter;
|
||||||
background: rgba(209, 58, 58, 0.1); // Using raw value since CSS variables don't work in rgba()
|
background: rgba(209, 58, 58, 0.1); // Using raw value since CSS variables don't work in rgba()
|
||||||
|
|
|
||||||
153
src/lib/components/sidebar/EditRaidPane.svelte
Normal file
153
src/lib/components/sidebar/EditRaidPane.svelte
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* EditRaidPane - Raid selection pane for the sidebar
|
||||||
|
*
|
||||||
|
* Allows users to browse and select a raid from organized groups.
|
||||||
|
* Features section tabs (Raids/Events/Solo), search, and sort toggle.
|
||||||
|
*/
|
||||||
|
import type { RaidFull } from '$lib/types/api/raid'
|
||||||
|
import type { Raid } from '$lib/types/api/entities'
|
||||||
|
import { createQuery } from '@tanstack/svelte-query'
|
||||||
|
import { raidQueries } from '$lib/api/queries/raid.queries'
|
||||||
|
import { RaidSection } from '$lib/utils/raidSection'
|
||||||
|
import RaidSectionTabs from '$lib/components/raid/RaidSectionTabs.svelte'
|
||||||
|
import RaidGroupList from '$lib/components/raid/RaidGroupList.svelte'
|
||||||
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
|
import Input from '$lib/components/ui/Input.svelte'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Currently selected raid */
|
||||||
|
currentRaid?: Raid | null
|
||||||
|
/** Callback when a raid is selected */
|
||||||
|
onSelect: (raid: RaidFull | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
let { currentRaid, onSelect }: Props = $props()
|
||||||
|
|
||||||
|
// State
|
||||||
|
let searchQuery = $state('')
|
||||||
|
let selectedSection = $state(RaidSection.Raid)
|
||||||
|
let sortAscending = $state(false)
|
||||||
|
|
||||||
|
// Fetch all raid groups
|
||||||
|
const groupsQuery = createQuery(() => raidQueries.groups())
|
||||||
|
|
||||||
|
// Filter groups by section and sort
|
||||||
|
const filteredGroups = $derived(() => {
|
||||||
|
if (!groupsQuery.data) return []
|
||||||
|
|
||||||
|
// Filter by section
|
||||||
|
let groups = groupsQuery.data.filter((group) => {
|
||||||
|
const groupSection =
|
||||||
|
typeof group.section === 'string' ? parseInt(group.section, 10) : group.section
|
||||||
|
return groupSection === selectedSection
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sort by difficulty
|
||||||
|
groups = [...groups].sort((a, b) => {
|
||||||
|
const diff = a.difficulty - b.difficulty
|
||||||
|
return sortAscending ? diff : -diff
|
||||||
|
})
|
||||||
|
|
||||||
|
return groups
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleRaidSelect(raid: RaidFull) {
|
||||||
|
// Toggle: clicking selected raid unselects it
|
||||||
|
if (raid.id === currentRaid?.id) {
|
||||||
|
onSelect(null)
|
||||||
|
} else {
|
||||||
|
onSelect(raid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSort() {
|
||||||
|
sortAscending = !sortAscending
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading = $derived(groupsQuery.isLoading)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="edit-raid-pane">
|
||||||
|
<!-- Search Input -->
|
||||||
|
<div class="search-section">
|
||||||
|
<Input
|
||||||
|
leftIcon="search"
|
||||||
|
placeholder="Search raids..."
|
||||||
|
bind:value={searchQuery}
|
||||||
|
contained
|
||||||
|
clearable
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls: Tabs + Sort -->
|
||||||
|
<div class="controls-section">
|
||||||
|
<RaidSectionTabs bind:value={selectedSection} />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
iconOnly
|
||||||
|
icon={sortAscending ? 'arrow-up' : 'arrow-down'}
|
||||||
|
onclick={toggleSort}
|
||||||
|
title="Toggle sort order"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Raid List -->
|
||||||
|
<div class="raid-list-container">
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="loading-state">
|
||||||
|
<span>Loading raids...</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<RaidGroupList
|
||||||
|
groups={filteredGroups()}
|
||||||
|
selectedRaidId={currentRaid?.id}
|
||||||
|
onSelect={handleRaidSelect}
|
||||||
|
{searchQuery}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as *;
|
||||||
|
@use '$src/themes/typography' as *;
|
||||||
|
@use '$src/themes/layout' as *;
|
||||||
|
@use '$src/themes/effects' as *;
|
||||||
|
@use '$src/themes/colors' as *;
|
||||||
|
|
||||||
|
.edit-raid-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-section {
|
||||||
|
padding: $unit-2x $unit-2x $unit $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit;
|
||||||
|
padding: $unit $unit-2x $unit-2x $unit-2x;
|
||||||
|
border-bottom: 1px solid var(--border-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.raid-list-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: $unit-4x;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
342
src/lib/components/sidebar/PartyEditSidebar.svelte
Normal file
342
src/lib/components/sidebar/PartyEditSidebar.svelte
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* PartyEditSidebar - Edit party metadata (settings, performance, video)
|
||||||
|
*
|
||||||
|
* This sidebar is opened via openPartyEditSidebar() and provides
|
||||||
|
* editing for battle settings, clear time, metrics, video URL, and raid.
|
||||||
|
*/
|
||||||
|
import DetailsSection from './details/DetailsSection.svelte'
|
||||||
|
import DetailRow from './details/DetailRow.svelte'
|
||||||
|
import Input from '$lib/components/ui/Input.svelte'
|
||||||
|
import BattleSettingsSection from '$lib/components/party/edit/BattleSettingsSection.svelte'
|
||||||
|
import ClearTimeInput from '$lib/components/party/edit/ClearTimeInput.svelte'
|
||||||
|
import YouTubeUrlInput from '$lib/components/party/edit/YouTubeUrlInput.svelte'
|
||||||
|
import MetricField from '$lib/components/party/edit/MetricField.svelte'
|
||||||
|
import EditRaidPane from '$lib/components/sidebar/EditRaidPane.svelte'
|
||||||
|
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||||
|
import { usePaneStack } from '$lib/stores/paneStack.svelte'
|
||||||
|
import { untrack } from 'svelte'
|
||||||
|
import type { Raid } from '$lib/types/api/entities'
|
||||||
|
import type { RaidFull } from '$lib/types/api/raid'
|
||||||
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
|
|
||||||
|
export interface PartyEditValues {
|
||||||
|
name: string
|
||||||
|
fullAuto: boolean
|
||||||
|
autoGuard: boolean
|
||||||
|
autoSummon: boolean
|
||||||
|
chargeAttack: boolean
|
||||||
|
clearTime: number | null
|
||||||
|
buttonCount: number | null
|
||||||
|
chainCount: number | null
|
||||||
|
summonCount: number | null
|
||||||
|
videoUrl: string | null
|
||||||
|
raid: Raid | null
|
||||||
|
raidId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Current party values */
|
||||||
|
initialValues: PartyEditValues
|
||||||
|
/** Party element for switch theming */
|
||||||
|
element?: ElementType
|
||||||
|
/** Callback when save is requested */
|
||||||
|
onSave?: (values: PartyEditValues) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
let { initialValues, element, onSave }: Props = $props()
|
||||||
|
|
||||||
|
// Get the pane stack for pushing EditRaidPane
|
||||||
|
const paneStack = usePaneStack()
|
||||||
|
|
||||||
|
// Local state - initialized from initialValues
|
||||||
|
let name = $state(initialValues.name)
|
||||||
|
let fullAuto = $state(initialValues.fullAuto)
|
||||||
|
let autoGuard = $state(initialValues.autoGuard)
|
||||||
|
let autoSummon = $state(initialValues.autoSummon)
|
||||||
|
let chargeAttack = $state(initialValues.chargeAttack)
|
||||||
|
let clearTime = $state(initialValues.clearTime)
|
||||||
|
let buttonCount = $state(initialValues.buttonCount)
|
||||||
|
let chainCount = $state(initialValues.chainCount)
|
||||||
|
let summonCount = $state(initialValues.summonCount)
|
||||||
|
let videoUrl = $state(initialValues.videoUrl)
|
||||||
|
let raid = $state<Raid | null>(initialValues.raid)
|
||||||
|
let raidId = $state<string | null>(initialValues.raidId)
|
||||||
|
|
||||||
|
// Check if any values have changed
|
||||||
|
const hasChanges = $derived(
|
||||||
|
name !== initialValues.name ||
|
||||||
|
fullAuto !== initialValues.fullAuto ||
|
||||||
|
autoGuard !== initialValues.autoGuard ||
|
||||||
|
autoSummon !== initialValues.autoSummon ||
|
||||||
|
chargeAttack !== initialValues.chargeAttack ||
|
||||||
|
clearTime !== initialValues.clearTime ||
|
||||||
|
buttonCount !== initialValues.buttonCount ||
|
||||||
|
chainCount !== initialValues.chainCount ||
|
||||||
|
summonCount !== initialValues.summonCount ||
|
||||||
|
videoUrl !== initialValues.videoUrl ||
|
||||||
|
raidId !== initialValues.raidId
|
||||||
|
)
|
||||||
|
|
||||||
|
// Expose save function for sidebar action button
|
||||||
|
export function save() {
|
||||||
|
const values: PartyEditValues = {
|
||||||
|
name,
|
||||||
|
fullAuto,
|
||||||
|
autoGuard,
|
||||||
|
autoSummon,
|
||||||
|
chargeAttack,
|
||||||
|
clearTime,
|
||||||
|
buttonCount,
|
||||||
|
chainCount,
|
||||||
|
summonCount,
|
||||||
|
videoUrl,
|
||||||
|
raid,
|
||||||
|
raidId
|
||||||
|
}
|
||||||
|
onSave?.(values)
|
||||||
|
sidebar.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sidebar action button state based on changes
|
||||||
|
$effect(() => {
|
||||||
|
// Read hasChanges to track it
|
||||||
|
const changed = hasChanges
|
||||||
|
// Use untrack to prevent tracking sidebar mutations
|
||||||
|
untrack(() => {
|
||||||
|
if (changed) {
|
||||||
|
sidebar.setAction(save, 'Save', element)
|
||||||
|
} else {
|
||||||
|
sidebar.setAction(undefined, 'Save', element)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleSettingsChange(
|
||||||
|
field: 'fullAuto' | 'autoGuard' | 'autoSummon' | 'chargeAttack',
|
||||||
|
value: boolean
|
||||||
|
) {
|
||||||
|
if (field === 'fullAuto') fullAuto = value
|
||||||
|
else if (field === 'autoGuard') autoGuard = value
|
||||||
|
else if (field === 'autoSummon') autoSummon = value
|
||||||
|
else chargeAttack = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRaidName(r: Raid | null): string {
|
||||||
|
if (!r) return ''
|
||||||
|
if (typeof r.name === 'string') return r.name
|
||||||
|
return r.name?.en || r.name?.ja || 'Unknown Raid'
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRaidPane() {
|
||||||
|
paneStack.push({
|
||||||
|
id: 'edit-raid',
|
||||||
|
title: 'Select Raid',
|
||||||
|
component: EditRaidPane,
|
||||||
|
props: {
|
||||||
|
currentRaid: raid,
|
||||||
|
onSelect: handleRaidSelected
|
||||||
|
},
|
||||||
|
scrollable: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRaidElementClass(r: Raid | null): string {
|
||||||
|
if (!r) return ''
|
||||||
|
const elementMap: Record<number, string> = {
|
||||||
|
1: 'wind',
|
||||||
|
2: 'fire',
|
||||||
|
3: 'water',
|
||||||
|
4: 'earth',
|
||||||
|
5: 'dark',
|
||||||
|
6: 'light'
|
||||||
|
}
|
||||||
|
return elementMap[r.element] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRaidSelected(selectedRaid: RaidFull | null) {
|
||||||
|
if (selectedRaid) {
|
||||||
|
// Convert RaidFull to Raid (they have compatible structures)
|
||||||
|
raid = {
|
||||||
|
id: selectedRaid.id,
|
||||||
|
slug: selectedRaid.slug,
|
||||||
|
name: selectedRaid.name,
|
||||||
|
level: selectedRaid.level,
|
||||||
|
element: selectedRaid.element,
|
||||||
|
group: selectedRaid.group
|
||||||
|
? {
|
||||||
|
id: selectedRaid.group.id,
|
||||||
|
name: selectedRaid.group.name,
|
||||||
|
section: String(selectedRaid.group.section),
|
||||||
|
order: selectedRaid.group.order,
|
||||||
|
difficulty: selectedRaid.group.difficulty,
|
||||||
|
hl: selectedRaid.group.hl,
|
||||||
|
extra: selectedRaid.group.extra,
|
||||||
|
guidebooks: selectedRaid.group.guidebooks
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
raidId = selectedRaid.id
|
||||||
|
} else {
|
||||||
|
raid = null
|
||||||
|
raidId = null
|
||||||
|
}
|
||||||
|
paneStack.pop()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="party-edit-sidebar">
|
||||||
|
<div class="top-fields">
|
||||||
|
<Input
|
||||||
|
label="Title"
|
||||||
|
bind:value={name}
|
||||||
|
placeholder="Enter party title..."
|
||||||
|
contained
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<YouTubeUrlInput label="Video" bind:value={videoUrl} contained />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DetailsSection title="Battle">
|
||||||
|
<DetailRow label="Raid" noHover compact>
|
||||||
|
{#snippet children()}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="raid-select-button {getRaidElementClass(raid)}"
|
||||||
|
onclick={openRaidPane}
|
||||||
|
>
|
||||||
|
{#if raid}
|
||||||
|
<span class="raid-name">{getRaidName(raid)}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="placeholder">Select raid...</span>
|
||||||
|
{/if}
|
||||||
|
<Icon name="chevron-right" size={16} class="chevron-icon" />
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</DetailRow>
|
||||||
|
</DetailsSection>
|
||||||
|
|
||||||
|
<BattleSettingsSection
|
||||||
|
bind:fullAuto
|
||||||
|
bind:autoGuard
|
||||||
|
bind:autoSummon
|
||||||
|
bind:chargeAttack
|
||||||
|
{element}
|
||||||
|
onchange={handleSettingsChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailsSection title="Performance">
|
||||||
|
<DetailRow label="Clear Time" noHover compact>
|
||||||
|
{#snippet children()}
|
||||||
|
<ClearTimeInput bind:value={clearTime} contained />
|
||||||
|
{/snippet}
|
||||||
|
</DetailRow>
|
||||||
|
<DetailRow label="Button Count" noHover compact>
|
||||||
|
{#snippet children()}
|
||||||
|
<MetricField bind:value={buttonCount} label="B" contained />
|
||||||
|
{/snippet}
|
||||||
|
</DetailRow>
|
||||||
|
<DetailRow label="Chain Count" noHover compact>
|
||||||
|
{#snippet children()}
|
||||||
|
<MetricField bind:value={chainCount} label="C" contained />
|
||||||
|
{/snippet}
|
||||||
|
</DetailRow>
|
||||||
|
<DetailRow label="Summon Count" noHover compact>
|
||||||
|
{#snippet children()}
|
||||||
|
<MetricField bind:value={summonCount} label="S" contained />
|
||||||
|
{/snippet}
|
||||||
|
</DetailRow>
|
||||||
|
</DetailsSection>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as *;
|
||||||
|
@use '$src/themes/typography' as *;
|
||||||
|
@use '$src/themes/layout' as *;
|
||||||
|
@use '$src/themes/effects' as *;
|
||||||
|
@use '$src/themes/colors' as *;
|
||||||
|
|
||||||
|
.party-edit-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit-3x;
|
||||||
|
padding-bottom: $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit-2x;
|
||||||
|
padding: 0 $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raid-select-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raid-name {
|
||||||
|
font-size: $font-regular;
|
||||||
|
font-weight: $medium;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
font-size: $font-regular;
|
||||||
|
font-weight: $medium;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron-icon {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Element-specific colors when raid is selected
|
||||||
|
.raid-select-button {
|
||||||
|
&.wind {
|
||||||
|
.raid-name,
|
||||||
|
.chevron-icon {
|
||||||
|
color: $wind-text-30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.fire {
|
||||||
|
.raid-name,
|
||||||
|
.chevron-icon {
|
||||||
|
color: $fire-text-30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.water {
|
||||||
|
.raid-name,
|
||||||
|
.chevron-icon {
|
||||||
|
color: $water-text-30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.earth {
|
||||||
|
.raid-name,
|
||||||
|
.chevron-icon {
|
||||||
|
color: $earth-text-30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.dark {
|
||||||
|
.raid-name,
|
||||||
|
.chevron-icon {
|
||||||
|
color: $dark-text-30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.light {
|
||||||
|
.raid-name,
|
||||||
|
.chevron-icon {
|
||||||
|
color: $light-text-30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
34
src/lib/features/party/openPartyEditSidebar.svelte.ts
Normal file
34
src/lib/features/party/openPartyEditSidebar.svelte.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||||
|
import PartyEditSidebar, {
|
||||||
|
type PartyEditValues
|
||||||
|
} from '$lib/components/sidebar/PartyEditSidebar.svelte'
|
||||||
|
|
||||||
|
type ElementType = 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light'
|
||||||
|
|
||||||
|
interface PartyEditSidebarOptions {
|
||||||
|
/** Current party values for editing */
|
||||||
|
initialValues: PartyEditValues
|
||||||
|
/** Party element for switch theming */
|
||||||
|
element?: ElementType
|
||||||
|
/** Callback when user saves changes */
|
||||||
|
onSave: (values: PartyEditValues) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the party edit sidebar for editing battle settings, performance metrics, and video URL.
|
||||||
|
*/
|
||||||
|
export function openPartyEditSidebar(options: PartyEditSidebarOptions) {
|
||||||
|
const { initialValues, element, onSave } = options
|
||||||
|
|
||||||
|
sidebar.openWithComponent('Edit Party Settings', PartyEditSidebar, {
|
||||||
|
initialValues,
|
||||||
|
element,
|
||||||
|
onSave
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closePartyEditSidebar() {
|
||||||
|
sidebar.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { PartyEditValues }
|
||||||
Loading…
Reference in a new issue