piclets / src /lib /components /Pages /Pictuary.svelte
Fraser's picture
drop style
d1bf3cf
<script lang="ts">
import { onMount } from 'svelte';
import { getCollectedPiclets, getCanonicalPiclets } from '$lib/db/piclets';
import type { PicletInstance } from '$lib/db/schema';
import { db } from '$lib/db';
import PicletCard from '../Piclets/PicletCard.svelte';
import PicletDetail from '../Piclets/PicletDetail.svelte';
import { CanonicalService } from '$lib/services/canonicalService';
let collectedPiclets: PicletInstance[] = $state([]);
let canonicalPiclets: PicletInstance[] = $state([]);
let isLoading = $state(true);
let selectedPiclet: PicletInstance | null = $state(null);
let searchQuery = $state('');
// Filter piclets based on search
let filteredPiclets = $derived.by(() => {
let piclets = collectedPiclets;
// Filter by search query
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
piclets = piclets.filter(p =>
p.objectName?.toLowerCase().includes(query) ||
p.nickname?.toLowerCase().includes(query) ||
p.typeId.toLowerCase().includes(query)
);
}
return piclets;
});
// Group piclets by object for display
let groupedPiclets = $derived.by(() => {
const groups = new Map<string, PicletInstance[]>();
filteredPiclets.forEach(piclet => {
const key = piclet.objectName || 'unknown';
const group = groups.get(key) || [];
group.push(piclet);
groups.set(key, group);
});
return Array.from(groups.entries()).sort((a, b) =>
a[0].localeCompare(b[0])
);
});
// Stats calculation
let stats = $derived.by(() => {
const totalDiscovered = collectedPiclets.length;
const uniqueObjects = new Set(collectedPiclets.map(p => p.objectName)).size;
const canonicalCount = collectedPiclets.filter(p => p.isCanonical).length;
const variationCount = collectedPiclets.filter(p => !p.isCanonical).length;
// Calculate total scans
const totalScans = collectedPiclets.reduce((sum, p) => sum + (p.scanCount || 1), 0);
// Calculate total rarity score
const rarityScore = collectedPiclets.reduce((sum, p) => {
const points = CanonicalService.calculateRarity(p.scanCount);
const multiplier = points === 'legendary' ? 100 : points === 'epic' ? 50 :
points === 'rare' ? 20 : points === 'uncommon' ? 10 : 5;
return sum + multiplier;
}, 0);
return {
totalDiscovered,
totalScans,
uniqueObjects,
canonicalCount,
variationCount,
rarityScore
};
});
onMount(async () => {
await loadPiclets();
});
async function loadPiclets() {
isLoading = true;
try {
collectedPiclets = await getCollectedPiclets();
canonicalPiclets = await getCanonicalPiclets();
// Sort by discovery date (most recent first)
collectedPiclets.sort((a, b) =>
(b.discoveredAt?.getTime() || 0) - (a.discoveredAt?.getTime() || 0)
);
} catch (error) {
console.error('Failed to load piclets:', error);
} finally {
isLoading = false;
}
}
function handlePicletClick(piclet: PicletInstance) {
selectedPiclet = piclet;
}
function handleDetailClose() {
selectedPiclet = null;
}
async function handlePicletDeleted() {
await loadPiclets();
}
</script>
<div class="pictuary-page">
<!-- Search -->
<div class="controls">
<input
type="text"
placeholder="Search piclets..."
bind:value={searchQuery}
class="search-input"
/>
</div>
<!-- Collection Grid -->
<div class="collection-container">
{#if isLoading}
<div class="loading">
<div class="spinner"></div>
<p>Loading collection...</p>
</div>
{:else if filteredPiclets.length === 0}
<div class="empty-state">
{#if collectedPiclets.length === 0}
<h2>No Piclets Discovered</h2>
<p>Start scanning objects to build your collection!</p>
{:else}
<h2>No Results</h2>
<p>Try adjusting your filters or search query</p>
{/if}
</div>
{:else}
<div class="piclet-grid">
{#each filteredPiclets as piclet (piclet.id)}
<div class="piclet-item" onclick={() => handlePicletClick(piclet)}>
<PicletCard piclet={piclet} />
{#if piclet.isCanonical}
<div class="canonical-badge">✨</div>
{/if}
{#if piclet.scanCount > 1}
<div class="scan-badge">{piclet.scanCount}×</div>
{/if}
</div>
{/each}
</div>
{/if}
</div>
</div>
<!-- Detail View -->
{#if selectedPiclet}
<PicletDetail
instance={selectedPiclet}
onClose={handleDetailClose}
onDeleted={handlePicletDeleted}
/>
{/if}
<style>
.pictuary-page {
height: 100%;
overflow-y: auto;
}
.controls {
padding: 1rem;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
gap: 1rem;
}
.search-input {
width: 100%;
padding: 0.75rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
}
.view-selector {
display: flex;
gap: 0.5rem;
}
.view-btn {
flex: 1;
padding: 0.5rem;
border: 1px solid #e0e0e0;
border-radius: 6px;
background: white;
color: #666;
cursor: pointer;
transition: all 0.2s;
}
.view-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.collection-container {
padding: 1rem;
min-height: 400px;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: white;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-state {
text-align: center;
padding: 3rem;
color: white;
}
.piclet-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
}
.piclet-item {
position: relative;
cursor: pointer;
transition: transform 0.2s;
}
.piclet-item:hover {
transform: scale(1.05);
}
.canonical-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: gold;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
}
.scan-badge {
position: absolute;
bottom: 0.5rem;
right: 0.5rem;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 0.125rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: bold;
}
</style>