piclets / src /lib /components /Pages /Pictuary.svelte
Fraser's picture
rm legacy
1f2c086
<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);
// Map roster positions for easy access
let rosterMap = $derived(() => {
const map = new Map<number, PicletInstance>();
rosterPiclets.forEach(piclet => {
if (piclet.rosterPosition !== undefined) {
map.set(piclet.rosterPosition, piclet);
}
});
return map;
});
// Get the most common type in the roster for background theming
let dominantType = $derived(() => {
if (rosterPiclets.length === 0) {
return PicletType.BEAST; // Default fallback
}
// Count type occurrences
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);
}
});
// Find the most common type
let maxCount = 0;
let mostCommonType = PicletType.BEAST;
typeCounts.forEach((count, type) => {
if (count > maxCount) {
maxCount = count;
mostCommonType = type;
}
});
return mostCommonType;
});
// Get background image path for the dominant type
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">
<!-- Roster Section -->
<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>
<!-- Storage 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}
<!-- Discovered Section -->
{#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>