725 lines
19 KiB
Vue
725 lines
19 KiB
Vue
<template>
|
|
<div class="columns">
|
|
<div class="content column-grow-5">
|
|
<h2>Mods</h2>
|
|
<section class="search-bar">
|
|
<div class="iconified-input column-grow-2">
|
|
<label class="hidden" for="search">Search Mods</label>
|
|
<input
|
|
id="search"
|
|
v-model="query"
|
|
type="search"
|
|
name="search"
|
|
placeholder="Search mods"
|
|
@input="onSearchChange(1)"
|
|
/>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<circle cx="11" cy="11" r="8" />
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
</svg>
|
|
</div>
|
|
<div class="sort-paginate">
|
|
<Multiselect
|
|
v-model="sortType"
|
|
class="sort-types"
|
|
placeholder="Select one"
|
|
track-by="display"
|
|
label="display"
|
|
:options="sortTypes"
|
|
:searchable="false"
|
|
:close-on-select="true"
|
|
:show-labels="false"
|
|
:allow-empty="false"
|
|
@input="onSearchChange(1)"
|
|
>
|
|
<template slot="singleLabel" slot-scope="{ option }">{{
|
|
option.display
|
|
}}</template>
|
|
</Multiselect>
|
|
<div class="mobile-filters-button">
|
|
<button @click="toggleFiltersMenu">Filter...</button>
|
|
</div>
|
|
</div>
|
|
<pagination
|
|
:current-page="currentPage"
|
|
:pages="pages"
|
|
@switch-page="onSearchChange"
|
|
></pagination>
|
|
</section>
|
|
<div class="results column-grow-4">
|
|
<div data-ea-publisher="modrinth-com" data-ea-type="text" />
|
|
<SearchResult
|
|
v-for="(result, index) in results"
|
|
:id="result.mod_id"
|
|
:key="result.mod_id"
|
|
:author="result.author"
|
|
:name="result.title"
|
|
:description="result.description"
|
|
:latest-version="result.versions[0]"
|
|
:created-at="result.date_created"
|
|
:updated-at="result.date_modified"
|
|
:downloads="result.downloads.toString()"
|
|
:icon-url="result.icon_url"
|
|
:author-url="result.author_url"
|
|
:page-url="result.page_url"
|
|
:categories="result.categories"
|
|
:is-ad="index === -1"
|
|
/>
|
|
<div v-if="results.length === 0" class="no-results">
|
|
<p>No results found for your query!</p>
|
|
</div>
|
|
</div>
|
|
<section v-if="pages.length > 1" class="search-bottom">
|
|
<Multiselect
|
|
v-model="maxResults"
|
|
class="max-results"
|
|
placeholder="Select one"
|
|
:options="[5, 10, 15, 20, 50, 100]"
|
|
:searchable="false"
|
|
:close-on-select="true"
|
|
:show-labels="false"
|
|
:allow-empty="false"
|
|
@input="onSearchChange(currentPage)"
|
|
>
|
|
</Multiselect>
|
|
<pagination
|
|
:current-page="currentPage"
|
|
:pages="pages"
|
|
@switch-page="onSearchChangeToTop"
|
|
></pagination>
|
|
</section>
|
|
</div>
|
|
<section id="filters" class="filters">
|
|
<div class="filters-wrapper">
|
|
<section class="filter-group">
|
|
<button class="filter-button-done" @click="toggleFiltersMenu">
|
|
Done
|
|
</button>
|
|
<button @click="clearFilters">Clear Filters</button>
|
|
<h3>Categories</h3>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Technology"
|
|
facet-name="categories:technology"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<TechCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Adventure"
|
|
facet-name="categories:adventure"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<AdventureCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Magic"
|
|
facet-name="categories:magic"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<MagicCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Utility"
|
|
facet-name="categories:utility"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<UtilityCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Decoration"
|
|
facet-name="categories:decoration"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<DecorationCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Library"
|
|
facet-name="categories:library"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<LibraryCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Cursed"
|
|
facet-name="categories:cursed"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<CursedCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Worldgen"
|
|
facet-name="categories:worldgen"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<WorldGenCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Storage"
|
|
facet-name="categories:storage"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<StorageCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Food"
|
|
facet-name="categories:food"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<FoodCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Equipment"
|
|
facet-name="categories:equipment"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<EquipmentCategory />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Misc"
|
|
facet-name="categories:misc"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<MiscCategory />
|
|
</SearchFilter>
|
|
<h3>Loaders</h3>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Forge"
|
|
facet-name="categories:forge"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<ForgeLoader />
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Fabric"
|
|
facet-name="categories:fabric"
|
|
@toggle="toggleFacet"
|
|
>
|
|
<FabricLoader />
|
|
</SearchFilter>
|
|
<h3>Platforms</h3>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Modrinth"
|
|
facet-name="host:modrinth"
|
|
@toggle="toggleFacet"
|
|
>
|
|
</SearchFilter>
|
|
<SearchFilter
|
|
:active-filters="facets"
|
|
display-name="Curseforge"
|
|
facet-name="host:curseforge"
|
|
@toggle="toggleFacet"
|
|
>
|
|
</SearchFilter>
|
|
<h3>Versions</h3>
|
|
</section>
|
|
<multiselect
|
|
v-model="selectedVersions"
|
|
:options="versions"
|
|
:loading="versions.length === 0"
|
|
:multiple="true"
|
|
:searchable="true"
|
|
:show-no-results="false"
|
|
:close-on-select="false"
|
|
:clear-on-select="false"
|
|
:show-labels="false"
|
|
:limit="6"
|
|
:hide-selected="true"
|
|
placeholder="Choose versions..."
|
|
@input="onSearchChange(1)"
|
|
></multiselect>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import Multiselect from 'vue-multiselect'
|
|
import axios from 'axios'
|
|
import SearchResult from '@/components/ModResult'
|
|
import Pagination from '@/components/Pagination'
|
|
import SearchFilter from '@/components/SearchFilter'
|
|
|
|
import TechCategory from '~/assets/images/categories/tech.svg?inline'
|
|
import AdventureCategory from '~/assets/images/categories/adventure.svg?inline'
|
|
import CursedCategory from '~/assets/images/categories/cursed.svg?inline'
|
|
import DecorationCategory from '~/assets/images/categories/decoration.svg?inline'
|
|
import EquipmentCategory from '~/assets/images/categories/equipment.svg?inline'
|
|
import FoodCategory from '~/assets/images/categories/food.svg?inline'
|
|
import LibraryCategory from '~/assets/images/categories/library.svg?inline'
|
|
import MagicCategory from '~/assets/images/categories/magic.svg?inline'
|
|
import MiscCategory from '~/assets/images/categories/misc.svg?inline'
|
|
import StorageCategory from '~/assets/images/categories/storage.svg?inline'
|
|
import UtilityCategory from '~/assets/images/categories/utility.svg?inline'
|
|
import WorldGenCategory from '~/assets/images/categories/worldgen.svg?inline'
|
|
|
|
import ForgeLoader from '~/assets/images/categories/forge.svg?inline'
|
|
import FabricLoader from '~/assets/images/categories/fabric.svg?inline'
|
|
|
|
export default {
|
|
auth: false,
|
|
components: {
|
|
SearchResult,
|
|
Pagination,
|
|
Multiselect,
|
|
SearchFilter,
|
|
TechCategory,
|
|
AdventureCategory,
|
|
CursedCategory,
|
|
DecorationCategory,
|
|
EquipmentCategory,
|
|
FoodCategory,
|
|
LibraryCategory,
|
|
MagicCategory,
|
|
MiscCategory,
|
|
StorageCategory,
|
|
UtilityCategory,
|
|
WorldGenCategory,
|
|
ForgeLoader,
|
|
FabricLoader,
|
|
},
|
|
async fetch() {
|
|
if (this.$route.query.q) this.query = this.$route.query.q
|
|
if (this.$route.query.f) {
|
|
const facets = this.$route.query.f.split(',')
|
|
|
|
for (const facet of facets) await this.toggleFacet(facet, false)
|
|
}
|
|
if (this.$route.query.v)
|
|
this.selectedVersions = this.$route.query.v.split(',')
|
|
if (this.$route.query.s) {
|
|
this.sortType.name = this.$route.query.s
|
|
this.sortType.display =
|
|
this.sortType.name.charAt(0).toUpperCase() + this.sortType.name.slice(1)
|
|
}
|
|
if (this.$route.query.m) {
|
|
this.maxResults = this.$route.query.m
|
|
}
|
|
if (this.$route.query.o)
|
|
this.currentPage = Math.ceil(this.$route.query.o / this.maxResults) + 1
|
|
|
|
await this.fillInitialVersions()
|
|
await this.onSearchChange(this.currentPage)
|
|
},
|
|
data() {
|
|
return {
|
|
query: '',
|
|
selectedVersions: [],
|
|
versions: [],
|
|
facets: [],
|
|
results: [],
|
|
pages: [],
|
|
currentPage: 1,
|
|
sortTypes: [
|
|
{ display: 'Relevance', name: 'relevance' },
|
|
{ display: 'Total Downloads', name: 'downloads' },
|
|
{ display: 'Newest', name: 'newest' },
|
|
{ display: 'Updated', name: 'updated' },
|
|
],
|
|
sortType: { display: 'Relevance', name: 'relevance' },
|
|
maxResults: 5,
|
|
}
|
|
},
|
|
methods: {
|
|
async fillInitialVersions() {
|
|
try {
|
|
const res = await axios.get(
|
|
'https://api.modrinth.com/api/v1/tag/game_version'
|
|
)
|
|
|
|
this.versions = res.data
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error(err)
|
|
}
|
|
},
|
|
async clearFilters() {
|
|
for (const facet of [...this.facets]) await this.toggleFacet(facet, true)
|
|
|
|
this.selectedVersions = []
|
|
await this.onSearchChange(1)
|
|
},
|
|
async toggleFacet(elementName, sendRequest) {
|
|
const index = this.facets.indexOf(elementName)
|
|
|
|
if (index !== -1) {
|
|
this.facets.splice(index, 1)
|
|
} else {
|
|
this.facets.push(elementName)
|
|
}
|
|
|
|
if (!sendRequest) await this.onSearchChange(1)
|
|
},
|
|
async onSearchChangeToTop(newPageNumber) {
|
|
if (process.client) window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
|
|
await this.onSearchChange(newPageNumber)
|
|
},
|
|
async onSearchChange(newPageNumber) {
|
|
if (this.query === null) return
|
|
|
|
try {
|
|
const params = [
|
|
`limit=${this.maxResults}`,
|
|
`index=${this.sortType.name}`,
|
|
]
|
|
|
|
if (this.query.length > 0) {
|
|
params.push(`query=${this.query.replace(/ /g, '+')}`)
|
|
}
|
|
|
|
if (this.facets.length > 0 || this.selectedVersions.length > 0) {
|
|
const formattedFacets = []
|
|
for (const facet of this.facets) {
|
|
formattedFacets.push([facet])
|
|
}
|
|
|
|
if (this.selectedVersions.length > 0) {
|
|
const versionFacets = []
|
|
for (const facet of this.selectedVersions) {
|
|
versionFacets.push('versions:' + facet)
|
|
}
|
|
formattedFacets.push(versionFacets)
|
|
}
|
|
|
|
params.push(`facets=${JSON.stringify(formattedFacets)}`)
|
|
}
|
|
|
|
const offset = (newPageNumber - 1) * this.maxResults
|
|
if (newPageNumber !== 1) {
|
|
params.push(`offset=${offset}`)
|
|
}
|
|
|
|
let url = 'https://api.modrinth.com/api/v1/mod'
|
|
|
|
if (params.length > 0) {
|
|
for (let i = 0; i < params.length; i++) {
|
|
url += i === 0 ? `?${params[i]}` : `&${params[i]}`
|
|
}
|
|
}
|
|
|
|
const res = await axios.get(url)
|
|
this.results = res.data.hits
|
|
|
|
const pageAmount = Math.ceil(res.data.total_hits / res.data.limit)
|
|
|
|
this.currentPage = newPageNumber
|
|
if (pageAmount > 4) {
|
|
if (this.currentPage + 1 >= pageAmount) {
|
|
this.pages = [
|
|
1,
|
|
pageAmount - 3,
|
|
pageAmount - 2,
|
|
pageAmount - 1,
|
|
pageAmount,
|
|
]
|
|
} else if (this.currentPage > 4) {
|
|
this.pages = [
|
|
1,
|
|
this.currentPage - 1,
|
|
this.currentPage,
|
|
this.currentPage + 1,
|
|
pageAmount,
|
|
]
|
|
} else {
|
|
this.pages = [1, 2, 3, 4, pageAmount]
|
|
}
|
|
} else {
|
|
this.pages = Array.from({ length: pageAmount }, (_, i) => i + 1)
|
|
}
|
|
|
|
if (process.client) {
|
|
url = `mods?q=${encodeURIComponent(this.query)}`
|
|
|
|
if (offset > 0) url += `&o=${offset}`
|
|
if (this.facets.length > 0)
|
|
url += `&f=${encodeURIComponent(this.facets)}`
|
|
if (this.selectedVersions.length > 0)
|
|
url += `&v=${encodeURIComponent(this.selectedVersions)}`
|
|
if (this.sortType.name !== 'relevance')
|
|
url += `&s=${encodeURIComponent(this.sortType.name)}`
|
|
if (this.maxResults > 5)
|
|
url += `&m=${encodeURIComponent(this.maxResults)}`
|
|
|
|
window.history.pushState(new Date(), 'Mods', url)
|
|
}
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error(err)
|
|
}
|
|
},
|
|
toggleFiltersMenu() {
|
|
const filters = document.getElementById('filters')
|
|
const currentlyActive = filters.className === 'filters active'
|
|
filters.className = `filters${currentlyActive ? '' : ' active'}`
|
|
document.body.style.overflow =
|
|
document.body.style.overflow !== 'hidden' ? 'hidden' : 'auto'
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
|
<style lang="scss">
|
|
.search-bar {
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
flex-flow: column;
|
|
.iconified-input {
|
|
width: 100%;
|
|
}
|
|
.sort-paginate {
|
|
display: flex;
|
|
width: 100%;
|
|
}
|
|
@media screen and (min-width: 900px) {
|
|
flex-flow: row;
|
|
.iconified-input {
|
|
width: auto;
|
|
}
|
|
.sort-paginate {
|
|
display: block;
|
|
width: auto;
|
|
}
|
|
}
|
|
}
|
|
|
|
.search-bottom {
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
select {
|
|
width: 100px;
|
|
margin-right: 20px;
|
|
}
|
|
}
|
|
|
|
.content {
|
|
min-height: 96vh;
|
|
}
|
|
|
|
.mobile-filters-button {
|
|
display: inline-block;
|
|
button {
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
border: 2px solid var(--color-grey-3);
|
|
border-radius: var(--size-rounded-sm);
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
// Hide button on larger screens where it's not needed
|
|
@media screen and (min-width: 900px) {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.filters {
|
|
overflow-y: auto;
|
|
background-color: var(--color-bg);
|
|
border-left: 1px solid var(--color-grey-2);
|
|
position: fixed;
|
|
width: 100vw;
|
|
right: -100vw;
|
|
max-height: 100vh;
|
|
min-width: 15%;
|
|
top: 3.5rem;
|
|
height: calc(100vh - 3.5rem);
|
|
transition: right 150ms;
|
|
flex-shrink: 0; // Stop shrinking when page contents change
|
|
|
|
.filters-wrapper {
|
|
padding: 0 0.75rem;
|
|
}
|
|
|
|
h3 {
|
|
color: #718096;
|
|
font-size: 0.8rem;
|
|
letter-spacing: 0.02rem;
|
|
margin-bottom: 0.5rem;
|
|
margin-top: 1.5rem;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
// Larger screens that don't need to collapse
|
|
@media screen and (min-width: 900px) {
|
|
position: sticky;
|
|
width: 215px;
|
|
padding-right: 1rem;
|
|
transition: none;
|
|
}
|
|
|
|
// Desktop
|
|
@media screen and (min-width: 1145px) {
|
|
top: 0;
|
|
height: 100vh;
|
|
}
|
|
|
|
&.active {
|
|
right: 0;
|
|
}
|
|
}
|
|
|
|
.filter-group {
|
|
margin-top: 1em;
|
|
|
|
button {
|
|
cursor: pointer;
|
|
width: 100%;
|
|
padding: 5px 0;
|
|
outline: none;
|
|
color: var(--color-grey-5);
|
|
background-color: var(--color-grey-1);
|
|
border: none;
|
|
border-radius: 5px;
|
|
|
|
&:hover {
|
|
background-color: var(--color-grey-2);
|
|
color: var(--color-text);
|
|
}
|
|
}
|
|
|
|
.filter-button-done {
|
|
display: block;
|
|
}
|
|
|
|
// Large screens that don't collapse
|
|
@media screen and (min-width: 900px) {
|
|
.filter-button-done {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
// Desktop
|
|
@media screen and (min-width: 1145px) {
|
|
margin-top: 2em;
|
|
}
|
|
}
|
|
|
|
.iconified-select {
|
|
margin-left: 1em;
|
|
align-items: center;
|
|
display: inline-flex;
|
|
flex-direction: row-reverse;
|
|
|
|
select {
|
|
padding-left: 2.5rem;
|
|
|
|
&:hover {
|
|
& + svg {
|
|
color: var(--color-grey-6);
|
|
}
|
|
}
|
|
|
|
&:focus {
|
|
& + svg {
|
|
color: var(--color-text);
|
|
}
|
|
}
|
|
}
|
|
|
|
svg {
|
|
color: var(--color-grey-5);
|
|
margin-right: -2rem;
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
}
|
|
|
|
select {
|
|
width: 220px;
|
|
padding: 0.5rem 1rem;
|
|
background: var(--color-bg);
|
|
border: 2px solid var(--color-grey-3);
|
|
border-radius: var(--size-rounded-sm);
|
|
color: var(--color-grey-9);
|
|
font-size: 1rem;
|
|
-webkit-appearance: none;
|
|
-moz-appearance: none;
|
|
appearance: none;
|
|
|
|
&:hover {
|
|
border-color: var(--color-grey-4);
|
|
color: var(--color-text);
|
|
}
|
|
}
|
|
|
|
.sort-types {
|
|
min-width: 200px;
|
|
border: 2px solid var(--color-grey-3);
|
|
border-radius: var(--size-rounded-sm);
|
|
|
|
.multiselect__tags {
|
|
padding: 10px 50px 0 8px;
|
|
border: none;
|
|
}
|
|
}
|
|
|
|
.no-results {
|
|
text-align: center;
|
|
padding: 20px 0;
|
|
font-size: 30px;
|
|
color: var(--color-text);
|
|
background-color: var(--color-grey-1);
|
|
}
|
|
|
|
.max-results {
|
|
max-width: 80px;
|
|
}
|
|
|
|
.multiselect__content-wrapper {
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.multiselect__tags {
|
|
border: 2px solid var(--color-grey-3);
|
|
border-radius: var(--size-rounded-sm);
|
|
}
|
|
|
|
.multiselect__tags,
|
|
.multiselect__spinner {
|
|
background: var(--color-bg);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.multiselect__spinner::before,
|
|
.multiselect__spinner::after {
|
|
border-top-color: var(--color-brand);
|
|
}
|
|
|
|
.multiselect__option--selected.multiselect__option--highlight,
|
|
.multiselect__option,
|
|
.multiselect__single,
|
|
.multiselect__input {
|
|
color: var(--color-text);
|
|
background: var(--color-bg);
|
|
}
|
|
|
|
.multiselect__option--highlight,
|
|
.multiselect__tag {
|
|
background: var(--color-brand);
|
|
}
|
|
</style>
|