Fix Discover URL filter parsing, improve search sidebar (#5104)

* fix category parsing on discover

* Make categories (loader, platform, etc) colored in discover, also add i18n

* fix formatting

* add localized strings

---------

Co-authored-by: Creeperkatze <178587183+Creeperkatze@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Creeperkatze
2026-01-16 00:01:09 +01:00
committed by GitHub
parent 169224560b
commit b0ed808745
4 changed files with 108 additions and 23 deletions

View File

@@ -23,7 +23,7 @@
></div>
<button
v-if="supportsNegativeFilter && !excluded"
v-tooltip="excluded ? 'Remove exclusion' : 'Exclude'"
v-tooltip="formatMessage(messages.excludeTooltip)"
class="flex border-none cursor-pointer items-center justify-center gap-2 rounded-xl bg-transparent px-2 py-1 text-sm font-semibold text-secondary [@media(hover:hover)]:opacity-0 transition-all hover:bg-button-bg hover:text-red focus-visible:bg-button-bg focus-visible:text-red active:scale-[0.96]"
@click="() => emit('toggleExclude', option)"
>
@@ -35,6 +35,7 @@
<script setup lang="ts">
import { BanIcon, CheckIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '../../composables/i18n'
import type { FilterOption } from '../../utils/search'
withDefaults(
@@ -49,10 +50,19 @@ withDefaults(
},
)
const { formatMessage } = useVIntl()
const emit = defineEmits<{
toggle: [option: FilterOption]
toggleExclude: [option: FilterOption]
}>()
const messages = defineMessages({
excludeTooltip: {
id: 'search.filter.option.exclusion.add.tooltip',
defaultMessage: 'Exclude',
},
})
</script>
<style scoped lang="scss">
.search-filter-option:hover,

View File

@@ -71,10 +71,15 @@
v-model="query"
class="!min-h-9 text-sm"
type="text"
:placeholder="`Search...`"
:placeholder="formatMessage(messages.searchPlaceholder)"
autocomplete="off"
/>
<Button v-if="query" class="r-btn" aria-label="Clear search" @click="() => (query = '')">
<Button
v-if="query"
class="r-btn"
:aria-label="formatMessage(messages.clearSearchAriaLabel)"
@click="() => (query = '')"
>
<XIcon aria-hidden="true" />
</Button>
</div>
@@ -95,9 +100,17 @@
@toggle-exclude="toggleNegativeFilter"
>
<slot name="option" :filter="filterType" :option="option">
<div v-if="typeof option.icon === 'string'" class="h-4 w-4" v-html="option.icon" />
<component :is="option.icon" v-else-if="option.icon" class="h-4 w-4" />
<span class="truncate text-sm">{{ option.formatted_name ?? option.id }}</span>
<span
v-if="option.icon"
class="inline-flex items-center justify-center shrink-0 h-4 w-4"
:style="iconStyle(option)"
>
<div v-if="typeof option.icon === 'string'" class="h-4 w-4" v-html="option.icon" />
<component :is="option.icon" v-else class="h-4 w-4" />
</span>
<span class="truncate text-sm" :style="iconStyle(option)">
{{ option.formatted_name ?? option.id }}
</span>
</slot>
</SearchFilterOption>
<button
@@ -109,7 +122,9 @@
class="h-4 w-4 transition-transform"
:class="{ 'rotate-180': showMore }"
/>
<span class="truncate text-sm">{{ showMore ? 'Show fewer' : 'Show more' }}</span>
<span class="truncate text-sm">
{{ showMore ? formatMessage(messages.showFewer) : formatMessage(messages.showMore) }}
</span>
</button>
</div>
</ScrollablePanel>
@@ -234,6 +249,22 @@ const scrollable = computed(
() => visibleOptions.value.length >= 10 && props.filterType.display === 'scrollable',
)
function iconStyle(option: FilterOption) {
// Match project page platform coloring (Forge/Fabric/Velocity/etc.) while leaving other
// filter icons unchanged.
if (
props.filterType.id === 'mod_loader' ||
props.filterType.id === 'modpack_loader' ||
props.filterType.id === 'plugin_loader' ||
props.filterType.id === 'plugin_platform' ||
props.filterType.id === 'shader_loader'
) {
return { color: `var(--color-platform-${option.id})` }
}
return undefined
}
function groupEnabled(group: string) {
return toggledGroups.value.includes(group)
}
@@ -315,6 +346,22 @@ function clearFilters() {
}
const messages = defineMessages({
searchPlaceholder: {
id: 'search.filter.option.search.placeholder',
defaultMessage: 'Search...',
},
clearSearchAriaLabel: {
id: 'search.filter.option.search.clear.aria_label',
defaultMessage: 'Clear search',
},
showFewer: {
id: 'search.filter.option.show_fewer',
defaultMessage: 'Show fewer',
},
showMore: {
id: 'search.filter.option.show_more',
defaultMessage: 'Show more',
},
unlockFilterButton: {
id: 'search.filter.locked.default.unlock',
defaultMessage: 'Unlock filter',

View File

@@ -920,6 +920,21 @@
"search.filter.locked.default.unlock": {
"defaultMessage": "Unlock filter"
},
"search.filter.option.exclusion.add.tooltip": {
"defaultMessage": "Exclude"
},
"search.filter.option.search.clear.aria_label": {
"defaultMessage": "Clear search"
},
"search.filter.option.search.placeholder": {
"defaultMessage": "Search..."
},
"search.filter.option.show_fewer": {
"defaultMessage": "Show fewer"
},
"search.filter.option.show_more": {
"defaultMessage": "Show more"
},
"search.filter_type.environment": {
"defaultMessage": "Environment"
},

View File

@@ -558,32 +558,45 @@ export function useSearch(
})
for (const key of Object.keys(route.query).filter((key) => !readParams.has(key))) {
const type = filters.value.find((type) => type.query_param === key)
if (type) {
const values = getParamValuesAsArray(route.query[key])
const types = filters.value.filter((type) => type.query_param === key)
if (types.length === 0) {
console.error(`Unknown filter type: ${key}`)
continue
}
for (const value of values) {
const negative = !value.includes(':') && value.includes('!=')
const values = getParamValuesAsArray(route.query[key])
for (const value of values) {
const negative = !value.includes(':') && value.includes('!=')
let matched = false
for (const type of types) {
const option = type.options.find((option) => getOptionValue(option, negative) === value)
if (!option) {
continue
}
if (!option && type.allows_custom_options) {
currentFilters.value.push({
type: type.id,
option: option.id,
negative: negative,
})
matched = true
break
}
if (!matched) {
const customType = types.find((type) => type.allows_custom_options)
if (customType) {
currentFilters.value.push({
type: type.id,
type: customType.id,
option: value.replace('!=', ':'),
negative: negative,
})
} else if (option) {
currentFilters.value.push({
type: type.id,
option: option.id,
negative: negative,
})
} else {
console.error(`Unknown filter option: ${value}`)
console.error(`Unknown filter option for ${key}: ${value}`)
}
}
} else {
console.error(`Unknown filter type: ${key}`)
}
}
}