add skill, element, proficiency filters to artifact collection
This commit is contained in:
parent
856e5017ea
commit
b4f58aa5af
2 changed files with 200 additions and 25 deletions
|
|
@ -44,6 +44,11 @@ export interface CollectionArtifactListParams {
|
||||||
artifactId?: string
|
artifactId?: string
|
||||||
proficiency?: number
|
proficiency?: number
|
||||||
rarity?: 'standard' | 'quirk'
|
rarity?: 'standard' | 'quirk'
|
||||||
|
// Skill filters - each slot accepts array of modifier IDs (OR logic within slot, AND across slots)
|
||||||
|
skill1?: number[]
|
||||||
|
skill2?: number[]
|
||||||
|
skill3?: number[]
|
||||||
|
skill4?: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -260,6 +265,22 @@ export class ArtifactAdapter extends BaseAdapter {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple collection artifacts in a single batch request
|
||||||
|
*/
|
||||||
|
async deleteCollectionArtifactsBatch(ids: string[]): Promise<{ deleted: number }> {
|
||||||
|
if (ids.length === 0) return { deleted: 0 }
|
||||||
|
|
||||||
|
const response = await this.request<{
|
||||||
|
meta: { deleted: number }
|
||||||
|
}>('/collection/artifacts/batch_destroy', {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: { ids }
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.meta
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Grid Artifacts (Equipped on Characters)
|
// Grid Artifacts (Equipped on Characters)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,121 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types'
|
import type { PageData } from './$types'
|
||||||
import type { CollectionArtifact } from '$lib/types/api/artifact'
|
import type { CollectionArtifact } from '$lib/types/api/artifact'
|
||||||
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
import { getContext } from 'svelte'
|
||||||
|
import { createInfiniteQuery, createQuery } from '@tanstack/svelte-query'
|
||||||
import { artifactQueries } from '$lib/api/queries/artifact.queries'
|
import { artifactQueries } from '$lib/api/queries/artifact.queries'
|
||||||
import CollectionArtifactDetailPane from '$lib/components/collection/CollectionArtifactDetailPane.svelte'
|
import CollectionArtifactDetailPane from '$lib/components/collection/CollectionArtifactDetailPane.svelte'
|
||||||
import CollectionArtifactCard from '$lib/components/collection/CollectionArtifactCard.svelte'
|
import CollectionArtifactCard from '$lib/components/collection/CollectionArtifactCard.svelte'
|
||||||
import CollectionArtifactRow from '$lib/components/collection/CollectionArtifactRow.svelte'
|
import CollectionArtifactRow from '$lib/components/collection/CollectionArtifactRow.svelte'
|
||||||
|
import SelectableCollectionCard from '$lib/components/collection/SelectableCollectionCard.svelte'
|
||||||
|
import SelectableCollectionRow from '$lib/components/collection/SelectableCollectionRow.svelte'
|
||||||
import Icon from '$lib/components/Icon.svelte'
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
import ViewModeToggle from '$lib/components/ui/ViewModeToggle.svelte'
|
import ViewModeToggle from '$lib/components/ui/ViewModeToggle.svelte'
|
||||||
|
import MultiSelect from '$lib/components/ui/MultiSelect.svelte'
|
||||||
import { IsInViewport } from 'runed'
|
import { IsInViewport } from 'runed'
|
||||||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||||
import { viewMode, type ViewMode } from '$lib/stores/viewMode.svelte'
|
import { viewMode, type ViewMode } from '$lib/stores/viewMode.svelte'
|
||||||
import Select from '$lib/components/ui/Select.svelte'
|
import Select from '$lib/components/ui/Select.svelte'
|
||||||
import { getArtifactImage } from '$lib/utils/images'
|
import { getArtifactImage } from '$lib/utils/images'
|
||||||
|
import { LOADED_IDS_KEY, type LoadedIdsContext } from '$lib/stores/selectionMode.svelte'
|
||||||
|
|
||||||
const { data }: { data: PageData } = $props()
|
const { data }: { data: PageData } = $props()
|
||||||
|
|
||||||
|
// Get loaded IDs context from layout
|
||||||
|
const loadedIdsContext = getContext<LoadedIdsContext | undefined>(LOADED_IDS_KEY)
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
let elementFilters = $state<number[]>([])
|
let elementFilters = $state<number[]>([])
|
||||||
|
let proficiencyFilters = $state<number[]>([])
|
||||||
let rarityFilter = $state<'all' | 'standard' | 'quirk'>('all')
|
let rarityFilter = $state<'all' | 'standard' | 'quirk'>('all')
|
||||||
|
|
||||||
|
// Skill filter state - array of modifier IDs per slot
|
||||||
|
let slot1Filters = $state<number[]>([])
|
||||||
|
let slot2Filters = $state<number[]>([])
|
||||||
|
let slot3Filters = $state<number[]>([])
|
||||||
|
let slot4Filters = $state<number[]>([])
|
||||||
|
|
||||||
|
// Element options for MultiSelect
|
||||||
|
const elementOptions = [
|
||||||
|
{ value: 1, label: 'Wind' },
|
||||||
|
{ value: 2, label: 'Fire' },
|
||||||
|
{ value: 3, label: 'Water' },
|
||||||
|
{ value: 4, label: 'Earth' },
|
||||||
|
{ value: 5, label: 'Dark' },
|
||||||
|
{ value: 6, label: 'Light' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Proficiency options for MultiSelect
|
||||||
|
const proficiencyOptions = [
|
||||||
|
{ value: 1, label: 'Sabre' },
|
||||||
|
{ value: 2, label: 'Dagger' },
|
||||||
|
{ value: 3, label: 'Axe' },
|
||||||
|
{ value: 4, label: 'Spear' },
|
||||||
|
{ value: 5, label: 'Bow' },
|
||||||
|
{ value: 6, label: 'Staff' },
|
||||||
|
{ value: 7, label: 'Melee' },
|
||||||
|
{ value: 8, label: 'Harp' },
|
||||||
|
{ value: 9, label: 'Gun' },
|
||||||
|
{ value: 10, label: 'Katana' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Query for artifact skills (cached 1 hour)
|
||||||
|
const skillsQuery = createQuery(() => artifactQueries.skills())
|
||||||
|
|
||||||
|
// Build options for each slot's MultiSelect
|
||||||
|
// Slots 1-2: group_i, Slot 3: group_ii, Slot 4: group_iii
|
||||||
|
const slot1Options = $derived(
|
||||||
|
(skillsQuery.data ?? [])
|
||||||
|
.filter((s) => s.skillGroup === 'group_i')
|
||||||
|
.map((s) => ({ value: s.modifier, label: s.name.en }))
|
||||||
|
)
|
||||||
|
const slot2Options = $derived(
|
||||||
|
(skillsQuery.data ?? [])
|
||||||
|
.filter((s) => s.skillGroup === 'group_i')
|
||||||
|
.map((s) => ({ value: s.modifier, label: s.name.en }))
|
||||||
|
)
|
||||||
|
const slot3Options = $derived(
|
||||||
|
(skillsQuery.data ?? [])
|
||||||
|
.filter((s) => s.skillGroup === 'group_ii')
|
||||||
|
.map((s) => ({ value: s.modifier, label: s.name.en }))
|
||||||
|
)
|
||||||
|
const slot4Options = $derived(
|
||||||
|
(skillsQuery.data ?? [])
|
||||||
|
.filter((s) => s.skillGroup === 'group_iii')
|
||||||
|
.map((s) => ({ value: s.modifier, label: s.name.en }))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if any filters are active (for clear button)
|
||||||
|
const hasActiveFilters = $derived(
|
||||||
|
elementFilters.length > 0 ||
|
||||||
|
proficiencyFilters.length > 0 ||
|
||||||
|
slot1Filters.length > 0 ||
|
||||||
|
slot2Filters.length > 0 ||
|
||||||
|
slot3Filters.length > 0 ||
|
||||||
|
slot4Filters.length > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
function clearAllFilters() {
|
||||||
|
elementFilters = []
|
||||||
|
proficiencyFilters = []
|
||||||
|
slot1Filters = []
|
||||||
|
slot2Filters = []
|
||||||
|
slot3Filters = []
|
||||||
|
slot4Filters = []
|
||||||
|
}
|
||||||
|
|
||||||
// Sentinel for infinite scroll
|
// Sentinel for infinite scroll
|
||||||
let sentinelEl = $state<HTMLElement>()
|
let sentinelEl = $state<HTMLElement>()
|
||||||
|
|
||||||
// Build filters for query
|
// Build filters for query
|
||||||
const queryFilters = $derived({
|
const queryFilters = $derived({
|
||||||
element: elementFilters.length > 0 ? elementFilters : undefined,
|
element: elementFilters.length > 0 ? elementFilters : undefined,
|
||||||
rarity: rarityFilter !== 'all' ? rarityFilter : undefined
|
proficiency: proficiencyFilters.length > 0 ? proficiencyFilters[0] : undefined,
|
||||||
|
rarity: rarityFilter !== 'all' ? rarityFilter : undefined,
|
||||||
|
skill1: slot1Filters.length > 0 ? slot1Filters : undefined,
|
||||||
|
skill2: slot2Filters.length > 0 ? slot2Filters : undefined,
|
||||||
|
skill3: slot3Filters.length > 0 ? slot3Filters : undefined,
|
||||||
|
skill4: slot4Filters.length > 0 ? slot4Filters : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
// Query for artifacts collection
|
// Query for artifacts collection
|
||||||
|
|
@ -44,6 +133,12 @@
|
||||||
return collectionQuery.data.pages.flatMap((page) => page.results ?? [])
|
return collectionQuery.data.pages.flatMap((page) => page.results ?? [])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Provide loaded IDs to layout for "Select all"
|
||||||
|
$effect(() => {
|
||||||
|
const ids = allArtifacts.map((a) => a.id)
|
||||||
|
loadedIdsContext?.setIds(ids)
|
||||||
|
})
|
||||||
|
|
||||||
// Infinite scroll
|
// Infinite scroll
|
||||||
const inViewport = new IsInViewport(() => sentinelEl, {
|
const inViewport = new IsInViewport(() => sentinelEl, {
|
||||||
rootMargin: '200px'
|
rootMargin: '200px'
|
||||||
|
|
@ -95,22 +190,62 @@
|
||||||
<div class="collection-page">
|
<div class="collection-page">
|
||||||
<!-- Filters bar -->
|
<!-- Filters bar -->
|
||||||
<div class="filters-bar">
|
<div class="filters-bar">
|
||||||
<Select
|
<div class="filter-group">
|
||||||
value={rarityFilter}
|
<Select
|
||||||
onValueChange={(v) => (rarityFilter = v as 'all' | 'standard' | 'quirk')}
|
value={rarityFilter}
|
||||||
options={[
|
onValueChange={(v) => (rarityFilter = v as 'all' | 'standard' | 'quirk')}
|
||||||
{ value: 'all', label: 'All' },
|
options={[
|
||||||
{ value: 'standard', label: 'Standard' },
|
{ value: 'all', label: 'All' },
|
||||||
{ value: 'quirk', label: 'Quirk' }
|
{ value: 'standard', label: 'Standard' },
|
||||||
]}
|
{ value: 'quirk', label: 'Quirk' }
|
||||||
size="small"
|
]}
|
||||||
/>
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
<ViewModeToggle
|
<MultiSelect
|
||||||
value={currentViewMode}
|
options={elementOptions}
|
||||||
onValueChange={handleViewModeChange}
|
bind:value={elementFilters}
|
||||||
neutral={true}
|
placeholder="Element"
|
||||||
/>
|
size="small"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
options={proficiencyOptions}
|
||||||
|
bind:value={proficiencyFilters}
|
||||||
|
placeholder="Proficiency"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MultiSelect
|
||||||
|
options={slot1Options}
|
||||||
|
bind:value={slot1Filters}
|
||||||
|
placeholder="Slot 1"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
options={slot2Options}
|
||||||
|
bind:value={slot2Filters}
|
||||||
|
placeholder="Slot 2"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
options={slot3Options}
|
||||||
|
bind:value={slot3Filters}
|
||||||
|
placeholder="Slot 3"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
options={slot4Options}
|
||||||
|
bind:value={slot4Filters}
|
||||||
|
placeholder="Slot 4"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if hasActiveFilters}
|
||||||
|
<button class="clear-filters-btn" onclick={clearAllFilters}>Clear</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ViewModeToggle value={currentViewMode} onValueChange={handleViewModeChange} neutral={true} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Collection grid -->
|
<!-- Collection grid -->
|
||||||
|
|
@ -134,19 +269,17 @@
|
||||||
{:else if currentViewMode === 'grid'}
|
{:else if currentViewMode === 'grid'}
|
||||||
<div class="artifact-grid">
|
<div class="artifact-grid">
|
||||||
{#each allArtifacts as artifact (artifact.id)}
|
{#each allArtifacts as artifact (artifact.id)}
|
||||||
<CollectionArtifactCard
|
<SelectableCollectionCard id={artifact.id} onClick={() => openArtifactDetails(artifact)}>
|
||||||
{artifact}
|
<CollectionArtifactCard {artifact} />
|
||||||
onClick={() => openArtifactDetails(artifact)}
|
</SelectableCollectionCard>
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="artifact-list">
|
<div class="artifact-list">
|
||||||
{#each allArtifacts as artifact (artifact.id)}
|
{#each allArtifacts as artifact (artifact.id)}
|
||||||
<CollectionArtifactRow
|
<SelectableCollectionRow id={artifact.id} onClick={() => openArtifactDetails(artifact)}>
|
||||||
{artifact}
|
<CollectionArtifactRow {artifact} />
|
||||||
onClick={() => openArtifactDetails(artifact)}
|
</SelectableCollectionRow>
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -195,6 +328,27 @@
|
||||||
gap: $unit-2x;
|
gap: $unit-2x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-filters-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: $unit-half $unit;
|
||||||
|
font-size: $font-small;
|
||||||
|
font-weight: $medium;
|
||||||
|
color: var(--accent-color);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.grid-area {
|
.grid-area {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue