Migrate teams route from [shortcode] to [id]
This commit is contained in:
parent
7cb055a79f
commit
65411ad2ae
6 changed files with 801 additions and 68 deletions
40
src/routes/teams/[id]/+page.server.ts
Normal file
40
src/routes/teams/[id]/+page.server.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import type { PageServerLoad } from './$types'
|
||||||
|
import { PartyService } from '$lib/services/party.service'
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params, fetch, locals }) => {
|
||||||
|
console.log('[p/[id]/+page.server.ts] Loading with id:', params.id)
|
||||||
|
|
||||||
|
// Get auth data directly from locals instead of parent()
|
||||||
|
const authUserId = locals.session?.account?.userId
|
||||||
|
|
||||||
|
// Try to fetch party data on the server
|
||||||
|
const partyService = new PartyService(fetch)
|
||||||
|
|
||||||
|
let partyFound = false
|
||||||
|
let party = null
|
||||||
|
let canEdit = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the party
|
||||||
|
party = await partyService.getByShortcode(params.id)
|
||||||
|
console.log('[p/[id]] Successfully fetched party on server:', party.id, party.name)
|
||||||
|
partyFound = true
|
||||||
|
|
||||||
|
// Determine if user can edit
|
||||||
|
console.log('[p/[id]] Auth user ID:', authUserId)
|
||||||
|
console.log('[p/[id]] Party user ID:', party.user?.id)
|
||||||
|
console.log('[p/[id]] Party user:', party.user)
|
||||||
|
canEdit = authUserId ? party.user?.id === authUserId : false
|
||||||
|
console.log('[p/[id]] Can edit?', canEdit)
|
||||||
|
} catch (err) {
|
||||||
|
console.log('[p/[id]] Error fetching party (expected for test ids):', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return party data with explicit serialization
|
||||||
|
return {
|
||||||
|
party: party ? structuredClone(party) : null,
|
||||||
|
canEdit: Boolean(canEdit),
|
||||||
|
partyFound,
|
||||||
|
authUserId: authUserId || null
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/routes/teams/[id]/+page.svelte
Normal file
15
src/routes/teams/[id]/+page.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types'
|
||||||
|
import Party from '$lib/components/party/Party.svelte'
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if data?.party}
|
||||||
|
<Party party={data.party} canEdit={data.canEdit || false} />
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
<h1>Party not found</h1>
|
||||||
|
<p>No party data available for this code.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
export let error: Error & { status?: number, message?: string }
|
|
||||||
export let status: number
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h1>{status === 404 ? 'Team Not Found' : 'Something went wrong'}</h1>
|
|
||||||
<p>
|
|
||||||
{status === 404
|
|
||||||
? 'We could not find a team with that code.'
|
|
||||||
: (error?.message || 'Please try again later.')}
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
section { padding: 1.5rem; }
|
|
||||||
h1 { margin: 0 0 0.5rem 0; font-size: 1.25rem; }
|
|
||||||
p { margin: 0; color: #555; }
|
|
||||||
:global(main) { display: block; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import type { PageServerLoad } from './$types'
|
|
||||||
import { error } from '@sveltejs/kit'
|
|
||||||
import { PartyService } from '$lib/services/party.service'
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch, params, parent }) => {
|
|
||||||
const { shortcode } = params
|
|
||||||
const partyService = new PartyService(fetch)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const party = await partyService.getByShortcode(shortcode)
|
|
||||||
|
|
||||||
const parentData = await parent()
|
|
||||||
const authUserId = (parentData as any)?.user?.id
|
|
||||||
|
|
||||||
const canEditServer = partyService.computeEditability(
|
|
||||||
party,
|
|
||||||
authUserId,
|
|
||||||
undefined,
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
party,
|
|
||||||
canEditServer: canEditServer.canEdit,
|
|
||||||
authUserId
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error loading party:', err)
|
|
||||||
if (err?.issues) console.error('Validation errors:', err.issues)
|
|
||||||
if (err.status === 404) throw error(404, 'Team not found')
|
|
||||||
throw error(err.status || 500, err.message || 'Failed to load team')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import Party from '$lib/components/party/Party.svelte'
|
|
||||||
import type { PageData } from './$types'
|
|
||||||
|
|
||||||
export let data: PageData
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Party
|
|
||||||
initial={data.party}
|
|
||||||
canEditServer={data.canEditServer}
|
|
||||||
authUserId={data.authUserId}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
@ -1 +1,747 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import WeaponGrid from '$lib/components/grids/WeaponGrid.svelte'
|
||||||
|
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 { setContext } from 'svelte'
|
||||||
|
import type { SearchResult } from '$lib/api/resources/search'
|
||||||
|
import { apiClient } from '$lib/api/client'
|
||||||
|
import { Dialog } from 'bits-ui'
|
||||||
|
import { replaceState } from '$app/navigation'
|
||||||
|
import { page } from '$app/stores'
|
||||||
|
|
||||||
|
// Get authentication status from page store
|
||||||
|
const isAuthenticated = $derived($page.data?.isAuthenticated ?? false)
|
||||||
|
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
|
||||||
|
|
||||||
|
function selectTab(key: typeof tabs[number]['key']) {
|
||||||
|
activeTab = key
|
||||||
|
sidebarOpen = true // Auto-open sidebar when switching tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid state
|
||||||
|
let weapons = $state<any[]>([])
|
||||||
|
let summons = $state<any[]>([])
|
||||||
|
let characters = $state<any[]>([])
|
||||||
|
let sidebarOpen = $state(true) // Start with sidebar open
|
||||||
|
let selectedSlot = $state<number | null>(null)
|
||||||
|
let isFirstItemForSlot = false // Track if this is the first item after clicking empty cell
|
||||||
|
|
||||||
|
// Party state
|
||||||
|
let partyId = $state<string | null>(null)
|
||||||
|
let shortcode = $state<string | null>(null)
|
||||||
|
let editKey = $state<string | null>(null)
|
||||||
|
let isCreatingParty = $state(false)
|
||||||
|
|
||||||
|
// Error dialog state
|
||||||
|
let errorDialogOpen = $state(false)
|
||||||
|
let errorMessage = $state('')
|
||||||
|
let errorDetails = $state<string[]>([])
|
||||||
|
|
||||||
|
// Update CSS variable when sidebar opens/closes
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const root = document.documentElement
|
||||||
|
if (sidebarOpen) {
|
||||||
|
root.style.setProperty('--main-max-width', '1140px')
|
||||||
|
} else {
|
||||||
|
root.style.setProperty('--main-max-width', '820px')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate if grids are full
|
||||||
|
let isWeaponGridFull = $derived(weapons.length >= 10) // 1 mainhand + 9 grid slots
|
||||||
|
let isSummonGridFull = $derived(summons.length >= 6) // 6 summon slots (main + 4 grid + friend)
|
||||||
|
let isCharacterGridFull = $derived(characters.length >= 5) // 5 character slots
|
||||||
|
|
||||||
|
let canAddMore = $derived(
|
||||||
|
activeTab === 'weapons' ? !isWeaponGridFull :
|
||||||
|
activeTab === 'summons' ? !isSummonGridFull :
|
||||||
|
!isCharacterGridFull
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle adding items from search
|
||||||
|
async function handleAddItems(items: SearchResult[]) {
|
||||||
|
console.log('Adding items:', items, 'to tab:', activeTab)
|
||||||
|
|
||||||
|
// Create party on first item if not already created
|
||||||
|
if (!partyId && !isCreatingParty && items.length > 0) {
|
||||||
|
isCreatingParty = true
|
||||||
|
const firstItem = items[0]
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Create the party (with local_id only for anonymous users)
|
||||||
|
const partyPayload: any = {
|
||||||
|
name: 'New Team',
|
||||||
|
visibility: 1, // 1 = Public, 2 = Unlisted, 3 = Private
|
||||||
|
element: firstItem.element || 0 // Use item's element or default to null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include localId for anonymous users
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
const localId = apiClient.getLocalId()
|
||||||
|
partyPayload.localId = localId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create party using the API client
|
||||||
|
const result = await apiClient.createParty(partyPayload)
|
||||||
|
console.log('Party created:', result)
|
||||||
|
|
||||||
|
// The response has { party, editKey? }
|
||||||
|
const response = result.party
|
||||||
|
partyId = response.id
|
||||||
|
shortcode = response.shortcode
|
||||||
|
|
||||||
|
if (!partyId || !shortcode) {
|
||||||
|
throw new Error('Party creation did not return ID or shortcode')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store edit key if present (handled by API client)
|
||||||
|
if (result.editKey) {
|
||||||
|
editKey = result.editKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Add the first item to the party
|
||||||
|
let position = selectedSlot !== null ? selectedSlot : -1 // Use selectedSlot if available
|
||||||
|
let itemAdded = false
|
||||||
|
try {
|
||||||
|
console.log('Adding item to party:', { partyId, itemId: firstItem.id, type: activeTab, position })
|
||||||
|
|
||||||
|
if (activeTab === 'weapons') {
|
||||||
|
// Use selectedSlot if available, otherwise default to mainhand
|
||||||
|
if (selectedSlot === null) position = -1
|
||||||
|
const addResult = await apiClient.addWeapon(
|
||||||
|
partyId,
|
||||||
|
firstItem.granblue_id,
|
||||||
|
position,
|
||||||
|
{ mainhand: position === -1 }
|
||||||
|
)
|
||||||
|
console.log('Weapon added:', addResult)
|
||||||
|
itemAdded = true
|
||||||
|
|
||||||
|
// Update local state with the added weapon
|
||||||
|
weapons = [...weapons, {
|
||||||
|
id: addResult.grid_weapon?.id || `temp-${Date.now()}`,
|
||||||
|
position,
|
||||||
|
object: {
|
||||||
|
granblueId: firstItem.granblue_id,
|
||||||
|
name: firstItem.name,
|
||||||
|
element: firstItem.element
|
||||||
|
},
|
||||||
|
mainhand: position === -1
|
||||||
|
}]
|
||||||
|
} else if (activeTab === 'summons') {
|
||||||
|
// Use selectedSlot if available, otherwise default to main summon
|
||||||
|
if (selectedSlot === null) position = -1
|
||||||
|
const addResult = await apiClient.addSummon(
|
||||||
|
partyId,
|
||||||
|
firstItem.granblue_id,
|
||||||
|
position,
|
||||||
|
{ main: position === -1, friend: position === 6 }
|
||||||
|
)
|
||||||
|
console.log('Summon added:', addResult)
|
||||||
|
itemAdded = true
|
||||||
|
|
||||||
|
// Update local state with the added summon
|
||||||
|
summons = [...summons, {
|
||||||
|
id: addResult.grid_summon?.id || `temp-${Date.now()}`,
|
||||||
|
position,
|
||||||
|
object: {
|
||||||
|
granblueId: firstItem.granblue_id,
|
||||||
|
name: firstItem.name,
|
||||||
|
element: firstItem.element
|
||||||
|
},
|
||||||
|
main: position === -1,
|
||||||
|
friend: position === 6
|
||||||
|
}]
|
||||||
|
} else if (activeTab === 'characters') {
|
||||||
|
// Use selectedSlot if available, otherwise default to first slot
|
||||||
|
if (selectedSlot === null) position = 0
|
||||||
|
const addResult = await apiClient.addCharacter(
|
||||||
|
partyId,
|
||||||
|
firstItem.granblue_id,
|
||||||
|
position,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
console.log('Character added:', addResult)
|
||||||
|
itemAdded = true
|
||||||
|
|
||||||
|
// Update local state with the added character
|
||||||
|
characters = [...characters, {
|
||||||
|
id: addResult.grid_character?.id || `temp-${Date.now()}`,
|
||||||
|
position,
|
||||||
|
object: {
|
||||||
|
granblueId: firstItem.granblue_id,
|
||||||
|
name: firstItem.name,
|
||||||
|
element: firstItem.element
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
selectedSlot = null // Reset after using
|
||||||
|
|
||||||
|
// Update URL without redirecting
|
||||||
|
if (itemAdded && shortcode) {
|
||||||
|
// Update the URL to reflect the new party without navigating
|
||||||
|
replaceState(`/teams/${shortcode}`, {})
|
||||||
|
// Continue to allow adding more items
|
||||||
|
}
|
||||||
|
} catch (addError: any) {
|
||||||
|
console.error('Failed to add first item:', addError)
|
||||||
|
// Show error to user but don't redirect
|
||||||
|
errorMessage = addError.message || 'Failed to add item to party'
|
||||||
|
errorDetails = addError.details || []
|
||||||
|
errorDialogOpen = true
|
||||||
|
// Still update URL to the created party even if item failed
|
||||||
|
if (shortcode) {
|
||||||
|
replaceState(`/teams/${shortcode}`, {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreatingParty = false // Reset flag after party creation completes
|
||||||
|
|
||||||
|
// If there are more items to add, continue processing them
|
||||||
|
if (items.length > 1) {
|
||||||
|
const remainingItems = items.slice(1)
|
||||||
|
await handleAddItems(remainingItems) // Recursive call to add remaining items
|
||||||
|
}
|
||||||
|
return // Exit after processing all items from party creation
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to create party:', error)
|
||||||
|
isCreatingParty = false
|
||||||
|
|
||||||
|
// Parse error message
|
||||||
|
if (error.message) {
|
||||||
|
errorMessage = error.message
|
||||||
|
} else {
|
||||||
|
errorMessage = 'Failed to create party'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse validation errors if present
|
||||||
|
if (error.details && Array.isArray(error.details)) {
|
||||||
|
errorDetails = error.details
|
||||||
|
} else if (error.errors && typeof error.errors === 'object') {
|
||||||
|
// Rails-style validation errors
|
||||||
|
errorDetails = Object.entries(error.errors).map(
|
||||||
|
([field, messages]) => `${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
errorDetails = []
|
||||||
|
}
|
||||||
|
|
||||||
|
errorDialogOpen = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If party already exists, add items using grid API
|
||||||
|
if (partyId && !isCreatingParty) {
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i]
|
||||||
|
let position = -1 // Default position
|
||||||
|
|
||||||
|
if (activeTab === 'weapons') {
|
||||||
|
// Use selectedSlot for first item if available
|
||||||
|
if (i === 0 && selectedSlot !== null && !weapons.find(w => w.position === selectedSlot)) {
|
||||||
|
position = selectedSlot
|
||||||
|
selectedSlot = null // Reset after using
|
||||||
|
} else {
|
||||||
|
// Find next empty weapon slot
|
||||||
|
const emptySlots = Array.from({ length: 10 }, (_, i) => i - 1)
|
||||||
|
.filter(i => !weapons.find(w => w.position === i))
|
||||||
|
if (emptySlots.length === 0) return // Grid full
|
||||||
|
position = emptySlots[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add weapon via API
|
||||||
|
const response = await apiClient.addWeapon(
|
||||||
|
partyId,
|
||||||
|
item.granblue_id, // Use granblue_id
|
||||||
|
position,
|
||||||
|
{ mainhand: position === -1 }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add to local state
|
||||||
|
weapons = [...weapons, {
|
||||||
|
id: response.grid_weapon?.id || `temp-${Date.now()}`,
|
||||||
|
position,
|
||||||
|
object: {
|
||||||
|
granblueId: item.granblue_id,
|
||||||
|
name: item.name,
|
||||||
|
element: item.element
|
||||||
|
},
|
||||||
|
mainhand: position === -1
|
||||||
|
}]
|
||||||
|
} else if (activeTab === 'summons') {
|
||||||
|
// Use selectedSlot for first item if available
|
||||||
|
if (i === 0 && selectedSlot !== null && !summons.find(s => s.position === selectedSlot)) {
|
||||||
|
position = selectedSlot
|
||||||
|
selectedSlot = null // Reset after using
|
||||||
|
} else {
|
||||||
|
// Find next empty summon slot
|
||||||
|
const emptySlots = [-1, 0, 1, 2, 3, 6] // main, 4 grid slots, friend
|
||||||
|
.filter(i => !summons.find(s => s.position === i))
|
||||||
|
if (emptySlots.length === 0) return // Grid full
|
||||||
|
position = emptySlots[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add summon via API
|
||||||
|
const response = await apiClient.addSummon(
|
||||||
|
partyId,
|
||||||
|
item.granblue_id, // Use granblue_id
|
||||||
|
position,
|
||||||
|
{ main: position === -1, friend: position === 6 }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add to local state
|
||||||
|
summons = [...summons, {
|
||||||
|
id: response.grid_summon?.id || `temp-${Date.now()}`,
|
||||||
|
position,
|
||||||
|
object: {
|
||||||
|
granblueId: item.granblue_id,
|
||||||
|
name: item.name,
|
||||||
|
element: item.element
|
||||||
|
},
|
||||||
|
main: position === -1,
|
||||||
|
friend: position === 6
|
||||||
|
}]
|
||||||
|
} else if (activeTab === 'characters') {
|
||||||
|
// Use selectedSlot for first item if available
|
||||||
|
if (i === 0 && selectedSlot !== null && !characters.find(c => c.position === selectedSlot)) {
|
||||||
|
position = selectedSlot
|
||||||
|
selectedSlot = null // Reset after using
|
||||||
|
} else {
|
||||||
|
// Find next empty character slot
|
||||||
|
const emptySlots = Array.from({ length: 5 }, (_, i) => i)
|
||||||
|
.filter(i => !characters.find(c => c.position === i))
|
||||||
|
if (emptySlots.length === 0) return // Grid full
|
||||||
|
position = emptySlots[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add character via API
|
||||||
|
const response = await apiClient.addCharacter(
|
||||||
|
partyId,
|
||||||
|
item.granblue_id, // Use granblue_id
|
||||||
|
position,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add to local state
|
||||||
|
characters = [...characters, {
|
||||||
|
id: response.grid_character?.id || `temp-${Date.now()}`,
|
||||||
|
position,
|
||||||
|
object: {
|
||||||
|
granblueId: item.granblue_id,
|
||||||
|
name: item.name,
|
||||||
|
element: item.element
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to add item:', error)
|
||||||
|
errorMessage = error.message || 'Failed to add item'
|
||||||
|
errorDetails = error.details || []
|
||||||
|
errorDialogOpen = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original local-only adding logic (before party creation)
|
||||||
|
if (activeTab === 'weapons') {
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
let position: number
|
||||||
|
// Use selectedSlot for first item if available
|
||||||
|
if (index === 0 && selectedSlot !== null && !weapons.find(w => w.position === selectedSlot)) {
|
||||||
|
position = selectedSlot
|
||||||
|
selectedSlot = null // Reset after using
|
||||||
|
} else {
|
||||||
|
// Find next empty slot
|
||||||
|
const availableSlots = emptySlots.filter(s => !weapons.find(w => w.position === s))
|
||||||
|
if (availableSlots.length === 0) return
|
||||||
|
position = availableSlots[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWeapon = {
|
||||||
|
id: `temp-${Date.now()}-${index}`,
|
||||||
|
position,
|
||||||
|
object: {
|
||||||
|
granblueId: item.granblue_id,
|
||||||
|
name: item.name,
|
||||||
|
element: item.element
|
||||||
|
},
|
||||||
|
mainhand: position === -1
|
||||||
|
}
|
||||||
|
console.log('Adding weapon:', newWeapon)
|
||||||
|
weapons = [...weapons, newWeapon]
|
||||||
|
})
|
||||||
|
console.log('Updated weapons array:', weapons)
|
||||||
|
} else if (activeTab === 'summons') {
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
let position: number
|
||||||
|
// Use selectedSlot for first item if available
|
||||||
|
if (index === 0 && selectedSlot !== null && !summons.find(s => s.position === selectedSlot)) {
|
||||||
|
position = selectedSlot
|
||||||
|
selectedSlot = null // Reset after using
|
||||||
|
} else {
|
||||||
|
// Find next empty slot
|
||||||
|
const availableSlots = emptySlots.filter(s => !summons.find(sum => sum.position === s))
|
||||||
|
if (availableSlots.length === 0) return
|
||||||
|
position = availableSlots[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
summons = [...summons, {
|
||||||
|
id: `temp-${Date.now()}-${index}`,
|
||||||
|
position,
|
||||||
|
object: {
|
||||||
|
granblueId: item.granblue_id,
|
||||||
|
name: item.name,
|
||||||
|
element: item.element
|
||||||
|
},
|
||||||
|
main: position === -1,
|
||||||
|
friend: position === 6
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
} else if (activeTab === 'characters') {
|
||||||
|
// Add characters to empty slots
|
||||||
|
const emptySlots = Array.from({ length: 5 }, (_, i) => i)
|
||||||
|
.filter(i => !characters.find(c => c.position === i))
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
let position: number
|
||||||
|
// Use selectedSlot for first item if available
|
||||||
|
if (index === 0 && selectedSlot !== null && !characters.find(c => c.position === selectedSlot)) {
|
||||||
|
position = selectedSlot
|
||||||
|
selectedSlot = null // Reset after using
|
||||||
|
} else {
|
||||||
|
// Find next empty slot
|
||||||
|
const availableSlots = emptySlots.filter(s => !characters.find(c => c.position === s))
|
||||||
|
if (availableSlots.length === 0) return
|
||||||
|
position = availableSlots[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
characters = [...characters, {
|
||||||
|
id: `temp-${Date.now()}-${index}`,
|
||||||
|
position,
|
||||||
|
object: {
|
||||||
|
granblueId: item.granblue_id,
|
||||||
|
name: item.name,
|
||||||
|
element: item.element
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove functions
|
||||||
|
function removeWeapon(itemId: string) {
|
||||||
|
console.log('Removing weapon:', itemId)
|
||||||
|
weapons = weapons.filter(w => w.id !== itemId)
|
||||||
|
return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters })
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSummon(itemId: string) {
|
||||||
|
console.log('Removing summon:', itemId)
|
||||||
|
summons = summons.filter(s => s.id !== itemId)
|
||||||
|
return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters })
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCharacter(itemId: string) {
|
||||||
|
console.log('Removing character:', itemId)
|
||||||
|
characters = characters.filter(c => c.id !== itemId)
|
||||||
|
return Promise.resolve({ id: 'new', shortcode: 'new', weapons, summons, characters })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide a minimal party context so Unit components can render safely.
|
||||||
|
setContext('party', {
|
||||||
|
getParty: () => ({ id: 'new', shortcode: 'new', weapons, summons, characters }),
|
||||||
|
updateParty: (updatedParty: any) => {
|
||||||
|
// Update the local state when party is updated
|
||||||
|
if (updatedParty.weapons) weapons = updatedParty.weapons
|
||||||
|
if (updatedParty.summons) summons = updatedParty.summons
|
||||||
|
if (updatedParty.characters) characters = updatedParty.characters
|
||||||
|
},
|
||||||
|
canEdit: () => true,
|
||||||
|
services: {
|
||||||
|
gridService: {
|
||||||
|
removeWeapon: (partyId: string, itemId: string) => removeWeapon(itemId),
|
||||||
|
removeSummon: (partyId: string, itemId: string) => removeSummon(itemId),
|
||||||
|
removeCharacter: (partyId: string, itemId: string) => removeCharacter(itemId),
|
||||||
|
addWeapon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
|
||||||
|
addSummon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
|
||||||
|
addCharacter: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
|
||||||
|
replaceWeapon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
|
||||||
|
replaceSummon: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } }),
|
||||||
|
replaceCharacter: () => Promise.resolve({ party: { id: 'new', shortcode: 'new', weapons, summons, characters } })
|
||||||
|
},
|
||||||
|
partyService: { getEditKey: () => null }
|
||||||
|
},
|
||||||
|
openPicker: (opts: { type: 'weapon' | 'summon' | 'character'; position: number; item?: any }) => {
|
||||||
|
selectedSlot = opts.position
|
||||||
|
sidebarOpen = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="page-container">
|
||||||
|
<section class="party-content">
|
||||||
|
<header class="party-header">
|
||||||
|
<div class="party-info">
|
||||||
|
<h1>Create a new team</h1>
|
||||||
|
<p class="description">Search and click items to add them to your grid</p>
|
||||||
|
</div>
|
||||||
|
<button class="toggle-sidebar" on:click={() => sidebarOpen = !sidebarOpen}>
|
||||||
|
{sidebarOpen ? 'Hide' : 'Show'} Search
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div class="party-content">
|
||||||
|
{#if activeTab === 'weapons'}
|
||||||
|
<WeaponGrid {weapons} />
|
||||||
|
{:else if activeTab === 'summons'}
|
||||||
|
<SummonGrid {summons} />
|
||||||
|
{:else}
|
||||||
|
<CharacterGrid {characters} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<SearchSidebar
|
||||||
|
open={sidebarOpen}
|
||||||
|
type={activeTab === 'weapons' ? 'weapon' : activeTab === 'summons' ? 'summon' : 'character'}
|
||||||
|
onClose={() => sidebarOpen = false}
|
||||||
|
onAddItems={handleAddItems}
|
||||||
|
canAddMore={canAddMore}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Error Dialog -->
|
||||||
|
<Dialog.Root bind:open={errorDialogOpen}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay class="dialog-overlay" />
|
||||||
|
<Dialog.Content class="dialog-content">
|
||||||
|
<Dialog.Title class="dialog-title">Error Creating Team</Dialog.Title>
|
||||||
|
<Dialog.Description class="dialog-description">
|
||||||
|
{errorMessage}
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
{#if errorDetails.length > 0}
|
||||||
|
<div class="error-details">
|
||||||
|
<p class="error-details-title">Details:</p>
|
||||||
|
<ul class="error-list">
|
||||||
|
{#each errorDetails as detail}
|
||||||
|
<li>{detail}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<Dialog.Close class="dialog-button">
|
||||||
|
OK
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Override the main element's padding for this page */
|
||||||
|
:global(main) {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.party-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.party-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.party-info h1 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-sidebar {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #3366ff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-sidebar:hover {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog styles */
|
||||||
|
:global(.dialog-overlay) {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dialog-content) {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 51;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-description {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-details {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-details-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list li {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #3366ff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button:hover {
|
||||||
|
background: #2857e0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue