add detail UI components and uncap utility
This commit is contained in:
parent
0b771fc405
commit
54491b1158
4 changed files with 272 additions and 0 deletions
58
src/lib/components/ui/DetailItem.svelte
Normal file
58
src/lib/components/ui/DetailItem.svelte
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
let {
|
||||
label,
|
||||
value,
|
||||
children
|
||||
}: {
|
||||
label: string
|
||||
value?: string | number | undefined
|
||||
children?: Snippet
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div class="detail-item">
|
||||
<span class="label">{label}</span>
|
||||
{#if children}
|
||||
<div class="value">
|
||||
{@render children()}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="value">{value || '—'}</span>
|
||||
{/if}
|
||||
</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;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: spacing.$unit;
|
||||
background: colors.$grey-90;
|
||||
border-radius: layout.$item-corner;
|
||||
font-size: typography.$font-regular;
|
||||
|
||||
&:hover {
|
||||
background: colors.$grey-80;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: typography.$medium;
|
||||
color: colors.$grey-50;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: colors.$grey-30;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
src/lib/components/ui/DetailsContainer.svelte
Normal file
46
src/lib/components/ui/DetailsContainer.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
let { title, children }: { title: string; children: Snippet } = $props()
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h4>{title}</h4>
|
||||
<div class="details">
|
||||
{@render children()}
|
||||
</div>
|
||||
</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;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit;
|
||||
padding: spacing.$unit-2x;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: colors.$grey-30;
|
||||
font-size: typography.$font-medium;
|
||||
font-weight: typography.$bold;
|
||||
margin: 0 0 spacing.$unit 0;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
127
src/lib/components/ui/DetailsHeader.svelte
Normal file
127
src/lib/components/ui/DetailsHeader.svelte
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
// Utility functions
|
||||
import { getElementLabel, getElementIcon } from '$lib/utils/element'
|
||||
import { getProficiencyLabel, getProficiencyIcon } from '$lib/utils/proficiency'
|
||||
|
||||
// Components
|
||||
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
|
||||
import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
type: 'character' | 'summon' | 'weapon'
|
||||
item: any // The character/summon/weapon object
|
||||
image: string
|
||||
}
|
||||
|
||||
let { type, item, image }: Props = $props()
|
||||
|
||||
// Extract commonly used fields
|
||||
const name = $derived(item?.name)
|
||||
const element = $derived(item?.element)
|
||||
const proficiency = $derived(item?.proficiency)
|
||||
const maxLevel = $derived(item?.max_level)
|
||||
const granblueId = $derived(item?.granblue_id)
|
||||
|
||||
// Helper function to get display name
|
||||
function getDisplayName(nameObj: string | { en?: string; ja?: string }): string {
|
||||
if (!nameObj) return 'Unknown'
|
||||
if (typeof nameObj === 'string') return nameObj
|
||||
return nameObj.en || nameObj.ja || 'Unknown'
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="container">
|
||||
<div class="info">
|
||||
<h2>{getDisplayName(name)}</h2>
|
||||
<div class="meta">
|
||||
{#if element !== undefined}
|
||||
<ElementLabel {element} size="medium" />
|
||||
{/if}
|
||||
{#if (type === 'character' || type === 'weapon') && proficiency}
|
||||
{#if Array.isArray(proficiency)}
|
||||
{#if proficiency[0] !== undefined}
|
||||
<ProficiencyLabel proficiency={proficiency[0]} size="medium" />
|
||||
{/if}
|
||||
{#if proficiency[1] !== undefined}
|
||||
<ProficiencyLabel proficiency={proficiency[1]} size="medium" />
|
||||
{/if}
|
||||
{:else if proficiency !== undefined}
|
||||
<ProficiencyLabel {proficiency} size="medium" />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image">
|
||||
<img
|
||||
src={image}
|
||||
alt={getDisplayName(name)}
|
||||
onerror={(e) => {
|
||||
const placeholder =
|
||||
type === 'character'
|
||||
? '/images/placeholders/placeholder-character-main.png'
|
||||
: type === 'summon'
|
||||
? '/images/placeholders/placeholder-summon-main.png'
|
||||
: '/images/placeholders/placeholder-weapon-main.png'
|
||||
;(e.currentTarget as HTMLImageElement).src = placeholder
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: spacing.$unit * 2;
|
||||
padding: spacing.$unit * 2;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
.image {
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 128px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
|
||||
h2 {
|
||||
font-size: typography.$font-xlarge;
|
||||
font-weight: typography.$bold;
|
||||
margin: 0 0 spacing.$unit 0;
|
||||
color: colors.$grey-30;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: spacing.$unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.details-hero {
|
||||
flex-direction: column;
|
||||
|
||||
.details-image img {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
41
src/lib/utils/uncap.ts
Normal file
41
src/lib/utils/uncap.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* Utility functions for character uncap calculations
|
||||
*/
|
||||
|
||||
export interface UncapData {
|
||||
flb: boolean
|
||||
ulb: boolean
|
||||
transcendence?: boolean
|
||||
}
|
||||
|
||||
export interface CharacterUncapData {
|
||||
special: boolean
|
||||
uncap: UncapData
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the maximum uncap level for a character based on their uncap data
|
||||
* @param special - Whether the character is special (limited/seasonal)
|
||||
* @param flb - Whether the character has FLB (4th uncap)
|
||||
* @param ulb - Whether the character has ULB (5th uncap)
|
||||
* @returns The maximum uncap level
|
||||
*/
|
||||
export function getMaxUncapLevel(special: boolean, flb: boolean, ulb: boolean): number {
|
||||
if (special) {
|
||||
// Special characters: 3 base + FLB + ULB
|
||||
return ulb ? 5 : flb ? 4 : 3
|
||||
} else {
|
||||
// Regular characters: 4 base + FLB + ULB/transcendence
|
||||
return ulb ? 6 : flb ? 5 : 4
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the maximum uncap level from character uncap data
|
||||
* @param character - Character data with uncap information
|
||||
* @returns The maximum uncap level
|
||||
*/
|
||||
export function getCharacterMaxUncapLevel(character: CharacterUncapData): number {
|
||||
const { special, uncap } = character
|
||||
return getMaxUncapLevel(special, uncap.flb, uncap.ulb)
|
||||
}
|
||||
Loading…
Reference in a new issue