New features (#592)

* New features

* Lots of bug fixes

* Fix respack creation

* Improve mobile nav with more project types

* Fix resolution sorting and remove icons

* Move cookie consent to top on small devices to get out of the way of navigation

* Move cookie consent + fix hydration

* Fix project editing + update search features

* Centralize hardcoding of loader/category names, fix cookie consent shadow, fix mobile navbar rounding

* Fix plugin platforms formatting

* Kitchen sink!

* Add support for display names

* LiteLoader formatting

* Fixed "show all loaders" toggle not resetting when changing pages

* Allow multiple loaders in version filter controls

* Fix clear filters button

* Revert "Add support for display names"

This reverts commit 370838763d86bcae51bf06c304248f7a1f8fc28f.

* Let's see how this goes. Upstream filters, attempt 1

* github? hello?

* No more "Server mod" on plugins

* Fix formatting of project types in project creation

* Move where project creation sets the resource pack loader

* Allow setting pixelated image-rendering

Allows to apply 'style' attribute to IMG tags with value
'image-rendering' set to 'pixelated', which can be useful for people who
use pixel art in their READMEs (to demonstrate items, for example).

* fix user page + hydration issue fix from Brawaru

* Rename to proxies

* Make categories use title case

* Always show project type on moderation page, improve project type display on project pages

* Remove invalid key

* Missed a check

* Fix browse menu animation

* Fix disabled button condition and minimum width for 2 lines

* Body -> Description in edit pages

* More casing consistency issues

* Fix duplicate version URLs

* Fix version creation

* Edit URLs, fix privacy page buttons

* Fix notifications popup overlaying

* Final merge fixes

Co-authored-by: Prospector <prospectordev@gmail.com>
Co-authored-by: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
This commit is contained in:
Geometrically 2022-08-14 12:42:58 -07:00 committed by GitHub
parent b16475b8bd
commit 673f7a82d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1152 additions and 348 deletions

View File

@ -1,5 +1,9 @@
export default function (to, from, savedPosition) {
if (to.name.startsWith('type-id') && from.name.startsWith('type-id')) {
if (
from == null ||
(to.name.startsWith('type-id') && from.name.startsWith('type-id')) ||
to.name === from.name
) {
return savedPosition
} else {
return { x: 0, y: 0 }

View File

@ -263,6 +263,14 @@
> :last-child {
margin-bottom: 0 !important;
}
@media screen and (max-width: 850px) {
iframe {
aspect-ratio: 16 / 9;
width: 100%;
height: auto;
}
}
}
.tooltip {
@ -826,12 +834,12 @@ label {
}
.vue-notification {
background: #44A4FC;
border-left: 5px solid #44A4FC;
background: #44a4fc;
border-left: 5px solid #44a4fc;
&.success {
background: #68CD86;
border-left-color: #68CD86;
background: #68cd86;
border-left-color: #68cd86;
}
&.warn {
@ -840,25 +848,43 @@ label {
}
&.error {
background: #E54D42;
border-left-color: #E54D42;
background: #e54d42;
border-left-color: #e54d42;
}
}
.vue-notification-group {
right: 25px !important;
bottom: 25px !important;
.vue-notification-template {
border-radius: var(--size-rounded-card);
margin: 0 0 25px 0;
.vue-notification-wrapper {
margin-bottom: 10px;
.notification-title {
font-size: var(--font-size-lg);
margin-right: auto;
.vue-notification-template {
border-radius: var(--size-rounded-card);
margin: 0;
.notification-title {
font-size: var(--font-size-lg);
margin-right: auto;
}
.notification-content {
font-size: var(--font-size-md);
}
}
.notification-content {
font-size: var(--font-size-md);
&:last-child {
margin: 0;
}
}
@media screen and (max-width: 750px) {
transition: bottom 0.25s ease-in-out;
bottom: calc(var(--size-mobile-navbar-height) + 10px) !important;
&.browse-menu-open {
bottom: calc(var(--size-mobile-navbar-height-expanded) + 10px) !important;
}
}
}

View File

@ -208,7 +208,9 @@ body {
--size-rounded-tooltip: 0.25rem;
--size-navbar-height: 3.5rem;
--size-mobile-navbar-height: 4rem;
--size-mobile-navbar-height: 3.5rem;
// --size-mobile-navbar-height-expanded: 10rem;
--size-mobile-navbar-height-expanded: 7.5rem;
--spacing-card-lg: 1.5rem;
--spacing-card-bg: 1rem;
@ -235,6 +237,10 @@ body {
--font-weight-text: var(--font-weight-medium);
--font-weight-heading: var(--font-weight-extrabold);
--font-weight-title: var(--font-weight-extrabold);
@media screen and (min-width: 501px) {
--size-mobile-navbar-height-expanded: 7rem;
}
}
svg {

View File

@ -3,13 +3,15 @@
<div
ref="container"
class="container"
:style="{ visibility: shown ? 'visible' : 'hidden' }"
:class="{ 'mobile-menu-open': mobileMenuOpen }"
:style="{
visibility: shown ? 'visible' : 'hidden',
}"
>
<div class="card banner">
<span>
Modrinth uses cookies for various purposes, including advertising.<br />
We encourage you to review your privacy settings by clicking on the
button below:
Modrinth uses cookies for various purposes. We encourage you to review
your privacy settings by clicking on the button below:
</span>
<div class="actions">
<button class="btn button" @click="review">Review</button>
@ -24,6 +26,12 @@
import scopes from '~/privacy-toggles'
export default {
name: 'CookieConsent',
props: {
mobileMenuOpen: {
type: Boolean,
default: true,
},
},
data() {
return {
shown: false,
@ -68,15 +76,18 @@ export default {
width: 100%;
text-align: center;
z-index: 20;
z-index: 2;
position: fixed;
bottom: 4rem;
right: 0;
bottom: 0;
.banner {
padding: 1rem;
font-size: 1.05rem;
border-radius: 0;
margin-bottom: 0;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.3);
padding: 1rem 1rem calc(var(--size-mobile-navbar-height) + 1rem);
transition: padding-bottom 0.25s ease-in-out;
}
.actions {
display: flex;
@ -89,11 +100,23 @@ export default {
}
}
@media screen and (min-width: 750px) {
bottom: 0;
.banner {
margin-bottom: 0;
}
&.mobile-menu-open {
.banner {
margin-bottom: 0;
padding-bottom: calc(var(--size-mobile-navbar-height-expanded) + 1rem);
}
}
@media screen and (min-width: 750px) {
.banner {
padding-bottom: 1rem;
}
&.mobile-menu-open {
bottom: 0;
}
}
@ -102,7 +125,9 @@ export default {
text-align: unset;
.banner {
max-width: 18vw;
border-radius: var(--size-rounded-card);
width: 18vw;
min-width: 16rem;
border-left: solid 5px var(--color-brand);
margin: 0 2rem 2rem 0;
}

View File

@ -28,13 +28,16 @@
</nuxt-link>
</p>
</div>
<div class="side-type">
<div
v-if="type !== 'resourcepack' && projectTypeDisplay !== 'plugin'"
class="side-type"
>
<div
v-if="clientSide === 'optional' && serverSide === 'optional'"
class="side-descriptor"
>
<InfoIcon aria-hidden="true" />
Universal {{ type }}
Universal {{ projectTypeDisplay }}
</div>
<div
v-else-if="
@ -44,7 +47,7 @@
class="side-descriptor"
>
<InfoIcon aria-hidden="true" />
Client {{ type }}
Client {{ projectTypeDisplay }}
</div>
<div
v-else-if="
@ -54,8 +57,16 @@
class="side-descriptor"
>
<InfoIcon aria-hidden="true" />
Server {{ type }}
Server {{ projectTypeDisplay }}
</div>
<div v-else-if="moderation" class="side-descriptor">
<InfoIcon aria-hidden="true" />
A {{ projectTypeDisplay }}
</div>
</div>
<div v-else-if="moderation" class="side-descriptor">
<InfoIcon aria-hidden="true" />
A {{ projectTypeDisplay }}
</div>
<p class="description">
{{ description }}
@ -217,6 +228,16 @@ export default {
required: false,
default: '',
},
moderation: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
projectTypeDisplay() {
return this.$getProjectTypeForDisplay(this.type, this.categories)
},
},
}
</script>

View File

@ -43,11 +43,15 @@ export default {
<style scoped>
button {
text-transform: capitalize;
margin: 0;
padding: 0;
text-transform: capitalize;
background-color: transparent;
border-radius: 0;
color: inherit;
}
button span::first-letter {
text-transform: uppercase;
}
</style>

View File

@ -5,16 +5,16 @@
>
<Multiselect
v-if="getValidLoaders().length > 1"
v-model="selectedLoader"
v-model="selectedLoaders"
:options="getValidLoaders()"
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
:multiple="false"
:multiple="true"
:searchable="false"
:show-no-results="false"
:close-on-select="true"
:clear-search-on-select="false"
:show-labels="false"
:allow-empty="false"
:allow-empty="true"
:disabled="getValidLoaders().length === 1"
placeholder="Filter loader..."
@input="updateVersionFilters()"
@ -51,10 +51,12 @@
/>
<button
title="Clear filters"
:disabled="selectedLoader === null && selectedGameVersions.length === 0"
:disabled="
selectedLoaders.length === 0 && selectedGameVersions.length === 0
"
class="iconified-button"
@click="
selectedLoader = null
selectedLoaders = []
selectedGameVersions = []
updateVersionFilters()
"
@ -90,7 +92,7 @@ export default {
cachedValidVersions: null,
cachedValidLoaders: null,
selectedGameVersions: [],
selectedLoader: null,
selectedLoaders: [],
}
},
methods: {
@ -124,8 +126,10 @@ export default {
this.selectedGameVersions.some((gameVersion) =>
projectVersion.game_versions.includes(gameVersion)
)) &&
(this.selectedLoader === null ||
projectVersion.loaders.includes(this.selectedLoader))
(this.selectedLoaders.length === 0 ||
this.selectedLoaders.some((loader) =>
projectVersion.loaders.includes(loader)
))
)
this.$emit('updateVersions', temp)
},

View File

@ -3,10 +3,7 @@
<span
v-for="category in categoriesFiltered"
:key="category.name"
v-html="
category.icon +
(category.name === 'modloader' ? 'ModLoader' : category.name)
"
v-html="category.icon + $formatCategory(category.name)"
/>
</div>
</template>
@ -33,7 +30,8 @@ export default {
.filter(
(x) =>
this.categories.includes(x.name) &&
(!x.project_type || x.project_type === this.type)
(!x.project_type || x.project_type === this.type) &&
x.name !== 'minecraft'
)
},
},
@ -52,7 +50,6 @@ export default {
flex-direction: row;
color: var(--color-icon);
margin-right: 1em;
text-transform: capitalize;
svg {
width: 1rem;

View File

@ -69,7 +69,6 @@ export default {
}
span {
text-transform: capitalize;
user-select: none;
}
}

View File

@ -1,12 +1,14 @@
<template>
<div class="layout">
<div ref="layout" class="layout">
<header class="site-header" role="presentation">
<section class="navbar columns" role="navigation">
<section class="skip column" role="presentation">
<a href="#main">Skip to Main Content</a>
<a v-if="registeredSkipLink" :href="registeredSkipLink.id">{{
registeredSkipLink.text
}}</a>
<a
v-show="!!registeredSkipLink"
:href="(registeredSkipLink || {}).id"
>{{ (registeredSkipLink || {}).text }}</a
>
</section>
<section class="logo column" role="presentation">
<NuxtLink to="/" aria-label="Modrinth home page">
@ -19,7 +21,13 @@
<NuxtLink to="/mods" class="tab">
<span>Mods</span>
</NuxtLink>
<NuxtLink to="/modpacks" class="tab tab--alpha">
<!-- <NuxtLink to="/plugins" class="tab">-->
<!-- <span>Plugins</span>-->
<!-- </NuxtLink>-->
<!-- <NuxtLink to="/resourcepacks" class="tab">-->
<!-- <span>Resource Packs</span>-->
<!-- </NuxtLink>-->
<NuxtLink to="/modpacks" class="tab">
<span>Modpacks</span>
</NuxtLink>
</div>
@ -162,23 +170,62 @@
</section>
</section>
</section>
<section class="mobile-navbar">
<NuxtLink to="/" class="tab">
<HomeIcon />
<span>Home</span>
</NuxtLink>
<NuxtLink to="/mods" class="tab">
<ModIcon />
<span>Mods</span>
</NuxtLink>
<NuxtLink to="/modpacks" class="tab">
<ModpackIcon />
<span>Modpacks</span>
</NuxtLink>
<button class="tab" @click="toggleMobileMenu()">
<HamburgerIcon />
<span>{{ isMobileMenuOpen ? 'Less' : 'More' }}</span>
</button>
<section ref="mobileNavBar" class="mobile-navbar">
<div class="top-row">
<NuxtLink to="/" class="tab" @click.native="closeBrowseMenu()">
<HomeIcon />
</NuxtLink>
<div class="spacer"></div>
<button class="tab browse" @click="toggleBrowseMenu()">
<DropdownIcon :class="{ closed: !isBrowseMenuOpen }" />
<span>Browse</span>
</button>
<div class="spacer"></div>
<button class="tab" @click="toggleMobileMenu()">
<HamburgerIcon v-if="!isMobileMenuOpen" />
<CrossIcon v-else />
</button>
</div>
<div
:class="{ 'disable-childern': !isBrowseMenuOpen }"
class="project-types"
>
<NuxtLink
:tabindex="isBrowseMenuOpen ? 0 : -1"
to="/mods"
class="tab"
@click.native="closeBrowseMenu()"
>
<span>Mods</span>
</NuxtLink>
<!-- <NuxtLink-->
<!-- :tabindex="isBrowseMenuOpen ? 0 : -1"-->
<!-- to="/plugins"-->
<!-- class="tab"-->
<!-- @click.native="closeBrowseMenu()"-->
<!-- >-->
<!-- <span>Plugins</span>-->
<!-- </NuxtLink>-->
<!-- <NuxtLink-->
<!-- :tabindex="isBrowseMenuOpen ? 0 : -1"-->
<!-- to="/resourcepacks"-->
<!-- class="tab"-->
<!-- @click.native="closeBrowseMenu()"-->
<!-- >-->
<!-- <span>Resource Packs</span>-->
<!-- </NuxtLink>-->
<NuxtLink
:tabindex="isBrowseMenuOpen ? 0 : -1"
to="/modpacks"
class="tab"
@click.native="closeBrowseMenu()"
>
<span>Modpacks</span>
</NuxtLink>
</div>
</section>
<section ref="mobileMenu" class="mobile-menu">
<div class="mobile-menu-wrapper">
@ -239,11 +286,12 @@
</section>
</header>
<main>
<CookieConsent />
<CookieConsent :mobile-menu-open="isBrowseMenuOpen" />
<notifications
group="main"
position="bottom right"
:max="5"
:class="{ 'browse-menu-open': isBrowseMenuOpen }"
:ignore-duplicates="true"
:duration="10000"
/>
@ -312,16 +360,15 @@ import ClickOutside from 'vue-click-outside'
import ModrinthLogo from '~/assets/images/text-logo.svg?inline'
import HamburgerIcon from '~/assets/images/utils/hamburger.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import NotificationIcon from '~/assets/images/sidebar/notifications.svg?inline'
import SettingsIcon from '~/assets/images/sidebar/settings.svg?inline'
import ShieldIcon from '~/assets/images/utils/shield.svg?inline'
import ModerationIcon from '~/assets/images/sidebar/admin.svg?inline'
import HomeIcon from '~/assets/images/sidebar/home.svg?inline'
import ModIcon from '~/assets/images/sidebar/mod.svg?inline'
import ModpackIcon from '~/assets/images/sidebar/modpack.svg?inline'
import MoonIcon from '~/assets/images/utils/moon.svg?inline'
import MoonIcon from '~/assets/images/utils/moon.svg?inline'
import SunIcon from '~/assets/images/utils/sun.svg?inline'
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
@ -343,8 +390,7 @@ export default {
GitHubIcon,
NotificationIcon,
HomeIcon,
ModIcon,
ModpackIcon,
CrossIcon,
HamburgerIcon,
CookieConsent,
SettingsIcon,
@ -365,6 +411,7 @@ export default {
branch: process.env.branch || 'master',
hash: process.env.hash || 'unknown',
isMobileMenuOpen: false,
isBrowseMenuOpen: false,
registeredSkipLink: null,
moderationNotifications: 0,
}
@ -429,6 +476,37 @@ export default {
document.body.style.overflowY !== 'hidden' ? 'hidden' : overflowStyle
this.isMobileMenuOpen = !currentlyActive
if (this.isMobileMenuOpen) {
this.$refs.mobileNavBar.className = `mobile-navbar`
this.$refs.layout.className = `layout`
this.isBrowseMenuOpen = false
}
},
toggleBrowseMenu() {
const currentlyActive =
this.$refs.mobileNavBar.className === 'mobile-navbar expanded'
this.$refs.mobileNavBar.className = `mobile-navbar${
currentlyActive ? '' : ' expanded'
}`
this.$refs.layout.className = `layout${
currentlyActive ? '' : ' expanded-mobile-nav'
}`
this.isBrowseMenuOpen = !currentlyActive
if (this.isBrowseMenuOpen) {
this.$refs.mobileMenu.className = `mobile-menu`
this.isMobileMenuOpen = false
}
},
closeBrowseMenu() {
this.$refs.mobileNavBar.className = `mobile-navbar`
this.$refs.layout.className = `layout`
this.isBrowseMenuOpen = false
},
async logout() {
this.$cookies.set('auth-token-reset', true)
@ -453,16 +531,6 @@ export default {
removeFocus() {
document.activeElement.blur() // This doesn't work, sadly. Help
},
async getModerationCount() {
const [projects, reports] = (
await Promise.all([
this.$axios.get(`moderation/projects`, this.$defaultHeaders()),
this.$axios.get(`report`, this.$defaultHeaders()),
])
).map((it) => it.data)
return projects.length + reports.length
},
},
}
</script>
@ -819,34 +887,36 @@ export default {
.mobile-navbar {
display: none;
width: 100%;
transition: height 0.25s ease-in-out;
height: var(--size-mobile-navbar-height);
position: fixed;
left: 0;
bottom: 0;
justify-content: center;
align-items: center;
background-color: var(--color-raised-bg);
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.3);
z-index: 6;
flex-direction: column;
border-radius: var(--size-rounded-card) var(--size-rounded-card) 0 0;
overflow: hidden;
.tab {
background: none;
display: flex;
flex-grow: 1;
flex-basis: 0;
justify-content: center;
align-items: center;
flex-direction: column;
flex-direction: row;
gap: 0.25rem;
font-weight: bold;
padding: 0;
margin: auto;
transition: color ease-in-out 0.15s;
color: var(--color-text-inactive);
text-align: center;
svg {
height: 1.75rem;
width: 1.75rem;
margin-bottom: 0.25rem;
}
&:hover,
@ -863,9 +933,72 @@ export default {
}
}
.top-row {
min-height: var(--size-mobile-navbar-height);
display: flex;
width: 100%;
.browse {
flex-grow: 10;
svg {
transition: transform 0.125s ease-in-out;
&.closed {
transform: rotate(180deg);
}
}
}
.tab {
&:first-child {
margin-left: 2rem;
}
&:last-child {
margin-right: 2rem;
}
}
.spacer {
flex-grow: 1;
}
}
.disable-childern {
a {
pointer-events: none;
}
}
.project-types {
margin-top: 0.5rem;
display: flex;
justify-content: center;
flex-wrap: wrap;
row-gap: 0.5rem;
.tab {
flex: 0 0 fit-content;
background-color: var(--color-button-bg);
padding: 0.5rem 1.25rem;
margin: 0 0.25rem;
border-radius: var(--size-rounded-max);
&.nuxt-link-exact-active {
background-color: var(--color-brand);
color: var(--color-brand-inverted);
}
}
}
@media screen and (max-width: 750px) {
display: flex;
}
&.expanded {
height: var(--size-mobile-navbar-height-expanded);
}
}
}
@ -874,12 +1007,13 @@ export default {
position: absolute;
top: 0;
background-color: var(--color-bg);
height: calc(100% - var(--size-mobile-navbar-height));
height: 100%;
width: 100%;
z-index: 5;
.mobile-menu-wrapper {
max-height: calc(100vh - var(--size-mobile-navbar-height));
margin-bottom: var(--size-mobile-navbar-height);
overflow-y: auto;
margin-top: auto;
@ -903,6 +1037,7 @@ export default {
&.nuxt-link-exact-active {
color: var(--color-button-text-active);
svg {
color: var(--color-brand);
}

View File

@ -155,6 +155,16 @@ export default {
component: resolve(__dirname, 'pages/search/modpacks.vue'),
name: 'modpacks',
},
{
path: '/plugins',
component: resolve(__dirname, 'pages/search/plugins.vue'),
name: 'plugins',
},
{
path: '/resourcepacks',
component: resolve(__dirname, 'pages/search/resourcepacks.vue'),
name: 'resourcepacks',
},
],
})
@ -222,7 +232,7 @@ export default {
'/search/**',
'/create/**',
],
routes: ['mods', 'modpacks'],
routes: ['mods', 'modpacks', 'resourcepacks', 'plugins'],
},
/*
** Axios module configuration
@ -309,7 +319,7 @@ export default {
},
hooks: {
render: {
routeDone(url) {
routeDone(url, result, context) {
setTimeout(() => {
axios
.post(
@ -323,6 +333,12 @@ export default {
{
headers: {
'Modrinth-Admin': process.env.ARIADNE_ADMIN_KEY || 'feedbeef',
'User-Agent':
context.req.rawHeaders[
context.req.rawHeaders.findIndex(
(x) => x === 'User-Agent'
) + 1
],
},
}
)

View File

@ -36,38 +36,46 @@
</nuxt-link>
<div
v-if="
project.client_side === 'optional' &&
project.server_side === 'optional'
project.project_type !== 'resourcepack' &&
projectTypeDisplay !== 'plugin'
"
class="side-descriptor"
>
<InfoIcon aria-hidden="true" />
Universal {{ project.project_type }}
</div>
<div
v-else-if="
(project.client_side === 'optional' ||
project.client_side === 'required') &&
(project.server_side === 'optional' ||
project.server_side === 'unsupported')
"
class="side-descriptor"
>
<InfoIcon aria-hidden="true" />
Client {{ project.project_type }}
</div>
<div
v-else-if="
(project.server_side === 'optional' ||
project.server_side === 'required') &&
(project.client_side === 'optional' ||
project.client_side === 'unsupported')
"
class="side-descriptor"
>
<InfoIcon aria-hidden="true" />
Server {{ project.project_type }}
<div
v-if="
project.client_side === 'optional' &&
project.server_side === 'optional'
"
class="side-descriptor"
>
<InfoIcon aria-hidden="true" />
Universal {{ projectTypeDisplay }}
</div>
<div
v-else-if="
(project.client_side === 'optional' ||
project.client_side === 'required') &&
(project.server_side === 'optional' ||
project.server_side === 'unsupported')
"
class="side-descriptor"
>
<InfoIcon aria-hidden="true" />
Client {{ projectTypeDisplay }}
</div>
<div
v-else-if="
(project.server_side === 'optional' ||
project.server_side === 'required') &&
(project.client_side === 'optional' ||
project.client_side === 'unsupported')
"
class="side-descriptor"
>
<InfoIcon aria-hidden="true" />
Server {{ projectTypeDisplay }}
</div>
</div>
<p class="description">
{{ project.description }}
</p>
@ -389,7 +397,7 @@
<nuxt-link
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}`"
}/version/${encodeURI(version.displayUrlEnding)}`"
class="top title-link"
>
{{ version.name }}
@ -398,15 +406,7 @@
v-if="version.game_versions.length > 0"
class="game-version item"
>
{{
version.loaders
.map((x) =>
x.toLowerCase() === 'modloader'
? 'ModLoader'
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ')
}}
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
{{ $formatVersion(version.game_versions) }}
</div>
<VersionBadge
@ -455,13 +455,25 @@
}}</a>
</div>
</div>
<div class="info">
<div
v-if="
project.project_type !== 'resourcepack' &&
projectTypeDisplay !== 'plugin'
"
class="info"
>
<div class="key">Client side</div>
<div class="value">
{{ project.client_side }}
</div>
</div>
<div class="info">
<div
v-if="
project.project_type !== 'resourcepack' &&
projectTypeDisplay !== 'plugin'
"
class="info"
>
<div class="key">Server side</div>
<div class="value">
{{ project.server_side }}
@ -647,7 +659,7 @@ export default {
Categories,
},
async asyncData(data) {
const projectTypes = ['mod', 'modpack']
const projectTypes = ['mod', 'modpack', 'resourcepack']
try {
if (
@ -706,6 +718,16 @@ export default {
project.body = (await data.$axios.get(project.body_url)).data
}
const loaders = []
versions.forEach((version) => {
version.loaders.forEach((loader) => {
if (!loaders.includes(loader)) {
loaders.push(loader)
}
})
})
return {
project,
versions,
@ -714,6 +736,7 @@ export default {
allMembers: members,
currentMember,
dependencies,
loaders,
}
} catch {
data.error({
@ -727,6 +750,10 @@ export default {
showKnownErrors: false,
}
},
fetch() {
this.versions = this.$computeVersions(this.versions)
this.featuredVersions = this.$computeVersions(this.featuredVersions)
},
head() {
return {
title: `${this.project.title} - ${
@ -779,6 +806,14 @@ export default {
],
}
},
computed: {
projectTypeDisplay() {
return this.$getProjectTypeForDisplay(
this.project.project_type,
this.loaders
)
},
},
methods: {
findPrimary(version) {
let file = version.files.find((x) => x.primary)
@ -1119,7 +1154,10 @@ export default {
.value {
width: 50%;
text-transform: capitalize;
&::first-letter {
text-transform: capitalize;
}
&.lowercase {
text-transform: none;

View File

@ -14,7 +14,7 @@
<nuxt-link
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}`"
}/version/${encodeURI(version.displayUrlEnding)}`"
>{{ version.name }}</nuxt-link
>
</h2>
@ -69,12 +69,6 @@ export default {
DownloadIcon,
VersionFilterControl,
},
data() {
return {
filteredVersions: this.versions,
}
},
auth: false,
props: {
project: {
type: Object,
@ -95,6 +89,12 @@ export default {
},
},
},
data() {
return {
filteredVersions: this.versions,
}
},
auth: false,
methods: {
updateVersions(updatedVersions) {
this.filteredVersions = updatedVersions

View File

@ -110,17 +110,13 @@
<Multiselect
id="categories"
v-model="newProject.categories"
:options="
$tag.categories
.filter((x) => x.project_type === project.project_type)
.map((it) => it.name)
"
:options="selectableCategories"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:loading="$tag.categories.length === 0"
:multiple="true"
:searchable="false"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-on-select="false"
@ -132,13 +128,46 @@
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
@input="setCategories"
/>
</label>
<label>
<span>
<h3>Additional Categories</h3>
<span class="no-padding">
Select up to 3 categories that will help others <br />
find your project.
</span>
</span>
<Multiselect
id="additional_categories"
v-model="newProject.additional_categories"
:options="selectableAdditionalCategories"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:loading="$tag.categories.length === 0"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-on-select="false"
:show-labels="false"
:max="255"
:limit="6"
:hide-selected="true"
placeholder="Choose additional categories"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
@input="setCategories"
/>
</label>
<label class="vertical-input">
<span>
<h3>Vanity URL (slug)<span class="required">*</span></h3>
<span class="slug-description"
>https://modrinth.com/{{ newProject.project_type.toLowerCase() }}/{{
>https://modrinth.com/{{ project.project_type.toLowerCase() }}/{{
newProject.slug ? newProject.slug : 'your-slug'
}}
</span>
@ -189,7 +218,10 @@
Reset
</button>
</section>
<section class="card game-sides">
<section
v-if="project.project_type !== 'resourcepack'"
class="card game-sides"
>
<div class="columns">
<div>
<h3>Supported environments</h3>
@ -241,7 +273,7 @@
for="body"
title="You can type an extended description of your project here."
>
Body<span class="required">*</span>
Description<span class="required">*</span>
</label>
</h3>
<span>
@ -500,6 +532,9 @@ export default {
donationPlatforms: [],
donationLinks: [],
selectableCategories: [],
selectableAdditionalCategories: [],
isProcessing: false,
previewImage: null,
compiledBody: '',
@ -544,6 +579,8 @@ export default {
this.serverSideType =
this.newProject.server_side.charAt(0) +
this.newProject.server_side.slice(1)
this.setCategories()
},
watch: {
license(newValue, oldValue) {
@ -584,6 +621,23 @@ export default {
this.DELETE_PROJECT = 1 << 7
},
methods: {
setCategories() {
this.selectableCategories = this.$tag.categories
.filter(
(x) =>
x.project_type === this.project.project_type &&
!this.newProject.additional_categories.includes(x.name)
)
.map((it) => it.name)
this.selectableAdditionalCategories = this.$tag.categories
.filter(
(x) =>
x.project_type === this.project.project_type &&
!this.newProject.categories.includes(x.name)
)
.map((it) => it.name)
},
checkFields() {
const reviewConditions =
this.newProject.body !== '' && this.newProject.versions.length > 0
@ -626,6 +680,7 @@ export default {
description: this.newProject.description,
body: this.newProject.body,
categories: this.newProject.categories,
additional_categories: this.newProject.additional_categories,
issues_url: this.newProject.issues_url
? this.newProject.issues_url
: null,

View File

@ -26,7 +26,7 @@
Back to list
</nuxt-link>
</div>
<div>
<div v-if="version">
<div v-if="mode === 'version'" class="version-header">
<h2>{{ version.name }}</h2>
@ -47,7 +47,7 @@
v-if="$auth.user"
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}`"
}/version/${encodeURI(version.displayUrlEnding)}`"
class="iconified-button"
>
<CrossIcon aria-hidden="true" />
@ -113,7 +113,7 @@
class="action iconified-button"
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}/edit`"
}/version/${encodeURI(version.displayUrlEnding)}/edit`"
@click.prevent="mode = 'edit'"
>
<EditIcon aria-hidden="true" />
@ -209,8 +209,8 @@
color="red"
/>
</div>
<div class="data">
<p class="title">Mod loaders</p>
<div v-if="project.project_type !== 'resourcepack'" class="data">
<p class="title">Loaders</p>
<multiselect
v-if="mode === 'edit' || mode === 'create'"
v-model="version.loaders"
@ -223,12 +223,7 @@
)
.map((it) => it.name)
"
:custom-label="
(value) =>
value === 'modloader'
? 'Risugami\'s ModLoader'
: value.charAt(0).toUpperCase() + value.slice(1)
"
:custom-label="(value) => $formatCategory(value)"
:loading="$tag.loaders.length === 0"
:multiple="true"
:searchable="false"
@ -238,18 +233,10 @@
:show-labels="false"
:limit="6"
:hide-selected="true"
placeholder="Choose mod loaders..."
placeholder="Choose loaders..."
/>
<p v-else class="value">
{{
version.loaders
.map((x) =>
x.toLowerCase() === 'modloader'
? "Risugami's ModLoader"
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ')
}}
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
</p>
</div>
<div v-if="mode === 'version'" class="data">
@ -365,7 +352,7 @@
dependency.project.slug
? dependency.project.slug
: dependency.project.id
}/version/${encodeURIComponent(
}/version/${encodeURI(
dependency.version.version_number
)}`
: `/${dependency.project.project_type}/${
@ -754,9 +741,18 @@ export default {
if (!this.version)
this.version = this.versions.find(
(x) => x.version_number === this.$route.params.version
(x) => x.displayUrlEnding === this.$route.params.version
)
if (!this.version) {
this.$nuxt.context.error({
statusCode: 404,
message: 'The page could not be found',
})
return
}
this.version = JSON.parse(JSON.stringify(this.version))
this.primaryFile =
this.version.files.find((file) => file.primary) ?? this.version.files[0]
@ -891,8 +887,9 @@ export default {
const index = this.versions.findIndex((x) => x.id === this.version.id)
editedVersions.splice(index, 1, version)
this.$emit('update:versions', editedVersions)
const newEditedVersions = this.$computeVersions(editedVersions)
this.$emit('update:versions', newEditedVersions)
this.$emit('update:featuredVersions', featuredVersions)
this.newFiles = []
@ -901,7 +898,7 @@ export default {
await this.$router.replace(
`/${this.project.project_type}/${
this.project.slug ? this.project.slug : this.project.id
}/version/${encodeURIComponent(this.version.version_number)}`
}/version/${encodeURI(newEditedVersions[index].displayUrlEnding)}`
)
} catch (err) {
this.$notify({
@ -921,6 +918,10 @@ export default {
const fileParts = this.newFiles.map((f, idx) => `${f.name}-${idx}`)
if (this.project.project_type === 'resourcepack') {
this.version.loaders = ['minecraft']
}
const newVersion = {
project_id: this.version.project_id,
file_parts: fileParts,
@ -958,15 +959,19 @@ export default {
).data
const newProject = JSON.parse(JSON.stringify(this.project))
newProject.versions = newProject.versions.concat([data])
newProject.versions = newProject.versions.concat([data.id])
const newVersions = this.$computeVersions(this.versions.concat([data]))
await this.$emit('update:project', newProject)
await this.$emit('update:versions', this.versions.concat([data]))
await this.$emit('update:versions', newVersions)
await this.$router.push(
`/${this.project.project_type}/${
this.project.slug ? this.project.slug : this.project.project_id
}/version/${encodeURIComponent(this.version.version_number)}`
}/version/${encodeURI(
newVersions[newVersions.length - 1].displayUrlEnding
)}`
)
} catch (err) {
this.$notify({

View File

@ -48,7 +48,7 @@
<nuxt-link
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}`"
}/version/${encodeURI(version.displayUrlEnding)}`"
>
{{ version.name }}
</nuxt-link>
@ -78,11 +78,7 @@
<p>
{{
version.loaders
.map((x) =>
x.toLowerCase() === 'modloader'
? 'ModLoader'
: x.charAt(0).toUpperCase() + x.slice(1)
)
.map((x) => $formatCategory(x))
.join(', ') +
' ' +
$formatVersion(version.game_versions)
@ -104,15 +100,7 @@
</td>
<td>
<p>
{{
version.loaders
.map((x) =>
x.toLowerCase() === 'modloader'
? 'ModLoader'
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ')
}}
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
</p>
<p>{{ $formatVersion(version.game_versions) }}</p>
</td>

View File

@ -54,16 +54,18 @@
<label>
<span>
<h3>Type<span class="required">*</span></h3>
<span class="no-padding">The type of project of your project.</span>
<span class="no-padding">The type of project your project is.</span>
</span>
<Multiselect
v-model="projectType"
placeholder="Select one"
label="display"
:options="projectTypes"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@input="setCategories(true)"
/>
</label>
<label>
@ -106,32 +108,59 @@
<multiselect
id="categories"
v-model="categories"
:options="
$tag.categories
.filter((x) => x.project_type === projectType.toLowerCase())
.map((it) => it.name)
"
:options="selectableCategories"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:loading="$tag.categories.length === 0"
:multiple="true"
:searchable="false"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-on-select="false"
:clear-on-select="true"
:show-labels="false"
:max="3"
:limit="6"
:hide-selected="true"
placeholder="Choose categories"
@input="setCategories(false)"
/>
</label>
<label>
<span>
<h3>Additional Categories</h3>
<span class="no-padding">
Select more categories that will help others <br />
find your project. These are searchable, but not <br />
displayed in search.
</span>
</span>
<multiselect
id="additional_categories"
v-model="additional_categories"
:show-no-results="false"
:options="selectableAdditionalCategories"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:loading="$tag.categories.length === 0"
:multiple="true"
:searchable="true"
:close-on-select="false"
:clear-on-select="true"
:show-labels="false"
:max="255"
:limit="6"
:hide-selected="true"
placeholder="Choose additional categories"
@input="setCategories(false)"
/>
</label>
<label>
<span>
<h3>Vanity URL (slug)<span class="required">*</span></h3>
<span class="slug-description"
>https://modrinth.com/{{ projectType.toLowerCase() }}/{{
>https://modrinth.com/{{ projectType.id }}/{{
slug ? slug : 'your-slug'
}}
</span>
@ -174,7 +203,13 @@
Reset
</button>
</section>
<section class="card game-sides">
<section
v-if="
projectType.realId !== 'resourcepack' &&
projectType.realId !== 'plugin'
"
class="card game-sides"
>
<div class="columns">
<div class="column">
<h3>Supported environments</h3>
@ -214,7 +249,7 @@
for="body"
title="You can type an extended description of your project here."
>
Body<span class="required">*</span>
Description<span class="required">*</span>
</label>
</h3>
<span>
@ -318,7 +353,7 @@
Your version must have a unique version number.
</li>
<li v-if="versions[currentVersionIndex].loaders.length < 1">
Your version must have the supported mod loaders selected.
Your version must have the supported loaders selected.
</li>
<li v-if="versions[currentVersionIndex].game_versions.length < 1">
Your version must have the supported Minecraft versions
@ -387,10 +422,10 @@
:allow-empty="false"
/>
</label>
<label>
<label v-if="projectType.realId !== 'resourcepack'">
<span>
<h3>Mod loaders<span class="required">*</span></h3>
<span> Select all mod loaders this version supports. </span>
<h3>Loaders<span class="required">*</span></h3>
<span> Select all loaders this version supports. </span>
</span>
<multiselect
v-model="versions[currentVersionIndex].loaders"
@ -401,19 +436,18 @@
}"
:options="
$tag.loaders
.filter((x) =>
x.supported_project_types.includes(
projectType.toLowerCase()
)
)
.filter((x) => {
if (projectType.realId === 'plugin') {
return $tag.loaderData.allPluginLoaders.includes(x.name)
} else if (projectType.realId === 'mod') {
return $tag.loaderData.modLoaders.includes(x.name)
}
return x.supported_project_types.includes(projectType.id)
})
.map((it) => it.name)
"
:custom-label="
(value) =>
value === 'modloader'
? 'Risugami\'s ModLoader'
: value.charAt(0).toUpperCase() + value.slice(1)
"
:custom-label="(value) => $formatCategory(value)"
:loading="$tag.loaders.length === 0"
:multiple="true"
:searchable="false"
@ -423,7 +457,7 @@
:show-labels="false"
:limit="6"
:hide-selected="true"
placeholder="Choose mod loaders..."
placeholder="Choose loaders..."
/>
</label>
<label>
@ -547,12 +581,9 @@
<h3>Files<span class="required">*</span></h3>
<span>
You may upload multiple files, but this should only be used for
cases like sources or Javadocs.
cases like sources or Javadocs for mods/plugins.
</span>
<p
v-if="projectType.toLowerCase() === 'modpack'"
aria-label="Warning"
>
<p v-if="projectType.id === 'modpack'" aria-label="Warning">
Modpack support is currently in alpha, and you may encounter
issues. Our documentation includes instructions on
<a
@ -578,9 +609,9 @@
class="file-input"
multiple
:accept="
projectType.toLowerCase() === 'modpack'
projectType.id === 'modpack'
? '.mrpack,application/x-modrinth-modpack+zip'
: projectType.toLowerCase() === 'mod'
: projectType.id === 'mod'
? '.jar,application/java-archive'
: '*'
"
@ -661,7 +692,7 @@
>
<th>Name</th>
<th>Version</th>
<th>Mod loaders</th>
<th>Project loaders</th>
<th>Minecraft versions</th>
<th>Release channel</th>
<th>Actions</th>
@ -1137,6 +1168,7 @@ export default {
body: '',
versions: [],
categories: [],
additional_categories: [],
issues_url: null,
source_url: null,
wiki_url: null,
@ -1145,8 +1177,41 @@ export default {
license: null,
license_url: null,
projectTypes: ['Mod', 'Modpack'],
projectType: 'Mod',
selectableCategories: [],
selectableAdditionalCategories: [],
projectTypes: [
{
display: 'Mod',
id: 'mod',
realId: 'mod',
},
{
display: 'Plugin',
id: 'mod',
realId: 'plugin',
},
{
display: 'Mod and plugin',
id: 'mod',
realId: 'mod+plugin',
},
{
display: 'Modpack',
id: 'modpack',
realId: 'modpack',
},
{
display: 'Resource pack',
id: 'resourcepack',
realId: 'resourcepack',
},
],
projectType: {
display: 'Mod',
id: 'mod',
realId: 'mod',
},
sideTypes: ['Required', 'Optional', 'Unsupported'],
clientSideType: 'Required',
@ -1171,6 +1236,9 @@ export default {
savingAsDraft: false,
}
},
fetch() {
this.setCategories()
},
watch: {
license(newValue, oldValue) {
if (newValue == null) {
@ -1199,6 +1267,28 @@ export default {
})
},
methods: {
setCategories(reset) {
this.selectableCategories = this.$tag.categories
.filter(
(x) =>
x.project_type === this.projectType.id &&
!this.additional_categories.includes(x.name)
)
.map((it) => it.name)
this.selectableAdditionalCategories = this.$tag.categories
.filter(
(x) =>
x.project_type === this.projectType.id &&
!this.categories.includes(x.name)
)
.map((it) => it.name)
if (reset) {
this.categories = []
this.additional_categories = []
}
},
checkFields() {
const reviewConditions = this.body !== '' && this.versions.length > 0
if (
@ -1221,6 +1311,7 @@ export default {
return false
},
async createDraft() {
this.setValues()
this.savingAsDraft = true
if (this.checkFields()) {
this.draft = true
@ -1228,11 +1319,21 @@ export default {
}
},
async createProjectForReview() {
this.setValues()
this.savingAsDraft = false
if (this.checkFields()) {
await this.createProject()
}
},
setValues() {
if (this.projectType.realId === 'resourcepack') {
this.clientSideType = 'required'
this.serverSideType = 'optional'
} else if (this.projectType.realId === 'plugin') {
this.clientSideType = 'unsupported'
this.serverSideType = 'required'
}
},
async createProject() {
this.$nuxt.$loading.start()
@ -1256,7 +1357,7 @@ export default {
'data',
JSON.stringify({
title: this.name,
project_type: this.projectType.toLowerCase(),
project_type: this.projectType.id,
slug: this.slug,
description: this.description,
body: this.body,
@ -1269,6 +1370,7 @@ export default {
},
],
categories: this.categories,
additional_categories: this.additional_categories,
issues_url: this.issues_url ? this.issues_url : null,
source_url: this.source_url ? this.source_url : null,
wiki_url: this.wiki_url ? this.wiki_url : null,
@ -1405,6 +1507,9 @@ export default {
saveVersion() {
const version = this.versions[this.currentVersionIndex]
if (this.projectType.realId === 'resourcepack') {
version.loaders = ['minecraft']
}
if (
version.version_number !== '' &&
version.releaseChannels !== null &&

View File

@ -89,6 +89,7 @@
:client-side="project.client_side"
:server-side="project.server_side"
:type="project.project_type"
:moderation="true"
>
<button
class="iconified-button"

View File

@ -76,8 +76,8 @@ import UpToDate from '~/assets/images/illustrations/up_to_date.svg?inline'
import ThisOrThat from '~/components/ui/ThisOrThat'
const NOTIFICATION_TYPES = {
'Team Invites': 'team_invite',
'Project Updates': 'project_update',
'Team invites': 'team_invite',
'Project updates': 'project_update',
}
export default {

View File

@ -34,28 +34,41 @@
Clear filters
</button>
<section aria-label="Category filters">
<h3
v-if="
$tag.categories.filter((x) => x.project_type === projectType)
.length > 0
"
class="sidebar-menu-heading"
>
Categories
</h3>
<SearchFilter
v-for="category in $tag.categories.filter(
(x) => x.project_type === projectType
)"
:key="category.name"
:active-filters="facets"
:display-name="category.name"
:facet-name="`categories:${category.name}`"
:icon="category.icon"
@toggle="toggleFacet"
/>
<div v-for="(categories, header) in categoriesMap" :key="header">
<h3
v-if="
categories.filter((x) => x.project_type === projectType)
.length > 0
"
class="sidebar-menu-heading"
>
{{ $formatCategoryHeader(header) }}
</h3>
<SearchFilter
v-for="category in categories
.filter((x) => x.project_type === projectType)
.sort((a, b) => {
if (header === 'resolutions') {
return (
a.name.replace(/\D/g, '') - b.name.replace(/\D/g, '')
)
}
return 0
})"
:key="category.name"
:active-filters="facets"
:display-name="$formatCategory(category.name)"
:facet-name="`categories:${category.name}`"
:icon="header === 'resolutions' ? null : category.icon"
@toggle="toggleFacet"
/>
</div>
</section>
<section aria-label="Loader filters">
<section
v-if="projectType !== 'resourcepack'"
aria-label="Loader filters"
>
<h3
v-if="
$tag.loaders.filter((x) =>
@ -69,6 +82,8 @@
<SearchFilter
v-for="loader in $tag.loaders.filter((x) => {
if (
projectType === 'mod' &&
!isPlugins &&
!showAllLoaders &&
x.name !== 'forge' &&
x.name !== 'fabric' &&
@ -76,18 +91,25 @@
) {
return false
}
return x.supported_project_types.includes(projectType)
if (projectType === 'mod' && showAllLoaders) {
return $tag.loaderData.modLoaders.includes(x.name)
}
return isPlugins
? $tag.loaderData.pluginLoaders.includes(x.name)
: x.supported_project_types.includes(projectType)
})"
:key="loader.name"
ref="loaderFilters"
:active-filters="orFacets"
:display-name="
loader.name === 'modloader' ? 'ModLoader' : loader.name
"
:display-name="$formatCategory(loader.name)"
:facet-name="`categories:${loader.name}`"
:icon="loader.icon"
@toggle="toggleOrFacet"
/>
<Checkbox
v-if="projectType === 'mod' && !isPlugins"
v-model="showAllLoaders"
:label="showAllLoaders ? 'Less' : 'More'"
description="Show all loaders"
@ -96,7 +118,34 @@
:collapsing-toggle-style="true"
/>
</section>
<section aria-label="Environment filters">
<section v-if="isPlugins" aria-label="Platform loader filters">
<h3
v-if="
$tag.loaders.filter((x) =>
x.supported_project_types.includes(projectType)
).length > 0
"
class="sidebar-menu-heading"
>
Proxies
</h3>
<SearchFilter
v-for="loader in $tag.loaders.filter((x) =>
$tag.loaderData.pluginPlatformLoaders.includes(x.name)
)"
:key="loader.name"
ref="platformFilters"
:active-filters="orFacets"
:display-name="$formatCategory(loader.name)"
:facet-name="`categories:${loader.name}`"
:icon="loader.icon"
@toggle="toggleOrFacet"
/>
</section>
<section
v-if="projectType !== 'resourcepack' && !isPlugins"
aria-label="Environment filters"
>
<h3 class="sidebar-menu-heading">Environments</h3>
<SearchFilter
:active-filters="selectedEnvironments"
@ -267,7 +316,7 @@
:icon-url="result.icon_url"
:client-side="result.client_side"
:server-side="result.server_side"
:categories="result.categories"
:categories="result.display_categories"
/>
<div v-if="results && results.length === 0" class="no-results">
<p>No results found for your query!</p>
@ -336,6 +385,7 @@ export default {
currentPage: 1,
projectType: 'mod',
isPlugins: false,
sortTypes: [
{ display: 'Relevance', name: 'relevance' },
@ -405,8 +455,39 @@ export default {
this.$route.name.length - 1
)
if (this.projectType === 'plugin') {
this.projectType = 'mod'
this.isPlugins = true
}
await this.onSearchChange(this.currentPage)
},
computed: {
categoriesMap() {
const categories = {}
for (const category of this.$tag.categories) {
if (categories[category.header]) {
categories[category.header].push(category)
} else {
categories[category.header] = [category]
}
}
const newVals = Object.keys(categories)
.sort()
.reduce((obj, key) => {
obj[key] = categories[key]
return obj
}, {})
for (const header of Object.keys(categories)) {
newVals[header].sort((a, b) => a.name.localeCompare(b.name))
}
return newVals
},
},
watch: {
'$route.path': {
async handler() {
@ -415,12 +496,21 @@ export default {
this.$route.name.length - 1
)
if (this.projectType === 'plugin') {
this.projectType = 'mod'
this.isPlugins = true
} else {
this.isPlugins = false
}
this.results = null
this.pages = []
this.currentPage = 1
this.query = ''
this.maxResults = 20
this.sortType = { display: 'Relevance', name: 'relevance' }
this.showAllLoaders = false
this.sidebarMenuOpen = false
await this.clearFilters()
},
@ -463,6 +553,18 @@ export default {
if (index !== -1) {
this.orFacets.splice(index, 1)
} else {
if (elementName === 'categories:purpur') {
this.orFacets.push('categories:paper')
this.orFacets.push('categories:spigot')
this.orFacets.push('categories:bukkit')
} else if (elementName === 'categories:paper') {
this.orFacets.push('categories:spigot')
this.orFacets.push('categories:bukkit')
} else if (elementName === 'categories:spigot') {
this.orFacets.push('categories:bukkit')
} else if (elementName === 'categories:waterfall') {
this.orFacets.push('categories:bungeecord')
}
this.orFacets.push(elementName)
}
@ -498,6 +600,7 @@ export default {
if (
this.facets.length > 0 ||
this.orFacets.length > 0 ||
this.selectedVersions.length > 0 ||
this.selectedEnvironments.length > 0 ||
this.projectType
@ -507,8 +610,19 @@ export default {
formattedFacets.push([facet])
}
// loaders specifier
if (this.orFacets.length > 0) {
formattedFacets.push(this.orFacets)
} else if (this.isPlugins) {
formattedFacets.push(
this.$tag.loaderData.allPluginLoaders.map(
(x) => `categories:${x}`
)
)
} else if (this.projectType === 'mod') {
formattedFacets.push(
this.$tag.loaderData.modLoaders.map((x) => `categories:${x}`)
)
}
if (this.selectedVersions.length > 0) {

View File

@ -5,9 +5,6 @@
<script>
export default {
name: 'Modpacks',
asyncData(ctx) {
ctx.params.projectType = 'modpack'
},
head: {
title: 'Modpacks - Modrinth',
meta: [

View File

@ -5,9 +5,6 @@
<script>
export default {
name: 'Mods',
asyncData(ctx) {
ctx.params.projectType = 'mod'
},
head: {
title: 'Mods - Modrinth',
meta: [

31
pages/search/plugins.vue Normal file
View File

@ -0,0 +1,31 @@
<template>
<div></div>
</template>
<script>
export default {
name: 'Plugins',
head: {
title: 'Plugins - Modrinth',
meta: [
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: 'Plugins - Modrinth',
},
{
hid: 'og:title',
name: 'og:title',
content: 'Plugins - Modrinth',
},
{
hid: 'og:url',
name: 'og:url',
content: `https://modrinth.com/plugins`,
},
],
},
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,31 @@
<template>
<div></div>
</template>
<script>
export default {
name: 'ResourcePacks',
head: {
title: 'Resource packs - Modrinth',
meta: [
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: 'Resource packs - Modrinth',
},
{
hid: 'og:title',
name: 'og:title',
content: 'Resource packs - Modrinth',
},
{
hid: 'og:url',
name: 'og:url',
content: `https://modrinth.com/resourcepacks`,
},
],
},
}
</script>
<style lang="scss" scoped></style>

View File

@ -204,6 +204,9 @@ export default {
display: flex;
flex-direction: row;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: flex-end;
}
}
</style>

View File

@ -90,7 +90,8 @@
<ProjectCard
v-for="project in selectedProjectType !== 'all'
? projects.filter(
(x) => x.project_type === selectedProjectType.slice(0, -1)
(x) =>
x.project_type === convertProjectType(selectedProjectType)
)
: projects"
:id="project.slug || project.id"
@ -246,13 +247,24 @@ export default {
const obj = { all: true }
for (const project of this.projects) {
obj[project.project_type + 's'] = true
if (project.project_type === 'resourcepack') {
obj['Resource Packs'] = true
} else {
obj[project.project_type + 's'] = true
}
}
return Object.keys(obj)
},
},
methods: {
convertProjectType(name) {
if (name === 'Resource Packs') {
return 'resourcepack'
} else {
return name.slice(0, -1)
}
},
sumDownloads() {
let sum = 0

View File

@ -1,7 +1,7 @@
export default ({ store }, inject) => {
inject('user', store.state.user)
inject('tag', store.state.tag)
inject('auth', store.state.auth)
export default (ctx, inject) => {
inject('user', ctx.store.state.user)
inject('tag', ctx.store.state.tag)
inject('auth', ctx.store.state.auth)
inject('defaultHeaders', () => {
const obj = { headers: {} }
@ -9,100 +9,143 @@ export default ({ store }, inject) => {
obj.headers['x-ratelimit-key'] = process.env.RATE_LIMIT_IGNORE_KEY || ''
}
if (store.state.auth.user) {
obj.headers.Authorization = store.state.auth.token
if (ctx.store.state.auth.user) {
obj.headers.Authorization = ctx.store.state.auth.token
}
return obj
})
inject('formatNumber', formatNumber)
inject('formatVersion', (versionArray) => {
const allVersions = store.state.tag.gameVersions.slice().reverse()
const allReleases = allVersions.filter((x) => x.version_type === 'release')
inject('formatVersion', (versionsArray) =>
formatVersions(versionsArray, ctx.store)
)
inject('formatBytes', formatBytes)
inject('formatProjectType', formatProjectType)
inject('formatCategory', formatCategory)
inject('formatCategoryHeader', formatCategoryHeader)
inject('computeVersions', (versions) => {
const versionsMap = {}
const intervals = []
let currentInterval = 0
for (let i = 0; i < versionArray.length; i++) {
const index = allVersions.findIndex((x) => x.version === versionArray[i])
const releaseIndex = allReleases.findIndex(
(x) => x.version === versionArray[i]
)
if (i === 0) {
intervals.push([[versionArray[i], index, releaseIndex]])
for (const version of versions.reverse()) {
if (versionsMap[version.version_number]) {
versionsMap[version.version_number].push(version)
} else {
const intervalBase = intervals[currentInterval]
if (
(index - intervalBase[intervalBase.length - 1][1] === 1 ||
releaseIndex - intervalBase[intervalBase.length - 1][2] === 1) &&
(allVersions[intervalBase[0][1]].version_type === 'release' ||
allVersions[index].version_type !== 'release')
) {
intervalBase[1] = [versionArray[i], index, releaseIndex]
} else {
currentInterval += 1
intervals[currentInterval] = [[versionArray[i], index, releaseIndex]]
}
versionsMap[version.version_number] = [version]
}
}
const newIntervals = []
for (let i = 0; i < intervals.length; i++) {
const interval = intervals[i]
const returnVersions = []
if (
interval.length === 2 &&
interval[0][2] !== -1 &&
interval[1][2] === -1
) {
let lastSnapshot = null
for (let j = interval[1][1]; j > interval[0][1]; j--) {
if (allVersions[j].version_type === 'release') {
newIntervals.push([
interval[0],
[
allVersions[j].version,
j,
allReleases.findIndex(
(x) => x.version === allVersions[j].version
),
],
])
for (const id in versionsMap) {
const versions = versionsMap[id]
if (lastSnapshot !== null && lastSnapshot !== j + 1) {
newIntervals.push([
[allVersions[lastSnapshot].version, lastSnapshot, -1],
interval[1],
])
if (versions.length === 1) {
versions[0].displayUrlEnding = versions[0].version_number
returnVersions.push(versions[0])
} else {
const reservedNames = {}
const seenLoaders = {}
const duplicateLoaderIndexes = []
for (let i = 0; i < versions.length; i++) {
const version = versions[i]
const value = version.loaders.join('+')
if (seenLoaders[value]) {
duplicateLoaderIndexes.push(i)
} else {
if (i !== 0) {
version.displayUrlEnding = `${version.version_number}-${value}`
} else {
newIntervals.push([interval[1]])
version.displayUrlEnding = version.version_number
}
break
} else {
lastSnapshot = j
reservedNames[version.displayUrlEnding] = true
version.displayName = version.loaders
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
.join(', ')
returnVersions.push(version)
seenLoaders[value] = true
}
}
} else {
newIntervals.push(interval)
const seenGameVersions = {}
const duplicateGameVersionIndexes = []
for (const i of duplicateLoaderIndexes) {
const version = versions[i]
const value = version.game_versions.join('+')
if (seenGameVersions[value]) {
duplicateGameVersionIndexes.push(i)
} else {
if (i !== 0) {
let setDisplayUrl = false
for (const gameVersion in version.game_versions) {
const displayUrlEnding = `${version.version_number}-${gameVersion}`
if (!reservedNames[version.version_number]) {
version.displayUrlEnding = displayUrlEnding
reservedNames[displayUrlEnding] = true
setDisplayUrl = true
break
}
}
if (!setDisplayUrl) {
version.displayUrlEnding = `${version.version_number}-${value}`
}
} else if (!reservedNames[version.version_number]) {
version.displayUrlEnding = version.version_number
reservedNames[version.version_number] = true
}
version.displayName = formatVersions(
version.game_versions,
ctx.store
)
returnVersions.push(version)
seenGameVersions[value] = true
}
}
for (const i in duplicateGameVersionIndexes) {
const version = versions[i]
version.displayUrlEnding = version.id
version.displayName = version.id
returnVersions.push(version)
}
}
}
const output = []
for (const interval of newIntervals) {
if (interval.length === 2) {
output.push(`${interval[0][0]}${interval[1][0]}`)
} else {
output.push(interval[0][0])
}
}
return output.join(', ')
return returnVersions.reverse()
})
inject('getProjectTypeForDisplay', (type, categories) => {
if (type === 'mod') {
const isPlugin = categories.some((category) => {
return ctx.store.state.tag.loaderData.allPluginLoaders.includes(
category
)
})
const isMod = categories.some((category) => {
return ctx.store.state.tag.loaderData.modLoaders.includes(category)
})
return isPlugin && isMod ? 'mod and plugin' : isPlugin ? 'plugin' : 'mod'
} else {
return formatProjectType(type)
}
})
inject('formatBytes', formatBytes)
}
export const formatNumber = (number) => {
@ -127,3 +170,126 @@ export const formatBytes = (bytes, decimals = 2) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}
export const formatProjectType = (name) => {
if (name === 'resourcepack') {
return 'resource pack'
}
return name.charAt(0).toUpperCase() + name.slice(1)
}
export const formatCategory = (name) => {
if (name === 'modloader') {
return "Risugami's ModLoader"
} else if (name === 'bungeecord') {
return 'BungeeCord'
} else if (name === 'liteloader') {
return 'LiteLoader'
} else if (name === 'game-mechanics') {
return 'Game Mechanics'
} else if (name === 'worldgen') {
return 'World Generation'
} else if (name === 'core-shaders') {
return 'Core Shaders'
} else if (name === 'gui') {
return 'GUI'
} else if (name === '8x-') {
return '8x or lower'
} else if (name === '512x+') {
return '512x or higher'
} else if (name === 'kitchen-sink') {
return 'Kitchen Sink'
}
return name.charAt(0).toUpperCase() + name.slice(1)
}
export const formatCategoryHeader = (name) => {
return name.charAt(0).toUpperCase() + name.slice(1)
}
export const formatVersions = (versionArray, store) => {
const allVersions = store.state.tag.gameVersions.slice().reverse()
const allReleases = allVersions.filter((x) => x.version_type === 'release')
const intervals = []
let currentInterval = 0
for (let i = 0; i < versionArray.length; i++) {
const index = allVersions.findIndex((x) => x.version === versionArray[i])
const releaseIndex = allReleases.findIndex(
(x) => x.version === versionArray[i]
)
if (i === 0) {
intervals.push([[versionArray[i], index, releaseIndex]])
} else {
const intervalBase = intervals[currentInterval]
if (
(index - intervalBase[intervalBase.length - 1][1] === 1 ||
releaseIndex - intervalBase[intervalBase.length - 1][2] === 1) &&
(allVersions[intervalBase[0][1]].version_type === 'release' ||
allVersions[index].version_type !== 'release')
) {
intervalBase[1] = [versionArray[i], index, releaseIndex]
} else {
currentInterval += 1
intervals[currentInterval] = [[versionArray[i], index, releaseIndex]]
}
}
}
const newIntervals = []
for (let i = 0; i < intervals.length; i++) {
const interval = intervals[i]
if (
interval.length === 2 &&
interval[0][2] !== -1 &&
interval[1][2] === -1
) {
let lastSnapshot = null
for (let j = interval[1][1]; j > interval[0][1]; j--) {
if (allVersions[j].version_type === 'release') {
newIntervals.push([
interval[0],
[
allVersions[j].version,
j,
allReleases.findIndex(
(x) => x.version === allVersions[j].version
),
],
])
if (lastSnapshot !== null && lastSnapshot !== j + 1) {
newIntervals.push([
[allVersions[lastSnapshot].version, lastSnapshot, -1],
interval[1],
])
} else {
newIntervals.push([interval[1]])
}
break
} else {
lastSnapshot = j
}
}
} else {
newIntervals.push(interval)
}
}
const output = []
for (const interval of newIntervals) {
if (interval.length === 2) {
output.push(`${interval[0][0]}${interval[1][0]}`)
} else {
output.push(interval[0][0])
}
}
return output.join(', ')
}

View File

@ -1,5 +1,8 @@
import xss from 'xss'
/**
* @type {import('xss').IFilterXSSOptions}
*/
const options = {
whiteList: {
...xss.whiteList,
@ -12,6 +15,12 @@ const options = {
h6: ['id'],
input: ['checked', 'disabled', 'type'],
iframe: ['width', 'height', 'allowfullscreen', 'frameborder'],
img: [...xss.whiteList.img, 'style'],
},
css: {
whiteList: {
'image-rendering': /^pixelated$/,
},
},
onIgnoreTagAttr: (tag, name, value) => {
// Allow iframes from acceptable sources

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -4,6 +4,21 @@ export const state = () => ({
gameVersions: [],
licenses: [],
donationPlatforms: [],
loaderData: {
pluginLoaders: ['bukkit', 'spigot', 'paper', 'purpur', 'sponge'],
pluginPlatformLoaders: ['bungeecord', 'waterfall', 'velocity'],
allPluginLoaders: [
'bukkit',
'spigot',
'paper',
'purpur',
'sponge',
'bungeecord',
'waterfall',
'velocity',
],
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift'],
},
})
export const mutations = {