piclets / src /lib /components /Piclets /PicletDetail.svelte
Fraser's picture
fix share log
384c29c
<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 { TYPE_DATA } from '$lib/types/picletTypes';
import AbilityDisplay from './AbilityDisplay.svelte';
import MoveDisplay from './MoveDisplay.svelte';
import { picletInstanceToBattleDefinition } from '$lib/utils/battleConversion';
import { recalculatePicletStats, getXpProgress, getXpTowardsNextLevel } from '$lib/services/levelingService';
import { isSpecialAbilityUnlocked } from '$lib/services/unlockLevels';
interface Props {
instance: PicletInstance;
onClose: () => void;
onDeleted?: () => void;
}
let { instance, onClose, onDeleted }: Props = $props();
let selectedTab = $state<'about' | 'abilities'>('about');
// Ensure stats are up-to-date with current level and nature
const updatedInstance = $derived(recalculatePicletStats(instance));
// Convert to battle definition to get enhanced ability data
const battleDefinition = $derived(picletInstanceToBattleDefinition(updatedInstance));
// XP and level calculations
const xpProgress = $derived(getXpProgress(updatedInstance.xp, updatedInstance.level, updatedInstance.tier));
const xpTowardsNext = $derived(getXpTowardsNextLevel(updatedInstance.xp, updatedInstance.level, updatedInstance.tier));
// 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;
const confirmed = confirm(`Are you sure you want to release ${instance.nickname || instance.typeId}? This action cannot be undone.`);
if (!confirmed) 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 handleShare() {
console.log("placeholder");
}
</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">{updatedInstance.nickname || updatedInstance.typeId}</h1>
<button
class="share-button"
onclick={handleShare}
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={updatedInstance.imageData || updatedInstance.imageUrl}
alt={updatedInstance.nickname || updatedInstance.typeId}
class="large-piclet-image"
/>
</div>
</div>
</div>
</div>
<!-- Level and Status Progress -->
<div class="level-xp-section">
<div class="level-info">
<span class="level-label">Level {updatedInstance.level}</span>
{#if updatedInstance.level < 100}
<span class="xp-label">{xpTowardsNext.current}/{xpTowardsNext.needed} to next level</span>
{:else}
<span class="xp-label">MAX LEVEL</span>
{/if}
</div>
<!-- HP Section -->
<div class="stat-row">
<div class="stat-label">HP</div>
<div class="hp-bar">
<div
class="hp-fill"
style="width: {(updatedInstance.currentHp / updatedInstance.maxHp) * 100}%; background-color: {getHpColor(updatedInstance.currentHp, updatedInstance.maxHp)}"
></div>
</div>
<div class="stat-value">{updatedInstance.currentHp}/{updatedInstance.maxHp}</div>
</div>
<!-- XP Section -->
{#if updatedInstance.level < 100}
<div class="stat-row">
<div class="stat-label">XP</div>
<div class="xp-bar">
<div
class="xp-fill"
style="width: {xpTowardsNext.percentage}%"
></div>
</div>
<div class="stat-value">{xpTowardsNext.current}/{xpTowardsNext.needed}</div>
</div>
{:else}
<div class="stat-row">
<div class="stat-label">XP</div>
<div class="max-level-indicator">MAX LEVEL</div>
</div>
{/if}
</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 === 'abilities'}
onclick={() => selectedTab = 'abilities'}
>
Abilities
</button>
</div>
<!-- Tab Content -->
<div class="tab-content">
{#if selectedTab === 'about'}
<div class="content-card">
<p class="description">{instance.description}</p>
<div class="divider"></div>
<h3 class="section-heading">Stats</h3>
<div class="stats-list">
<div class="stat-row">
<span>Attack</span>
<span class="stat-value">{updatedInstance.attack}</span>
</div>
<div class="stat-row">
<span>Defense</span>
<span class="stat-value">{updatedInstance.defense}</span>
</div>
<div class="stat-row">
<span>Field Attack</span>
<span class="stat-value">{updatedInstance.fieldAttack}</span>
</div>
<div class="stat-row">
<span>Field Defense</span>
<span class="stat-value">{updatedInstance.fieldDefense}</span>
</div>
<div class="stat-row">
<span>Speed</span>
<span class="stat-value">{updatedInstance.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">{updatedInstance.bst}</span>
</div>
<div class="summary-item">
<span class="summary-label">Tier</span>
<span class="summary-value">{updatedInstance.tier.toUpperCase()}</span>
</div>
</div>
</div>
{:else if selectedTab === 'abilities'}
<div class="content-card">
<h3 class="section-heading">Special Ability</h3>
{#if isSpecialAbilityUnlocked(updatedInstance.specialAbilityUnlockLevel, updatedInstance.level)}
<AbilityDisplay
ability={updatedInstance.specialAbility}
expanded={true}
/>
{:else}
<div class="locked-ability">
<div class="lock-header">
<span class="lock-icon">🔒</span>
<span class="lock-text">Unlocks at Level {updatedInstance.specialAbilityUnlockLevel}</span>
</div>
<div class="locked-content">
<h4>{updatedInstance.specialAbility.name}</h4>
<p>This special ability will be unlocked when {updatedInstance.nickname} reaches level {updatedInstance.specialAbilityUnlockLevel}.</p>
</div>
</div>
{/if}
<div class="divider"></div>
<h3 class="section-heading">Moves</h3>
<div class="moves-list">
{#each updatedInstance.moves as move, index}
{#if move.unlockLevel <= updatedInstance.level}
<MoveDisplay
{move}
expanded={true}
/>
{:else}
<div class="locked-move">
<div class="lock-header">
<span class="lock-icon">🔒</span>
<span class="lock-text">Unlocks at Level {move.unlockLevel}</span>
</div>
<div class="locked-content">
<h4>{move.name}</h4>
<p>This move will be unlocked when {updatedInstance.nickname} reaches level {move.unlockLevel}.</p>
</div>
</div>
{/if}
{/each}
</div>
</div>
{/if}
</div>
<!-- Actions -->
<div class="bottom-actions">
<button class="btn btn-danger" onclick={handleDelete}>Release Piclet</button>
</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;
}
/* Bottom Actions */
.bottom-actions {
padding: 16px;
background: white;
border-top: 0.5px solid #c6c6c8;
text-align: center;
}
.section-heading {
font-size: 18px;
font-weight: 600;
color: #495057;
margin: 0 0 12px 0;
}
.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%;
}
/* Enhanced ability and move display styles */
.moves-list {
display: flex;
flex-direction: column;
gap: 4px;
}
/* Locked content styles */
.locked-ability,
.locked-move {
background: #f8f9fa;
border: 1px dashed #dee2e6;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
opacity: 0.7;
}
.lock-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.lock-icon {
font-size: 16px;
}
.lock-text {
font-size: 12px;
font-weight: 600;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.locked-content h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #495057;
}
.locked-content p {
margin: 0;
font-size: 14px;
color: #6c757d;
font-style: italic;
}
/* Level and Status Section */
.level-xp-section {
background: white;
margin: 0 16px 16px;
border-radius: 12px;
padding: 16px;
border: 0.5px solid #c6c6c8;
}
.level-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.level-label {
font-size: 18px;
font-weight: 700;
color: #1a1a1a;
}
.xp-label {
font-size: 14px;
font-weight: 500;
color: #8e8e93;
}
/* Stat Rows (HP and XP bars) */
.stat-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.stat-row:last-child {
margin-bottom: 0;
}
.stat-label {
font-size: 12px;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.3px;
width: 24px;
flex-shrink: 0;
}
/* HP Bar */
.hp-bar {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
flex: 1;
min-width: 80px;
}
.hp-fill {
height: 100%;
transition: width 0.5s ease, background-color 0.3s ease;
}
/* XP Bar */
.xp-bar {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
flex: 1;
min-width: 80px;
}
.xp-fill {
height: 100%;
background: #2196f3;
transition: width 1.2s ease-out;
}
.stat-value {
font-size: 12px;
font-weight: 600;
color: #666;
min-width: 60px;
text-align: right;
flex-shrink: 0;
}
.max-level-indicator {
font-size: 12px;
font-weight: 600;
color: #ff6f00;
background: rgba(255, 111, 0, 0.1);
border: 1px solid #ff6f00;
border-radius: 4px;
padding: 2px 8px;
flex: 1;
text-align: center;
}
@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>