fix: render related characters

This commit is contained in:
Justin Edmund 2025-11-30 14:57:43 -08:00
parent d5a22baa0a
commit 5df563198b
3 changed files with 308 additions and 189 deletions

View file

@ -73,6 +73,7 @@ export interface Weapon {
export interface Character { export interface Character {
id: string id: string
granblueId: string granblueId: string
characterId?: number
name: { name: {
en?: string en?: string
ja?: string ja?: string
@ -186,6 +187,16 @@ export class EntityAdapter extends BaseAdapter {
}) })
} }
/**
* Gets related characters (same character_id) for a given character
*/
async getRelatedCharacters(id: string): Promise<Character[]> {
return this.request<Character[]>(`/characters/${id}/related`, {
method: 'GET',
cacheTTL: 600000 // Cache for 10 minutes
})
}
/** /**
* Gets canonical summon data by ID * Gets canonical summon data by ID
*/ */

View file

@ -39,7 +39,8 @@
const granblueId = $derived(item?.granblue_id) const granblueId = $derived(item?.granblue_id)
// Get element name for button styling // Get element name for button styling
const elementName = $derived((() => { const elementName = $derived(
(() => {
const elementMap: Record<number, string | undefined> = { const elementMap: Record<number, string | undefined> = {
0: undefined, // Null element 0: undefined, // Null element
1: 'wind', 1: 'wind',
@ -50,7 +51,8 @@
6: 'light' 6: 'light'
} }
return elementMap[element] || undefined return elementMap[element] || undefined
})()) })()
)
// Helper function to get display name // Helper function to get display name
function getDisplayName(nameObj: string | { en?: string; ja?: string }): string { function getDisplayName(nameObj: string | { en?: string; ja?: string }): string {
@ -60,7 +62,7 @@
} }
</script> </script>
<section class="container"> <header class="container">
<div class="left"> <div class="left">
<div class="image"> <div class="image">
<img <img
@ -109,7 +111,14 @@
<Button <Button
variant="primary" variant="primary"
size="medium" size="medium"
element={elementName as "fire" | "water" | "earth" | "wind" | "light" | "dark" | undefined} element={elementName as
| 'fire'
| 'water'
| 'earth'
| 'wind'
| 'light'
| 'dark'
| undefined}
onclick={onSave} onclick={onSave}
disabled={isSaving} disabled={isSaving}
> >
@ -120,7 +129,7 @@
{/if} {/if}
</div> </div>
{/if} {/if}
</section> </header>
<style lang="scss"> <style lang="scss">
@use '$src/themes/colors' as colors; @use '$src/themes/colors' as colors;
@ -135,9 +144,9 @@
gap: spacing.$unit * 2; gap: spacing.$unit * 2;
padding: spacing.$unit * 2; padding: spacing.$unit * 2;
border-bottom: 1px solid #e5e5e5; border-bottom: 1px solid #e5e5e5;
position: sticky; // position: sticky;
top: 0; // top: 0;
z-index: 10; // z-index: 10;
background: white; background: white;
border-top-left-radius: layout.$card-corner; border-top-left-radius: layout.$card-corner;
border-top-right-radius: layout.$card-corner; border-top-right-radius: layout.$card-corner;

View file

@ -7,6 +7,7 @@
// TanStack Query // TanStack Query
import { createQuery } from '@tanstack/svelte-query' import { createQuery } from '@tanstack/svelte-query'
import { entityQueries } from '$lib/api/queries/entity.queries' import { entityQueries } from '$lib/api/queries/entity.queries'
import { entityAdapter } from '$lib/api/adapters/entity.adapter'
import { withInitialData } from '$lib/query/ssr' import { withInitialData } from '$lib/query/ssr'
// Utility functions // Utility functions
@ -43,6 +44,16 @@
// Edit mode state // Edit mode state
let editMode = $state(false) let editMode = $state(false)
// Query for related characters (same character_id)
const relatedQuery = createQuery(() => ({
queryKey: ['characters', 'related', character?.id],
queryFn: async () => {
if (!character?.id) return []
return entityAdapter.getRelatedCharacters(character.id)
},
enabled: !!character?.characterId && !editMode
}))
let isSaving = $state(false) let isSaving = $state(false)
let saveError = $state<string | null>(null) let saveError = $state<string | null>(null)
let saveSuccess = $state(false) let saveSuccess = $state(false)
@ -51,6 +62,7 @@
let editData = $state({ let editData = $state({
name: character?.name || '', name: character?.name || '',
granblueId: character?.granblueId || '', granblueId: character?.granblueId || '',
characterId: character?.characterId ?? (null as number | null),
rarity: character?.rarity || 1, rarity: character?.rarity || 1,
element: character?.element || 0, element: character?.element || 0,
race1: character?.race?.[0] ?? null, race1: character?.race?.[0] ?? null,
@ -76,6 +88,7 @@
editData = { editData = {
name: character.name || '', name: character.name || '',
granblueId: character.granblueId || '', granblueId: character.granblueId || '',
characterId: character.characterId ?? null,
rarity: character.rarity || 1, rarity: character.rarity || 1,
element: character.element || 0, element: character.element || 0,
race1: character.race?.[0] ?? null, race1: character.race?.[0] ?? null,
@ -114,6 +127,7 @@
editData = { editData = {
name: character.name || '', name: character.name || '',
granblueId: character.granblueId || '', granblueId: character.granblueId || '',
characterId: character.characterId ?? null,
rarity: character.rarity || 1, rarity: character.rarity || 1,
element: character.element || 0, element: character.element || 0,
race1: character.race?.[0] ?? null, race1: character.race?.[0] ?? null,
@ -145,9 +159,10 @@
const payload = { const payload = {
name: editData.name, name: editData.name,
granblue_id: editData.granblueId, granblue_id: editData.granblueId,
character_id: editData.characterId,
rarity: editData.rarity, rarity: editData.rarity,
element: editData.element, element: editData.element,
race: [editData.race1, editData.race2].filter(r => r !== null && r !== undefined), race: [editData.race1, editData.race2].filter((r) => r !== null && r !== undefined),
gender: editData.gender, gender: editData.gender,
proficiency: [editData.proficiency1, editData.proficiency2], proficiency: [editData.proficiency1, editData.proficiency2],
hp: { hp: {
@ -210,14 +225,18 @@
const transcendence = $derived(uncap.transcendence ?? false) const transcendence = $derived(uncap.transcendence ?? false)
const special = $derived(editMode ? editData.special : (character?.special ?? false)) const special = $derived(editMode ? editData.special : (character?.special ?? false))
const uncapLevel = $derived(getCharacterMaxUncapLevel({ special, uncap: { flb, ulb, transcendence } })) const uncapLevel = $derived(
getCharacterMaxUncapLevel({ special, uncap: { flb, ulb, transcendence } })
)
const transcendenceStage = $derived(transcendence ? 5 : 0) const transcendenceStage = $derived(transcendence ? 5 : 0)
// Get element name for checkbox theming // Get element name for checkbox theming
const elementName = $derived.by(() => { const elementName = $derived.by(() => {
const el = editMode ? editData.element : character?.element const el = editMode ? editData.element : character?.element
const label = getElementLabel(el) const label = getElementLabel(el)
return label !== '—' && label !== 'Null' ? label.toLowerCase() as 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' : undefined return label !== '—' && label !== 'Null'
? (label.toLowerCase() as 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light')
: undefined
}) })
</script> </script>
@ -248,6 +267,7 @@
</div> </div>
{/if} {/if}
<section class="details">
<DetailsContainer title="Metadata"> <DetailsContainer title="Metadata">
{#if editMode} {#if editMode}
<DetailItem <DetailItem
@ -263,11 +283,18 @@
editable={true} editable={true}
type="text" type="text"
/> />
<DetailItem
label="Character ID"
bind:value={editData.characterId}
editable={true}
type="number"
/>
{:else} {:else}
<DetailItem label="Rarity" value={getRarityLabel(character.rarity)} /> <DetailItem label="Rarity" value={getRarityLabel(character.rarity)} />
<DetailItem label="Granblue ID" value={character.granblueId} /> <DetailItem label="Granblue ID" value={character.granblueId} />
{/if} {/if}
</DetailsContainer> </DetailsContainer>
<DetailsContainer title="Details"> <DetailsContainer title="Details">
{#if character.uncap} {#if character.uncap}
<DetailItem label="Uncap"> <DetailItem label="Uncap">
@ -285,8 +312,20 @@
{/if} {/if}
{#if editMode} {#if editMode}
<DetailItem label="FLB" bind:value={editData.flb} editable={true} type="checkbox" element={elementName} /> <DetailItem
<DetailItem label="ULB" bind:value={editData.ulb} editable={true} type="checkbox" element={elementName} /> label="FLB"
bind:value={editData.flb}
editable={true}
type="checkbox"
element={elementName}
/>
<DetailItem
label="ULB"
bind:value={editData.ulb}
editable={true}
type="checkbox"
element={elementName}
/>
<DetailItem <DetailItem
label="Transcendence" label="Transcendence"
bind:value={editData.transcendence} bind:value={editData.transcendence}
@ -353,8 +392,14 @@
<DetailItem label="Race 2" value={getRaceLabel(character.race?.[1])} /> <DetailItem label="Race 2" value={getRaceLabel(character.race?.[1])} />
{/if} {/if}
<DetailItem label="Gender" value={getGenderLabel(character.gender)} /> <DetailItem label="Gender" value={getGenderLabel(character.gender)} />
<DetailItem label="Proficiency 1" value={getProficiencyLabel(character.proficiency?.[0] ?? 0)} /> <DetailItem
<DetailItem label="Proficiency 2" value={getProficiencyLabel(character.proficiency?.[1] ?? 0)} /> label="Proficiency 1"
value={getProficiencyLabel(character.proficiency?.[0] ?? 0)}
/>
<DetailItem
label="Proficiency 2"
value={getProficiencyLabel(character.proficiency?.[1] ?? 0)}
/>
{/if} {/if}
</DetailsContainer> </DetailsContainer>
@ -421,6 +466,24 @@
{/if} {/if}
{/if} {/if}
</DetailsContainer> </DetailsContainer>
{#if !editMode && relatedQuery.data?.length}
<DetailsContainer title="Related Units">
<div class="related-units">
{#each relatedQuery.data as related}
<a href="/database/characters/{related.id}" class="related-unit">
<img
src={getCharacterImage(related.granblueId, 'grid', '01')}
alt={related.name.en}
class="related-image"
/>
<span class="related-name">{related.name.en}</span>
</a>
{/each}
</div>
</DetailsContainer>
{/if}
</section>
</div> </div>
{:else} {:else}
<div class="not-found"> <div class="not-found">
@ -461,9 +524,13 @@
background: white; background: white;
border-radius: layout.$card-corner; border-radius: layout.$card-corner;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: visible; // Changed from hidden to allow sticky header overflow: visible;
margin-top: spacing.$unit-2x;
position: relative; position: relative;
.details {
display: flex;
flex-direction: column;
}
} }
.edit-controls { .edit-controls {
@ -494,4 +561,36 @@
opacity: 1; opacity: 1;
} }
} }
.related-units {
display: flex;
flex-wrap: wrap;
gap: spacing.$unit-2x;
padding: spacing.$unit-2x;
}
.related-unit {
display: flex;
flex-direction: column;
align-items: center;
text-decoration: none;
color: colors.$grey-30;
&:hover .related-image {
transform: scale(1.05);
}
}
.related-image {
width: 128px;
height: auto;
border-radius: layout.$item-corner;
transition: transform 0.2s ease;
}
.related-name {
margin-top: spacing.$unit;
font-size: typography.$font-small;
text-align: center;
}
</style> </style>