Files
AstralRinth/theseus_gui/src/pages/Browse.vue
Geometrically a62d931fe2 Wire Profile Backend to Frontend (#71)
* Search updates

* fixes2

* Some more work

* start instance page wiring

* Pack installation + Profile viewing

* Remove print statement

* Fix disappearing profiles

* fix compile err

* Finish Instance Running

* remove print statement

* fix prettier

* Fix clippy + early return
2023-04-08 18:54:38 -07:00

361 lines
9.4 KiB
Vue

<script setup>
import { ref } from 'vue'
import { ofetch } from 'ofetch'
import {
Pagination,
ProjectCard,
Checkbox,
Button,
ClearIcon,
SearchIcon,
DropdownSelect,
SearchFilter,
Card,
ClientIcon,
ServerIcon,
AnimatedLogo,
} from 'omorphia'
import Multiselect from 'vue-multiselect'
import { useSearch } from '@/store/state'
import { get_categories, get_loaders, get_game_versions } from '@/helpers/tags'
const searchStore = useSearch()
const showSnapshots = ref(false)
const loading = ref(true)
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories(),
get_loaders(),
get_game_versions(),
])
const getSearchResults = async (shouldLoad = false) => {
const queryString = searchStore.getQueryString()
if (shouldLoad === true) {
loading.value = true
}
const response = await ofetch(`https://api.modrinth.com/v2/search${queryString}`)
loading.value = false
searchStore.setSearchResults(response)
}
getSearchResults(true)
const toggleFacet = async (facet) => {
const index = searchStore.facets.indexOf(facet)
if (index !== -1) searchStore.facets.splice(index, 1)
else searchStore.facets.push(facet)
await getSearchResults()
}
const toggleOrFacet = async (orFacet) => {
const index = searchStore.orFacets.indexOf(orFacet)
if (index !== -1) searchStore.orFacets.splice(index, 1)
else searchStore.orFacets.push(orFacet)
await getSearchResults()
}
const switchPage = async (page) => {
searchStore.currentPage = parseInt(page)
if (page === 1) searchStore.offset = 0
else searchStore.offset = (searchStore.currentPage - 1) * searchStore.limit
await getSearchResults()
}
const handleReset = async () => {
searchStore.resetFilters()
await getSearchResults()
}
</script>
<template>
<div class="search-container">
<aside class="filter-panel">
<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 class="categories">
<h2>Categories</h2>
<div
v-for="category in categories.filter((cat) => cat.project_type === 'modpack')"
:key="category.name"
>
<SearchFilter
:active-filters="searchStore.facets"
:icon="category.icon"
:display-name="category.name"
:facet-name="`categories:${encodeURIComponent(category.name)}`"
class="filter-checkbox"
@toggle="toggleFacet"
/>
</div>
</div>
<div class="loaders">
<h2>Loaders</h2>
<div
v-for="loader in loaders.filter((l) => l.supported_project_types?.includes('modpack'))"
: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 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 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)
"
: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"
>
Open source
</Checkbox>
</div>
</aside>
<div class="search">
<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 modpacks..."
@input="getSearchResults"
/>
</div>
<span>Sort by</span>
<DropdownSelect
v-model="searchStore.filter"
name="Sort dropdown"
:options="[
'Relevance',
'Download count',
'Follow count',
'Recently published',
'Recently updated',
]"
class="sort-dropdown"
@change="getSearchResults"
/>
<span>Show per page</span>
<DropdownSelect
v-model="searchStore.limit"
name="Limit dropdown"
:options="[5, 10, 15, 20, 50, 100]"
:default-value="searchStore.limit.toString()"
:model-value="searchStore.limit.toString()"
class="limit-dropdown"
@change="getSearchResults"
/>
</div>
</Card>
<Pagination
:page="searchStore.currentPage"
:count="searchStore.pageCount"
@switch-page="switchPage"
/>
<AnimatedLogo v-if="loading" class="loading" />
<section v-else class="project-list display-mode--list instance-results" role="list">
<ProjectCard
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"
:categories="[
...categories.filter(
(cat) =>
result?.display_categories.includes(cat.name) && cat.project_type === 'modpack'
),
...loaders.filter(
(loader) =>
result?.display_categories.includes(loader.name) &&
loader.supported_project_types?.includes('modpack')
),
]"
: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"
/>
</section>
</div>
</div>
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss">
.search-panel-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
margin-top: 1rem;
padding: 0.8rem !important;
.search-panel {
display: flex;
align-items: center;
justify-content: space-evenly;
width: 100%;
gap: 1rem;
margin: 1rem auto;
white-space: nowrap;
.sort-dropdown {
min-width: 12.18rem;
}
.limit-dropdown {
width: 5rem;
}
.iconified-input {
width: 75%;
input {
flex-basis: initial;
}
}
}
.filter-panel {
button {
display: flex;
align-items: center;
justify-content: space-evenly;
svg {
margin-right: 0.4rem;
}
}
}
}
.search-container {
display: flex;
.filter-panel {
position: fixed;
width: 16rem;
background: var(--color-raised-bg);
padding: 1rem 1rem 3rem 1rem;
display: flex;
flex-direction: column;
min-height: 100vh;
height: fit-content;
max-height: 100%;
overflow-y: auto;
h2 {
color: var(--color-contrast);
margin-top: 1rem;
margin-bottom: 0.5rem;
font-size: 1.16rem;
}
.filter-checkbox {
margin-bottom: 0.3rem;
font-size: 1rem;
text-transform: capitalize;
svg {
display: flex;
align-self: center;
justify-self: center;
}
button.checkbox {
border: none;
}
}
}
.search {
margin: 0 1rem 0 17rem;
width: 100%;
.loading {
margin: 2rem;
text-align: center;
}
}
}
</style>