extend DatabaseGridWithProvider to support jobs

- add searchJobs method and job-specific filters to search adapter
- add jobs case to DatabaseProvider
- extend DatabaseGridWithProvider for jobs resource
- hide collection filters for jobs (no element/rarity/series)
- extend filterParams and listNavigation for job entity
This commit is contained in:
Justin Edmund 2026-01-04 02:50:59 -08:00
parent 23527c727e
commit ec4b4bc3ae
6 changed files with 114 additions and 26 deletions

View file

@ -377,6 +377,67 @@ export class SearchAdapter extends BaseAdapter {
})
}
/**
* Searches for jobs with specific filters
*
* @param params - Search parameters with job-specific filters
* @returns Promise resolving to job search results
*/
async searchJobs(params: SearchParams = {}): Promise<SearchResponse> {
const body: any = {
locale: params.locale || 'en',
page: params.page || 1
}
if (params.per) {
body.per = params.per
}
if (params.query) {
body.query = params.query
}
if (params.sortBy) {
body.sort = params.sortBy
}
if (params.sortOrder) {
body.order = params.sortOrder
}
// Build job-specific filters
if (params.filters) {
const filters: any = {}
if (params.filters.row?.length) {
filters.row = params.filters.row
}
if (params.filters.proficiency?.length) {
filters.proficiency = params.filters.proficiency
}
if (params.filters.masterLevel !== undefined) {
filters.masterLevel = params.filters.masterLevel
}
if (params.filters.ultimateMastery !== undefined) {
filters.ultimateMastery = params.filters.ultimateMastery
}
if (params.filters.accessory !== undefined) {
filters.accessory = params.filters.accessory
}
if (Object.keys(filters).length > 0) {
body.filters = filters
}
}
return this.request<SearchResponse>('/search/jobs', {
method: 'POST',
body: { search: body },
credentials: 'omit',
cacheTTL: params.query ? 300000 : 0,
headers: params.per ? { 'X-Per-Page': String(params.per) } : undefined
})
}
/**
* Clears all cached search results
* Useful when entity data has been updated

View file

@ -191,6 +191,23 @@ export interface SearchFilters {
/** Filter special characters */
special?: boolean | undefined
// Job-specific filters
/** Filter by job row (1-5, ex, ex2, o1) */
row?: string[] | undefined
/** Filter by proficiency (for jobs - matches either proficiency1 or proficiency2) */
proficiency?: number[] | undefined
/** Filter jobs with master level */
masterLevel?: boolean | undefined
/** Filter jobs with ultimate mastery */
ultimateMastery?: boolean | undefined
/** Filter jobs that support accessories */
accessory?: boolean | undefined
/** Custom filters for specific use cases */
[key: string]: any
}

View file

@ -28,7 +28,7 @@
import type { Snippet } from 'svelte'
interface Props {
resource: 'weapons' | 'characters' | 'summons'
resource: 'weapons' | 'characters' | 'summons' | 'jobs'
columns: IColumn[]
pageSize?: number
leftActions?: Snippet
@ -45,9 +45,14 @@
// Derive entity type from resource
const entityType = $derived(
resource === 'characters' ? 'character' : resource === 'summons' ? 'summon' : 'weapon'
resource === 'characters' ? 'character' :
resource === 'summons' ? 'summon' :
resource === 'jobs' ? 'job' : 'weapon'
)
// Jobs don't use the standard CollectionFilters component
const supportsCollectionFilters = $derived(resource !== 'jobs')
// Fetch weapon series list for URL slug mapping (only for weapons)
const weaponSeriesQuery = createQuery(() =>
queryOptions({
@ -403,29 +408,31 @@
{@render headerActions()}
{/if}
<Button
variant="ghost"
size="small"
onclick={() => (showFilters = !showFilters)}
class="filter-toggle {hasActiveFilters ? 'has-active' : ''}"
>
Filters
{#if hasActiveFilters}
<span class="filter-count {selectedElement ?? ''}">
{elementFilters.length +
rarityFilters.length +
seriesFilters.length +
proficiencyFilters.length +
seasonFilters.length}
</span>
{/if}
</Button>
{#if supportsCollectionFilters}
<Button
variant="ghost"
size="small"
onclick={() => (showFilters = !showFilters)}
class="filter-toggle {hasActiveFilters ? 'has-active' : ''}"
>
Filters
{#if hasActiveFilters}
<span class="filter-count {selectedElement ?? ''}">
{elementFilters.length +
rarityFilters.length +
seriesFilters.length +
proficiencyFilters.length +
seasonFilters.length}
</span>
{/if}
</Button>
{/if}
<input type="text" placeholder="Search..." bind:value={searchTerm} />
</div>
</div>
{#if showFilters}
{#if showFilters && supportsCollectionFilters}
<div class="filters-row">
<CollectionFilters
entityType={resource === 'characters'

View file

@ -4,7 +4,7 @@ import type { SearchParams } from '$lib/api/adapters/search.adapter'
import type { SearchFilters } from '$lib/api/adapters/types'
interface DatabaseProviderOptions {
resource: 'weapons' | 'characters' | 'summons'
resource: 'weapons' | 'characters' | 'summons' | 'jobs'
pageSize?: number
}
@ -19,7 +19,7 @@ interface APIResponse {
}
export class DatabaseProvider extends RestDataProvider<any> {
private resource: 'weapons' | 'characters' | 'summons'
private resource: 'weapons' | 'characters' | 'summons' | 'jobs'
private pageSize: number
private currentPage: number = 1
private totalCount: number = 0
@ -72,6 +72,9 @@ export class DatabaseProvider extends RestDataProvider<any> {
case 'summons':
result = await searchAdapter.searchSummons(searchParams)
break
case 'jobs':
result = await searchAdapter.searchJobs(searchParams)
break
default:
throw new Error(`Unknown resource type: ${this.resource}`)
}

View file

@ -181,7 +181,7 @@ function parseParamArray<T>(
*/
export function parseFiltersFromUrl(
searchParams: URLSearchParams,
entityType: 'character' | 'weapon' | 'summon',
entityType: 'character' | 'weapon' | 'summon' | 'job',
weaponSeriesList?: WeaponSeries[]
): ParsedFilters {
const element = parseParamArray(searchParams, 'element', PARAM_TO_ELEMENT)
@ -252,7 +252,7 @@ export function buildUrlFromFilters(
filters: CollectionFilterState,
searchQuery: string,
page: number,
entityType: 'character' | 'weapon' | 'summon',
entityType: 'character' | 'weapon' | 'summon' | 'job',
weaponSeriesList?: WeaponSeries[]
): URLSearchParams {
const params = new URLSearchParams()

View file

@ -8,13 +8,13 @@ const STORAGE_KEY = 'database_list_url'
interface StoredListUrl {
url: string
resource: 'characters' | 'weapons' | 'summons'
resource: 'characters' | 'weapons' | 'summons' | 'jobs'
}
/**
* Store the current list URL before navigating to a detail page
*/
export function storeListUrl(url: string, resource: 'characters' | 'weapons' | 'summons'): void {
export function storeListUrl(url: string, resource: 'characters' | 'weapons' | 'summons' | 'jobs'): void {
try {
const data: StoredListUrl = { url, resource }
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data))