add sidebar components for descriptions and details
This commit is contained in:
parent
fc32e18ea8
commit
838e09d17b
27 changed files with 965 additions and 893 deletions
276
src/lib/components/DescriptionRenderer.svelte
Normal file
276
src/lib/components/DescriptionRenderer.svelte
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<script lang="ts">
|
||||
import type { JSONContent } from '@tiptap/core'
|
||||
|
||||
interface Props {
|
||||
content?: string
|
||||
truncate?: boolean
|
||||
maxLines?: number
|
||||
}
|
||||
|
||||
let { content, truncate = false, maxLines = 3 }: Props = $props()
|
||||
|
||||
// Convert TipTap JSON to HTML manually
|
||||
function jsonToHtml(node: JSONContent): string {
|
||||
if (!node) return ''
|
||||
|
||||
// Handle text nodes
|
||||
if (node.type === 'text') {
|
||||
let text = node.text || ''
|
||||
|
||||
// Apply marks (formatting)
|
||||
if (node.marks) {
|
||||
node.marks.forEach(mark => {
|
||||
switch (mark.type) {
|
||||
case 'bold':
|
||||
text = `<strong>${text}</strong>`
|
||||
break
|
||||
case 'italic':
|
||||
text = `<em>${text}</em>`
|
||||
break
|
||||
case 'strike':
|
||||
text = `<s>${text}</s>`
|
||||
break
|
||||
case 'underline':
|
||||
text = `<u>${text}</u>`
|
||||
break
|
||||
case 'highlight':
|
||||
text = `<mark>${text}</mark>`
|
||||
break
|
||||
case 'link':
|
||||
text = `<a href="${mark.attrs?.href}" target="_blank" rel="noopener noreferrer">${text}</a>`
|
||||
break
|
||||
case 'code':
|
||||
text = `<code>${text}</code>`
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// Handle different node types
|
||||
switch (node.type) {
|
||||
case 'doc':
|
||||
return (node.content || []).map(jsonToHtml).join('')
|
||||
|
||||
case 'paragraph':
|
||||
const content = (node.content || []).map(jsonToHtml).join('')
|
||||
return `<p>${content || '<br>'}</p>`
|
||||
|
||||
case 'heading':
|
||||
const level = node.attrs?.level || 1
|
||||
const headingContent = (node.content || []).map(jsonToHtml).join('')
|
||||
return `<h${level}>${headingContent}</h${level}>`
|
||||
|
||||
case 'bulletList':
|
||||
const listItems = (node.content || []).map(jsonToHtml).join('')
|
||||
return `<ul>${listItems}</ul>`
|
||||
|
||||
case 'orderedList':
|
||||
const orderedItems = (node.content || []).map(jsonToHtml).join('')
|
||||
return `<ol>${orderedItems}</ol>`
|
||||
|
||||
case 'listItem':
|
||||
const itemContent = (node.content || []).map(jsonToHtml).join('')
|
||||
return `<li>${itemContent}</li>`
|
||||
|
||||
case 'blockquote':
|
||||
const quoteContent = (node.content || []).map(jsonToHtml).join('')
|
||||
return `<blockquote>${quoteContent}</blockquote>`
|
||||
|
||||
case 'codeBlock':
|
||||
const codeContent = (node.content || []).map(n => n.text || '').join('')
|
||||
return `<pre><code>${codeContent}</code></pre>`
|
||||
|
||||
case 'hardBreak':
|
||||
return '<br>'
|
||||
|
||||
case 'horizontalRule':
|
||||
return '<hr>'
|
||||
|
||||
case 'youtube':
|
||||
// For now, show a link to the video in truncated view
|
||||
const videoUrl = node.attrs?.src || ''
|
||||
return `<p><a href="${videoUrl}" target="_blank" rel="noopener noreferrer">📹 View Video</a></p>`
|
||||
|
||||
case 'mention':
|
||||
// Handle game item mentions
|
||||
const mentionName = node.attrs?.id?.name?.en || node.attrs?.id?.granblue_en || 'Unknown'
|
||||
const wikiUrl = `https://gbf.wiki/${mentionName}`
|
||||
return `<a href="${wikiUrl}" target="_blank" rel="noopener noreferrer" class="mention">${mentionName}</a>`
|
||||
|
||||
default:
|
||||
// For unknown types, try to render content if it exists
|
||||
if (node.content) {
|
||||
return (node.content || []).map(jsonToHtml).join('')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// Parse content - handle both JSON and plain text
|
||||
function parseContent(content?: string): string {
|
||||
if (!content) return ''
|
||||
|
||||
// Try to parse as JSON first
|
||||
try {
|
||||
const json = JSON.parse(content) as JSONContent
|
||||
return jsonToHtml(json)
|
||||
} catch {
|
||||
// If not JSON, treat as plain text
|
||||
// Convert double newlines to paragraphs and single newlines to br tags
|
||||
const paragraphs = content.split('\n\n')
|
||||
const formatted = paragraphs
|
||||
.map((p) => {
|
||||
const lines = p.split('\n')
|
||||
return `<p>${lines.join('<br />')}</p>`
|
||||
})
|
||||
.join('')
|
||||
return formatted
|
||||
}
|
||||
}
|
||||
|
||||
const parsedHTML = $derived(parseContent(content))
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="description-content"
|
||||
class:truncate
|
||||
style={truncate ? `--max-lines: ${maxLines}` : ''}
|
||||
>
|
||||
{@html parsedHTML}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/colors' as *;
|
||||
@use '$src/themes/spacing' as *;
|
||||
|
||||
.description-content {
|
||||
color: var(--text-primary);
|
||||
font-size: $font-regular;
|
||||
line-height: 1.6;
|
||||
|
||||
// Basic HTML styling for generated content
|
||||
:global {
|
||||
p {
|
||||
margin: 0 0 $unit 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-weight: $bold;
|
||||
margin: $unit 0 $unit-half 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $font-xlarge;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $font-large;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $font-medium;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-blue);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
mark {
|
||||
background: rgba(255, 237, 76, 0.3);
|
||||
color: var(--text-primary);
|
||||
padding: 0 $unit-fourth;
|
||||
border-radius: 2px;
|
||||
font-weight: $medium;
|
||||
}
|
||||
|
||||
.mention {
|
||||
color: var(--accent-blue);
|
||||
font-weight: $medium;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 0 0 $unit 0;
|
||||
padding-left: $unit-3x;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: $unit-half 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--button-bg);
|
||||
padding: 2px $unit-half;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--button-bg);
|
||||
padding: $unit;
|
||||
border-radius: $unit-half;
|
||||
overflow-x: auto;
|
||||
margin: $unit 0;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--accent-blue);
|
||||
padding-left: $unit-2x;
|
||||
margin: $unit 0;
|
||||
font-style: italic;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--button-bg);
|
||||
margin: $unit-2x 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.truncate {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: var(--max-lines, 3);
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
// Hide block elements that might break truncation
|
||||
:global {
|
||||
pre, blockquote, ul, ol {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
138
src/lib/components/sidebar/DescriptionSidebar.svelte
Normal file
138
src/lib/components/sidebar/DescriptionSidebar.svelte
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<script lang="ts">
|
||||
import DescriptionRenderer from '$lib/components/DescriptionRenderer.svelte'
|
||||
import Button from '$lib/components/ui/Button.svelte'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
description?: string
|
||||
canEdit?: boolean
|
||||
onEdit?: () => void
|
||||
}
|
||||
|
||||
let { title, description, canEdit = false, onEdit }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="description-sidebar">
|
||||
<div class="content-section">
|
||||
{#if title}
|
||||
<div class="party-title">
|
||||
<h2>{title}</h2>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if description}
|
||||
<div class="description-content">
|
||||
<DescriptionRenderer content={description} truncate={false} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<p>No description available for this party.</p>
|
||||
{#if canEdit}
|
||||
<Button
|
||||
variant="primary"
|
||||
onclick={onEdit}
|
||||
>
|
||||
Add Description
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if canEdit && description}
|
||||
<div class="actions-section">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={onEdit}
|
||||
class="edit-button"
|
||||
>
|
||||
Edit Description
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as *;
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/effects' as *;
|
||||
|
||||
.description-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: $unit-2x;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.content-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.party-title {
|
||||
margin-bottom: $unit-3x;
|
||||
padding-bottom: $unit-2x;
|
||||
border-bottom: 1px solid var(--button-bg);
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: $font-xlarge;
|
||||
font-weight: $bold;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.description-content {
|
||||
// Allow the content to scroll if it's very long
|
||||
overflow-y: auto;
|
||||
padding-right: $unit;
|
||||
|
||||
// Custom scrollbar styling
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--button-bg);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--text-secondary);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: $unit-4x;
|
||||
min-height: 200px;
|
||||
|
||||
p {
|
||||
margin: 0 0 $unit-2x 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-regular;
|
||||
}
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
margin-top: $unit-2x;
|
||||
padding-top: $unit-2x;
|
||||
border-top: 1px solid var(--button-bg);
|
||||
|
||||
:global(.edit-button) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
441
src/lib/components/sidebar/DetailsSidebar.svelte
Normal file
441
src/lib/components/sidebar/DetailsSidebar.svelte
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
<script lang="ts">
|
||||
import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||
import { getElementLabel } from '$lib/utils/element'
|
||||
import { getRarityLabel } from '$lib/utils/rarity'
|
||||
import { getProficiencyLabel } from '$lib/utils/proficiency'
|
||||
import { getRaceLabel } from '$lib/utils/race'
|
||||
import { getGenderLabel } from '$lib/utils/gender'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
|
||||
interface Props {
|
||||
type: 'character' | 'weapon' | 'summon'
|
||||
item: GridCharacter | GridWeapon | GridSummon
|
||||
}
|
||||
|
||||
let { type, item }: Props = $props()
|
||||
|
||||
// Helper to get the actual item data
|
||||
function getItemData() {
|
||||
if (type === 'character') {
|
||||
return (item as GridCharacter).character
|
||||
} else if (type === 'weapon') {
|
||||
return (item as GridWeapon).weapon
|
||||
} else {
|
||||
return (item as GridSummon).summon
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for localized names
|
||||
function displayName(input: any): string {
|
||||
if (!input) return '—'
|
||||
const maybe = input.name ?? input
|
||||
if (typeof maybe === 'string') return maybe
|
||||
if (maybe && typeof maybe === 'object') {
|
||||
return maybe.en || maybe.ja || '—'
|
||||
}
|
||||
return '—'
|
||||
}
|
||||
|
||||
// Get the item's actual data
|
||||
const itemData = $derived(getItemData())
|
||||
|
||||
// Grid item info (uncap levels from the grid item itself)
|
||||
const gridUncapLevel = $derived(
|
||||
type === 'character' ? (item as GridCharacter).uncapLevel :
|
||||
type === 'weapon' ? (item as GridWeapon).uncapLevel :
|
||||
(item as GridSummon).uncapLevel
|
||||
)
|
||||
|
||||
const gridTranscendence = $derived(
|
||||
type === 'character' ? (item as GridCharacter).transcendenceStep :
|
||||
type === 'weapon' ? (item as GridWeapon).transcendenceStep :
|
||||
(item as GridSummon).transcendenceStep
|
||||
)
|
||||
|
||||
// Get image URL based on type
|
||||
function getImageUrl(): string {
|
||||
if (!itemData?.granblueId) {
|
||||
return type === 'character' ? '/images/placeholders/placeholder-character-main.png' :
|
||||
type === 'weapon' ? '/images/placeholders/placeholder-weapon-main.png' :
|
||||
'/images/placeholders/placeholder-summon-main.png'
|
||||
}
|
||||
|
||||
const id = itemData.granblueId
|
||||
|
||||
if (type === 'character') {
|
||||
let pose = '01'
|
||||
if (gridTranscendence && gridTranscendence > 0) pose = '04'
|
||||
else if (gridUncapLevel && gridUncapLevel >= 5) pose = '03'
|
||||
else if (gridUncapLevel && gridUncapLevel > 2) pose = '02'
|
||||
return `/images/character-main/${id}_${pose}.jpg`
|
||||
} else if (type === 'weapon') {
|
||||
return `/images/weapon-main/${id}.jpg`
|
||||
} else {
|
||||
return `/images/summon-main/${id}.jpg`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="details-sidebar">
|
||||
<div class="item-header">
|
||||
<img
|
||||
src={getImageUrl()}
|
||||
alt={displayName(itemData)}
|
||||
class="item-image"
|
||||
/>
|
||||
<div class="item-title">
|
||||
<h2>{displayName(itemData)}</h2>
|
||||
{#if itemData?.granblueId}
|
||||
<span class="granblue-id">ID: {itemData.granblueId}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details-section">
|
||||
<h3>Basic Information</h3>
|
||||
<div class="detail-row">
|
||||
<span class="label">Rarity</span>
|
||||
<span class="value">{getRarityLabel(itemData?.rarity)}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="label">Element</span>
|
||||
<span class="value">{getElementLabel(itemData?.element)}</span>
|
||||
</div>
|
||||
|
||||
{#if type === 'character'}
|
||||
{#if itemData?.race && itemData.race.length > 0}
|
||||
<div class="detail-row">
|
||||
<span class="label">Race</span>
|
||||
<span class="value">
|
||||
{itemData.race.map(r => getRaceLabel(r)).filter(Boolean).join(', ') || '—'}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="detail-row">
|
||||
<span class="label">Gender</span>
|
||||
<span class="value">{getGenderLabel(itemData?.gender)}</span>
|
||||
</div>
|
||||
{#if itemData?.proficiency && itemData.proficiency.length > 0}
|
||||
<div class="detail-row">
|
||||
<span class="label">Proficiencies</span>
|
||||
<span class="value">
|
||||
{itemData.proficiency.map(p => getProficiencyLabel(p)).filter(Boolean).join(', ') || '—'}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if type === 'weapon'}
|
||||
<div class="detail-row">
|
||||
<span class="label">Proficiency</span>
|
||||
<span class="value">{getProficiencyLabel(itemData?.proficiency?.[0])}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="details-section">
|
||||
<h3>Uncap Status</h3>
|
||||
<div class="uncap-display">
|
||||
<UncapIndicator
|
||||
type={type}
|
||||
uncapLevel={gridUncapLevel}
|
||||
transcendenceStage={gridTranscendence}
|
||||
special={itemData?.special}
|
||||
flb={itemData?.uncap?.flb}
|
||||
ulb={itemData?.uncap?.ulb}
|
||||
transcendence={itemData?.uncap?.transcendence}
|
||||
editable={false}
|
||||
/>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="label">Current Uncap</span>
|
||||
<span class="value">{gridUncapLevel ?? 0}★</span>
|
||||
</div>
|
||||
{#if gridTranscendence && gridTranscendence > 0}
|
||||
<div class="detail-row">
|
||||
<span class="label">Transcendence</span>
|
||||
<span class="value">Stage {gridTranscendence}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if itemData?.uncap}
|
||||
<div class="detail-row">
|
||||
<span class="label">Available Uncaps</span>
|
||||
<span class="value">
|
||||
{[
|
||||
itemData.uncap.flb && 'FLB',
|
||||
itemData.uncap.ulb && 'ULB',
|
||||
itemData.uncap.transcendence && 'Transcendence'
|
||||
].filter(Boolean).join(', ') || 'Standard'}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="details-section">
|
||||
<h3>Stats</h3>
|
||||
{#if itemData?.hp}
|
||||
<div class="stats-group">
|
||||
<h4>HP</h4>
|
||||
<div class="detail-row">
|
||||
<span class="label">Base</span>
|
||||
<span class="value">{itemData.hp.minHp ?? '—'}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="label">Max</span>
|
||||
<span class="value">{itemData.hp.maxHp ?? '—'}</span>
|
||||
</div>
|
||||
{#if itemData.uncap?.flb && itemData.hp.maxHpFlb}
|
||||
<div class="detail-row">
|
||||
<span class="label">Max (FLB)</span>
|
||||
<span class="value">{itemData.hp.maxHpFlb}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if itemData.uncap?.ulb && itemData.hp.maxHpUlb}
|
||||
<div class="detail-row">
|
||||
<span class="label">Max (ULB)</span>
|
||||
<span class="value">{itemData.hp.maxHpUlb}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if itemData?.atk}
|
||||
<div class="stats-group">
|
||||
<h4>Attack</h4>
|
||||
<div class="detail-row">
|
||||
<span class="label">Base</span>
|
||||
<span class="value">{itemData.atk.minAtk ?? '—'}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="label">Max</span>
|
||||
<span class="value">{itemData.atk.maxAtk ?? '—'}</span>
|
||||
</div>
|
||||
{#if itemData.uncap?.flb && itemData.atk.maxAtkFlb}
|
||||
<div class="detail-row">
|
||||
<span class="label">Max (FLB)</span>
|
||||
<span class="value">{itemData.atk.maxAtkFlb}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if itemData.uncap?.ulb && itemData.atk.maxAtkUlb}
|
||||
<div class="detail-row">
|
||||
<span class="label">Max (ULB)</span>
|
||||
<span class="value">{itemData.atk.maxAtkUlb}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if type === 'weapon' && itemData?.weaponSkills && itemData.weaponSkills.length > 0}
|
||||
<div class="details-section">
|
||||
<h3>Skills</h3>
|
||||
<div class="skills-list">
|
||||
{#each itemData.weaponSkills as skill}
|
||||
<div class="skill-item">
|
||||
<h4>{displayName(skill) || 'Unknown Skill'}</h4>
|
||||
{#if skill.description}
|
||||
<p>{skill.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if type === 'summon' && itemData?.summonAuras && itemData.summonAuras.length > 0}
|
||||
<div class="details-section">
|
||||
<h3>Auras</h3>
|
||||
<div class="auras-list">
|
||||
{#each itemData.summonAuras as aura}
|
||||
<div class="aura-item">
|
||||
<h4>{displayName(aura) || 'Unknown Aura'}</h4>
|
||||
{#if aura.description}
|
||||
<p>{aura.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if type === 'weapon' && itemData?.weaponKeys && itemData.weaponKeys.length > 0}
|
||||
<div class="details-section">
|
||||
<h3>Weapon Keys</h3>
|
||||
<div class="keys-list">
|
||||
{#each itemData.weaponKeys as key}
|
||||
<div class="key-item">
|
||||
<span class="key-slot">Slot {key.slot}</span>
|
||||
<span class="key-name">{displayName(key.weaponKey1) || '—'}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if type === 'character' && itemData?.special}
|
||||
<div class="details-section">
|
||||
<div class="detail-row special-indicator">
|
||||
<span class="label">Special Character</span>
|
||||
<span class="value">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as typography;
|
||||
@use '$src/themes/layout' as layout;
|
||||
|
||||
.details-sidebar {
|
||||
padding: spacing.$unit-2x;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
gap: spacing.$unit-2x;
|
||||
align-items: flex-start;
|
||||
margin-bottom: spacing.$unit-3x;
|
||||
padding-bottom: spacing.$unit-2x;
|
||||
border-bottom: 1px solid var(--border-color, colors.$grey-70);
|
||||
}
|
||||
|
||||
.item-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: layout.$item-corner;
|
||||
object-fit: cover;
|
||||
background: colors.$grey-80;
|
||||
border: 1px solid colors.$grey-70;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
flex: 1;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 calc(spacing.$unit * 0.5) 0;
|
||||
font-size: typography.$font-xlarge;
|
||||
font-weight: typography.$medium;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
}
|
||||
|
||||
.granblue-id {
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-secondary, colors.$grey-50);
|
||||
}
|
||||
}
|
||||
|
||||
.details-section {
|
||||
margin-bottom: spacing.$unit-3x;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 calc(spacing.$unit * 1.5) 0;
|
||||
font-size: typography.$font-regular;
|
||||
font-weight: typography.$medium;
|
||||
color: var(--text-secondary, colors.$grey-40);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: spacing.$unit 0 calc(spacing.$unit * 0.5) 0;
|
||||
font-size: typography.$font-regular;
|
||||
font-weight: typography.$medium;
|
||||
color: var(--text-primary, colors.$grey-20);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: calc(spacing.$unit * 0.75) 0;
|
||||
border-bottom: 1px solid rgba(colors.$grey-70, 0.5);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: typography.$font-regular;
|
||||
color: var(--text-secondary, colors.$grey-50);
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: typography.$font-regular;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
font-weight: typography.$medium;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.uncap-display {
|
||||
margin-bottom: calc(spacing.$unit * 1.5);
|
||||
padding: spacing.$unit;
|
||||
background: colors.$grey-90;
|
||||
border-radius: calc(layout.$item-corner * 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stats-group {
|
||||
margin-bottom: spacing.$unit-2x;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.skills-list,
|
||||
.auras-list {
|
||||
.skill-item,
|
||||
.aura-item {
|
||||
padding: spacing.$unit;
|
||||
background: colors.$grey-90;
|
||||
border-radius: calc(layout.$item-corner * 0.5);
|
||||
margin-bottom: spacing.$unit;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 calc(spacing.$unit * 0.5) 0;
|
||||
font-size: typography.$font-regular;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-secondary, colors.$grey-50);
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.keys-list {
|
||||
.key-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: calc(spacing.$unit * 0.75);
|
||||
background: colors.$grey-90;
|
||||
border-radius: calc(layout.$item-corner * 0.5);
|
||||
margin-bottom: calc(spacing.$unit * 0.5);
|
||||
|
||||
.key-slot {
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-secondary, colors.$grey-50);
|
||||
}
|
||||
|
||||
.key-name {
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
font-weight: typography.$medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.special-indicator {
|
||||
.value {
|
||||
color: var(--color-success, #4caf50);
|
||||
font-size: typography.$font-large;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -18,20 +18,10 @@
|
|||
headerActions?: Snippet
|
||||
}
|
||||
|
||||
const {
|
||||
open = false,
|
||||
title,
|
||||
onclose,
|
||||
children,
|
||||
headerActions
|
||||
}: Props = $props()
|
||||
const { open = false, title, onclose, children, headerActions }: Props = $props()
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="sidebar"
|
||||
class:open
|
||||
style:--sidebar-width={open ? SIDEBAR_WIDTH : '0'}
|
||||
>
|
||||
<aside class="sidebar" class:open style:--sidebar-width={open ? SIDEBAR_WIDTH : '0'}>
|
||||
{#if title}
|
||||
<SidebarHeader {title} {onclose} actions={headerActions} />
|
||||
{/if}
|
||||
|
|
@ -51,6 +41,9 @@
|
|||
@use '$src/themes/effects' as *;
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100vh;
|
||||
background: var(--bg-primary);
|
||||
border-left: 1px solid var(--border-primary);
|
||||
|
|
@ -60,6 +53,7 @@
|
|||
width: 0;
|
||||
overflow: hidden;
|
||||
transition: width $duration-slide ease-in-out;
|
||||
z-index: 50;
|
||||
|
||||
&.open {
|
||||
width: var(--sidebar-width);
|
||||
|
|
@ -68,24 +62,50 @@
|
|||
.sidebar-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: $unit-2x;
|
||||
|
||||
// Smooth scrolling
|
||||
scroll-behavior: smooth;
|
||||
|
||||
// Better scrollbar styling to match main content
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary, #f1f1f1);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border-primary, #888);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--text-secondary, #555);
|
||||
}
|
||||
}
|
||||
|
||||
// Improve mobile scrolling performance
|
||||
@media (max-width: 768px) {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile styles - overlay approach
|
||||
@media (max-width: 768px) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
transform: translateX(100%);
|
||||
transition: transform $duration-slide ease-in-out, width 0s;
|
||||
width: 100vw !important;
|
||||
transition:
|
||||
transform $duration-slide ease-in-out,
|
||||
width 0s;
|
||||
width: 90vw !important;
|
||||
max-width: 400px;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
|
||||
&.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import DescriptionSidebar from '$lib/components/sidebar/DescriptionSidebar.svelte'
|
||||
|
||||
interface DescriptionSidebarOptions {
|
||||
title?: string
|
||||
description?: string
|
||||
canEdit?: boolean
|
||||
onEdit?: () => void
|
||||
}
|
||||
|
||||
export function openDescriptionSidebar(options: DescriptionSidebarOptions) {
|
||||
const { title, description, canEdit = false, onEdit } = options
|
||||
|
||||
// Open the sidebar with the description component
|
||||
sidebar.openWithComponent('Description', DescriptionSidebar, {
|
||||
title,
|
||||
description,
|
||||
canEdit,
|
||||
onEdit
|
||||
})
|
||||
}
|
||||
|
||||
export function closeDescriptionSidebar() {
|
||||
sidebar.close()
|
||||
}
|
||||
46
src/lib/features/details/openDetailsSidebar.svelte.ts
Normal file
46
src/lib/features/details/openDetailsSidebar.svelte.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import DetailsSidebar from '$lib/components/sidebar/DetailsSidebar.svelte'
|
||||
import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||
|
||||
interface DetailsSidebarOptions {
|
||||
type: 'weapon' | 'character' | 'summon'
|
||||
item: GridCharacter | GridWeapon | GridSummon
|
||||
}
|
||||
|
||||
export function openDetailsSidebar(options: DetailsSidebarOptions) {
|
||||
const { type, item } = options
|
||||
|
||||
// Get the item name for the title
|
||||
let itemName = 'Details'
|
||||
if (type === 'character' && (item as GridCharacter).character) {
|
||||
const char = (item as GridCharacter).character
|
||||
itemName = getName(char)
|
||||
} else if (type === 'weapon' && (item as GridWeapon).weapon) {
|
||||
const weapon = (item as GridWeapon).weapon
|
||||
itemName = getName(weapon)
|
||||
} else if (type === 'summon' && (item as GridSummon).summon) {
|
||||
const summon = (item as GridSummon).summon
|
||||
itemName = getName(summon)
|
||||
}
|
||||
|
||||
// Open the sidebar with the details component
|
||||
const title = itemName !== 'Details' ? itemName : `${type.charAt(0).toUpperCase() + type.slice(1)} Details`
|
||||
sidebar.openWithComponent(title, DetailsSidebar, {
|
||||
type,
|
||||
item
|
||||
})
|
||||
}
|
||||
|
||||
function getName(obj: any): string {
|
||||
if (!obj) return 'Details'
|
||||
const name = obj.name ?? obj
|
||||
if (typeof name === 'string') return name
|
||||
if (name && typeof name === 'object') {
|
||||
return name.en || name.ja || 'Details'
|
||||
}
|
||||
return 'Details'
|
||||
}
|
||||
|
||||
export function closeDetailsSidebar() {
|
||||
sidebar.close()
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import { PUBLIC_SIERO_API_URL } from '$env/static/public'
|
||||
|
||||
/**
|
||||
* Utility functions for API route handlers
|
||||
* These routes act as proxies to the Rails API
|
||||
*/
|
||||
|
||||
export const API_BASE = new URL(PUBLIC_SIERO_API_URL || 'http://localhost:3000').href
|
||||
|
||||
/**
|
||||
* Build a full URL for the Rails API
|
||||
*/
|
||||
export function buildApiUrl(path: string, params?: Record<string, any>): string {
|
||||
const url = new URL(path.startsWith('http') ? path : `${API_BASE}${path}`, API_BASE)
|
||||
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined || value === null) continue
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((x) => url.searchParams.append(key, String(x)))
|
||||
} else {
|
||||
url.searchParams.set(key, String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract headers that should be forwarded to the Rails API
|
||||
*/
|
||||
export function extractHeaders(request: Request): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
if (editKey) {
|
||||
headers['X-Edit-Key'] = editKey
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* Common error handler for API routes
|
||||
*/
|
||||
export function handleApiError(error: any, action: string) {
|
||||
console.error(`Error ${action}:`, error)
|
||||
return {
|
||||
error: `Failed to ${action}`,
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../_utils'
|
||||
|
||||
/**
|
||||
* POST /api/parties - Create a new party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ request, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const headers = extractHeaders(request)
|
||||
|
||||
// Forward to Rails API
|
||||
// The server-side fetch will automatically add Bearer token if user is authenticated
|
||||
const response = await fetch(buildApiUrl('/parties'), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
return json(handleApiError(error, 'create party'), { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../_utils'
|
||||
|
||||
/**
|
||||
* PUT /api/parties/[id] - Update a party
|
||||
* DELETE /api/parties/[id] - Delete a party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
|
||||
export const PUT: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const body = await request.json()
|
||||
const headers = extractHeaders(request)
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl(`/parties/${id}`), {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
return json(handleApiError(error, 'update party'), { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const DELETE: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const headers = extractHeaders(request)
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl(`/parties/${id}`), {
|
||||
method: 'DELETE',
|
||||
headers
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
}
|
||||
|
||||
// Handle error responses
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
return json(errorData, { status: response.status })
|
||||
} catch (error) {
|
||||
return json(handleApiError(error, 'delete party'), { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../_utils'
|
||||
|
||||
/**
|
||||
* POST /api/parties/[id]/characters - Add character to party
|
||||
* DELETE /api/parties/[id]/characters - Remove character from party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
|
||||
export const POST: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Transform to Rails API format
|
||||
const railsBody = {
|
||||
character: {
|
||||
party_id: params.id,
|
||||
character_id: body.characterId,
|
||||
position: body.position,
|
||||
uncap_level: body.uncapLevel ?? 3,
|
||||
transcendence_step: body.transcendenceStep ?? 0,
|
||||
perpetuity: body.perpetuity ?? false
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl('/characters'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(railsBody)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
console.error('Error adding character:', error)
|
||||
return json(
|
||||
{ error: 'Failed to add character' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const DELETE: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward to Rails API - use grid_characters endpoint with the ID
|
||||
const response = await fetch(buildApiUrl(`/grid_characters/${body.gridCharacterId}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// DELETE might not return a body
|
||||
const text = await response.text()
|
||||
const data = text ? JSON.parse(text) : {}
|
||||
return json(data, { status: response.status })
|
||||
}
|
||||
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
return json(errorData, { status: response.status })
|
||||
} catch (error) {
|
||||
console.error('Error removing character:', error)
|
||||
return json(
|
||||
{ error: 'Failed to remove character' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../../_utils'
|
||||
|
||||
/**
|
||||
* PUT /api/parties/[id]/characters/[characterId] - Update character in party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
|
||||
export const PUT: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Transform to Rails API format
|
||||
const railsBody = {
|
||||
character: body
|
||||
}
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl(`/grid_characters/${params.characterId}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(railsBody)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
console.error('Error updating character:', error)
|
||||
return json(
|
||||
{ error: 'Failed to update character' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../_utils'
|
||||
|
||||
/**
|
||||
* POST /api/parties/[id]/grid_characters - Add character to party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
|
||||
export const POST: RequestHandler = async ({ request, params, fetch, cookies }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl('/characters'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${cookies.get('access_token')}`,
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify({
|
||||
character: {
|
||||
party_id: params.id,
|
||||
...body
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
console.error('Error adding character:', error)
|
||||
return json(
|
||||
{ error: 'Failed to add character' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { json } from '@sveltejs/kit'
|
||||
import type { RequestHandler } from './$types'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../../../_utils'
|
||||
|
||||
export const PUT: RequestHandler = async ({ params, request, fetch, cookies }) => {
|
||||
const { id: partyId, characterId } = params
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward the request to the Rails API
|
||||
const apiResponse = await fetch(
|
||||
buildApiUrl(`/parties/${partyId}/grid_characters/${characterId}/position`),
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${cookies.get('access_token')}`,
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const error = await apiResponse.json().catch(() => ({ error: 'Failed to update character position' }))
|
||||
return json(error, { status: apiResponse.status })
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
return json(data)
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { json } from '@sveltejs/kit'
|
||||
import type { RequestHandler } from './$types'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../../_utils'
|
||||
|
||||
export const POST: RequestHandler = async ({ params, request, fetch, cookies }) => {
|
||||
const { id: partyId } = params
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward the request to the Rails API
|
||||
const apiResponse = await fetch(
|
||||
buildApiUrl(`/parties/${partyId}/grid_characters/swap`),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${cookies.get('access_token')}`,
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const error = await apiResponse.json().catch(() => ({ error: 'Failed to swap characters' }))
|
||||
return json(error, { status: apiResponse.status })
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
return json(data)
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../_utils'
|
||||
|
||||
/**
|
||||
* POST /api/parties/[id]/grid_summons - Add summon to party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
|
||||
export const POST: RequestHandler = async ({ request, params, fetch, cookies }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl('/summons'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${cookies.get('access_token')}`,
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify({
|
||||
summon: {
|
||||
party_id: params.id,
|
||||
...body
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
console.error('Error adding summon:', error)
|
||||
return json(
|
||||
{ error: 'Failed to add summon' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { json } from '@sveltejs/kit'
|
||||
import type { RequestHandler } from './$types'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../../../_utils'
|
||||
|
||||
export const PUT: RequestHandler = async ({ params, request, fetch, cookies }) => {
|
||||
const { id: partyId, summonId } = params
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward the request to the Rails API
|
||||
const apiResponse = await fetch(
|
||||
buildApiUrl(`/parties/${partyId}/grid_summons/${summonId}/position`),
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${cookies.get('access_token')}`,
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const error = await apiResponse.json().catch(() => ({ error: 'Failed to update summon position' }))
|
||||
return json(error, { status: apiResponse.status })
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
return json(data)
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { json } from '@sveltejs/kit'
|
||||
import type { RequestHandler } from './$types'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../../_utils'
|
||||
|
||||
export const POST: RequestHandler = async ({ params, request, fetch, cookies }) => {
|
||||
const { id: partyId } = params
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward the request to the Rails API
|
||||
const apiResponse = await fetch(
|
||||
buildApiUrl(`/parties/${partyId}/grid_summons/swap`),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${cookies.get('access_token')}`,
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const error = await apiResponse.json().catch(() => ({ error: 'Failed to swap summons' }))
|
||||
return json(error, { status: apiResponse.status })
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
return json(data)
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../_utils'
|
||||
|
||||
/**
|
||||
* POST /api/parties/[id]/grid_weapons - Add weapon to party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
|
||||
export const POST: RequestHandler = async ({ request, params, fetch, cookies }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const headers = {
|
||||
...extractHeaders(request),
|
||||
Authorization: `Bearer ${cookies.get('access_token')}`
|
||||
}
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl('/weapons'), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
weapon: {
|
||||
party_id: params.id,
|
||||
...body
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
return json(handleApiError(error, 'add weapon'), { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { json } from '@sveltejs/kit'
|
||||
import type { RequestHandler } from './$types'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../../../_utils'
|
||||
|
||||
export const PUT: RequestHandler = async ({ params, request, fetch, cookies }) => {
|
||||
const { id: partyId, weaponId } = params
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward the request to the Rails API
|
||||
const apiResponse = await fetch(
|
||||
buildApiUrl(`/parties/${partyId}/grid_weapons/${weaponId}/position`),
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${cookies.get('access_token')}`,
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const error = await apiResponse.json().catch(() => ({ error: 'Failed to update weapon position' }))
|
||||
return json(error, { status: apiResponse.status })
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
return json(data)
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { json } from '@sveltejs/kit'
|
||||
import type { RequestHandler } from './$types'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../../_utils'
|
||||
|
||||
export const POST: RequestHandler = async ({ params, request, fetch, cookies }) => {
|
||||
const { id: partyId } = params
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward the request to the Rails API
|
||||
const apiResponse = await fetch(
|
||||
buildApiUrl(`/parties/${partyId}/grid_weapons/swap`),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${cookies.get('access_token')}`,
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const error = await apiResponse.json().catch(() => ({ error: 'Failed to swap weapons' }))
|
||||
return json(error, { status: apiResponse.status })
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
return json(data)
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../_utils'
|
||||
|
||||
/**
|
||||
* POST /api/parties/[id]/summons - Add summon to party
|
||||
* DELETE /api/parties/[id]/summons - Remove summon from party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
|
||||
export const POST: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Transform to Rails API format
|
||||
const railsBody = {
|
||||
summon: {
|
||||
party_id: params.id,
|
||||
summon_id: body.summonId,
|
||||
position: body.position,
|
||||
main: body.position === -1 || body.main,
|
||||
friend: body.position === 6 || body.friend,
|
||||
quick_summon: body.quickSummon ?? false,
|
||||
uncap_level: body.uncapLevel ?? 3,
|
||||
transcendence_step: body.transcendenceStep ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl('/summons'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(railsBody)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
console.error('Error adding summon:', error)
|
||||
return json(
|
||||
{ error: 'Failed to add summon' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const DELETE: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward to Rails API - use grid_summons endpoint with the ID
|
||||
const response = await fetch(buildApiUrl(`/grid_summons/${body.gridSummonId}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// DELETE might not return a body
|
||||
const text = await response.text()
|
||||
const data = text ? JSON.parse(text) : {}
|
||||
return json(data, { status: response.status })
|
||||
}
|
||||
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
return json(errorData, { status: response.status })
|
||||
} catch (error) {
|
||||
console.error('Error removing summon:', error)
|
||||
return json(
|
||||
{ error: 'Failed to remove summon' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../../_utils'
|
||||
|
||||
/**
|
||||
* PUT /api/parties/[id]/summons/[summonId] - Update summon in party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
|
||||
export const PUT: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Transform to Rails API format
|
||||
const railsBody = {
|
||||
summon: body
|
||||
}
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl(`/grid_summons/${params.summonId}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(railsBody)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
console.error('Error updating summon:', error)
|
||||
return json(
|
||||
{ error: 'Failed to update summon' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../_utils'
|
||||
|
||||
/**
|
||||
* POST /api/parties/[id]/weapons - Add weapon to party
|
||||
* DELETE /api/parties/[id]/weapons - Remove weapon from party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
|
||||
export const POST: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Transform to Rails API format
|
||||
const railsBody = {
|
||||
weapon: {
|
||||
party_id: params.id,
|
||||
weapon_id: body.weaponId,
|
||||
position: body.position,
|
||||
mainhand: body.position === -1 || body.mainhand,
|
||||
uncap_level: body.uncapLevel ?? 3,
|
||||
transcendence_step: body.transcendenceStep ?? 0,
|
||||
element: body.element
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl('/weapons'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(railsBody)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
return json(handleApiError(error, 'adding weapon'), { status: 500 })
|
||||
return json(
|
||||
{ error: 'Failed to add weapon' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const DELETE: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
console.log('DELETE weapon request:', { body, params })
|
||||
|
||||
// Forward to Rails API - use grid_weapons endpoint with the ID
|
||||
const response = await fetch(buildApiUrl(`/grid_weapons/${body.gridWeaponId}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// DELETE might not return a body
|
||||
const text = await response.text()
|
||||
const data = text ? JSON.parse(text) : {}
|
||||
return json(data, { status: response.status })
|
||||
}
|
||||
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
return json(errorData, { status: response.status })
|
||||
} catch (error) {
|
||||
return json(handleApiError(error, 'removing weapon'), { status: 500 })
|
||||
return json(
|
||||
{ error: 'Failed to remove weapon' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../../../_utils'
|
||||
|
||||
/**
|
||||
* PUT /api/parties/[id]/weapons/[weaponId] - Update weapon in party
|
||||
* Proxies to Rails API with proper authentication
|
||||
*/
|
||||
|
||||
export const PUT: RequestHandler = async ({ request, params, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Transform to Rails API format
|
||||
const railsBody = {
|
||||
weapon: body
|
||||
}
|
||||
|
||||
// Forward to Rails API
|
||||
const response = await fetch(buildApiUrl(`/grid_weapons/${params.weaponId}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(railsBody)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
return json(data, { status: response.status })
|
||||
} catch (error) {
|
||||
return json(handleApiError(error, 'updating weapon'), { status: 500 })
|
||||
return json(
|
||||
{ error: 'Failed to update weapon' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../_utils'
|
||||
|
||||
export const POST: RequestHandler = async ({ request, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward to Rails API with automatic auth via handleFetch
|
||||
const response = await fetch(buildApiUrl('/characters/update_uncap'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }))
|
||||
return json(error, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return json(data)
|
||||
} catch (error) {
|
||||
console.error('Error updating character uncap:', error)
|
||||
return json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../_utils'
|
||||
|
||||
export const POST: RequestHandler = async ({ request, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward to Rails API with automatic auth via handleFetch
|
||||
const response = await fetch(buildApiUrl('/summons/update_uncap'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }))
|
||||
return json(error, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return json(data)
|
||||
} catch (error) {
|
||||
console.error('Error updating summon uncap:', error)
|
||||
return json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { json, type RequestHandler } from '@sveltejs/kit'
|
||||
import { buildApiUrl, extractHeaders, handleApiError } from '../../_utils'
|
||||
|
||||
export const POST: RequestHandler = async ({ request, fetch }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const editKey = request.headers.get('X-Edit-Key')
|
||||
|
||||
// Forward to Rails API with automatic auth via handleFetch
|
||||
const response = await fetch(buildApiUrl('/weapons/update_uncap'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: response.statusText }))
|
||||
return json(error, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return json(data)
|
||||
} catch (error) {
|
||||
console.error('Error updating weapon uncap:', error)
|
||||
return json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue