added details components for database section
This commit is contained in:
parent
bca4843885
commit
a8dfe28b07
2 changed files with 209 additions and 48 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue