diff --git a/src/lib/api/queries/search.queries.ts b/src/lib/api/queries/search.queries.ts index 512be5ad..ddd80934 100644 --- a/src/lib/api/queries/search.queries.ts +++ b/src/lib/api/queries/search.queries.ts @@ -23,6 +23,8 @@ export interface SearchFilters { proficiency2?: number[] subaura?: boolean extra?: boolean + // Series filter (by slug) - works for weapons, summons, and characters + series?: string[] // Character-specific filters season?: number[] characterSeries?: number[] @@ -100,6 +102,9 @@ function buildSearchParams( if (filters.extra !== undefined) { apiFilters.extra = filters.extra } + if (filters.series && filters.series.length > 0) { + apiFilters.series = filters.series + } // Character-specific filters if (filters.season && filters.season.length > 0) { apiFilters.season = filters.season diff --git a/src/lib/components/sidebar/SearchContent.svelte b/src/lib/components/sidebar/SearchContent.svelte index 8c969e81..c88c8fac 100644 --- a/src/lib/components/sidebar/SearchContent.svelte +++ b/src/lib/components/sidebar/SearchContent.svelte @@ -6,9 +6,15 @@ import type { SearchResult } from '$lib/api/adapters/search.adapter' import { searchQueries, type SearchFilters } from '$lib/api/queries/search.queries' import { collectionQueries } from '$lib/api/queries/collection.queries' + import { entityQueries } from '$lib/api/queries/entity.queries' import Button from '../ui/Button.svelte' + import Select from '../ui/Select.svelte' import Icon from '../Icon.svelte' + import Input from '../ui/Input.svelte' import CharacterTags from '$lib/components/tags/CharacterTags.svelte' + import ElementPicker from '../ui/element-picker/ElementPicker.svelte' + import RarityPicker from '../ui/rarity-picker/RarityPicker.svelte' + import ProficiencyPicker from '../ui/proficiency-picker/ProficiencyPicker.svelte' import { useInfiniteLoader } from '$lib/stores/loaderState.svelte' import { getCharacterImage, getWeaponImage, getSummonImage } from '$lib/features/database/detail/image' import type { AddItemResult, SearchMode } from '$lib/types/api/search' @@ -41,12 +47,12 @@ let elementFilters = $state([]) let rarityFilters = $state([]) let proficiencyFilters = $state([]) + let seriesFilter = $state(undefined) // Search mode state (only available when authUserId is provided) let searchMode = $state('all') // Refs - let searchInput: HTMLInputElement let sentinelEl = $state() // Constants @@ -60,25 +66,7 @@ { value: 6, label: 'Light', color: 'var(--light-bg)' } ] - const rarities = [ - { value: 1, label: 'R' }, - { value: 2, label: 'SR' }, - { value: 3, label: 'SSR' } - ] - - const proficiencies = [ - { value: 1, label: 'Sabre' }, - { value: 2, label: 'Dagger' }, - { value: 3, label: 'Spear' }, - { value: 4, label: 'Axe' }, - { value: 5, label: 'Staff' }, - { value: 6, label: 'Gun' }, - { value: 7, label: 'Melee' }, - { value: 8, label: 'Bow' }, - { value: 9, label: 'Harp' }, - { value: 10, label: 'Katana' } - ] - + // Debounce search query changes $effect(() => { const query = searchQuery @@ -98,6 +86,28 @@ } }) + // Series query - fetch list based on current type + const seriesQuery = createQuery(() => { + switch (type) { + case 'weapon': + return entityQueries.weaponSeriesList() + case 'character': + return entityQueries.characterSeriesList() + case 'summon': + return entityQueries.summonSeriesList() + } + }) + + // Build series options for dropdown (use ID for API filtering) + const seriesOptions = $derived.by(() => { + const data = seriesQuery.data + if (!data) return [] + return data.map((s) => ({ + value: s.id, + label: s.name.en + })) + }) + // Build filters object for query // Use requiredProficiencies for mainhand selection if set, otherwise use user-selected filters const effectiveProficiencies = $derived( @@ -107,7 +117,8 @@ const filters = $derived({ element: elementFilters.length > 0 ? elementFilters : undefined, rarity: rarityFilters.length > 0 ? rarityFilters : undefined, - proficiency: type === 'weapon' && effectiveProficiencies ? effectiveProficiencies : undefined + proficiency: type === 'weapon' && effectiveProficiencies ? effectiveProficiencies : undefined, + series: seriesFilter ? [seriesFilter] : undefined }) // Helper to map collection items to search result format with collectionId @@ -271,41 +282,26 @@ searchResults.length === 0 && !activeQuery.isLoading && !activeQuery.isError ) - // Focus search input on mount - $effect(() => { - if (searchInput) { - searchInput.focus() - } - }) - function handleItemClick(item: AddItemResult) { if (canAddMore) { onAddItems([item]) } } - function toggleElementFilter(element: number) { - if (elementFilters.includes(element)) { - elementFilters = elementFilters.filter(e => e !== element) - } else { - elementFilters = [...elementFilters, element] - } + function handleElementChange(value: number | number[]) { + elementFilters = Array.isArray(value) ? value : value !== undefined ? [value] : [] } - function toggleRarityFilter(rarity: number) { - if (rarityFilters.includes(rarity)) { - rarityFilters = rarityFilters.filter(r => r !== rarity) - } else { - rarityFilters = [...rarityFilters, rarity] - } + function handleRarityChange(value: number | number[]) { + rarityFilters = Array.isArray(value) ? value : value !== undefined ? [value] : [] } - function toggleProficiencyFilter(prof: number) { - if (proficiencyFilters.includes(prof)) { - proficiencyFilters = proficiencyFilters.filter(p => p !== prof) - } else { - proficiencyFilters = [...proficiencyFilters, prof] - } + function handleSeriesChange(value: string | undefined) { + seriesFilter = value + } + + function handleProficiencyChange(value: number | number[]) { + proficiencyFilters = Array.isArray(value) ? value : value !== undefined ? [value] : [] } function getImageUrl(item: AddItemResult): string { @@ -333,12 +329,13 @@
-
@@ -363,59 +360,78 @@ {/if}
- -
- -
- {#each elements as element} - - {/each} -
-
- - -
- -
- {#each rarities as rarity} - - {/each} -
-
- - - {#if type === 'weapon' && !requiredProficiencies} + +
- -
- {#each proficiencies as prof} - - {/each} +
+ + {#if rarityFilters.length > 0} + { e.preventDefault(); rarityFilters = [] }}>Clear + {/if}
+ +
+ +
+
+ + {#if elementFilters.length > 0} + { e.preventDefault(); elementFilters = [] }}>Clear + {/if} +
+ +
+
+ + + {#if (type === 'weapon' || type === 'character') && !requiredProficiencies} +
+
+ + {#if proficiencyFilters.length > 0} + { e.preventDefault(); proficiencyFilters = [] }}>Clear + {/if} +
+
{/if} + + +
+
+ + {#if seriesFilter} + { e.preventDefault(); seriesFilter = undefined }}>Clear + {/if} +
+ + {/if} +{:else} + +{/if} diff --git a/src/lib/components/ui/element-picker/ElementPickerSegmented.svelte b/src/lib/components/ui/element-picker/ElementPickerSegmented.svelte new file mode 100644 index 00000000..7604db00 --- /dev/null +++ b/src/lib/components/ui/element-picker/ElementPickerSegmented.svelte @@ -0,0 +1,350 @@ + + + + +{#if showClear} +
+
+ {#if multiple} + + {#each elements as element} + + {#snippet children()} + + {getLabel(element)} + + {/snippet} + + {/each} + + {:else} + + {#each elements as element} + + {#snippet children()} + + {getLabel(element)} + + {/snippet} + + {/each} + + {/if} +
+ {#if hasSelection} + + {/if} +
+{:else} +
+ {#if multiple} + + {#each elements as element} + + {#snippet children()} + + {getLabel(element)} + + {/snippet} + + {/each} + + {:else} + + {#each elements as element} + + {#snippet children()} + + {getLabel(element)} + + {/snippet} + + {/each} + + {/if} +
+{/if} + + diff --git a/src/lib/components/ui/proficiency-picker/ProficiencyPicker.svelte b/src/lib/components/ui/proficiency-picker/ProficiencyPicker.svelte new file mode 100644 index 00000000..c1f61886 --- /dev/null +++ b/src/lib/components/ui/proficiency-picker/ProficiencyPicker.svelte @@ -0,0 +1,126 @@ + + + + +{#if shouldUseDropdown} + {#if multiple} + + {:else} + + {/if} +{:else} + +{/if} diff --git a/src/lib/components/ui/rarity-picker/RarityPickerSegmented.svelte b/src/lib/components/ui/rarity-picker/RarityPickerSegmented.svelte new file mode 100644 index 00000000..4af1a6f6 --- /dev/null +++ b/src/lib/components/ui/rarity-picker/RarityPickerSegmented.svelte @@ -0,0 +1,302 @@ + + + + +{#if showClear} +
+
+ {#if multiple} + + {#each RARITY_DISPLAY_ORDER as rarity} + + {#snippet children()} + + {getLabel(rarity)} + + {/snippet} + + {/each} + + {:else} + + {#each RARITY_DISPLAY_ORDER as rarity} + + {#snippet children()} + + {getLabel(rarity)} + + {/snippet} + + {/each} + + {/if} +
+ {#if hasSelection} + + {/if} +
+{:else} +
+ {#if multiple} + + {#each RARITY_DISPLAY_ORDER as rarity} + + {#snippet children()} + + {getLabel(rarity)} + + {/snippet} + + {/each} + + {:else} + + {#each RARITY_DISPLAY_ORDER as rarity} + + {#snippet children()} + + {getLabel(rarity)} + + {/snippet} + + {/each} + + {/if} +
+{/if} + + diff --git a/src/lib/utils/element.ts b/src/lib/utils/element.ts index 184212f1..c18221a1 100644 --- a/src/lib/utils/element.ts +++ b/src/lib/utils/element.ts @@ -64,4 +64,14 @@ export function getOppositeElement(element?: number): number | undefined { if (element === undefined || element === null) return undefined const elementData = ELEMENTS[element] return elementData?.opposite_id +} + +/** + * Get the path to the element image from /images/elements/ + * Used by ElementPicker component + */ +export function getElementImage(element?: number): string { + if (element === undefined || element === null) return '' + const label = ELEMENT_LABELS[element]?.toLowerCase() ?? 'null' + return `/images/elements/${label}.png` } \ No newline at end of file diff --git a/src/lib/utils/proficiency.ts b/src/lib/utils/proficiency.ts index e0a508bb..529f6579 100644 --- a/src/lib/utils/proficiency.ts +++ b/src/lib/utils/proficiency.ts @@ -32,3 +32,9 @@ export function getProficiencyOptions() { label })) } + +export function getProficiencyImage(proficiency: number): string { + const label = PROFICIENCY_LABELS[proficiency] + if (!label || label === 'None') return '' + return `/images/proficiencies/${label.toLowerCase()}.png` +} diff --git a/src/lib/utils/rarity.ts b/src/lib/utils/rarity.ts index e736794c..841bb711 100644 --- a/src/lib/utils/rarity.ts +++ b/src/lib/utils/rarity.ts @@ -34,4 +34,10 @@ export function getRarityClass(rarity: number): string { default: return '' } +} + +export function getRarityImage(rarity: number): string { + const label = RARITY_LABELS[rarity] + if (!label) return '' + return `/images/rarity/${label.toLowerCase()}.png` } \ No newline at end of file diff --git a/src/themes/_colors.scss b/src/themes/_colors.scss index aa794e6c..2ac9f9ed 100644 --- a/src/themes/_colors.scss +++ b/src/themes/_colors.scss @@ -94,6 +94,7 @@ $wind-text-20: #006a45; $wind-text-30: #1dc688; $wind-bg-00: #30c372; $wind-bg-10: #3ee489; +$wind-bg-15: #86f2bb; $wind-bg-20: #cdffed; $fire-text-00: #3f0202; @@ -102,6 +103,7 @@ $fire-text-20: #6e0000; $fire-text-30: #ec5c5c; $fire-bg-00: #e05555; $fire-bg-10: #fa6d6d; +$fire-bg-15: #fd9d9d; $fire-bg-20: #ffcdcd; $water-text-00: #03263b; @@ -110,6 +112,7 @@ $water-text-20: #00639c; $water-text-30: #5cb7ec; $water-bg-00: #4aabe3; $water-bg-10: #6cc9ff; +$water-bg-15: #9ddbff; $water-bg-20: #cdedff; $earth-text-00: #321602; @@ -119,6 +122,7 @@ $earth-text-20: #8e3c0b; $earth-text-30: #ec985c; $earth-bg-00: #df8849; $earth-bg-10: #fd9f5b; +$earth-bg-15: #fec194; $earth-bg-20: #ffe2cd; $light-text-00: #3d3700; @@ -127,6 +131,7 @@ $light-text-20: #715100; $light-text-30: #c59c0c; $light-bg-00: #cab91c; $light-bg-10: #e8d633; +$light-bg-15: #f4e880; $light-bg-20: #fffacd; $dark-text-00: #23002f; @@ -135,6 +140,7 @@ $dark-text-20: #560075; $dark-text-30: #c65cec; $dark-bg-00: #ba63d8; $dark-bg-10: #de7bff; +$dark-bg-15: #e8a4ff; $dark-bg-20: #f2cdff; $transparent--stroke--light: rgba(0, 0, 0, 0.9); @@ -528,6 +534,13 @@ $segmented--control--background--segment--text--dark: $grey-60; $segmented--control--background--segment--text--hover--dark: $grey-70; $segmented--control--background--segment--text--checked--dark: $grey-90; +// Color Definitions: Picker (icon-based pickers like Element, Rarity, Proficiency) +// Uses darker backgrounds than segmented controls for better visibility +$picker--item--bg--hover--light: $grey-70; +$picker--item--bg--hover--dark: $grey-40; +$picker--item--bg--selected--light: $grey-75; +$picker--item--bg--selected--dark: $grey-45; + // Color Definitions: Element / Wind $wind--bg--light: $wind-bg-10; $wind--bg--dark: $wind-bg-10; diff --git a/src/themes/themes.scss b/src/themes/themes.scss index ac6270ff..b31c6664 100644 --- a/src/themes/themes.scss +++ b/src/themes/themes.scss @@ -229,6 +229,10 @@ --segmented-control-background-segment-text-hover: #{colors.$segmented--control--background--segment--text--hover--light}; --segmented-control-background-segment-text-checked: #{colors.$segmented--control--background--segment--text--checked--light}; + // Light - Picker (icon-based pickers) + --picker-item-bg-hover: #{colors.$picker--item--bg--hover--light}; + --picker-item-bg-selected: #{colors.$picker--item--bg--selected--light}; + // Light - Extra Weapons --extra-purple-bg: #{colors.$extra--purple--bg--light}; --extra-purple-card-bg: #{colors.$extra--purple--card--bg--light}; @@ -297,6 +301,15 @@ --light-nav-selected-bg: #{colors.$light-bg-20}; --dark-nav-selected-bg: #{colors.$dark-bg-20}; + // Light - Element navigation hover background (between bg and nav-selected) + --null-nav-hover-bg: #{colors.$grey-80}; + --wind-nav-hover-bg: #{colors.$wind-bg-15}; + --fire-nav-hover-bg: #{colors.$fire-bg-15}; + --water-nav-hover-bg: #{colors.$water-bg-15}; + --earth-nav-hover-bg: #{colors.$earth-bg-15}; + --light-nav-hover-bg: #{colors.$light-bg-15}; + --dark-nav-hover-bg: #{colors.$dark-bg-15}; + // Item detail backgrounds (same colors as nav selected) --null-item-detail-bg: #{colors.$grey-85}; --wind-item-detail-bg: #{colors.$wind-bg-20}; @@ -598,6 +611,10 @@ html[data-theme='dark'] { --segmented-control-background-segment-text-hover: #{colors.$segmented--control--background--segment--text--hover--dark}; --segmented-control-background-segment-text-checked: #{colors.$segmented--control--background--segment--text--checked--dark}; + // Dark - Picker (icon-based pickers) + --picker-item-bg-hover: #{colors.$picker--item--bg--hover--dark}; + --picker-item-bg-selected: #{colors.$picker--item--bg--selected--dark}; + // Dark - Extra Weapons --extra-purple-bg: #{colors.$extra--purple--bg--dark}; --extra-purple-card-bg: #{colors.$extra--purple--card--bg--dark}; @@ -666,6 +683,15 @@ html[data-theme='dark'] { --light-nav-selected-bg: #{colors.$light-bg-20}; --dark-nav-selected-bg: #{colors.$dark-bg-20}; + // Dark - Element navigation hover background (same as light theme) + --null-nav-hover-bg: #{colors.$grey-80}; + --wind-nav-hover-bg: #{colors.$wind-bg-15}; + --fire-nav-hover-bg: #{colors.$fire-bg-15}; + --water-nav-hover-bg: #{colors.$water-bg-15}; + --earth-nav-hover-bg: #{colors.$earth-bg-15}; + --light-nav-hover-bg: #{colors.$light-bg-15}; + --dark-nav-hover-bg: #{colors.$dark-bg-15}; + // Item detail backgrounds (same colors as nav selected) --null-item-detail-bg: #{colors.$grey-85}; --wind-item-detail-bg: #{colors.$wind-bg-20};