fix job skill types and slot styling
- update JobSkill type with emp/base boolean flags - use new skill fields in jobUtils and components - update job adapter with locale and filter params - restyle empty skill slots with cleaner placeholder - simplify ML badge to not show level number
This commit is contained in:
parent
0379cff81e
commit
5403aebe48
7 changed files with 138 additions and 52 deletions
|
|
@ -21,6 +21,8 @@ export interface SearchJobSkillsParams {
|
|||
jobId: string // Required for API
|
||||
page?: number
|
||||
per?: number
|
||||
locale?: string
|
||||
filters?: { group?: number }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -114,12 +116,13 @@ export class JobAdapter extends BaseAdapter {
|
|||
body: {
|
||||
search: {
|
||||
query: params.query || '',
|
||||
job: params.jobId
|
||||
},
|
||||
page: params.page || 1,
|
||||
per: params.per || 50
|
||||
},
|
||||
cacheTTL: 60000 // Cache for 1 minute
|
||||
job: params.jobId,
|
||||
page: params.page || 1,
|
||||
locale: params.locale || 'en',
|
||||
filters: params.filters || {}
|
||||
}
|
||||
}
|
||||
// Note: No caching - filters change frequently and cache key bug causes stale data
|
||||
})
|
||||
|
||||
// Transform the response to match the expected format
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@
|
|||
{#if job.masterLevel || job.ultimateMastery}
|
||||
<div class="job-badges">
|
||||
{#if job.masterLevel}
|
||||
<span class="badge master">ML{job.masterLevel}</span>
|
||||
<span class="badge master">ML</span>
|
||||
{/if}
|
||||
{#if job.ultimateMastery}
|
||||
<span class="badge ultimate">UM</span>
|
||||
|
|
@ -110,7 +110,7 @@
|
|||
{#if job.masterLevel || job.ultimateMastery}
|
||||
<div class="job-badges">
|
||||
{#if job.masterLevel}
|
||||
<span class="badge master">ML{job.masterLevel}</span>
|
||||
<span class="badge master">ML</span>
|
||||
{/if}
|
||||
{#if job.ultimateMastery}
|
||||
<span class="badge ultimate">UM</span>
|
||||
|
|
|
|||
|
|
@ -23,9 +23,8 @@
|
|||
function getSkillColorClass(skill: JobSkill): string {
|
||||
if (skill.main) return 'skill-main'
|
||||
if (skill.sub) return 'skill-sub'
|
||||
// Use category for additional classification
|
||||
if (skill.category === 2) return 'skill-emp'
|
||||
if (skill.category === 1) return 'skill-base'
|
||||
if (skill.emp) return 'skill-emp'
|
||||
if (skill.base) return 'skill-base'
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -112,8 +112,10 @@
|
|||
|
||||
{#snippet EmptyState({ slot })}
|
||||
<div class="empty-content">
|
||||
<Icon name="plus" size={20} />
|
||||
<span>Slot {slot + 1}</span>
|
||||
<div class="placeholder-icon">
|
||||
<Icon name="plus" size={16} />
|
||||
</div>
|
||||
<span class="placeholder-text">Select a skill</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
|
|
@ -154,12 +156,19 @@
|
|||
}
|
||||
|
||||
&.empty {
|
||||
border-style: dashed;
|
||||
background: var(--placeholder-bg);
|
||||
background: transparent;
|
||||
|
||||
&.editable:hover {
|
||||
background: var(--button-contained-bg-hover);
|
||||
border-style: solid;
|
||||
background: var(--button-bg-hover);
|
||||
|
||||
.placeholder-icon {
|
||||
border-color: var(--border-medium);
|
||||
box-shadow: var(--hover-shadow);
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
color: var(--text-tertiary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,16 +231,30 @@
|
|||
|
||||
.empty-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
gap: $unit;
|
||||
padding: $unit;
|
||||
color: var(--text-tertiary);
|
||||
height: 60px;
|
||||
height: 100%;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
.placeholder-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid transparent;
|
||||
border-radius: $unit-half;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--icon-secondary);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-tertiary);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import JobSkillItem from '../job/JobSkillItem.svelte'
|
||||
import Button from '../ui/Button.svelte'
|
||||
import Input from '../ui/Input.svelte'
|
||||
import Select from '../ui/Select.svelte'
|
||||
import Icon from '../Icon.svelte'
|
||||
import * as m from '$lib/paraglide/messages'
|
||||
|
||||
|
|
@ -27,13 +28,28 @@
|
|||
onRemoveSkill
|
||||
}: Props = $props()
|
||||
|
||||
// Skill category filter options
|
||||
// Values match Rails search_controller expectations:
|
||||
// -1: All, 0-3: Color categories, 4: EMP, 5: Base
|
||||
const skillCategoryOptions = [
|
||||
{ value: -1, label: 'All Skills' },
|
||||
{ value: 2, label: 'Damaging' },
|
||||
{ value: 0, label: 'Buffing' },
|
||||
{ value: 1, label: 'Debuffing' },
|
||||
{ value: 3, label: 'Healing' },
|
||||
{ value: 4, label: 'EMP' },
|
||||
{ value: 5, label: 'Base' }
|
||||
]
|
||||
|
||||
// State
|
||||
let searchQuery = $state('')
|
||||
let skillCategory = $state(-1) // -1 = All
|
||||
let error = $state<string | undefined>()
|
||||
let sentinelEl = $state<HTMLElement>()
|
||||
let skillsResource = $state<ReturnType<typeof createInfiniteScrollResource<JobSkill>> | null>(null)
|
||||
let lastSearchQuery = ''
|
||||
let lastJobId: string | undefined
|
||||
let lastCategory = -1
|
||||
|
||||
// Check if slot is locked
|
||||
const slotLocked = $derived(targetSlot === 0)
|
||||
|
|
@ -44,9 +60,10 @@
|
|||
// Manage resource creation and search updates
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
function updateSearch() {
|
||||
function updateSearch(forceRebuild = false) {
|
||||
const jobId = job?.id
|
||||
const locked = slotLocked
|
||||
const category = skillCategory
|
||||
|
||||
// Clean up if no job or locked
|
||||
if (!jobId || locked) {
|
||||
|
|
@ -56,21 +73,29 @@
|
|||
}
|
||||
lastJobId = undefined
|
||||
lastSearchQuery = ''
|
||||
lastCategory = -1
|
||||
return
|
||||
}
|
||||
|
||||
// Create new resource if job changed
|
||||
if (jobId !== lastJobId) {
|
||||
// Rebuild resource if job or category changed
|
||||
const needsRebuild = forceRebuild || jobId !== lastJobId || category !== lastCategory
|
||||
|
||||
if (needsRebuild) {
|
||||
if (skillsResource) {
|
||||
skillsResource.destroy()
|
||||
}
|
||||
|
||||
// Capture current values for the closure
|
||||
const currentQuery = searchQuery
|
||||
const currentCategory = category
|
||||
|
||||
const resource = createInfiniteScrollResource<JobSkill>({
|
||||
fetcher: async (page) => {
|
||||
const response = await jobAdapter.searchSkills({
|
||||
query: lastSearchQuery,
|
||||
query: currentQuery,
|
||||
jobId,
|
||||
page
|
||||
page,
|
||||
...(currentCategory >= 0 ? { filters: { group: currentCategory } } : {})
|
||||
})
|
||||
return response
|
||||
},
|
||||
|
|
@ -81,19 +106,21 @@
|
|||
skillsResource = resource
|
||||
lastJobId = jobId
|
||||
lastSearchQuery = searchQuery
|
||||
lastCategory = category
|
||||
resource.load()
|
||||
}
|
||||
// Reload if search query changed
|
||||
// Reload if only search query changed
|
||||
else if (searchQuery !== lastSearchQuery && skillsResource) {
|
||||
lastSearchQuery = searchQuery
|
||||
skillsResource.reset()
|
||||
skillsResource.load()
|
||||
// Need to rebuild to capture new query in closure
|
||||
updateSearch(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for job changes
|
||||
// Watch for job and category changes
|
||||
$effect(() => {
|
||||
job // Track job
|
||||
skillCategory // Track category
|
||||
updateSearch()
|
||||
})
|
||||
|
||||
|
|
@ -198,6 +225,16 @@
|
|||
disabled={!canSearch}
|
||||
fullWidth={true}
|
||||
/>
|
||||
<div class="filter-row">
|
||||
<Select
|
||||
options={skillCategoryOptions}
|
||||
bind:value={skillCategory}
|
||||
placeholder="Filter by category"
|
||||
disabled={!canSearch}
|
||||
size="small"
|
||||
fullWidth={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="skills-container">
|
||||
|
|
@ -235,10 +272,19 @@
|
|||
<div class="empty-state">
|
||||
<Icon name="search-x" size={32} />
|
||||
<p>No skills found</p>
|
||||
{#if searchQuery}
|
||||
<Button size="small" variant="ghost" on:click={() => searchQuery = ''}>
|
||||
Clear search
|
||||
</Button>
|
||||
{#if searchQuery || skillCategory >= 0}
|
||||
<div class="clear-filters">
|
||||
{#if searchQuery}
|
||||
<Button size="small" variant="ghost" on:click={() => searchQuery = ''}>
|
||||
Clear search
|
||||
</Button>
|
||||
{/if}
|
||||
{#if skillCategory >= 0}
|
||||
<Button size="small" variant="ghost" on:click={() => skillCategory = -1}>
|
||||
Clear filter
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -341,11 +387,19 @@
|
|||
}
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
padding: $unit-2x 0;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
.skills-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
|
@ -372,6 +426,12 @@
|
|||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.clear-filters {
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
margin-top: $unit;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state :global(svg) {
|
||||
|
|
|
|||
|
|
@ -91,20 +91,24 @@ export interface Job {
|
|||
row: number
|
||||
order: number
|
||||
proficiency: [number, number]
|
||||
masterLevel?: number
|
||||
ultimateMastery?: number
|
||||
masterLevel?: boolean // Whether this job supports master level
|
||||
ultimateMastery?: boolean // Whether this job supports ultimate mastery
|
||||
accessory?: boolean
|
||||
accessoryType?: number
|
||||
}
|
||||
|
||||
// JobSkill entity
|
||||
// JobSkill entity from JobSkillBlueprint
|
||||
export interface JobSkill {
|
||||
id: string
|
||||
name: LocalizedName
|
||||
slug: string
|
||||
category: number
|
||||
main: boolean
|
||||
sub: boolean
|
||||
color: number // Skill category (0-3 for colors, relates to skill type)
|
||||
main: boolean // Primary job skill
|
||||
sub: boolean // Sub-skill (transferable)
|
||||
emp: boolean // EMP skill
|
||||
base: boolean // Base skill (for advanced jobs)
|
||||
order: number // Display order
|
||||
job: Job // Associated job
|
||||
}
|
||||
|
||||
// JobAccessory entity
|
||||
|
|
|
|||
|
|
@ -154,9 +154,8 @@ export function isSkillSlotLocked(
|
|||
export function getSkillCategoryName(skill: JobSkill): string {
|
||||
if (skill.main) return 'Main'
|
||||
if (skill.sub) return 'Subskill'
|
||||
// Use category field for additional classification
|
||||
if (skill.category === 2) return 'EMP'
|
||||
if (skill.category === 1) return 'Base'
|
||||
if (skill.emp) return 'EMP'
|
||||
if (skill.base) return 'Base'
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
|
|
@ -167,9 +166,8 @@ export function getSkillCategoryName(skill: JobSkill): string {
|
|||
export function getSkillCategoryColor(skill: JobSkill): string {
|
||||
if (skill.main) return 'var(--skill-main)'
|
||||
if (skill.sub) return 'var(--skill-sub)'
|
||||
// Use category field for additional classification
|
||||
if (skill.category === 2) return 'var(--skill-emp)'
|
||||
if (skill.category === 1) return 'var(--skill-base)'
|
||||
if (skill.emp) return 'var(--skill-emp)'
|
||||
if (skill.base) return 'var(--skill-base)'
|
||||
return 'var(--skill-default)'
|
||||
}
|
||||
|
||||
|
|
@ -226,9 +224,8 @@ export function countSkillsByType(skills: JobSkillList): {
|
|||
if (skill) {
|
||||
if (skill.main) counts.main++
|
||||
else if (skill.sub) counts.sub++
|
||||
// Use category field for additional classification
|
||||
else if (skill.category === 2) counts.emp++
|
||||
else if (skill.category === 1) counts.base++
|
||||
else if (skill.emp) counts.emp++
|
||||
else if (skill.base) counts.base++
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue