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">
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 {
label,
value,
children
value = $bindable(),
children,
editable = false,
type = 'text',
options,
placeholder
}: {
label: string
value?: string | number | undefined
children?: Snippet
editable?: boolean
type?: 'text' | 'number' | 'select' | 'checkbox'
options?: SelectOption[]
placeholder?: string
} = $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>
<div class="detail-item">
<div class="detail-item" class:editable>
<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">
{@render children()}
</div>
@ -30,6 +77,7 @@
@use '$src/themes/layout' as layout;
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography' as typography;
@use '$src/themes/effects' as effects;
.detail-item {
display: flex;
@ -39,14 +87,26 @@
background: colors.$grey-90;
border-radius: layout.$item-corner;
font-size: typography.$font-regular;
min-height: calc(spacing.$unit * 5);
&:hover {
&:hover:not(.editable) {
background: colors.$grey-80;
}
&.editable:hover,
&.editable:focus-within {
background: var(--input-bg-hover);
}
&.editable {
background: var(--input-bg);
}
.label {
font-weight: typography.$medium;
color: colors.$grey-50;
flex-shrink: 0;
margin-right: spacing.$unit-2x;
}
.value {
@ -54,5 +114,39 @@
display: flex;
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>

View file

@ -1,22 +1,35 @@
<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'
import Button from './Button.svelte'
// Props
interface Props {
type: 'character' | 'summon' | 'weapon'
item: any // The character/summon/weapon object
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
const name = $derived(item?.name)
@ -25,6 +38,20 @@
const maxLevel = $derived(item?.max_level)
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
function getDisplayName(nameObj: string | { en?: string; ja?: string }): string {
if (!nameObj) return 'Unknown'
@ -34,42 +61,65 @@
</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" />
<div class="left">
<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>
<div class="info">
<h2>{getDisplayName(name)}</h2>
<div class="meta">
{#if element !== undefined}
<ElementLabel {element} size="medium" />
{/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 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>
{#if showEdit}
<div class="right">
{#if editMode}
<Button variant="secondary" size="medium" onclick={onCancel} disabled={isSaving}>
Cancel
</Button>
<Button
variant="primary"
size="medium"
element={elementName}
onclick={onSave}
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save'}
</Button>
{:else}
<Button variant="secondary" size="medium" onclick={onEdit}>Edit</Button>
{/if}
</div>
{/if}
</section>
<style lang="scss">
@ -81,10 +131,28 @@
.container {
display: flex;
align-items: center;
justify-content: center;
justify-content: space-between;
gap: spacing.$unit * 2;
padding: spacing.$unit * 2;
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 {
flex-shrink: 0;
@ -92,8 +160,7 @@
img {
width: 128px;
height: auto;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: layout.$item-corner;
}
}
@ -116,11 +183,11 @@
}
@media (max-width: 768px) {
.details-hero {
.container {
flex-direction: column;
.details-image img {
width: 150px;
.image img {
width: 80px;
}
}
}