|
<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); |
|
|
|
|
|
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"> |
|
|
|
<div class="header-card"> |
|
<div class="card-background" style="--type-color: {typeColor}; --type-logo: url('{typeLogoPath}')"> |
|
|
|
<div class="logo-background"></div> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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 { |
|
flex: 1; |
|
overflow-y: auto; |
|
-webkit-overflow-scrolling: touch; |
|
} |
|
|
|
|
|
.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 { |
|
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 { |
|
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-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; |
|
} |
|
|
|
|
|
.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 { |
|
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> |