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