Search UI improvements (#107)

* Base impl

* Make project type selectable

* Update Browse.vue

* address changes

* Quick create

* Run linter

* fix merge

* Addressed changes

* Installation improvements

* Run lint

* resourcepacks

* automatic installation of dependencies

* Fix bugs with search

* Addressed changes

* Run linter

* Fixed direct install not working

* Remove back to search

* Update Index.vue

* Addressed some changes

* Shader fix

* fix resetting

* Update Browse.vue

* Direct install from search

* More improvements

* Update SearchCard.vue

* Card V2

* Run linter

* add instance ignoring

* Update Browse.vue

* Finalize changes

* Update SearchCard.vue

* More adjustments

* Fix out of order rendering

* Run linter

* Fix issues

* Add unlisteners

---------

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
Adrian O.V
2023-05-16 22:25:00 -04:00
committed by GitHub
parent 3fa0e99de2
commit c6e2133e15
18 changed files with 835 additions and 293 deletions

View File

@@ -3,7 +3,6 @@ import { computed, onMounted, ref, watch } from 'vue'
import { ofetch } from 'ofetch'
import {
Pagination,
ProjectCard,
Checkbox,
Button,
ClearIcon,
@@ -15,21 +14,37 @@ import {
ServerIcon,
NavRow,
formatCategoryHeader,
formatCategory,
} from 'omorphia'
import Multiselect from 'vue-multiselect'
import { useSearch } from '@/store/state'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { get_categories, get_loaders, get_game_versions } from '@/helpers/tags'
import { useRoute } from 'vue-router'
import Instance from '@/components/ui/Instance.vue'
import SearchCard from '@/components/ui/SearchCard.vue'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import Instance from '@/components/ui/Instance.vue'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
const route = useRoute()
const searchStore = useSearch()
searchStore.projectType = route.params.projectType
const showVersions = ref(true)
const showLoaders = ref(true)
const showVersions = computed(
() => searchStore.instanceContext === null || searchStore.ignoreInstance
)
const showLoaders = computed(
() =>
searchStore.projectType !== 'datapack' &&
searchStore.projectType !== 'resourcepack' &&
searchStore.projectType !== 'shader' &&
(searchStore.instanceContext === null || searchStore.ignoreInstance)
)
const confirmModal = ref(null)
const modInstallModal = ref(null)
const incompatibilityWarningModal = ref(null)
const breadcrumbs = useBreadcrumbs()
@@ -40,6 +55,12 @@ const categories = ref([])
const loaders = ref([])
const availableGameVersions = ref([])
breadcrumbs.setContext({ name: 'Browse', link: route.path })
if (searchStore.projectType === 'modpack') {
searchStore.instanceContext = null
}
onMounted(async () => {
;[categories.value, loaders.value, availableGameVersions.value] = await Promise.all([
get_categories(),
@@ -72,12 +93,6 @@ const sortedCategories = computed(() => {
const getSearchResults = async () => {
const queryString = searchStore.getQueryString()
if (searchStore.instanceContext) {
showVersions.value = false
showLoaders.value = !(
searchStore.projectType === 'mod' || searchStore.projectType === 'resourcepack'
)
}
const response = await ofetch(`https://api.modrinth.com/v2/search${queryString}`)
searchStore.setSearchResults(response)
}
@@ -119,131 +134,141 @@ const switchPage = async (page) => {
watch(
() => route.params.projectType,
async (projectType) => {
searchStore.projectType = projectType ?? 'modpack'
breadcrumbs.setContext({ name: 'Browse', link: route.path })
if (!projectType) return
searchStore.projectType = projectType
breadcrumbs.setContext({ name: 'Browse', link: `/browse/${searchStore.projectType}` })
await handleReset()
await switchPage(1)
}
)
const handleInstanceSwitch = async (value) => {
searchStore.ignoreInstance = value
await switchPage(1)
}
</script>
<template>
<div class="search-container">
<aside class="filter-panel">
<Instance v-if="searchStore.instanceContext" :instance="searchStore.instanceContext" small />
<Button
role="button"
:disabled="
!(
searchStore.facets.length > 0 ||
searchStore.orFacets.length > 0 ||
searchStore.environments.server === true ||
searchStore.environments.client === true ||
searchStore.openSource === true ||
searchStore.activeVersions.length > 0
)
"
@click="handleReset"
><ClearIcon />Clear Filters</Button
>
<div
v-for="categoryList in Array.from(sortedCategories)"
:key="categoryList[0]"
class="categories"
>
<h2>{{ formatCategoryHeader(categoryList[0]) }}</h2>
<div v-for="category in categoryList[1]" :key="category.name">
<SearchFilter
:active-filters="searchStore.facets"
:icon="category.icon"
:display-name="category.name"
:facet-name="`categories:${encodeURIComponent(category.name)}`"
<Instance v-if="searchStore.instanceContext" :instance="searchStore.instanceContext" small>
<template #content>
<Checkbox
:model-value="searchStore.ignoreInstance"
:checked="searchStore.ignoreInstance"
label="Unfilter loader & version"
class="filter-checkbox"
@toggle="toggleFacet"
@update:model-value="(value) => handleInstanceSwitch(value)"
/>
</div>
</div>
<div
v-if="
showLoaders &&
searchStore.projectType !== 'datapack' &&
searchStore.projectType !== 'resourcepack'
"
class="loaders"
>
<h2>Loaders</h2>
<div
v-for="loader in loaders.filter(
(l) =>
(searchStore.projectType !== 'mod' &&
l.supported_project_types?.includes(searchStore.projectType)) ||
(searchStore.projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name))
)"
:key="loader"
>
<SearchFilter
:active-filters="searchStore.orFacets"
:icon="loader.icon"
:display-name="loader.name"
:facet-name="`categories:${encodeURIComponent(loader.name)}`"
class="filter-checkbox"
@toggle="toggleOrFacet"
/>
</div>
</div>
<div v-if="searchStore.projectType !== 'datapack'" class="environment">
<h2>Environments</h2>
<SearchFilter
v-model="searchStore.environments.client"
display-name="Client"
facet-name="client"
class="filter-checkbox"
@click="getSearchResults"
>
<ClientIcon aria-hidden="true" />
</SearchFilter>
<SearchFilter
v-model="searchStore.environments.server"
display-name="Server"
facet-name="server"
class="filter-checkbox"
@click="getSearchResults"
>
<ServerIcon aria-hidden="true" />
</SearchFilter>
</div>
<div v-if="showVersions" class="versions">
<h2>Minecraft versions</h2>
<Checkbox v-model="showSnapshots" class="filter-checkbox">Show snapshots</Checkbox>
<multiselect
v-model="searchStore.activeVersions"
:options="
showSnapshots
? availableGameVersions.map((x) => x.version)
: availableGameVersions
.filter((it) => it.version_type === 'release')
.map((x) => x.version)
</template>
</Instance>
<Card class="search-panel-card">
<Button
role="button"
:disabled="
!(
searchStore.facets.length > 0 ||
searchStore.orFacets.length > 0 ||
searchStore.environments.server === true ||
searchStore.environments.client === true ||
searchStore.openSource === true ||
searchStore.activeVersions.length > 0
)
"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-search-on-select="false"
:show-labels="false"
placeholder="Choose versions..."
@update:model-value="getSearchResults"
/>
</div>
<div class="open-source">
<h2>Open source</h2>
<Checkbox
v-model="searchStore.openSource"
class="filter-checkbox"
@click="getSearchResults"
@click="handleReset"
><ClearIcon />Clear Filters</Button
>
Open source
</Checkbox>
</div>
<div v-if="showLoaders" class="loaders">
<h2>Loaders</h2>
<div
v-for="loader in loaders.filter(
(l) =>
(searchStore.projectType !== 'mod' &&
l.supported_project_types?.includes(searchStore.projectType)) ||
(searchStore.projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name))
)"
:key="loader"
>
<SearchFilter
:active-filters="searchStore.orFacets"
:icon="loader.icon"
:display-name="formatCategory(loader.name)"
:facet-name="`categories:${encodeURIComponent(loader.name)}`"
class="filter-checkbox"
@toggle="toggleOrFacet"
/>
</div>
</div>
<div
v-for="categoryList in Array.from(sortedCategories)"
:key="categoryList[0]"
class="categories"
>
<h2>{{ formatCategoryHeader(categoryList[0]) }}</h2>
<div v-for="category in categoryList[1]" :key="category.name">
<SearchFilter
:active-filters="searchStore.facets"
:icon="category.icon"
:display-name="formatCategory(category.name)"
:facet-name="`categories:${encodeURIComponent(category.name)}`"
class="filter-checkbox"
@toggle="toggleFacet"
/>
</div>
</div>
<div v-if="searchStore.projectType !== 'datapack'" class="environment">
<h2>Environments</h2>
<SearchFilter
v-model="searchStore.environments.client"
display-name="Client"
facet-name="client"
class="filter-checkbox"
@click="getSearchResults"
>
<ClientIcon aria-hidden="true" />
</SearchFilter>
<SearchFilter
v-model="searchStore.environments.server"
display-name="Server"
facet-name="server"
class="filter-checkbox"
@click="getSearchResults"
>
<ServerIcon aria-hidden="true" />
</SearchFilter>
</div>
<div v-if="showVersions" class="versions">
<h2>Minecraft versions</h2>
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" />
<multiselect
v-model="searchStore.activeVersions"
:options="
showSnapshots
? availableGameVersions.map((x) => x.version)
: availableGameVersions
.filter((it) => it.version_type === 'release')
.map((x) => x.version)
"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-search-on-select="false"
:show-labels="false"
placeholder="Choose versions..."
@update:model-value="getSearchResults"
/>
</div>
<div class="open-source">
<h2>Open source</h2>
<Checkbox
v-model="searchStore.openSource"
class="filter-checkbox"
label="Open source"
@click="getSearchResults"
/>
</div>
</Card>
</aside>
<div class="search">
<Card class="project-type-container">
@@ -267,16 +292,16 @@ watch(
/>
</Card>
<Card class="search-panel-container">
<div class="search-panel">
<div class="iconified-input">
<SearchIcon aria-hidden="true" />
<input
v-model="searchStore.searchInput"
type="text"
:placeholder="`Search ${searchStore.projectType}s...`"
@input="getSearchResults"
/>
</div>
<div class="iconified-input">
<SearchIcon aria-hidden="true" />
<input
v-model="searchStore.searchInput"
type="text"
:placeholder="`Search ${searchStore.projectType}s...`"
@input="getSearchResults"
/>
</div>
<div class="inline-option">
<span>Sort by</span>
<DropdownSelect
v-model="searchStore.filter"
@@ -291,6 +316,8 @@ watch(
class="sort-dropdown"
@change="getSearchResults"
/>
</div>
<div class="inline-option">
<span>Show per page</span>
<DropdownSelect
v-model="searchStore.limit"
@@ -310,19 +337,11 @@ watch(
/>
<SplashScreen v-if="loading" />
<section v-else class="project-list display-mode--list instance-results" role="list">
<ProjectCard
<SearchCard
v-for="result in searchStore.searchResults"
:id="`${result?.project_id}/`"
:key="result?.project_id"
class="result-project-item"
:type="result?.project_type"
:name="result?.title"
:description="result?.description"
:icon-url="result?.icon_url"
:downloads="result?.downloads?.toString()"
:follows="result?.follows?.toString()"
:created-at="result?.date_created"
:updated-at="result?.date_modified"
:project="result"
:instance="searchStore.instanceContext"
:categories="[
...categories.filter(
(cat) =>
@@ -335,16 +354,21 @@ watch(
loader.supported_project_types?.includes(searchStore.projectType)
),
]"
:project-type-display="result?.project_type"
project-type-url="project"
:server-side="result?.server_side"
:client-side="result?.client_side"
:show-updated-date="false"
:color="result?.color"
:confirm-modal="confirmModal"
:mod-install-modal="modInstallModal"
:incompatibility-warning-modal="incompatibilityWarningModal"
/>
</section>
<Pagination
:page="searchStore.currentPage"
:count="searchStore.pageCount"
@switch-page="switchPage"
/>
</div>
</div>
<InstallConfirmModal ref="confirmModal" />
<InstanceInstallModal ref="modInstallModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
@@ -360,39 +384,47 @@ watch(
margin-top: 1rem;
}
.search-panel-container {
.search-panel-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
margin-top: 1rem;
padding: 0.8rem !important;
background-color: var(--color-bg) !important;
margin-bottom: 0;
min-height: min-content !important;
}
.search-panel {
.iconified-input {
input {
max-width: 20rem !important;
}
}
.search-panel-container {
display: inline-flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
width: 100%;
padding: 1rem !important;
white-space: nowrap;
gap: 1rem;
.inline-option {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
width: 100%;
gap: 1rem;
margin: 1rem auto;
white-space: nowrap;
gap: 0.5rem;
.sort-dropdown {
min-width: 12.18rem;
max-width: 12.25rem;
}
.limit-dropdown {
width: 5rem;
}
}
.iconified-input {
width: 75%;
input {
flex-basis: initial;
}
}
.iconified-input {
flex-grow: 1;
}
.filter-panel {
@@ -413,13 +445,14 @@ watch(
.filter-panel {
position: fixed;
width: 19rem;
width: 20rem;
background: var(--color-raised-bg);
padding: 1rem;
display: flex;
flex-direction: column;
height: 100%;
max-height: calc(100vh - 3rem);
height: fit-content;
min-height: calc(100vh - 3.25rem);
max-height: calc(100vh - 3.25rem);
overflow-y: auto;
h2 {
@@ -432,7 +465,6 @@ watch(
.filter-checkbox {
margin-bottom: 0.3rem;
font-size: 1rem;
text-transform: capitalize;
svg {
display: flex;
@@ -447,8 +479,8 @@ watch(
}
.search {
margin: 0 1rem 0 20rem;
width: calc(100% - 21rem);
margin: 0 1rem 0 21rem;
width: calc(100% - 22rem);
.loading {
margin: 2rem;