Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/ui/src/components/search/SearchFilterOption.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
>
Expand All @@ -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(
Expand All @@ -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,
Expand Down
59 changes: 53 additions & 6 deletions packages/ui/src/components/search/SearchSidebarFilter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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
Expand All @@ -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>
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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',
Expand Down
15 changes: 15 additions & 0 deletions packages/ui/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
45 changes: 29 additions & 16 deletions packages/ui/src/utils/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

const values = getParamValuesAsArray(route.query[key])

for (const value of values) {
const negative = !value.includes(':') && value.includes('!=')
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
}

currentFilters.value.push({
type: type.id,
option: option.id,
negative: negative,
})
matched = true
break
}

if (!option && type.allows_custom_options) {
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}`)
}
}
}
Expand Down
Loading