Skip to content
Open
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
26 changes: 25 additions & 1 deletion apps/frontend/src/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@
:class="{ 'gap-4': !flags.projectTypesPrimaryNav }"
>
<template v-if="flags.projectTypesPrimaryNav">
<ButtonStyled
type="transparent"
:highlighted="route.name === 'discover-all' || route.path === '/discover/all'"
:highlighted-style="
route.name === 'discover-all' ? 'main-nav-primary' : 'main-nav-secondary'
"
>
<nuxt-link to="/discover/all">
<BoxIcon aria-hidden="true" />
{{ formatMessage(commonMessages.allProjectType) }}
</nuxt-link>
</ButtonStyled>
<ButtonStyled
type="transparent"
:highlighted="route.name === 'discover-mods' || route.path.startsWith('/mod/')"
Expand Down Expand Up @@ -188,6 +200,10 @@
>
<TeleportOverflowMenu
:options="[
{
id: 'all',
action: '/discover/all',
},
{
id: 'mods',
action: '/discover/mods',
Expand Down Expand Up @@ -220,7 +236,11 @@
hoverable
>
<BoxIcon
v-if="route.name === 'discover-mods' || route.path.startsWith('/mod/')"
v-if="route.name === 'discover-all' || route.path === '/discover/all'"
aria-hidden="true"
/>
<BoxIcon
v-else-if="route.name === 'discover-mods' || route.path.startsWith('/mod/')"
aria-hidden="true"
/>
<PaintbrushIcon
Expand Down Expand Up @@ -258,6 +278,10 @@
<span class="contents md:hidden">{{ formatMessage(navMenuMessages.discover) }}</span>
<DropdownIcon aria-hidden="true" class="h-5 w-5" />

<template #all>
<BoxIcon aria-hidden="true" />
{{ formatMessage(commonMessages.allProjectType) }}
</template>
<template #mods>
<BoxIcon aria-hidden="true" />
{{ formatMessage(commonProjectTypeCategoryMessages.mod) }}
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -4664,6 +4664,9 @@
"settings.display.notification.developer-mode-deactivated.title": {
"message": "Developer mode deactivated"
},
"settings.display.project-list-layouts.all": {
"message": "All projects page"
},
"settings.display.project-list-layouts.datapack": {
"message": "Data Packs page"
},
Expand Down
7 changes: 6 additions & 1 deletion apps/frontend/src/pages/discover.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { commonProjectTypeCategoryMessages, NavTabs, useVIntl } from '@modrinth/ui'
import { commonMessages, commonProjectTypeCategoryMessages, NavTabs, useVIntl } from '@modrinth/ui'

const { formatMessage } = useVIntl()

Expand All @@ -10,6 +10,11 @@ const route = useRoute()
const allowTabChanging = computed(() => !route.query.sid)

const selectableProjectTypes = [
{
label: formatMessage(commonMessages.allProjectType),
href: `/discover/all`,
type: 'all',
},
{
label: formatMessage(commonProjectTypeCategoryMessages.mod),
href: `/discover/mods`,
Expand Down
9 changes: 6 additions & 3 deletions apps/frontend/src/pages/discover/[type]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ watch(
(val) => debug('projectType.id changed:', val),
)

const resultsDisplayLocation = computed<DisplayLocation | undefined>(
() => projectType.value?.id as DisplayLocation,
const resultsDisplayLocation = computed<DisplayLocation | undefined>(() =>
currentType.value === 'all' ? 'all' : (projectType.value?.id as DisplayLocation),
)
const resultsDisplayMode = computed<DisplayMode>(() =>
resultsDisplayLocation.value
Expand Down Expand Up @@ -380,7 +380,10 @@ const advancedFiltersCollapsed = computed({
},
})

const projectTypeId = computed(() => projectType.value?.id ?? 'mod')
const projectTypeId = computed(() => {
if (currentType.value === 'all') return 'all'
return projectType.value?.id ?? 'mod'
})

debug('projectTypeId:', projectTypeId.value)
watch(projectTypeId, (val) => debug('projectTypeId changed:', val))
Expand Down
10 changes: 10 additions & 0 deletions apps/frontend/src/pages/settings/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ const projectListLayouts = defineMessages({
defaultMessage:
'Select your preferred layout for each page that displays project lists on this device.',
},
all: {
id: 'settings.display.project-list-layouts.all',
defaultMessage: 'All projects page',
},
mod: {
id: 'settings.display.project-list-layouts.mod',
defaultMessage: 'Mods page',
Expand Down Expand Up @@ -420,6 +424,12 @@ const listTypes = computed(() => {
}
})

types.unshift({
id: 'all' as DisplayLocation,
name: 'All projects',
display: 'all projects search page',
})

types.push({
id: 'user' as DisplayLocation,
name: 'User profiles',
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/src/plugins/cosmetics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import type { DarkTheme } from './theme/index.ts'
export type DisplayMode = 'list' | 'gallery' | 'grid'

export type DisplayLocation =
| 'all'
| 'mod'
| 'plugin'
| 'resourcepack'
| 'modpack'
| 'shader'
| 'datapack'
| 'server'
| 'user'
| 'collection'

Expand Down Expand Up @@ -43,6 +45,7 @@ export default defineNuxtPlugin({
hideModrinthAppPromos: false,
preferredDarkTheme: 'dark',
searchDisplayMode: {
all: 'list',
mod: 'list',
plugin: 'list',
resourcepack: 'gallery',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,19 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
LOADER_FILTER_TYPES.includes(f.type as (typeof LOADER_FILTER_TYPES)[number]),
) || ['resourcepack', 'datapack'].includes(options.projectType.value),
)
const loadersNotForThisType = computed(
() =>
const loadersNotForThisType = computed(() => {
if (options.projectType.value === 'all') return []
return (
options.tags.value?.loaders
?.filter((loader) => !loader.supported_project_types.includes(options.projectType.value))
?.map((loader) => loader.name) ?? [],
)
?.filter(
(loader) =>
!loader.supported_project_types.includes(
options.projectType.value as unknown as ProjectType,
),
)
?.map((loader) => loader.name) ?? []
)
})
const deprioritizedTags = computed(() => [
...selectedFilterTags.value,
...loadersNotForThisType.value,
Expand Down
193 changes: 188 additions & 5 deletions packages/ui/src/layouts/shared/browse-tab/sidebar.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<script setup lang="ts">
import { InfoIcon, XIcon } from '@modrinth/assets'
import { DropdownIcon, InfoIcon, XIcon } from '@modrinth/assets'
import { computed, toValue } from 'vue'

import Accordion from '#ui/components/base/Accordion.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import Toggle from '#ui/components/base/Toggle.vue'
import SearchSidebarFilter from '#ui/components/search/SearchSidebarFilter.vue'
import { useVIntl } from '#ui/composables/i18n'
import { commonMessages } from '#ui/utils/common-messages'
import { commonMessages, formatProjectTypeSentence } from '#ui/utils/common-messages'
import type { FilterType } from '#ui/utils/search'

import { injectBrowseManager } from './providers/browse-manager'

Expand All @@ -17,6 +19,89 @@ const isApp = computed(() => ctx.variant === 'app')
const lockedMessages = computed(() => toValue(ctx.lockedFilterMessages))
const hiddenFilterTypes = computed(() => ctx.hiddenFilterTypes?.value ?? [])

const activeTypes = computed(
() =>
ctx.activeProjectTypes?.value?.filter((t) => t !== 'all' && t !== 'minecraft_java_server') ??
[],
)
const showWrappers = computed(() => activeTypes.value.length !== 1)

const nonCategoryFilters = computed(() => {
if (!showWrappers.value) {
return ctx.filters.value.filter(
(f) => f.display !== 'none' && !hiddenFilterTypes.value.includes(f.id),
)
}
return ctx.filters.value.filter(
(f) =>
f.category_group === undefined &&
f.display !== 'none' &&
!hiddenFilterTypes.value.includes(f.id),
)
})

const topFilters = computed(() =>
nonCategoryFilters.value.filter((f) => f.id !== 'license' && f.id !== 'advanced'),
)
const bottomFilters = computed(() =>
nonCategoryFilters.value.filter((f) => f.id === 'license' || f.id === 'advanced'),
)

const categoryGroups = computed(() => {
if (!showWrappers.value) return []

const groups = new Map<string, { groupName: string; filters: FilterType[] }>()

const allCategoryFilters = ctx.filters.value.filter(
(f) =>
f.category_group !== undefined &&
f.display !== 'none' &&
!hiddenFilterTypes.value.includes(f.id),
)

for (const filter of allCategoryFilters) {
const supported = filter.supported_project_types ?? []
const applicable = supported.filter(
(t) => activeTypes.value.length === 0 || activeTypes.value.includes(t),
)

for (const type of applicable) {
const rawName = formatProjectTypeSentence(formatMessage, type, 2)
const name = rawName.replace(/\b\w/g, (c) => c.toUpperCase())

if (!groups.has(name)) {
groups.set(name, { groupName: name, filters: [] })
}

const clonedFilter = { ...filter }
if (clonedFilter.options) {
clonedFilter.options = clonedFilter.options.filter((opt) => {
if (filter.id === 'loader') {
const isPlugin = ['paper', 'spigot', 'purpur', 'folia', 'bukkit', 'sponge'].includes(
opt.id,
)
const isMod = ['fabric', 'forge', 'quilt', 'neoforge', 'liteloader', 'rift'].includes(
opt.id,
)

if (type === 'mod' && isPlugin) return false
if (type === 'plugin' && isMod) return false
}

if (!opt.supported_project_types) return true
return opt.supported_project_types.includes(type)
})
}

if (clonedFilter.options && clonedFilter.options.length === 0) continue

groups.get(name)!.filters.push(clonedFilter)
}
}

return Array.from(groups.values())
})

const advancedFiltersCollapsed = computed(() => ctx.advancedFiltersCollapsed?.value ?? true)

function setAdvancedFiltersCollapsed(collapsed: boolean) {
Expand Down Expand Up @@ -185,9 +270,107 @@ function getFilterOpenByDefault(filterId: string): boolean {
</template>
<template v-else>
<SearchSidebarFilter
v-for="filter in ctx.filters.value.filter(
(f) => f.display !== 'none' && !hiddenFilterTypes.includes(f.id),
)"
v-for="filter in topFilters"
:key="`filter-${filter.id}`"
v-model:selected-filters="ctx.currentFilters.value"
v-model:toggled-groups="ctx.toggledGroups.value"
v-model:overridden-provided-filter-types="ctx.overriddenProvidedFilterTypes.value"
:provided-filters="ctx.providedFilters?.value ?? []"
:filter-type="filter"
:class="filterClass"
:button-class="buttonClass"
:content-class="contentClass"
:inner-panel-class="innerPanelClass"
:open-by-default="getFilterOpenByDefault(filter.id)"
@on-open="() => filter.id === 'advanced' && setAdvancedFiltersCollapsed(false)"
@on-close="() => filter.id === 'advanced' && setAdvancedFiltersCollapsed(true)"
>
<template #header>
<h3 :class="isApp ? 'text-base m-0' : 'm-0 text-lg font-semibold'">
{{ filter.formatted_name }}
</h3>
</template>
<template
v-if="
lockedMessages?.gameVersionShaderMessage &&
ctx.projectType.value === 'shader' &&
filter.id === 'game_version'
"
#prefix
>
<div class="mb-4 grid grid-cols-[auto_1fr] gap-2 px-3 text-sm font-medium text-blue">
<InfoIcon class="mt-1 size-4" />
<span>{{ lockedMessages.gameVersionShaderMessage }}</span>
</div>
</template>
<template v-if="lockedMessages?.gameVersion" #locked-game_version>
{{ lockedMessages.gameVersion }}
</template>
<template v-if="lockedMessages?.modLoader" #locked-mod_loader>
{{ lockedMessages.modLoader }}
</template>
<template v-if="lockedMessages?.environment" #locked-environment>
{{ lockedMessages.environment }}
</template>
<template v-if="lockedMessages?.syncButton" #sync-button>
{{ lockedMessages.syncButton }}
</template>
</SearchSidebarFilter>

<Accordion
v-for="group in categoryGroups"
:key="`category-group-${group.groupName}`"
:class="filterClass"
:button-class="buttonClass"
:content-class="isApp ? 'mt-0 mb-3' : 'mb-2 mx-0'"
:open-by-default="true"
>
<template #button="{ open }">
<div class="flex items-center gap-1 w-full text-contrast">
<h3 :class="isApp ? 'text-sm m-0' : 'm-0 text-base font-semibold'">
{{ group.groupName }}
</h3>
<DropdownIcon
class="ml-auto size-5 transition-transform duration-300 shrink-0 text-primary group-hover:text-contrast"
:class="{ 'rotate-180': open }"
/>
</div>
</template>
<template #default>
<div class="flex flex-col gap-2">
<SearchSidebarFilter
v-for="filter in group.filters"
:key="`filter-${filter.id}`"
v-model:selected-filters="ctx.currentFilters.value"
v-model:toggled-groups="ctx.toggledGroups.value"
v-model:overridden-provided-filter-types="ctx.overriddenProvidedFilterTypes.value"
:provided-filters="ctx.providedFilters?.value ?? []"
:filter-type="{
...filter,
display: filter.display === 'all' ? 'scrollable' : filter.display,
}"
class="bg-transparent border-0"
:button-class="
isApp
? 'button-animation flex flex-col gap-1 px-3 py-2 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg'
: 'button-animation flex flex-col gap-1 px-4 py-2 w-full bg-transparent cursor-pointer border-none'
"
:content-class="isApp ? 'mt-2 mb-2' : 'mb-2 mx-3'"
:inner-panel-class="innerPanelClass"
:open-by-default="getFilterOpenByDefault(filter.id)"
>
<template #header>
<h3 :class="isApp ? 'text-sm m-0' : 'm-0 text-base font-semibold'">
{{ filter.formatted_name }}
</h3>
</template>
</SearchSidebarFilter>
</div>
</template>
</Accordion>

<SearchSidebarFilter
v-for="filter in bottomFilters"
:key="`filter-${filter.id}`"
v-model:selected-filters="ctx.currentFilters.value"
v-model:toggled-groups="ctx.toggledGroups.value"
Expand Down
Loading