add detail UI components and uncap utility

This commit is contained in:
Justin Edmund 2025-09-17 13:39:28 -07:00
parent 0b771fc405
commit 54491b1158
4 changed files with 272 additions and 0 deletions

View 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>

View 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>

View 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
View 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)
}