piclets / src /lib /components /Piclets /PicletDetail.svelte
Fraser's picture
q3
3eb1d35
raw
history blame
14.4 kB
<script lang="ts">
import { onMount } from 'svelte';
import type { PicletInstance } from '$lib/db/schema';
import { deletePicletInstance } from '$lib/db/piclets';
import { uiStore } from '$lib/stores/ui';
import { downloadPicletCard } from '$lib/services/picletExport';
import { TYPE_DATA } from '$lib/types/picletTypes';
interface Props {
instance: PicletInstance;
onClose: () => void;
onDeleted?: () => void;
}
let { instance, onClose, onDeleted }: Props = $props();
let showDeleteConfirm = $state(false);
let selectedTab = $state<'about' | 'stats' | 'actions'>('about');
let expandedMoves = $state(new Set<number>());
let isSharing = $state(false);
// Type-based styling
const typeData = $derived(TYPE_DATA[instance.primaryType]);
const typeColor = $derived(typeData.color);
const typeLogoPath = $derived(`/classes/${instance.primaryType}.png`);
onMount(() => {
uiStore.openDetailPage();
return () => {
uiStore.closeDetailPage();
};
});
async function handleDelete() {
if (!instance.id) return;
try {
await deletePicletInstance(instance.id);
onDeleted?.();
onClose();
} catch (err) {
console.error('Failed to delete piclet:', err);
}
}
function getStatPercentage(value: number, max: number = 255): number {
return Math.round((value / max) * 100);
}
function getHpColor(current: number, max: number): string {
const ratio = current / max;
if (ratio < 0.2) return '#ff3b30';
if (ratio < 0.5) return '#ff9500';
return '#34c759';
}
function toggleMoveExpanded(index: number) {
if (expandedMoves.has(index)) {
expandedMoves.delete(index);
} else {
expandedMoves.add(index);
}
expandedMoves = new Set(expandedMoves);
}
async function handleShare() {
isSharing = true;
try {
await downloadPicletCard(instance);
} catch (err) {
console.error('Failed to share piclet:', err);
alert('Failed to create shareable image');
} finally {
isSharing = false;
}
}
</script>
<div class="detail-page">
<div class="content-scroll">
<!-- Header Card -->
<div class="header-card">
<div class="card-background" style="--type-color: {typeColor}; --type-logo: url('{typeLogoPath}')">
<!-- Faded Logo Background -->
<div class="logo-background"></div>
<!-- Card Header -->
<div class="card-header">
<button
class="back-btn-card"
onclick={onClose}
aria-label="Go back"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 12H5m0 0l7 7m-7-7l7-7"></path>
</svg>
</button>
<h1 class="card-title">{instance.nickname || instance.typeId}</h1>
<button
class="share-button"
onclick={handleShare}
disabled={isSharing}
aria-label="Share Piclet"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="18" cy="5" r="3"></circle>
<circle cx="6" cy="12" r="3"></circle>
<circle cx="18" cy="19" r="3"></circle>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
</svg>
</button>
</div>
<!-- Large Image Section -->
<div class="large-image-section">
<div class="large-image-container">
<img
src={instance.imageData || instance.imageUrl}
alt={instance.nickname || instance.typeId}
class="large-piclet-image"
/>
</div>
</div>
</div>
</div>
<!-- Tab Bar -->
<div class="tab-bar" style="--type-color: {typeColor}">
<button
class="tab-button"
class:active={selectedTab === 'about'}
onclick={() => selectedTab = 'about'}
>
About
</button>
<button
class="tab-button"
class:active={selectedTab === 'stats'}
onclick={() => selectedTab = 'stats'}
>
Stats
</button>
<button
class="tab-button"
class:active={selectedTab === 'actions'}
onclick={() => selectedTab = 'actions'}
>
Actions
</button>
</div>
<!-- Tab Content -->
<div class="tab-content">
{#if selectedTab === 'about'}
<div class="content-card">
<p class="description">{instance.concept}</p>
</div>
{:else if selectedTab === 'stats'}
<div class="content-card">
<div class="stats-list">
<div class="stat-row">
<span>Attack</span>
<span class="stat-value">{instance.attack}</span>
</div>
<div class="stat-row">
<span>Defense</span>
<span class="stat-value">{instance.defense}</span>
</div>
<div class="stat-row">
<span>Field Attack</span>
<span class="stat-value">{instance.fieldAttack}</span>
</div>
<div class="stat-row">
<span>Field Defense</span>
<span class="stat-value">{instance.fieldDefense}</span>
</div>
<div class="stat-row">
<span>Speed</span>
<span class="stat-value">{instance.speed}</span>
</div>
</div>
<div class="divider"></div>
<div class="stat-summary">
<div class="summary-item">
<span class="summary-label">BST</span>
<span class="summary-value">{instance.bst}</span>
</div>
<div class="summary-item">
<span class="summary-label">Tier</span>
<span class="summary-value">{instance.tier.toUpperCase()}</span>
</div>
</div>
</div>
{:else if selectedTab === 'actions'}
<div class="content-card">
{#each instance.moves as move, index}
<button
class="move-card"
onclick={() => toggleMoveExpanded(index)}
>
<div class="move-header">
<span class="move-name">{move.name}</span>
<div class="move-badges">
{#if move.power > 0}
<span class="power-badge">PWR {move.power}</span>
{/if}
</div>
</div>
<p class="move-description">{move.description}</p>
<div class="move-stats">
<span>Accuracy: {move.accuracy}%</span>
<span>PP: {move.currentPp}/{move.pp}</span>
</div>
{#if expandedMoves.has(index)}
<div class="move-details">
<p>Type: {move.type}</p>
</div>
{/if}
</button>
{/each}
</div>
{/if}
</div>
<!-- Actions -->
<div class="bottom-actions">
{#if showDeleteConfirm}
<p class="delete-confirm">Are you sure you want to release this piclet?</p>
<button class="btn btn-danger" onclick={handleDelete}>Yes, Release</button>
<button class="btn btn-secondary" onclick={() => showDeleteConfirm = false}>Cancel</button>
{:else}
<button class="btn btn-danger" onclick={() => showDeleteConfirm = true}>Release Piclet</button>
{/if}
</div>
</div>
</div>
<style>
.detail-page {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f2f2f7;
z-index: 1000;
display: flex;
flex-direction: column;
}
/* Content Scroll */
.content-scroll {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* Header Card */
.header-card {
margin-bottom: 16px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
position: relative;
}
.card-background {
background: linear-gradient(135deg, var(--type-color, #4CAF50) 0%, color-mix(in srgb, var(--type-color, #4CAF50) 80%, white) 100%);
padding: 24px;
padding-top: calc(24px + env(safe-area-inset-top, 0));
position: relative;
overflow: hidden;
}
.logo-background {
position: absolute;
bottom: 5px;
right: 5px;
width: 120px;
height: 120px;
background-image: var(--type-logo);
background-size: contain;
background-repeat: no-repeat;
background-position: center;
opacity: 0.15;
pointer-events: none;
z-index: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
position: relative;
z-index: 2;
}
.card-title {
margin: 0;
font-size: 24px;
font-weight: bold;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
flex: 1;
text-align: center;
}
.back-btn-card {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
cursor: pointer;
padding: 8px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.back-btn-card:hover {
background: rgba(255, 255, 255, 0.3);
}
.share-button {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
cursor: pointer;
padding: 8px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.share-button:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
.share-button:active {
transform: scale(0.95);
}
.share-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.share-button svg {
width: 20px;
height: 20px;
}
.large-image-section {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
}
.large-image-container {
display: flex;
align-items: center;
justify-content: center;
width: 360px;
height: 360px;
}
.large-piclet-image {
width: 360px;
height: 360px;
object-fit: contain;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
}
/* Tab Bar */
.tab-bar {
margin: 0 16px 16px;
height: 36px;
background: #e5e5ea;
border-radius: 12px;
display: flex;
padding: 2px;
}
.tab-button {
flex: 1;
background: none;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
color: #8e8e93;
cursor: pointer;
transition: all 0.2s;
}
.tab-button.active {
background: var(--type-color, #4CAF50);
color: white;
box-shadow: 0 2px 4px color-mix(in srgb, var(--type-color, #4CAF50) 30%, transparent);
}
/* Tab Content */
.tab-content {
margin: 0 16px 16px;
}
.content-card {
background: white;
border-radius: 12px;
padding: 16px;
border: 0.5px solid #c6c6c8;
}
.description {
margin: 0 0 16px;
font-size: 16px;
line-height: 1.4;
color: #000;
}
/* Stats Tab */
.stats-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.stat-row {
display: flex;
justify-content: space-between;
font-size: 15px;
}
.stat-value {
font-weight: 600;
}
.divider {
height: 1px;
background: #e5e5ea;
margin: 16px 0;
}
.stat-summary {
display: flex;
justify-content: space-around;
}
.summary-item {
text-align: center;
}
.summary-label {
display: block;
font-size: 14px;
color: #8e8e93;
margin-bottom: 4px;
}
.summary-value {
font-size: 16px;
font-weight: bold;
color: #000;
}
/* Actions Tab */
.move-card {
width: 100%;
background: #f2f2f7;
border: 1px solid rgba(0, 123, 255, 0.3);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
cursor: pointer;
text-align: left;
transition: all 0.2s;
}
.move-card:active {
transform: scale(0.98);
}
.move-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.move-name {
font-size: 16px;
font-weight: bold;
color: #000;
}
.move-badges {
display: flex;
gap: 6px;
}
.power-badge {
background: rgba(255, 59, 48, 0.2);
color: #ff3b30;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.move-description {
margin: 0 0 12px;
font-size: 14px;
color: #666;
line-height: 1.3;
}
.move-stats {
display: flex;
gap: 16px;
font-size: 13px;
color: #8e8e93;
}
.move-details {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e5e5ea;
font-size: 14px;
color: #666;
}
.move-details p {
margin: 0;
}
/* Bottom Actions */
.bottom-actions {
padding: 16px;
background: white;
border-top: 0.5px solid #c6c6c8;
text-align: center;
}
.delete-confirm {
margin: 0 0 1rem;
color: #ff3b30;
font-size: 16px;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
}
.btn:active {
transform: scale(0.95);
}
.btn-danger {
background: #ff3b30;
color: white;
width: 100%;
}
.btn-secondary {
background: #e5e5ea;
color: #333;
margin-top: 8px;
width: 100%;
}
@media (min-width: 768px) {
.detail-page {
position: relative;
max-width: 600px;
margin: 0 auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
}
</style>