refactor database detail pages with new UI components
This commit is contained in:
parent
aa5d1d2c22
commit
3eb00135f8
3 changed files with 214 additions and 650 deletions
|
|
@ -1,11 +1,24 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
// SvelteKit imports
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
// Utility functions
|
||||
import { getRarityLabel } from '$lib/utils/rarity'
|
||||
import { getElementLabel, getElementIcon } from '$lib/utils/element'
|
||||
import { getElementLabel } from '$lib/utils/element'
|
||||
import { getProficiencyLabel } from '$lib/utils/proficiency'
|
||||
import { getRaceLabel } from '$lib/utils/race'
|
||||
import { getGenderLabel } from '$lib/utils/gender'
|
||||
import { getCharacterMaxUncapLevel } from '$lib/utils/uncap'
|
||||
|
||||
// Components
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||
import DetailsHeader from '$lib/components/ui/DetailsHeader.svelte'
|
||||
|
||||
// Types
|
||||
import type { PageData } from './$types'
|
||||
|
||||
let { data }: { data: PageData } = $props()
|
||||
|
|
@ -13,101 +26,70 @@
|
|||
// Get character from server data
|
||||
const character = $derived(data.character)
|
||||
|
||||
// Helper function to get character name
|
||||
function getCharacterName(nameObj: any): string {
|
||||
if (!nameObj) return 'Unknown Character'
|
||||
if (typeof nameObj === 'string') return nameObj
|
||||
return nameObj.en || nameObj.ja || 'Unknown Character'
|
||||
}
|
||||
|
||||
// Helper function to get character image
|
||||
function getCharacterImage(character: any): string {
|
||||
if (!character?.granblue_id) return '/images/placeholders/placeholder-character-main.png'
|
||||
return `/images/character-main/${character.granblue_id}_01.jpg`
|
||||
return `/images/character-grid/${character.granblue_id}_01.jpg`
|
||||
}
|
||||
|
||||
// Calculate uncap properties for the indicator
|
||||
const uncap = $derived(character?.uncap ?? {})
|
||||
const flb = $derived(uncap.flb ?? false)
|
||||
const ulb = $derived(uncap.ulb ?? false)
|
||||
const transcendence = $derived(uncap.transcendence ?? false)
|
||||
const special = $derived(character?.special ?? false)
|
||||
|
||||
const uncapLevel = $derived(getCharacterMaxUncapLevel({ special, uncap }))
|
||||
const transcendenceStage = $derived(transcendence ? 5 : 0)
|
||||
</script>
|
||||
|
||||
<div class="character-detail">
|
||||
<div class="page-header">
|
||||
<button class="back-button" onclick={() => goto('/database/characters')}>
|
||||
← Back to Characters
|
||||
</button>
|
||||
<h1>Character Details</h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if character}
|
||||
<div class="character-content">
|
||||
<div class="character-hero">
|
||||
<div class="character-image">
|
||||
<img
|
||||
src={getCharacterImage(character)}
|
||||
alt={getCharacterName(character.name)}
|
||||
onerror={(e) => { e.currentTarget.src = '/images/placeholders/placeholder-character-main.png' }}
|
||||
/>
|
||||
</div>
|
||||
<div class="character-info">
|
||||
<h2 class="character-name">{getCharacterName(character.name)}</h2>
|
||||
<div class="character-meta">
|
||||
<div class="meta-item">
|
||||
<span class="label">Rarity:</span>
|
||||
<span class="value">{getRarityLabel(character.rarity)}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Element:</span>
|
||||
<div class="element-display">
|
||||
{#if character.element}
|
||||
<img
|
||||
src={getElementIcon(character.element)}
|
||||
alt={getElementLabel(character.element)}
|
||||
class="element-icon"
|
||||
/>
|
||||
<span class="value">{getElementLabel(character.element)}</span>
|
||||
{:else}
|
||||
<span class="value">—</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Max Level:</span>
|
||||
<span class="value">{character.max_level || '—'}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Granblue ID:</span>
|
||||
<span class="value">{character.granblue_id || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<DetailsHeader type="character" item={character} image={getCharacterImage(character)} />
|
||||
|
||||
<div class="character-details">
|
||||
<h3>Details</h3>
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">Race:</span>
|
||||
<span class="value">{getRaceLabel(character.race)}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Gender:</span>
|
||||
<span class="value">{getGenderLabel(character.gender)}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Base HP:</span>
|
||||
<span class="value">{character.base_hp || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Base Attack:</span>
|
||||
<span class="value">{character.base_attack || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Max HP:</span>
|
||||
<span class="value">{character.max_hp || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Max Attack:</span>
|
||||
<span class="value">{character.max_attack || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DetailsContainer title="Metadata">
|
||||
<DetailItem label="Rarity" value={getRarityLabel(character.rarity)} />
|
||||
<DetailItem label="Granblue ID" value={character.granblue_id} />
|
||||
</DetailsContainer>
|
||||
<DetailsContainer title="Details">
|
||||
{#if character.uncap}
|
||||
<DetailItem label="Uncap">
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
{uncapLevel}
|
||||
{transcendenceStage}
|
||||
{flb}
|
||||
{ulb}
|
||||
{transcendence}
|
||||
{special}
|
||||
editable={false}
|
||||
/>
|
||||
</DetailItem>
|
||||
{/if}
|
||||
<DetailItem label="Element" value={getElementLabel(character.element)} />
|
||||
<DetailItem label="Race" value={getRaceLabel(character.race)} />
|
||||
<DetailItem label="Gender" value={getGenderLabel(character.gender)} />
|
||||
|
||||
<DetailItem label="Proficiency 1" value={getProficiencyLabel(character.proficiency[0])} />
|
||||
<DetailItem label="Proficiency 2" value={getProficiencyLabel(character.proficiency[1])} />
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="HP Stats">
|
||||
<DetailItem label="Base HP" value={character.hp?.min_hp} />
|
||||
<DetailItem label="Max HP" value={character.hp?.max_hp} />
|
||||
{#if flb}
|
||||
<DetailItem label="Max HP (FLB)" value={character.hp?.max_hp_flb} />
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="Attack Stats">
|
||||
<DetailItem label="Base Attack" value={character.atk?.min_atk} />
|
||||
<DetailItem label="Max Attack" value={character.atk?.max_atk} />
|
||||
{#if flb}
|
||||
<DetailItem label="Max Attack (FLB)" value={character.atk?.max_atk_flb} />
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="not-found">
|
||||
|
|
@ -119,51 +101,15 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/layout' as layout;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
|
||||
.character-detail {
|
||||
padding: spacing.$unit * 2;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: spacing.$unit * 2;
|
||||
|
||||
.back-button {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: spacing.$unit * 0.5 spacing.$unit;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: typography.$font-small;
|
||||
margin-bottom: spacing.$unit;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: typography.$font-xxlarge;
|
||||
font-weight: typography.$bold;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: spacing.$unit * 4;
|
||||
|
||||
.loading-spinner {
|
||||
font-size: typography.$font-medium;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
|
|
@ -179,120 +125,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.character-content {
|
||||
.content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border-radius: layout.$card-corner;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.character-hero {
|
||||
display: flex;
|
||||
gap: spacing.$unit * 2;
|
||||
padding: spacing.$unit * 2;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
.character-image {
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.character-info {
|
||||
flex: 1;
|
||||
|
||||
.character-name {
|
||||
font-size: typography.$font-xlarge;
|
||||
font-weight: typography.$bold;
|
||||
margin: 0 0 spacing.$unit 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.character-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit * 0.5;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: spacing.$unit * 0.5;
|
||||
|
||||
.label {
|
||||
font-weight: typography.$medium;
|
||||
color: #666;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.element-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: spacing.$unit * 0.25;
|
||||
|
||||
.element-icon {
|
||||
width: 25px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.character-details {
|
||||
padding: spacing.$unit * 2;
|
||||
|
||||
h3 {
|
||||
font-size: typography.$font-large;
|
||||
font-weight: typography.$bold;
|
||||
margin: 0 0 spacing.$unit 0;
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: spacing.$unit;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: spacing.$unit * 0.5;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
|
||||
.label {
|
||||
font-weight: typography.$medium;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.character-hero {
|
||||
flex-direction: column;
|
||||
|
||||
.character-image img {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
margin-top: spacing.$unit-2x;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
import { goto } from '$app/navigation'
|
||||
import { getRarityLabel } from '$lib/utils/rarity'
|
||||
import { getElementLabel, getElementIcon } from '$lib/utils/element'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||
import DetailsHeader from '$lib/components/ui/DetailsHeader.svelte'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
let { data }: { data: PageData } = $props()
|
||||
|
|
@ -11,101 +15,78 @@
|
|||
// Get summon from server data
|
||||
const summon = $derived(data.summon)
|
||||
|
||||
// Helper function to get summon name
|
||||
function getSummonName(nameObj: any): string {
|
||||
if (!nameObj) return 'Unknown Summon'
|
||||
if (typeof nameObj === 'string') return nameObj
|
||||
return nameObj.en || nameObj.ja || 'Unknown Summon'
|
||||
}
|
||||
|
||||
// Helper function to get summon image
|
||||
function getSummonImage(summon: any): string {
|
||||
if (!summon?.granblue_id) return '/images/placeholders/placeholder-summon-main.png'
|
||||
return `/images/summon-main/${summon.granblue_id}.jpg`
|
||||
return `/images/summon-grid/${summon.granblue_id}.jpg`
|
||||
}
|
||||
|
||||
// Calculate uncap properties for the indicator
|
||||
const uncap = $derived(summon?.uncap ?? {})
|
||||
const flb = $derived(uncap.flb ?? false)
|
||||
const ulb = $derived(uncap.ulb ?? false)
|
||||
const transcendence = $derived(uncap.transcendence ?? false)
|
||||
|
||||
// Calculate maximum uncap level based on available uncaps
|
||||
// Summons: 3 base + FLB + ULB + transcendence
|
||||
const getMaxUncapLevel = () => {
|
||||
return transcendence ? 6 : ulb ? 5 : flb ? 4 : 3
|
||||
}
|
||||
|
||||
const uncapLevel = $derived(getMaxUncapLevel())
|
||||
// For details view, show maximum transcendence stage when available
|
||||
const transcendenceStage = $derived(transcendence ? 5 : 0)
|
||||
</script>
|
||||
|
||||
<div class="summon-detail">
|
||||
<div class="page-header">
|
||||
<button class="back-button" onclick={() => goto('/database/summons')}>
|
||||
← Back to Summons
|
||||
</button>
|
||||
<h1>Summon Details</h1>
|
||||
</div>
|
||||
|
||||
{#if summon}
|
||||
<div class="summon-content">
|
||||
<div class="summon-hero">
|
||||
<div class="summon-image">
|
||||
<img
|
||||
src={getSummonImage(summon)}
|
||||
alt={getSummonName(summon.name)}
|
||||
onerror={(e) => { e.currentTarget.src = '/images/placeholders/placeholder-summon-main.png' }}
|
||||
/>
|
||||
</div>
|
||||
<div class="summon-info">
|
||||
<h2 class="summon-name">{getSummonName(summon.name)}</h2>
|
||||
<div class="summon-meta">
|
||||
<div class="meta-item">
|
||||
<span class="label">Rarity:</span>
|
||||
<span class="value">{getRarityLabel(summon.rarity)}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Element:</span>
|
||||
<div class="element-display">
|
||||
{#if summon.element}
|
||||
<img
|
||||
src={getElementIcon(summon.element)}
|
||||
alt={getElementLabel(summon.element)}
|
||||
class="element-icon"
|
||||
/>
|
||||
<span class="value">{getElementLabel(summon.element)}</span>
|
||||
{:else}
|
||||
<span class="value">—</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Max Level:</span>
|
||||
<span class="value">{summon.max_level || '—'}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Granblue ID:</span>
|
||||
<span class="value">{summon.granblue_id || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DetailsHeader type="summon" item={summon} image={getSummonImage(summon)} />
|
||||
|
||||
<div class="summon-details">
|
||||
<h3>Stats</h3>
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">Base HP:</span>
|
||||
<span class="value">{summon.base_hp || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Base Attack:</span>
|
||||
<span class="value">{summon.base_attack || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Max HP:</span>
|
||||
<span class="value">{summon.max_hp || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Max Attack:</span>
|
||||
<span class="value">{summon.max_attack || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Plus Bonus:</span>
|
||||
<span class="value">{summon.plus_bonus ? 'Yes' : 'No'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Series:</span>
|
||||
<span class="value">{summon.series || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DetailsContainer title="HP Stats">
|
||||
<DetailItem label="Base HP" value={summon.hp?.min_hp} />
|
||||
<DetailItem label="Max HP" value={summon.hp?.max_hp} />
|
||||
{#if flb}
|
||||
<DetailItem label="Max HP (FLB)" value={summon.hp?.max_hp_flb} />
|
||||
{/if}
|
||||
{#if ulb}
|
||||
<DetailItem label="Max HP (ULB)" value={summon.hp?.max_hp_ulb} />
|
||||
{/if}
|
||||
{#if transcendence}
|
||||
<DetailItem label="Max HP (XLB)" value={summon.hp?.max_hp_xlb} />
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="Attack Stats">
|
||||
<DetailItem label="Base Attack" value={summon.atk?.min_atk} />
|
||||
<DetailItem label="Max Attack" value={summon.atk?.max_atk} />
|
||||
{#if flb}
|
||||
<DetailItem label="Max Attack (FLB)" value={summon.atk?.max_atk_flb} />
|
||||
{/if}
|
||||
{#if ulb}
|
||||
<DetailItem label="Max Attack (ULB)" value={summon.atk?.max_atk_ulb} />
|
||||
{/if}
|
||||
{#if transcendence}
|
||||
<DetailItem label="Max Attack (XLB)" value={summon.atk?.max_atk_xlb} />
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="Details">
|
||||
<DetailItem label="Series" value={summon.series} />
|
||||
{#if summon.uncap}
|
||||
<DetailItem label="Uncap">
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
{uncapLevel}
|
||||
{transcendenceStage}
|
||||
{flb}
|
||||
{ulb}
|
||||
{transcendence}
|
||||
editable={false}
|
||||
/>
|
||||
</DetailItem>
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<div class="summon-abilities">
|
||||
<h3>Call Effect</h3>
|
||||
|
|
@ -159,22 +140,22 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/layout' as layout;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
|
||||
.summon-detail {
|
||||
padding: spacing.$unit * 2;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: spacing.$unit-2x 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: spacing.$unit * 2;
|
||||
margin-bottom: spacing.$unit-2x;
|
||||
|
||||
.back-button {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: spacing.$unit * 0.5 spacing.$unit;
|
||||
padding: spacing.$unit-half spacing.$unit;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: typography.$font-small;
|
||||
|
|
@ -193,22 +174,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: spacing.$unit * 4;
|
||||
|
||||
.loading-spinner {
|
||||
font-size: typography.$font-medium;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: spacing.$unit * 0.5 spacing.$unit;
|
||||
padding: spacing.$unit-half spacing.$unit;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: spacing.$unit;
|
||||
|
|
@ -226,71 +200,8 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.summon-hero {
|
||||
display: flex;
|
||||
gap: spacing.$unit * 2;
|
||||
padding: spacing.$unit * 2;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
.summon-image {
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.summon-info {
|
||||
flex: 1;
|
||||
|
||||
.summon-name {
|
||||
font-size: typography.$font-xlarge;
|
||||
font-weight: typography.$bold;
|
||||
margin: 0 0 spacing.$unit 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.summon-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit * 0.5;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: spacing.$unit * 0.5;
|
||||
|
||||
.label {
|
||||
font-weight: typography.$medium;
|
||||
color: #666;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.element-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: spacing.$unit * 0.25;
|
||||
|
||||
.element-icon {
|
||||
width: 25px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summon-details,
|
||||
.summon-abilities {
|
||||
padding: spacing.$unit * 2;
|
||||
padding: spacing.$unit-2x;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
&:last-child {
|
||||
|
|
@ -303,30 +214,6 @@
|
|||
margin: 0 0 spacing.$unit 0;
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: spacing.$unit;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: spacing.$unit * 0.5;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
|
||||
.label {
|
||||
font-weight: typography.$medium;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.abilities-section {
|
||||
margin-bottom: spacing.$unit * 2;
|
||||
|
||||
|
|
@ -362,18 +249,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.summon-hero {
|
||||
flex-direction: column;
|
||||
|
||||
.summon-image img {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
import { getRarityLabel } from '$lib/utils/rarity'
|
||||
import { getElementLabel, getElementIcon } from '$lib/utils/element'
|
||||
import { getProficiencyLabel, getProficiencyIcon } from '$lib/utils/proficiency'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
import DetailsContainer from '$lib/components/ui/DetailsContainer.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||
import DetailsHeader from '$lib/components/ui/DetailsHeader.svelte'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
let { data }: { data: PageData } = $props()
|
||||
|
|
@ -12,123 +16,78 @@
|
|||
// Get weapon from server data
|
||||
const weapon = $derived(data.weapon)
|
||||
|
||||
// Helper function to get weapon name
|
||||
function getWeaponName(nameObj: any): string {
|
||||
if (!nameObj) return 'Unknown Weapon'
|
||||
if (typeof nameObj === 'string') return nameObj
|
||||
return nameObj.en || nameObj.ja || 'Unknown Weapon'
|
||||
}
|
||||
|
||||
// Helper function to get weapon image
|
||||
function getWeaponImage(weapon: any): string {
|
||||
if (!weapon?.granblue_id) return '/images/placeholders/placeholder-weapon-main.png'
|
||||
|
||||
// Handle element-specific weapons (primal weapons)
|
||||
if (weapon.element === 0 && weapon.instance_element) {
|
||||
return `/images/weapon-main/${weapon.granblue_id}_${weapon.instance_element}.jpg`
|
||||
return `/images/weapon-grid/${weapon.granblue_id}_${weapon.instance_element}.jpg`
|
||||
}
|
||||
return `/images/weapon-main/${weapon.granblue_id}.jpg`
|
||||
return `/images/weapon-grid/${weapon.granblue_id}.jpg`
|
||||
}
|
||||
|
||||
// Calculate uncap properties for the indicator
|
||||
const uncap = $derived(weapon?.uncap ?? {})
|
||||
const flb = $derived(uncap.flb ?? false)
|
||||
const ulb = $derived(uncap.ulb ?? false)
|
||||
const transcendence = $derived(uncap.transcendence ?? false)
|
||||
|
||||
// Calculate maximum uncap level based on available uncaps
|
||||
// Weapons: 3 base + FLB + ULB + transcendence
|
||||
const getMaxUncapLevel = () => {
|
||||
return transcendence ? 6 : ulb ? 5 : flb ? 4 : 3
|
||||
}
|
||||
|
||||
const uncapLevel = $derived(getMaxUncapLevel())
|
||||
// For details view, show maximum transcendence stage when available
|
||||
const transcendenceStage = $derived(transcendence ? 5 : 0)
|
||||
</script>
|
||||
|
||||
<div class="weapon-detail">
|
||||
<div class="page-header">
|
||||
<button class="back-button" onclick={() => goto('/database/weapons')}>
|
||||
← Back to Weapons
|
||||
</button>
|
||||
<h1>Weapon Details</h1>
|
||||
</div>
|
||||
|
||||
{#if weapon}
|
||||
<div class="weapon-content">
|
||||
<div class="weapon-hero">
|
||||
<div class="weapon-image">
|
||||
<img
|
||||
src={getWeaponImage(weapon)}
|
||||
alt={getWeaponName(weapon.name)}
|
||||
onerror={(e) => {
|
||||
e.currentTarget.src = '/images/placeholders/placeholder-weapon-main.png'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="weapon-info">
|
||||
<h2 class="weapon-name">{getWeaponName(weapon.name)}</h2>
|
||||
<div class="weapon-meta">
|
||||
<div class="meta-item">
|
||||
<span class="label">Rarity:</span>
|
||||
<span class="value">{getRarityLabel(weapon.rarity)}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Element:</span>
|
||||
<div class="element-display">
|
||||
{#if weapon.element}
|
||||
<img
|
||||
src={getElementIcon(weapon.element)}
|
||||
alt={getElementLabel(weapon.element)}
|
||||
class="element-icon"
|
||||
/>
|
||||
<span class="value">{getElementLabel(weapon.element)}</span>
|
||||
{:else}
|
||||
<span class="value">—</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Proficiency:</span>
|
||||
<div class="proficiency-display">
|
||||
{#if weapon.proficiency}
|
||||
<img
|
||||
src={getProficiencyIcon(weapon.proficiency)}
|
||||
alt={getProficiencyLabel(weapon.proficiency)}
|
||||
class="proficiency-icon"
|
||||
/>
|
||||
<span class="value">{getProficiencyLabel(weapon.proficiency)}</span>
|
||||
{:else}
|
||||
<span class="value">—</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Max Level:</span>
|
||||
<span class="value">{weapon.max_level || '—'}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Granblue ID:</span>
|
||||
<span class="value">{weapon.granblue_id || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DetailsHeader type="weapon" item={weapon} image={getWeaponImage(weapon)} />
|
||||
|
||||
<div class="weapon-details">
|
||||
<h3>Stats</h3>
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">Base HP:</span>
|
||||
<span class="value">{weapon.base_hp || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Base Attack:</span>
|
||||
<span class="value">{weapon.base_attack || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Max HP:</span>
|
||||
<span class="value">{weapon.max_hp || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Max Attack:</span>
|
||||
<span class="value">{weapon.max_attack || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Skill Level Cap:</span>
|
||||
<span class="value">{weapon.skill_level_cap || '—'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">Plus Bonus:</span>
|
||||
<span class="value">{weapon.plus_bonus ? 'Yes' : 'No'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DetailsContainer title="Level & Skill">
|
||||
<DetailItem label="Max Level" value={weapon.max_level} />
|
||||
<DetailItem label="Max Skill Level" value={weapon.skill_level_cap} />
|
||||
{#if weapon.uncap}
|
||||
<DetailItem label="Uncap">
|
||||
<UncapIndicator
|
||||
type="weapon"
|
||||
{uncapLevel}
|
||||
{transcendenceStage}
|
||||
{flb}
|
||||
{ulb}
|
||||
{transcendence}
|
||||
editable={false}
|
||||
/>
|
||||
</DetailItem>
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="HP Stats">
|
||||
<DetailItem label="Base HP" value={weapon.hp?.min_hp} />
|
||||
<DetailItem label="Max HP" value={weapon.hp?.max_hp} />
|
||||
{#if flb}
|
||||
<DetailItem label="Max HP (FLB)" value={weapon.hp?.max_hp_flb} />
|
||||
{/if}
|
||||
{#if ulb}
|
||||
<DetailItem label="Max HP (ULB)" value={weapon.hp?.max_hp_ulb} />
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<DetailsContainer title="Attack Stats">
|
||||
<DetailItem label="Base Attack" value={weapon.atk?.min_atk} />
|
||||
<DetailItem label="Max Attack" value={weapon.atk?.max_atk} />
|
||||
{#if flb}
|
||||
<DetailItem label="Max Attack (FLB)" value={weapon.atk?.max_atk_flb} />
|
||||
{/if}
|
||||
{#if ulb}
|
||||
<DetailItem label="Max Attack (ULB)" value={weapon.atk?.max_atk_ulb} />
|
||||
{/if}
|
||||
</DetailsContainer>
|
||||
|
||||
<div class="weapon-skills">
|
||||
<h3>Skills</h3>
|
||||
|
|
@ -156,17 +115,17 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/layout' as layout;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
|
||||
.weapon-detail {
|
||||
padding: spacing.$unit * 2;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: spacing.$unit-2x 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: spacing.$unit * 2;
|
||||
margin-bottom: spacing.$unit-2x;
|
||||
|
||||
.back-button {
|
||||
background: #f8f9fa;
|
||||
|
|
@ -190,17 +149,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: spacing.$unit * 4;
|
||||
|
||||
.loading-spinner {
|
||||
font-size: typography.$font-medium;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
|
|
@ -223,71 +175,6 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.weapon-hero {
|
||||
display: flex;
|
||||
gap: spacing.$unit * 2;
|
||||
padding: spacing.$unit * 2;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
.weapon-image {
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.weapon-info {
|
||||
flex: 1;
|
||||
|
||||
.weapon-name {
|
||||
font-size: typography.$font-xlarge;
|
||||
font-weight: typography.$bold;
|
||||
margin: 0 0 spacing.$unit 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.weapon-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit * 0.5;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: spacing.$unit * 0.5;
|
||||
|
||||
.label {
|
||||
font-weight: typography.$medium;
|
||||
color: #666;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.element-display,
|
||||
.proficiency-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: spacing.$unit * 0.25;
|
||||
|
||||
.element-icon,
|
||||
.proficiency-icon {
|
||||
width: 25px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weapon-details,
|
||||
.weapon-skills {
|
||||
padding: spacing.$unit * 2;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
|
@ -302,30 +189,6 @@
|
|||
margin: 0 0 spacing.$unit 0;
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: spacing.$unit;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: spacing.$unit * 0.5;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
|
||||
.label {
|
||||
font-weight: typography.$medium;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
|
|
@ -361,15 +224,6 @@
|
|||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.weapon-hero {
|
||||
flex-direction: column;
|
||||
|
||||
.weapon-image img {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.details-grid,
|
||||
.skills-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue