added details components for database section

This commit is contained in:
Justin Edmund 2025-09-17 22:14:19 -07:00
parent bca4843885
commit a8dfe28b07
2 changed files with 209 additions and 48 deletions

View file

@ -2,21 +2,68 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte' import type { Snippet } from 'svelte'
import Input from './Input.svelte'
import Select from './Select.svelte'
interface SelectOption {
value: string | number
label: string
disabled?: boolean
}
let { let {
label, label,
value, value = $bindable(),
children children,
editable = false,
type = 'text',
options,
placeholder
}: { }: {
label: string label: string
value?: string | number | undefined value?: string | number | undefined
children?: Snippet children?: Snippet
editable?: boolean
type?: 'text' | 'number' | 'select' | 'checkbox'
options?: SelectOption[]
placeholder?: string
} = $props() } = $props()
// For checkbox type, convert value to boolean
let checkboxValue = $state(type === 'checkbox' ? Boolean(value) : false)
$effect(() => {
if (type === 'checkbox') {
value = checkboxValue as any
}
})
</script> </script>
<div class="detail-item"> <div class="detail-item" class:editable>
<span class="label">{label}</span> <span class="label">{label}</span>
{#if children} {#if editable}
<div class="edit-value">
{#if type === 'select' && options}
<Select bind:value {options} {placeholder} size="medium" contained />
{:else if type === 'checkbox'}
<label class="checkbox-wrapper">
<input type="checkbox" bind:checked={checkboxValue} class="checkbox" />
<span class="checkbox-label">{checkboxValue ? 'Yes' : 'No'}</span>
</label>
{:else if type === 'number'}
<Input
bind:value
type="number"
variant="number"
bound={true}
{placeholder}
alignRight={true}
/>
{:else}
<Input bind:value type="text" bound={true} {placeholder} alignRight={true} />
{/if}
</div>
{:else if children}
<div class="value"> <div class="value">
{@render children()} {@render children()}
</div> </div>
@ -30,6 +77,7 @@
@use '$src/themes/layout' as layout; @use '$src/themes/layout' as layout;
@use '$src/themes/spacing' as spacing; @use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography; @use '$src/themes/typography' as typography;
@use '$src/themes/effects' as effects;
.detail-item { .detail-item {
display: flex; display: flex;
@ -39,14 +87,26 @@
background: colors.$grey-90; background: colors.$grey-90;
border-radius: layout.$item-corner; border-radius: layout.$item-corner;
font-size: typography.$font-regular; font-size: typography.$font-regular;
min-height: calc(spacing.$unit * 5);
&:hover { &:hover:not(.editable) {
background: colors.$grey-80; background: colors.$grey-80;
} }
&.editable:hover,
&.editable:focus-within {
background: var(--input-bg-hover);
}
&.editable {
background: var(--input-bg);
}
.label { .label {
font-weight: typography.$medium; font-weight: typography.$medium;
color: colors.$grey-50; color: colors.$grey-50;
flex-shrink: 0;
margin-right: spacing.$unit-2x;
} }
.value { .value {
@ -54,5 +114,39 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.edit-value {
flex: 1;
display: flex;
flex-grow: 0;
justify-content: flex-end;
:global(.input),
:global(.select) {
min-width: 180px;
}
:global(.input.number) {
min-width: 120px;
}
}
.checkbox-wrapper {
display: flex;
align-items: center;
gap: spacing.$unit;
cursor: pointer;
.checkbox {
width: spacing.$unit-2x;
height: spacing.$unit-2x;
cursor: pointer;
}
.checkbox-label {
color: var(--text-primary);
user-select: none;
}
}
} }
</style> </style>

View file

@ -1,22 +1,35 @@
<svelte:options runes={true} /> <svelte:options runes={true} />
<script lang="ts"> <script lang="ts">
// Utility functions
import { getElementLabel, getElementIcon } from '$lib/utils/element'
import { getProficiencyLabel, getProficiencyIcon } from '$lib/utils/proficiency'
// Components // Components
import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte' import ProficiencyLabel from '$lib/components/labels/ProficiencyLabel.svelte'
import ElementLabel from '$lib/components/labels/ElementLabel.svelte' import ElementLabel from '$lib/components/labels/ElementLabel.svelte'
import Button from './Button.svelte'
// Props // Props
interface Props { interface Props {
type: 'character' | 'summon' | 'weapon' type: 'character' | 'summon' | 'weapon'
item: any // The character/summon/weapon object item: any // The character/summon/weapon object
image: string image: string
onEdit?: () => void // Optional edit handler
showEdit?: boolean // Whether to show the edit button
editMode?: boolean // Whether currently in edit mode
onSave?: () => void // Save handler
onCancel?: () => void // Cancel handler
isSaving?: boolean // Whether currently saving
} }
let { type, item, image }: Props = $props() let {
type,
item,
image,
onEdit,
showEdit = false,
editMode = false,
onSave,
onCancel,
isSaving = false
}: Props = $props()
// Extract commonly used fields // Extract commonly used fields
const name = $derived(item?.name) const name = $derived(item?.name)
@ -25,6 +38,20 @@
const maxLevel = $derived(item?.max_level) const maxLevel = $derived(item?.max_level)
const granblueId = $derived(item?.granblue_id) const granblueId = $derived(item?.granblue_id)
// Get element name for button styling
const elementName = $derived((() => {
const elementMap: Record<number, string> = {
0: undefined, // Null element
1: 'wind',
2: 'fire',
3: 'water',
4: 'earth',
5: 'dark',
6: 'light'
}
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 {
if (!nameObj) return 'Unknown' if (!nameObj) return 'Unknown'
@ -34,42 +61,65 @@
</script> </script>
<section class="container"> <section class="container">
<div class="info"> <div class="left">
<h2>{getDisplayName(name)}</h2> <div class="image">
<div class="meta"> <img
{#if element !== undefined} src={image}
<ElementLabel {element} size="medium" /> alt={getDisplayName(name)}
{/if} onerror={(e) => {
{#if (type === 'character' || type === 'weapon') && proficiency} const placeholder =
{#if Array.isArray(proficiency)} type === 'character'
{#if proficiency[0] !== undefined} ? '/images/placeholders/placeholder-character-main.png'
<ProficiencyLabel proficiency={proficiency[0]} size="medium" /> : type === 'summon'
{/if} ? '/images/placeholders/placeholder-summon-main.png'
{#if proficiency[1] !== undefined} : '/images/placeholders/placeholder-weapon-main.png'
<ProficiencyLabel proficiency={proficiency[1]} size="medium" /> ;(e.currentTarget as HTMLImageElement).src = placeholder
{/if} }}
{:else if proficiency !== undefined} />
<ProficiencyLabel {proficiency} size="medium" /> </div>
<div class="info">
<h2>{getDisplayName(name)}</h2>
<div class="meta">
{#if element !== undefined}
<ElementLabel {element} size="medium" />
{/if} {/if}
{/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>
</div> </div>
<div class="image"> {#if showEdit}
<img <div class="right">
src={image} {#if editMode}
alt={getDisplayName(name)} <Button variant="secondary" size="medium" onclick={onCancel} disabled={isSaving}>
onerror={(e) => { Cancel
const placeholder = </Button>
type === 'character' <Button
? '/images/placeholders/placeholder-character-main.png' variant="primary"
: type === 'summon' size="medium"
? '/images/placeholders/placeholder-summon-main.png' element={elementName}
: '/images/placeholders/placeholder-weapon-main.png' onclick={onSave}
;(e.currentTarget as HTMLImageElement).src = placeholder disabled={isSaving}
}} >
/> {isSaving ? 'Saving...' : 'Save'}
</div> </Button>
{:else}
<Button variant="secondary" size="medium" onclick={onEdit}>Edit</Button>
{/if}
</div>
{/if}
</section> </section>
<style lang="scss"> <style lang="scss">
@ -81,10 +131,28 @@
.container { .container {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
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;
top: 0;
z-index: 10;
background: white;
border-top-left-radius: layout.$card-corner;
border-top-right-radius: layout.$card-corner;
.left {
display: flex;
align-items: center;
gap: spacing.$unit-2x;
}
.right {
display: flex;
gap: spacing.$unit;
align-items: center;
}
.image { .image {
flex-shrink: 0; flex-shrink: 0;
@ -92,8 +160,7 @@
img { img {
width: 128px; width: 128px;
height: auto; height: auto;
border-radius: 8px; border-radius: layout.$item-corner;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
} }
@ -116,11 +183,11 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.details-hero { .container {
flex-direction: column; flex-direction: column;
.details-image img { .image img {
width: 150px; width: 80px;
} }
} }
} }