|
|
<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(''); |
|
|
|
|
|
|
|
|
let filteredPiclets = $derived.by(() => { |
|
|
let piclets = collectedPiclets; |
|
|
|
|
|
|
|
|
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; |
|
|
}); |
|
|
|
|
|
|
|
|
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]) |
|
|
); |
|
|
}); |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
const totalScans = collectedPiclets.reduce((sum, p) => sum + (p.scanCount || 1), 0); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
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> |