add skill, element, proficiency filters to artifact collection

This commit is contained in:
Justin Edmund 2025-12-19 00:40:15 -08:00
parent 856e5017ea
commit b4f58aa5af
2 changed files with 200 additions and 25 deletions

View file

@ -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)
// ============================================ // ============================================

View file

@ -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;
} }