|
<script lang="ts"> |
|
import { onMount } from 'svelte'; |
|
import { getCaughtPiclets, getRosterPiclets, moveToRoster, swapRosterPositions, moveToStorage, getUncaughtPiclets } from '$lib/db/piclets'; |
|
import type { PicletInstance } from '$lib/db/schema'; |
|
import PicletCard from '../Piclets/PicletCard.svelte'; |
|
import EmptySlotCard from '../Piclets/EmptySlotCard.svelte'; |
|
import DraggablePicletCard from '../Piclets/DraggablePicletCard.svelte'; |
|
import RosterSlot from '../Piclets/RosterSlot.svelte'; |
|
import PicletDetail from '../Piclets/PicletDetail.svelte'; |
|
import AddToRosterDialog from '../Piclets/AddToRosterDialog.svelte'; |
|
import ViewAll from './ViewAll.svelte'; |
|
import { PicletType } from '$lib/types/picletTypes'; |
|
|
|
let rosterPiclets: PicletInstance[] = $state([]); |
|
let storagePiclets: PicletInstance[] = $state([]); |
|
let discoveredPiclets: PicletInstance[] = $state([]); |
|
let isLoading = $state(true); |
|
let currentlyDragging: PicletInstance | null = $state(null); |
|
let selectedPiclet: PicletInstance | null = $state(null); |
|
let addToRosterPosition: number | null = $state(null); |
|
let viewAllMode: 'storage' | 'discovered' | null = $state(null); |
|
|
|
|
|
let rosterMap = $derived(() => { |
|
const map = new Map<number, PicletInstance>(); |
|
rosterPiclets.forEach(piclet => { |
|
if (piclet.rosterPosition !== undefined) { |
|
map.set(piclet.rosterPosition, piclet); |
|
} |
|
}); |
|
return map; |
|
}); |
|
|
|
|
|
let dominantType = $derived(() => { |
|
if (rosterPiclets.length === 0) { |
|
return PicletType.BEAST; // Default fallback |
|
} |
|
|
|
|
|
const typeCounts = new Map<PicletType, number>(); |
|
rosterPiclets.forEach(piclet => { |
|
if (piclet.primaryType) { |
|
const count = typeCounts.get(piclet.primaryType) || 0; |
|
typeCounts.set(piclet.primaryType, count + 1); |
|
} |
|
}); |
|
|
|
|
|
let maxCount = 0; |
|
let mostCommonType = PicletType.BEAST; |
|
typeCounts.forEach((count, type) => { |
|
if (count > maxCount) { |
|
maxCount = count; |
|
mostCommonType = type; |
|
} |
|
}); |
|
|
|
return mostCommonType; |
|
}); |
|
|
|
|
|
let backgroundImagePath = $derived(`/classes/${dominantType}.png`); |
|
|
|
async function loadPiclets() { |
|
try { |
|
// Run type migration first time to fix any invalid types |
|
|
|
const allInstances = await getCaughtPiclets(); |
|
|
|
// Filter based on rosterPosition instead of isInRoster |
|
rosterPiclets = allInstances.filter(p => |
|
p.rosterPosition !== undefined && |
|
p.rosterPosition !== null && |
|
p.rosterPosition >= 0 && |
|
p.rosterPosition <= 5 |
|
); |
|
storagePiclets = allInstances.filter(p => |
|
p.rosterPosition === undefined || |
|
p.rosterPosition === null || |
|
p.rosterPosition < 0 || |
|
p.rosterPosition > 5 |
|
); |
|
|
|
// Get all uncaught piclets (discovered but not caught) |
|
discoveredPiclets = await getUncaughtPiclets(); |
|
} catch (err) { |
|
console.error('Failed to load piclets:', err); |
|
} finally { |
|
isLoading = false; |
|
} |
|
} |
|
|
|
onMount(() => { |
|
loadPiclets(); |
|
}); |
|
|
|
function handleRosterClick(position: number) { |
|
const piclet = rosterMap().get(position); |
|
if (piclet) { |
|
selectedPiclet = piclet; |
|
} else { |
|
addToRosterPosition = position; |
|
} |
|
} |
|
|
|
function handleStorageClick(piclet: PicletInstance) { |
|
selectedPiclet = piclet; |
|
} |
|
|
|
|
|
function handleDragStart(instance: PicletInstance) { |
|
currentlyDragging = instance; |
|
} |
|
|
|
function handleDragEnd() { |
|
currentlyDragging = null; |
|
} |
|
|
|
async function handleRosterDrop(position: number, dragData: any) { |
|
if (!dragData.instanceId) return; |
|
|
|
try { |
|
const draggedPiclet = [...rosterPiclets, ...storagePiclets].find(p => p.id === dragData.instanceId); |
|
if (!draggedPiclet) return; |
|
|
|
const targetPiclet = rosterMap().get(position); |
|
|
|
if (dragData.fromRoster && targetPiclet) { |
|
// Swap two roster positions |
|
await swapRosterPositions( |
|
dragData.instanceId, |
|
dragData.fromPosition, |
|
targetPiclet.id!, |
|
position |
|
); |
|
} else { |
|
// Move to roster (possibly replacing existing) |
|
await moveToRoster(dragData.instanceId, position); |
|
} |
|
|
|
await loadPiclets(); |
|
} catch (err) { |
|
console.error('Failed to handle drop:', err); |
|
} |
|
} |
|
</script> |
|
|
|
{#if viewAllMode === 'storage'} |
|
<ViewAll |
|
title="Storage" |
|
type="storage" |
|
items={storagePiclets} |
|
onBack={() => viewAllMode = null} |
|
onItemsChanged={loadPiclets} |
|
onDragStart={handleDragStart} |
|
onDragEnd={handleDragEnd} |
|
/> |
|
{:else if viewAllMode === 'discovered'} |
|
<ViewAll |
|
title="Discovered" |
|
type="discovered" |
|
items={discoveredPiclets} |
|
onBack={() => viewAllMode = null} |
|
/> |
|
{:else} |
|
<div class="pictuary-page" style="--bg-image: url('{backgroundImagePath}')"> |
|
{#if isLoading} |
|
<div class="loading-state"> |
|
<div class="spinner"></div> |
|
<p>Loading collection...</p> |
|
</div> |
|
{:else if rosterPiclets.length === 0 && storagePiclets.length === 0 && discoveredPiclets.length === 0} |
|
<div class="empty-state"> |
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path> |
|
<circle cx="12" cy="13" r="4"></circle> |
|
</svg> |
|
<h3>No Piclets Yet</h3> |
|
<p>Take photos to discover new Piclets!</p> |
|
</div> |
|
{:else} |
|
<div class="content"> |
|
|
|
<section class="roster-section"> |
|
<h2>Roster</h2> |
|
<div class="roster-grid"> |
|
{#each Array(6) as _, position} |
|
<RosterSlot |
|
{position} |
|
piclet={rosterMap().get(position)} |
|
size={110} |
|
onDrop={handleRosterDrop} |
|
onPicletClick={(piclet) => handleRosterClick(position)} |
|
onEmptyClick={handleRosterClick} |
|
onDragStart={handleDragStart} |
|
onDragEnd={handleDragEnd} |
|
/> |
|
{/each} |
|
</div> |
|
</section> |
|
|
|
|
|
{#if storagePiclets.length > 0} |
|
<section class="storage-section"> |
|
<div class="section-header"> |
|
<h2>Storage ({storagePiclets.length})</h2> |
|
{#if storagePiclets.length > 10} |
|
<button class="view-all-btn">View All</button> |
|
{/if} |
|
</div> |
|
<div class="horizontal-scroll"> |
|
{#each storagePiclets.slice(0, 10) as piclet} |
|
<DraggablePicletCard |
|
instance={piclet} |
|
size={110} |
|
onClick={() => handleStorageClick(piclet)} |
|
onDragStart={handleDragStart} |
|
onDragEnd={handleDragEnd} |
|
/> |
|
{/each} |
|
</div> |
|
</section> |
|
{/if} |
|
|
|
|
|
{#if discoveredPiclets.length > 0} |
|
<section class="discovered-section"> |
|
<div class="section-header"> |
|
<h2>Discovered ({discoveredPiclets.length})</h2> |
|
{#if discoveredPiclets.length > 10} |
|
<button class="view-all-btn">View All</button> |
|
{/if} |
|
</div> |
|
<div class="horizontal-scroll"> |
|
{#each discoveredPiclets.slice(0, 10) as piclet} |
|
<PicletCard |
|
piclet={piclet} |
|
size={100} |
|
onClick={() => selectedPiclet = piclet} |
|
/> |
|
{/each} |
|
</div> |
|
</section> |
|
{/if} |
|
</div> |
|
{/if} |
|
|
|
{#if selectedPiclet} |
|
<PicletDetail |
|
instance={selectedPiclet} |
|
onClose={() => selectedPiclet = null} |
|
onDeleted={loadPiclets} |
|
/> |
|
{/if} |
|
|
|
{#if addToRosterPosition !== null} |
|
<AddToRosterDialog |
|
position={addToRosterPosition} |
|
availablePiclets={storagePiclets} |
|
onClose={() => addToRosterPosition = null} |
|
onAdded={loadPiclets} |
|
/> |
|
{/if} |
|
</div> |
|
{/if} |
|
|
|
<style> |
|
.pictuary-page { |
|
height: 100%; |
|
overflow-y: auto; |
|
-webkit-overflow-scrolling: touch; |
|
background: white; |
|
position: relative; |
|
} |
|
|
|
.pictuary-page::before { |
|
content: ''; |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background-image: var(--bg-image); |
|
background-size: 300px 300px; |
|
background-repeat: no-repeat; |
|
background-position: center bottom; |
|
opacity: 0.03; |
|
pointer-events: none; |
|
z-index: 0; |
|
} |
|
|
|
|
|
.loading-state, |
|
.empty-state { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
height: calc(100% - 100px); |
|
padding: 2rem; |
|
text-align: center; |
|
position: relative; |
|
z-index: 1; |
|
} |
|
|
|
.spinner { |
|
width: 40px; |
|
height: 40px; |
|
border: 3px solid #f3f3f3; |
|
border-top: 3px solid #007bff; |
|
border-radius: 50%; |
|
animation: spin 1s linear infinite; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.empty-state svg { |
|
color: #8e8e93; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.empty-state h3 { |
|
margin: 0 0 0.5rem; |
|
font-size: 1.25rem; |
|
font-weight: 600; |
|
color: #333; |
|
} |
|
|
|
.empty-state p { |
|
margin: 0; |
|
color: #666; |
|
} |
|
|
|
.content { |
|
padding: 0 1rem 100px; |
|
position: relative; |
|
z-index: 1; |
|
} |
|
|
|
section { |
|
margin-bottom: 2rem; |
|
} |
|
|
|
section h2 { |
|
font-size: 1.5rem; |
|
font-weight: bold; |
|
color: #8e8e93; |
|
margin: 0 0 0.75rem; |
|
} |
|
|
|
.section-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 0.75rem; |
|
} |
|
|
|
.section-header h2 { |
|
margin: 0; |
|
} |
|
|
|
.view-all-btn { |
|
background: none; |
|
border: none; |
|
color: #007bff; |
|
font-size: 1rem; |
|
cursor: pointer; |
|
padding: 0; |
|
} |
|
|
|
.roster-grid { |
|
display: grid; |
|
grid-template-columns: repeat(3, 1fr); |
|
grid-template-rows: repeat(2, 1fr); |
|
gap: 12px; |
|
} |
|
|
|
.horizontal-scroll { |
|
display: flex; |
|
gap: 8px; |
|
overflow-x: auto; |
|
-webkit-overflow-scrolling: touch; |
|
padding-bottom: 8px; |
|
} |
|
|
|
.horizontal-scroll::-webkit-scrollbar { |
|
height: 4px; |
|
} |
|
|
|
.horizontal-scroll::-webkit-scrollbar-track { |
|
background: #f1f1f1; |
|
border-radius: 2px; |
|
} |
|
|
|
.horizontal-scroll::-webkit-scrollbar-thumb { |
|
background: #888; |
|
border-radius: 2px; |
|
} |
|
|
|
@keyframes spin { |
|
to { transform: rotate(360deg); } |
|
} |
|
</style> |