Project, Search, User redesign (#1281)

* New project page

* fix silly icon tailwind classes

* Start new versions page, add new ButtonStyled component

* Pagination and finish mocking up versions page functionality

* green download button

* hover animation

* New Modal, Avatar refactor, subpages in NavTabs

* lint

* Download modal

* New user page + fix lint

* fix ui lint

* Download animation fix

* Versions filter + finish project page

* Improve consistency of buttons on home page

* Fix ButtonStyled breaking

* Fix margin on version summary

* finish search, new modals, user + project page mobile

* fix gallery image pages

* New project header

* Fix gallery tab showing improperly

* Use auto direction + position for all popouts

* Preliminary user page

* test to see if this fixes login stuff

* remove extra slash

* Add version actions, move download button on versions page

* Listed -> public

* Shorten download modal selector height

* Fix user menu open direction

* Change breakpoint for header collapse

* Only underline title

* Tighten padding on stats a little

* New nav

* Make mobile breakpoint more consistent

* fix header breakpoint regression

* Add sign in button

* Fix edit icon color

* Fix margin at top of screen

* Fix user bios and ad width

* Fix user nav showing when there's only one type of project

* Fix plural projects on user page & extract i18n

* Remove ads on mobile for now

* Fix overflow menu showing hidden items

* NavTabs on mobile

* Fix navbar z index

* Search filter overhaul + negative filters

* fix no-max-height

* port version filters, fix following/collections, lint

* hide promos

* ui lint

* Disable modal background animation to reduce reported motion sickness

* Hide install with modrinth app button on mobile

---------

Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Prospector <prospectordev@gmail.com>
This commit is contained in:
Geometrically
2024-08-20 23:03:16 -07:00
committed by GitHub
parent a19ce0458a
commit 2d416d491c
101 changed files with 5361 additions and 4488 deletions

View File

@@ -1,10 +1,7 @@
<template>
<div
:class="{
'search-page': true,
'normal-page': true,
'alt-layout': cosmetics.searchLayout,
}"
class="new-page sidebar experimental-styles-within"
:class="{ 'alt-layout': cosmetics.searchLayout }"
>
<Head>
<Title>Search {{ projectType.display }}s - Modrinth</Title>
@@ -12,206 +9,126 @@
<aside
:class="{
'normal-page__sidebar': true,
open: sidebarMenuOpen,
}"
aria-label="Filters"
>
<section class="card filters-card" role="presentation">
<div class="sidebar-menu" :class="{ 'sidebar-menu_open': sidebarMenuOpen }">
<AdPlaceholder />
<section class="card gap-1" :class="{ 'max-lg:!hidden': !sidebarMenuOpen }">
<div class="flex items-center gap-2">
<div class="iconified-input w-full">
<label class="hidden" for="search">Search</label>
<SearchIcon aria-hidden="true" />
<input
id="search"
v-model="queryFilter"
name="search"
type="search"
placeholder="Search filters..."
autocomplete="off"
/>
</div>
<button
:disabled="
onlyOpenSource === false &&
selectedEnvironments.length === 0 &&
selectedVersions.length === 0 &&
facets.length === 0 &&
orFacets.length === 0
v-if="
!(
onlyOpenSource === false &&
selectedEnvironments.length === 0 &&
selectedVersions.length === 0 &&
facets.length === 0 &&
orFacets.length === 0 &&
negativeFacets.length === 0
)
"
class="iconified-button"
v-tooltip="`Reset all filters`"
class="btn icon-only"
@click="clearFilters"
>
<ClearIcon aria-hidden="true" />
Clear filters
<FilterXIcon />
</button>
<section aria-label="Category filters">
<div v-for="(categories, header) in categoriesMap" :key="header">
<h3
v-if="categories.filter((x) => x.project_type === projectType.actual).length > 0"
class="sidebar-menu-heading"
>
{{ $formatCategoryHeader(header) }}
</h3>
<template v-if="header === 'resolutions'">
<SearchFilter
v-for="category in categories.filter(
(x) => x.project_type === projectType.actual,
)"
:key="category.name"
:active-filters="orFacets"
:display-name="$formatCategory(category.name)"
:facet-name="`categories:'${encodeURIComponent(category.name)}'`"
:icon="null"
@toggle="toggleOrFacet"
/>
</template>
<template v-else>
<SearchFilter
v-for="category in categories.filter(
(x) => x.project_type === projectType.actual,
)"
:key="category.name"
:active-filters="facets"
:display-name="$formatCategory(category.name)"
:facet-name="`categories:'${encodeURIComponent(category.name)}'`"
:icon="category.icon"
@toggle="toggleFacet"
/>
</template>
</div>
</section>
<section
v-if="projectType.id !== 'resourcepack' && projectType.id !== 'datapack'"
aria-label="Loader filters"
>
<h3
v-if="
tags.loaders.filter((x) => x.supported_project_types.includes(projectType.actual))
.length > 0
"
class="sidebar-menu-heading"
>
Loaders
</h3>
<SearchFilter
v-for="loader in tags.loaders.filter((x) => {
if (projectType.id === 'mod') {
return (
tags.loaderData.modLoaders.includes(x.name) &&
!tags.loaderData.hiddenModLoaders.includes(x.name)
);
} else if (projectType.id === 'plugin') {
return tags.loaderData.pluginLoaders.includes(x.name);
} else if (projectType.id === 'datapack') {
return tags.loaderData.dataPackLoaders.includes(x.name);
} else {
return x.supported_project_types.includes(projectType.actual);
}
})"
:key="loader.name"
ref="loaderFilters"
:active-filters="orFacets"
:display-name="$formatCategory(loader.name)"
:facet-name="`categories:'${encodeURIComponent(loader.name)}'`"
:icon="loader.icon"
@toggle="toggleOrFacet"
/>
<template v-if="projectType.id === 'mod' && showAllLoaders">
<SearchFilter
v-for="loader in tags.loaders.filter((x) => {
return (
tags.loaderData.modLoaders.includes(x.name) &&
tags.loaderData.hiddenModLoaders.includes(x.name)
);
})"
:key="loader.name"
ref="loaderFilters"
:active-filters="orFacets"
:display-name="$formatCategory(loader.name)"
:facet-name="`categories:'${encodeURIComponent(loader.name)}'`"
:icon="loader.icon"
@toggle="toggleOrFacet"
/>
</template>
<Checkbox
v-if="projectType.id === 'mod'"
v-model="showAllLoaders"
:label="showAllLoaders ? 'Less' : 'More'"
description="Show all loaders"
style="margin-bottom: 0.5rem"
:border="false"
:collapsing-toggle-style="true"
/>
</section>
<section v-if="projectType.id === 'plugin'" aria-label="Platform loader filters">
<h3
v-if="
tags.loaders.filter((x) => x.supported_project_types.includes(projectType.actual))
.length > 0
"
class="sidebar-menu-heading"
>
Proxies
</h3>
<SearchFilter
v-for="loader in tags.loaders.filter((x) =>
tags.loaderData.pluginPlatformLoaders.includes(x.name),
)"
:key="loader.name"
ref="platformFilters"
:active-filters="orFacets"
:display-name="$formatCategory(loader.name)"
:facet-name="`categories:'${encodeURIComponent(loader.name)}'`"
:icon="loader.icon"
@toggle="toggleOrFacet"
/>
</section>
<section
v-if="!['resourcepack', 'plugin', 'shader', 'datapack'].includes(projectType.id)"
aria-label="Environment filters"
>
<h3 class="sidebar-menu-heading">Environments</h3>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Client"
facet-name="client"
@toggle="toggleEnv"
>
<ClientIcon aria-hidden="true" />
</SearchFilter>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Server"
facet-name="server"
@toggle="toggleEnv"
>
<ServerIcon aria-hidden="true" />
</SearchFilter>
</section>
<h3 class="sidebar-menu-heading">Minecraft versions</h3>
<Checkbox
v-model="showSnapshots"
label="Show all versions"
description="Show all versions"
style="margin-bottom: 0.5rem"
:border="false"
/>
<multiselect
v-model="selectedVersions"
:options="
showSnapshots
? tags.gameVersions.map((x) => x.version)
: tags.gameVersions
.filter((it) => it.version_type === 'release')
.map((x) => x.version)
</div>
<div
v-for="(categories, header, index) in filters"
:key="header"
:class="`border-0 border-b border-solid border-button-bg py-2 last:border-b-0`"
>
<button
class="flex !w-full bg-transparent px-0 py-2 font-extrabold text-contrast transition-all active:scale-[0.98]"
@click="
() => {
filterAccordions[index].isOpen
? filterAccordions[index].close()
: filterAccordions[index].open();
}
"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-search-on-select="false"
:show-labels="false"
:selectable="() => selectedVersions.length <= 6"
placeholder="Choose versions..."
@update:model-value="onSearchChange(1)"
/>
<h3 class="sidebar-menu-heading">Open source</h3>
<Checkbox
v-model="onlyOpenSource"
label="Open source only"
style="margin-bottom: 0.5rem"
:border="false"
@update:model-value="onSearchChange(1)"
/>
>
<template v-if="header === 'gameVersion'"> Game versions </template>
<template v-else>
{{ $formatCategoryHeader(header) }}
</template>
<DropdownIcon
class="ml-auto h-5 w-5 transition-transform"
:class="{ 'rotate-180': filterAccordions[index]?.isOpen }"
/>
</button>
<Accordion ref="filterAccordions" :open-by-default="true">
<ScrollablePanel
:class="{ 'h-[18rem]': categories.length >= 8 && header === 'gameVersion' }"
:no-max-height="header !== 'gameVersion'"
>
<div class="mr-1 flex flex-col gap-1">
<div v-for="category in categories" :key="category.name" class="group flex gap-1">
<button
:class="`flex !w-full items-center gap-2 truncate rounded-xl px-2 py-1 text-sm font-semibold transition-all active:scale-[0.98] ${filterSelected(category) ? 'bg-brand-highlight text-contrast hover:brightness-125' : negativeFilterSelected(category) ? 'bg-highlight-red text-contrast hover:brightness-125' : 'bg-transparent text-secondary hover:bg-button-bg'}`"
@click="
negativeFilterSelected(category)
? toggleNegativeFilter(category)
: toggleFilter(category)
"
>
<ClientIcon v-if="category.name === 'client'" class="h-4 w-4" />
<ServerIcon v-else-if="category.name === 'server'" class="h-4 w-4" />
<div v-if="category.icon" class="h-4" v-html="category.icon" />
<span class="truncate text-sm">{{ $formatCategory(category.name) }}</span>
<BanIcon
v-if="negativeFilterSelected(category)"
:class="`ml-auto h-4 w-4 shrink-0 transition-opacity group-hover:opacity-100 ${negativeFilterSelected(category) ? '' : 'opacity-0'}`"
aria-hidden="true"
/>
<CheckIcon
v-else
:class="`ml-auto h-4 w-4 shrink-0 transition-opacity group-hover:opacity-100 ${filterSelected(category) ? '' : 'opacity-0'}`"
aria-hidden="true"
/>
</button>
<button
v-if="
(category.type === 'or' || category.type === 'normal') &&
!negativeFilterSelected(category)
"
v-tooltip="negativeFilterSelected(category) ? 'Include' : 'Exclude'"
class="hidden items-center justify-center gap-2 rounded-xl bg-transparent px-2 py-1 text-sm font-semibold text-secondary transition-all hover:bg-button-bg hover:text-red active:scale-[0.96] group-hover:flex"
@click="toggleNegativeFilter(category)"
>
<BanIcon
class="h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
aria-hidden="true"
/>
</button>
</div>
</div>
</ScrollablePanel>
<Checkbox
v-if="header === 'gameVersion'"
v-model="showSnapshots"
class="mx-2"
:label="`Show all versions`"
/>
<Checkbox
v-if="header === 'loaders' && projectType.id === 'mod'"
v-model="showAllLoaders"
class="mx-2"
:label="`Show all loaders`"
/>
</Accordion>
</div>
</section>
</aside>
@@ -288,13 +205,6 @@
</button>
</div>
</div>
<Pagination
:page="currentPage"
:count="pageCount"
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="pagination-before"
@switch-page="onSearchChange"
/>
<LogoAnimated v-if="searchLoading && !noLoad" />
<div v-else-if="results && results.hits && results.hits.length === 0" class="no-results">
<p>No results found for your query!</p>
@@ -332,38 +242,41 @@
/>
</div>
</div>
<pagination
:page="currentPage"
:count="pageCount"
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="pagination-after"
@switch-page="onSearchChangeToTop"
/>
<div class="pagination-after">
<pagination
:page="currentPage"
:count="pageCount"
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="justify-end"
@switch-page="onSearchChangeToTop"
/>
</div>
</section>
</div>
</template>
<script setup>
import { Multiselect } from "vue-multiselect";
import { Promotion } from "@modrinth/ui";
import { Promotion, Pagination, ScrollablePanel, Checkbox } from "@modrinth/ui";
import { BanIcon, DropdownIcon, CheckIcon, FilterXIcon } from "@modrinth/assets";
import ProjectCard from "~/components/ui/ProjectCard.vue";
import Pagination from "~/components/ui/Pagination.vue";
import SearchFilter from "~/components/ui/search/SearchFilter.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
import ClientIcon from "~/assets/images/categories/client.svg?component";
import ServerIcon from "~/assets/images/categories/server.svg?component";
import SearchIcon from "~/assets/images/utils/search.svg?component";
import ClearIcon from "~/assets/images/utils/clear.svg?component";
import FilterIcon from "~/assets/images/utils/filter.svg?component";
import GridIcon from "~/assets/images/utils/grid.svg?component";
import ListIcon from "~/assets/images/utils/list.svg?component";
import ImageIcon from "~/assets/images/utils/image.svg?component";
import Accordion from "~/components/ui/Accordion.vue";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
const sidebarMenuOpen = ref(false);
const showAllLoaders = ref(false);
const filterAccordions = ref([]);
const data = useNuxtApp();
const route = useNativeRoute();
@@ -374,6 +287,7 @@ const auth = await useAuth();
const query = ref("");
const facets = ref([]);
const orFacets = ref([]);
const negativeFacets = ref([]);
const selectedVersions = ref([]);
const onlyOpenSource = ref(false);
const showSnapshots = ref(false);
@@ -413,6 +327,9 @@ if (route.query.f) {
if (route.query.g) {
orFacets.value = getArrayOrString(route.query.g);
}
if (route.query.nf) {
negativeFacets.value = getArrayOrString(route.query.nf);
}
if (route.query.v) {
selectedVersions.value = getArrayOrString(route.query.v);
}
@@ -477,6 +394,7 @@ const {
if (
facets.value.length > 0 ||
orFacets.value.length > 0 ||
negativeFacets.value.length > 0 ||
selectedVersions.value.length > 0 ||
selectedEnvironments.value.length > 0 ||
projectType.value
@@ -486,6 +404,10 @@ const {
formattedFacets.push([facet]);
}
for (const facet of negativeFacets.value) {
formattedFacets.push([facet.replace(":", "!=")]);
}
// loaders specifier
if (orFacets.value.length > 0) {
formattedFacets.push(orFacets.value);
@@ -616,6 +538,10 @@ function getSearchUrl(offset, useObj) {
queryItems.push(`g=${encodeURIComponent(orFacets.value)}`);
obj.g = orFacets.value;
}
if (negativeFacets.value.length > 0) {
queryItems.push(`nf=${encodeURIComponent(negativeFacets.value)}`);
obj.nf = negativeFacets.value;
}
if (selectedVersions.value.length > 0) {
queryItems.push(`v=${encodeURIComponent(selectedVersions.value)}`);
obj.v = selectedVersions.value;
@@ -654,103 +580,16 @@ function getSearchUrl(offset, useObj) {
return useObj ? obj : url;
}
const categoriesMap = computed(() => {
const categories = {};
for (const category of data.$sortedCategories()) {
if (categories[category.header]) {
categories[category.header].push(category);
} else {
categories[category.header] = [category];
}
}
return Object.keys(categories).reduce((obj, key) => {
obj[key] = categories[key];
return obj;
}, {});
});
function clearFilters() {
for (const facet of [...facets.value]) {
toggleFacet(facet, true);
}
for (const facet of [...orFacets.value]) {
toggleOrFacet(facet, true);
}
facets.value = [];
orFacets.value = [];
negativeFacets.value = [];
onlyOpenSource.value = false;
selectedVersions.value = [];
selectedEnvironments.value = [];
onSearchChange(1);
}
function toggleFacet(elementName, doNotSendRequest = false) {
const index = facets.value.indexOf(elementName);
if (index !== -1) {
facets.value.splice(index, 1);
} else {
facets.value.push(elementName);
}
if (!doNotSendRequest) {
onSearchChange(1);
}
}
function toggleOrFacet(elementName, doNotSendRequest) {
const index = orFacets.value.indexOf(elementName);
if (index !== -1) {
orFacets.value.splice(index, 1);
} else {
if (elementName === "categories:purpur") {
if (!orFacets.value.includes("categories:paper")) {
orFacets.value.push("categories:paper");
}
if (!orFacets.value.includes("categories:spigot")) {
orFacets.value.push("categories:spigot");
}
if (!orFacets.value.includes("categories:bukkit")) {
orFacets.value.push("categories:bukkit");
}
} else if (elementName === "categories:paper") {
if (!orFacets.value.includes("categories:spigot")) {
orFacets.value.push("categories:spigot");
}
if (!orFacets.value.includes("categories:bukkit")) {
orFacets.value.push("categories:bukkit");
}
} else if (elementName === "categories:spigot") {
if (!orFacets.value.includes("categories:bukkit")) {
orFacets.value.push("categories:bukkit");
}
} else if (elementName === "categories:waterfall") {
if (!orFacets.value.includes("categories:bungeecord")) {
orFacets.value.push("categories:bungeecord");
}
}
orFacets.value.push(elementName);
}
if (!doNotSendRequest) {
onSearchChange(1);
}
}
function toggleEnv(environment, sendRequest) {
const index = selectedEnvironments.value.indexOf(environment);
if (index !== -1) {
selectedEnvironments.value.splice(index, 1);
} else {
selectedEnvironments.value.push(environment);
}
if (!sendRequest) {
onSearchChange(1);
}
}
function onSearchChangeToTop(newPageNumber) {
if (import.meta.client) {
window.scrollTo({ top: 0, behavior: "smooth" });
@@ -796,6 +635,225 @@ function setClosestMaxResults() {
});
}
}
const queryFilter = ref("");
const filters = computed(() => {
const filters = {};
if (projectType.value.id !== "resourcepack" && projectType.value.id !== "datapack") {
const loaders = tags.value.loaders
.filter((x) => {
if (projectType.value.id === "mod" && !showAllLoaders.value) {
return (
tags.value.loaderData.modLoaders.includes(x.name) &&
!tags.value.loaderData.hiddenModLoaders.includes(x.name)
);
} else if (projectType.value.id === "mod" && showAllLoaders.value) {
return tags.value.loaderData.modLoaders.includes(x.name);
} else if (projectType.value.id === "plugin") {
return tags.value.loaderData.pluginLoaders.includes(x.name);
} else if (projectType.value.id === "datapack") {
return tags.value.loaderData.dataPackLoaders.includes(x.name);
} else {
return x.supported_project_types.includes(projectType.value.actual);
}
})
.slice();
loaders.sort((a, b) => {
const isAHidden = tags.value.loaderData.hiddenModLoaders.includes(a.name);
const isBHidden = tags.value.loaderData.hiddenModLoaders.includes(b.name);
// Sort hidden mod loaders (true) after visible ones (false)
if (isAHidden && !isBHidden) return 1;
if (!isAHidden && isBHidden) return -1;
return 0; // No sorting if both are hidden or both are visible
});
if (loaders.length > 0) {
filters.loaders = loaders.map((x) => ({
icon: x.icon,
name: x.name,
type: "or",
facet: `categories:${x.name}`,
}));
}
if (projectType.value.id === "plugin") {
const platforms = tags.value.loaders.filter((x) =>
tags.value.loaderData.pluginPlatformLoaders.includes(x.name),
);
filters.platforms = platforms.map((x) => ({
icon: x.icon,
name: x.name,
type: "or",
facet: `categories:${x.name}`,
}));
}
}
filters.gameVersion = tags.value.gameVersions
.filter((x) => (showSnapshots.value ? true : x.version_type === "release"))
.map((x) => ({ name: x.version, type: "gameVersion" }));
if (!["resourcepack", "plugin", "shader", "datapack"].includes(projectType.value.id)) {
filters.environment = [
{ name: "client", type: "env" },
{ name: "server", type: "env" },
];
}
for (const category of data.$sortedCategories()) {
if (category.project_type === projectType.value.actual) {
const parsedCategory = {
name: category.name,
icon: category.icon,
facet: `categories:${category.name}`,
type: category.header === "resolutions" ? "or" : "normal",
};
if (filters[category.header]) {
filters[category.header].push(parsedCategory);
} else {
filters[category.header] = [parsedCategory];
}
}
}
filters.license = [{ name: "Open source only", type: "license" }];
const filteredObj = {};
for (const [key, value] of Object.entries(filters)) {
const filters = queryFilter.value
? value.filter((x) => x.name.toLowerCase().includes(queryFilter.value.toLowerCase()))
: value;
if (filters.length > 0) {
filteredObj[key] = filters;
}
}
return filteredObj;
});
function filterSelected(filter) {
if (filter.type === "or") {
return orFacets.value.includes(filter.facet);
} else if (filter.type === "normal") {
return facets.value.includes(filter.facet);
} else if (filter.type === "env") {
return selectedEnvironments.value.includes(filter.name);
} else if (filter.type === "gameVersion") {
return selectedVersions.value.includes(filter.name);
} else if (filter.type === "license") {
return onlyOpenSource.value;
}
}
function negativeFilterSelected(filter) {
if (filter.type === "or" || filter.type === "normal") {
return negativeFacets.value.includes(filter.facet);
}
}
function toggleNegativeFilter(filter) {
const elementName = filter.facet;
if (filterSelected(filter)) {
if (filter.type === "or") {
const index = orFacets.value.indexOf(elementName);
orFacets.value.splice(index, 1);
} else if (filter.type === "normal") {
const index = facets.value.indexOf(elementName);
facets.value.splice(index, 1);
}
}
if (filter.type === "or" || filter.type === "normal") {
const index = negativeFacets.value.indexOf(elementName);
if (index !== -1) {
negativeFacets.value.splice(index, 1);
} else {
negativeFacets.value.push(elementName);
}
}
onSearchChange(1);
}
function toggleFilter(filter, doNotSendRequest) {
const elementName = filter.facet;
if (negativeFilterSelected(filter)) {
const index = negativeFacets.value.indexOf(elementName);
negativeFacets.value.splice(index, 1);
}
if (filter.type === "or") {
const index = orFacets.value.indexOf(elementName);
if (index !== -1) {
orFacets.value.splice(index, 1);
} else {
if (elementName === "categories:purpur") {
if (!orFacets.value.includes("categories:paper")) {
orFacets.value.push("categories:paper");
}
if (!orFacets.value.includes("categories:spigot")) {
orFacets.value.push("categories:spigot");
}
if (!orFacets.value.includes("categories:bukkit")) {
orFacets.value.push("categories:bukkit");
}
} else if (elementName === "categories:paper") {
if (!orFacets.value.includes("categories:spigot")) {
orFacets.value.push("categories:spigot");
}
if (!orFacets.value.includes("categories:bukkit")) {
orFacets.value.push("categories:bukkit");
}
} else if (elementName === "categories:spigot") {
if (!orFacets.value.includes("categories:bukkit")) {
orFacets.value.push("categories:bukkit");
}
} else if (elementName === "categories:waterfall") {
if (!orFacets.value.includes("categories:bungeecord")) {
orFacets.value.push("categories:bungeecord");
}
}
orFacets.value.push(elementName);
}
} else if (filter.type === "normal") {
const index = facets.value.indexOf(elementName);
if (index !== -1) {
facets.value.splice(index, 1);
} else {
facets.value.push(elementName);
}
} else if (filter.type === "env") {
const index = selectedEnvironments.value.indexOf(filter.name);
if (index !== -1) {
selectedEnvironments.value.splice(index, 1);
} else {
selectedEnvironments.value.push(filter.name);
}
} else if (filter.type === "gameVersion") {
const index = selectedVersions.value.indexOf(filter.name);
if (index !== -1) {
selectedVersions.value.splice(index, 1);
} else {
selectedVersions.value.push(filter.name);
}
} else if (filter.type === "license") {
onlyOpenSource.value = !onlyOpenSource.value;
}
if (!doNotSendRequest) {
onSearchChange(1);
}
}
</script>
<style lang="scss" scoped>
@@ -816,12 +874,6 @@ function setClosestMaxResults() {
@media screen and (min-width: 1024px) {
display: block;
}
// Hide on mobile unless open
display: none;
&.open {
display: block;
}
}
.filters-card {