You've already forked AstralRinth
forked from didirus/AstralRinth
Project, Search, User redesign (#1281)
* New project page * fix silly icon tailwind classes * Start new versions page, add new ButtonStyled component * Pagination and finish mocking up versions page functionality * green download button * hover animation * New Modal, Avatar refactor, subpages in NavTabs * lint * Download modal * New user page + fix lint * fix ui lint * Download animation fix * Versions filter + finish project page * Improve consistency of buttons on home page * Fix ButtonStyled breaking * Fix margin on version summary * finish search, new modals, user + project page mobile * fix gallery image pages * New project header * Fix gallery tab showing improperly * Use auto direction + position for all popouts * Preliminary user page * test to see if this fixes login stuff * remove extra slash * Add version actions, move download button on versions page * Listed -> public * Shorten download modal selector height * Fix user menu open direction * Change breakpoint for header collapse * Only underline title * Tighten padding on stats a little * New nav * Make mobile breakpoint more consistent * fix header breakpoint regression * Add sign in button * Fix edit icon color * Fix margin at top of screen * Fix user bios and ad width * Fix user nav showing when there's only one type of project * Fix plural projects on user page & extract i18n * Remove ads on mobile for now * Fix overflow menu showing hidden items * NavTabs on mobile * Fix navbar z index * Search filter overhaul + negative filters * fix no-max-height * port version filters, fix following/collections, lint * hide promos * ui lint * Disable modal background animation to reduce reported motion sickness * Hide install with modrinth app button on mobile --------- Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Prospector <prospectordev@gmail.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,5 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<VersionFilterControl :versions="props.versions" />
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
class="pagination-before"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
<div class="card changelog-wrapper">
|
||||
<div
|
||||
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
||||
@@ -60,17 +52,23 @@
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
class="pagination-before"
|
||||
class="mb-2 flex justify-end"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
</div>
|
||||
<div class="normal-page__sidebar">
|
||||
<AdPlaceholder />
|
||||
<VersionFilterControl :versions="props.versions" @switch-page="switchPage" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import DownloadIcon from "~/assets/images/utils/download.svg?component";
|
||||
import { Pagination } from "@modrinth/ui";
|
||||
import { DownloadIcon } from "@modrinth/assets";
|
||||
|
||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
|
||||
import Pagination from "~/components/ui/Pagination.vue";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
@@ -106,11 +104,11 @@ useSeoMeta({
|
||||
const router = useNativeRouter();
|
||||
const route = useNativeRoute();
|
||||
|
||||
const currentPage = ref(Number(route.query.p ?? 1));
|
||||
const currentPage = ref(Number(route.query.page ?? 1));
|
||||
const filteredVersions = computed(() => {
|
||||
const selectedGameVersions = getArrayOrString(route.query.g) ?? [];
|
||||
const selectedLoaders = getArrayOrString(route.query.l) ?? [];
|
||||
const selectedVersionTypes = getArrayOrString(route.query.c) ?? [];
|
||||
const selectedGameVersions = getArrayOrString(route.query.gameVersion) ?? [];
|
||||
const selectedLoaders = getArrayOrString(route.query.platform) ?? [];
|
||||
const selectedVersionTypes = getArrayOrString(route.query.type) ?? [];
|
||||
|
||||
return props.versions.filter(
|
||||
(projectVersion) =>
|
||||
@@ -131,7 +129,7 @@ function switchPage(page) {
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
p: currentPage.value !== 1 ? currentPage.value : undefined,
|
||||
page: currentPage.value !== 1 ? currentPage.value : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -250,4 +248,8 @@ function switchPage(page) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.brand-button {
|
||||
color: var(--color-accent-contrast);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -794,4 +794,8 @@ export default defineNuxtComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.brand-button {
|
||||
color: var(--color-accent-contrast);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,526 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="project.body"
|
||||
class="markdown-body card"
|
||||
v-html="renderHighlightedString(project.body || '')"
|
||||
/>
|
||||
<NewModal ref="modalLicense" :header="project.license.name ? project.license.name : 'License'">
|
||||
<template #title>
|
||||
<Avatar :src="project.icon_url" :alt="project.title" class="icon" size="32px" no-shadow />
|
||||
<span class="text-lg font-extrabold text-contrast">
|
||||
{{ project.license.name ? project.license.name : "License" }}
|
||||
</span>
|
||||
</template>
|
||||
<div
|
||||
class="markdown-body"
|
||||
v-html="
|
||||
renderString(licenseText).isEmpty ? 'Loading license text...' : renderString(licenseText)
|
||||
"
|
||||
/>
|
||||
</NewModal>
|
||||
<section class="normal-page__content">
|
||||
<div
|
||||
v-if="project.body"
|
||||
class="markdown-body card"
|
||||
v-html="renderHighlightedString(project.body || '')"
|
||||
/>
|
||||
</section>
|
||||
<div class="normal-page__sidebar">
|
||||
<AdPlaceholder />
|
||||
<div v-if="versions.length > 0" class="card flex-card experimental-styles-within">
|
||||
<h2>{{ formatMessage(compatibilityMessages.title) }}</h2>
|
||||
<section>
|
||||
<h3>{{ formatMessage(compatibilityMessages.minecraftJava) }}</h3>
|
||||
<div class="tag-list">
|
||||
<div
|
||||
v-for="version in getVersionsToDisplay(project)"
|
||||
:key="`version-tag-${version}`"
|
||||
class="tag-list__item"
|
||||
>
|
||||
{{ version }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3>{{ formatMessage(compatibilityMessages.platforms) }}</h3>
|
||||
<div class="tag-list">
|
||||
<div
|
||||
v-for="platform in project.loaders"
|
||||
:key="`platform-tag-${platform}`"
|
||||
:class="`tag-list__item`"
|
||||
:style="`--_color: var(--color-platform-${platform})`"
|
||||
>
|
||||
<svg v-html="tags.loaders.find((x) => x.name === platform).icon"></svg>
|
||||
{{ formatCategory(platform) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
|
||||
<div class="status-list">
|
||||
<div class="status-list__item status-list__item--color-green">
|
||||
<CheckIcon />
|
||||
Singleplayer
|
||||
</div>
|
||||
<div
|
||||
v-if="project.client_side !== 'unsupported' && project.server_side !== 'unsupported'"
|
||||
class="status-list__item status-list__item--color-green"
|
||||
>
|
||||
<CheckIcon />
|
||||
Client and server
|
||||
</div>
|
||||
<div
|
||||
v-if="project.client_side === 'required' && project.server_side === 'unsupported'"
|
||||
class="status-list__item status-list__item--color-green"
|
||||
>
|
||||
<CheckIcon />
|
||||
Client
|
||||
</div>
|
||||
<div
|
||||
v-if="project.server_side === 'required' && project.client_side === 'unsupported'"
|
||||
class="status-list__item status-list__item--color-green"
|
||||
>
|
||||
<CheckIcon />
|
||||
Server
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
project.client_side === 'optional' ||
|
||||
(project.client_side === 'required' && project.server_side === 'optional')
|
||||
"
|
||||
class="status-list__item status-list__item--color-orange"
|
||||
>
|
||||
<CheckIcon />
|
||||
Client <span class="text-sm">(Limited functionality)</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
project.server_side === 'optional' ||
|
||||
(project.server_side === 'required' && project.client_side === 'optional')
|
||||
"
|
||||
class="status-list__item status-list__item--color-orange"
|
||||
>
|
||||
<CheckIcon />
|
||||
Server <span class="text-sm">(Limited functionality)</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="project.client_side === 'unsupported'"
|
||||
class="status-list__item status-list__item--color-red"
|
||||
>
|
||||
<XIcon />
|
||||
Client
|
||||
</div>
|
||||
<div
|
||||
v-if="project.server_side === 'unsupported'"
|
||||
class="status-list__item status-list__item--color-red"
|
||||
>
|
||||
<XIcon />
|
||||
Server
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
project.issues_url ||
|
||||
project.source_url ||
|
||||
project.wiki_url ||
|
||||
project.discord_url ||
|
||||
project.donation_urls.length > 0
|
||||
"
|
||||
class="card flex-card experimental-styles-within"
|
||||
>
|
||||
<h2>{{ formatMessage(linksMessages.title) }}</h2>
|
||||
<div class="links-list">
|
||||
<a
|
||||
v-if="project.issues_url"
|
||||
:href="project.issues_url"
|
||||
:target="$external()"
|
||||
rel="noopener nofollow ugc"
|
||||
>
|
||||
<IssuesIcon aria-hidden="true" />
|
||||
{{ formatMessage(linksMessages.issues) }}
|
||||
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||
</a>
|
||||
<a
|
||||
v-if="project.source_url"
|
||||
:href="project.source_url"
|
||||
:target="$external()"
|
||||
rel="noopener nofollow ugc"
|
||||
>
|
||||
<CodeIcon aria-hidden="true" />
|
||||
{{ formatMessage(linksMessages.source) }}
|
||||
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||
</a>
|
||||
<a
|
||||
v-if="project.wiki_url"
|
||||
:href="project.wiki_url"
|
||||
:target="$external()"
|
||||
rel="noopener nofollow ugc"
|
||||
>
|
||||
<WikiIcon aria-hidden="true" />
|
||||
{{ formatMessage(linksMessages.wiki) }}
|
||||
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||
</a>
|
||||
<a
|
||||
v-if="project.discord_url"
|
||||
:href="project.discord_url"
|
||||
:target="$external()"
|
||||
rel="noopener nofollow ugc"
|
||||
>
|
||||
<DiscordIcon class="shrink" aria-hidden="true" />
|
||||
{{ formatMessage(linksMessages.discord) }}
|
||||
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||
</a>
|
||||
<hr
|
||||
v-if="
|
||||
(project.issues_url || project.source_url || project.wiki_url || project.discord_url) &&
|
||||
project.donation_urls.length > 0
|
||||
"
|
||||
/>
|
||||
<a
|
||||
v-for="(donation, index) in project.donation_urls"
|
||||
:key="index"
|
||||
:href="donation.url"
|
||||
:target="$external()"
|
||||
rel="noopener nofollow ugc"
|
||||
>
|
||||
<BuyMeACoffeeIcon v-if="donation.id === 'bmac'" aria-hidden="true" />
|
||||
<PatreonIcon v-else-if="donation.id === 'patreon'" aria-hidden="true" />
|
||||
<KoFiIcon v-else-if="donation.id === 'ko-fi'" aria-hidden="true" />
|
||||
<PayPalIcon v-else-if="donation.id === 'paypal'" aria-hidden="true" />
|
||||
<OpenCollectiveIcon v-else-if="donation.id === 'open-collective'" aria-hidden="true" />
|
||||
<HeartIcon v-else-if="donation.id === 'github'" />
|
||||
<CurrencyIcon v-else />
|
||||
<span v-if="donation.id === 'bmac'">{{ formatMessage(linksMessages.donateBmac) }}</span>
|
||||
<span v-else-if="donation.id === 'patreon'">{{
|
||||
formatMessage(linksMessages.donatePatreon)
|
||||
}}</span>
|
||||
<span v-else-if="donation.id === 'paypal'">{{
|
||||
formatMessage(linksMessages.donatePayPal)
|
||||
}}</span>
|
||||
<span v-else-if="donation.id === 'ko-fi'">{{
|
||||
formatMessage(linksMessages.donateKoFi)
|
||||
}}</span>
|
||||
<span v-else-if="donation.id === 'github'">{{
|
||||
formatMessage(linksMessages.donateGithub)
|
||||
}}</span>
|
||||
<span v-else>{{ formatMessage(linksMessages.donateGeneric) }}</span>
|
||||
<ExternalIcon aria-hidden="true" class="external-icon" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card flex-card experimental-styles-within">
|
||||
<h2>{{ formatMessage(creatorsMessages.title) }}</h2>
|
||||
<div class="details-list">
|
||||
<template v-if="organization">
|
||||
<nuxt-link
|
||||
class="details-list__item details-list__item--type-large"
|
||||
:to="`/organization/${organization.slug}`"
|
||||
>
|
||||
<Avatar :src="organization.icon_url" :alt="organization.name" size="32px" />
|
||||
<div class="rows">
|
||||
<span>
|
||||
{{ organization.name }}
|
||||
</span>
|
||||
<span class="details-list__item__text--style-secondary">Organization</span>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
<hr v-if="members.length > 0" />
|
||||
</template>
|
||||
<nuxt-link
|
||||
v-for="member in members"
|
||||
:key="`member-${member.id}`"
|
||||
class="details-list__item details-list__item--type-large"
|
||||
:to="'/user/' + member.user.username"
|
||||
>
|
||||
<Avatar :src="member.avatar_url" :alt="member.name" size="32px" circle />
|
||||
<div class="rows">
|
||||
<span class="flex items-center gap-1">
|
||||
{{ member.name }}
|
||||
<CrownIcon
|
||||
v-if="member.is_owner"
|
||||
v-tooltip="formatMessage(creatorsMessages.owner)"
|
||||
class="text-brand-orange"
|
||||
/>
|
||||
</span>
|
||||
<span class="details-list__item__text--style-secondary">{{ member.role }}</span>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card flex-card experimental-styles-within">
|
||||
<h2>{{ formatMessage(detailsMessages.title) }}</h2>
|
||||
<div class="details-list">
|
||||
<div class="details-list__item">
|
||||
<BookTextIcon aria-hidden="true" />
|
||||
<div>
|
||||
Licensed
|
||||
<a
|
||||
v-if="project.license.url"
|
||||
class="text-link hover:underline"
|
||||
:href="project.license.url"
|
||||
:target="$external()"
|
||||
rel="noopener nofollow ugc"
|
||||
>
|
||||
{{ licenseIdDisplay }}
|
||||
<ExternalIcon aria-hidden="true" class="external-icon ml-1 mt-[-1px] inline" />
|
||||
</a>
|
||||
<span
|
||||
v-else-if="
|
||||
project.license.id === 'LicenseRef-All-Rights-Reserved' ||
|
||||
!project.license.id.includes('LicenseRef')
|
||||
"
|
||||
class="text-link hover:underline"
|
||||
@click="(event) => getLicenseData(event)"
|
||||
>
|
||||
{{ licenseIdDisplay }}
|
||||
</span>
|
||||
<span v-else>{{ licenseIdDisplay }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="project.approved"
|
||||
v-tooltip="$dayjs(project.approved).format('MMMM D, YYYY [at] h:mm A')"
|
||||
class="details-list__item"
|
||||
>
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{ formatMessage(detailsMessages.published, { date: publishedDate }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
|
||||
class="details-list__item"
|
||||
>
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{ formatMessage(detailsMessages.created, { date: createdDate }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="project.status === 'processing' && project.queued"
|
||||
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')"
|
||||
class="details-list__item"
|
||||
>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{ formatMessage(detailsMessages.submitted, { date: submittedDate }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="versions.length > 0 && project.updated"
|
||||
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
|
||||
class="details-list__item"
|
||||
>
|
||||
<VersionIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{ formatMessage(detailsMessages.updated, { date: updatedDate }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||
<script setup>
|
||||
import {
|
||||
CheckIcon,
|
||||
XIcon,
|
||||
CalendarIcon,
|
||||
IssuesIcon,
|
||||
WikiIcon,
|
||||
OpenCollectiveIcon,
|
||||
DiscordIcon,
|
||||
ScaleIcon,
|
||||
KoFiIcon,
|
||||
BookTextIcon,
|
||||
PayPalIcon,
|
||||
CrownIcon,
|
||||
BuyMeACoffeeIcon,
|
||||
CurrencyIcon,
|
||||
PatreonIcon,
|
||||
HeartIcon,
|
||||
VersionIcon,
|
||||
ExternalIcon,
|
||||
CodeIcon,
|
||||
} from "@modrinth/assets";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
import { NewModal, Avatar } from "@modrinth/ui";
|
||||
|
||||
import { formatCategory, renderString } from "@modrinth/utils";
|
||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||
import { getVersionsToDisplay } from "~/helpers/projects.js";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
methods: { renderHighlightedString },
|
||||
});
|
||||
|
||||
const tags = useTags();
|
||||
const { formatMessage } = useVIntl();
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
|
||||
const compatibilityMessages = defineMessages({
|
||||
title: {
|
||||
id: "project.about.compatibility.title",
|
||||
defaultMessage: "Compatibility",
|
||||
},
|
||||
minecraftJava: {
|
||||
id: "project.about.compatibility.game.minecraftJava",
|
||||
defaultMessage: "Minecraft: Java Edition",
|
||||
},
|
||||
platforms: {
|
||||
id: "project.about.compatibility.platforms",
|
||||
defaultMessage: "Platforms",
|
||||
},
|
||||
environments: {
|
||||
id: "project.about.compatibility.environments",
|
||||
defaultMessage: "Environments",
|
||||
},
|
||||
});
|
||||
const linksMessages = defineMessages({
|
||||
title: {
|
||||
id: "project.about.links.title",
|
||||
defaultMessage: "Links",
|
||||
},
|
||||
issues: {
|
||||
id: "project.about.links.issues",
|
||||
defaultMessage: "Report issues",
|
||||
},
|
||||
source: {
|
||||
id: "project.about.links.source",
|
||||
defaultMessage: "View source",
|
||||
},
|
||||
wiki: {
|
||||
id: "project.about.links.wiki",
|
||||
defaultMessage: "Visit wiki",
|
||||
},
|
||||
discord: {
|
||||
id: "project.about.links.discord",
|
||||
defaultMessage: "Join Discord server",
|
||||
},
|
||||
donateGeneric: {
|
||||
id: "project.about.links.donate.generic",
|
||||
defaultMessage: "Donate",
|
||||
},
|
||||
donateGitHub: {
|
||||
id: "project.about.links.donate.github",
|
||||
defaultMessage: "Sponsor on GitHub",
|
||||
},
|
||||
donateBmac: {
|
||||
id: "project.about.links.donate.bmac",
|
||||
defaultMessage: "Buy Me a Coffee",
|
||||
},
|
||||
donatePatreon: {
|
||||
id: "project.about.links.donate.patreon",
|
||||
defaultMessage: "Donate on Patreon",
|
||||
},
|
||||
donatePayPal: {
|
||||
id: "project.about.links.donate.paypal",
|
||||
defaultMessage: "Donate on PayPal",
|
||||
},
|
||||
donateKoFi: {
|
||||
id: "project.about.links.donate.kofi",
|
||||
defaultMessage: "Donate on Ko-fi",
|
||||
},
|
||||
donateGithub: {
|
||||
id: "project.about.links.donate.github",
|
||||
defaultMessage: "Sponsor on GitHub",
|
||||
},
|
||||
});
|
||||
const creatorsMessages = defineMessages({
|
||||
title: {
|
||||
id: "project.about.creators.title",
|
||||
defaultMessage: "Creators",
|
||||
},
|
||||
owner: {
|
||||
id: "project.about.creators.owner",
|
||||
defaultMessage: "Project owner",
|
||||
},
|
||||
});
|
||||
const detailsMessages = defineMessages({
|
||||
title: {
|
||||
id: "project.about.details.title",
|
||||
defaultMessage: "Details",
|
||||
},
|
||||
licensed: {
|
||||
id: "project.about.details.licensed",
|
||||
defaultMessage: "Licensed {license}",
|
||||
},
|
||||
created: {
|
||||
id: "project.about.details.created",
|
||||
defaultMessage: "Created {date}",
|
||||
},
|
||||
submitted: {
|
||||
id: "project.about.details.submitted",
|
||||
defaultMessage: "Submitted {date}",
|
||||
},
|
||||
published: {
|
||||
id: "project.about.details.published",
|
||||
defaultMessage: "Published {date}",
|
||||
},
|
||||
updated: {
|
||||
id: "project.about.details.updated",
|
||||
defaultMessage: "Updated {date}",
|
||||
},
|
||||
});
|
||||
|
||||
const modalLicense = ref(null);
|
||||
const licenseText = ref("");
|
||||
|
||||
const createdDate = computed(() =>
|
||||
props.project.published ? formatRelativeTime(props.project.published) : "unknown",
|
||||
);
|
||||
const submittedDate = computed(() =>
|
||||
props.project.queued ? formatRelativeTime(props.project.queued) : "unknown",
|
||||
);
|
||||
const publishedDate = computed(() =>
|
||||
props.project.approved ? formatRelativeTime(props.project.approved) : "unknown",
|
||||
);
|
||||
const updatedDate = computed(() =>
|
||||
props.project.updated ? formatRelativeTime(props.project.updated) : "unknown",
|
||||
);
|
||||
|
||||
const licenseIdDisplay = computed(() => {
|
||||
const id = props.project.license.id;
|
||||
|
||||
if (id === "LicenseRef-All-Rights-Reserved") {
|
||||
return "ARR";
|
||||
} else if (id.includes("LicenseRef")) {
|
||||
return id.replaceAll("LicenseRef-", "").replaceAll("-", " ");
|
||||
} else {
|
||||
return id;
|
||||
}
|
||||
});
|
||||
|
||||
async function getLicenseData(event) {
|
||||
modalLicense.value.show(event);
|
||||
|
||||
try {
|
||||
const text = await useBaseFetch(`tag/license/${props.project.license.id}`);
|
||||
licenseText.value = text.body || "License text could not be retrieved.";
|
||||
} catch {
|
||||
licenseText.value = "License text could not be retrieved.";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -201,7 +201,7 @@ async function setStatus(status) {
|
||||
|
||||
svg {
|
||||
&.good {
|
||||
color: var(--color-brand-green);
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
&.bad {
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
<label for="project-visibility">
|
||||
<span class="label__title">Visibility</span>
|
||||
<div class="label__description">
|
||||
Listed and archived projects are visible in search. Unlisted projects are published, but
|
||||
Public and archived projects are visible in search. Unlisted projects are published, but
|
||||
not visible in search or on user profiles. Private projects are only accessible by
|
||||
members of the project.
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
class="small-multiselect"
|
||||
placeholder="Select one"
|
||||
:options="tags.approvedStatuses"
|
||||
:custom-label="(value) => $formatProjectStatus(value)"
|
||||
:custom-label="(value) => formatProjectStatus(value)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
@@ -243,6 +243,7 @@
|
||||
<script setup>
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
|
||||
import { formatProjectStatus } from "@modrinth/utils";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
|
||||
import FileInput from "~/components/ui/FileInput.vue";
|
||||
@@ -423,7 +424,7 @@ const deleteIcon = async () => {
|
||||
|
||||
svg {
|
||||
&.good {
|
||||
color: var(--color-brand-green);
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
&.bad {
|
||||
|
||||
164
apps/frontend/src/pages/[type]/[id]/version/[version]-new.vue
Normal file
164
apps/frontend/src/pages/[type]/[id]/version/[version]-new.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="normal-page__content flex flex-col gap-4">
|
||||
<nuxt-link
|
||||
:to="versionsListLink"
|
||||
class="flex w-fit items-center gap-1 text-brand-blue hover:underline"
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
{{
|
||||
hasBackLink ? formatMessage(messages.backToVersions) : formatMessage(messages.allVersions)
|
||||
}}
|
||||
</nuxt-link>
|
||||
<div class="flex gap-3">
|
||||
<VersionChannelIndicator :channel="version.version_type" large />
|
||||
<div class="flex flex-col gap-1">
|
||||
<h1 class="m-0 text-xl font-extrabold leading-none text-contrast">
|
||||
{{ version.version_number }}
|
||||
</h1>
|
||||
<span class="text-sm font-semibold text-secondary"> {{ version.name }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button><DownloadIcon /> Download</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button><ShareIcon /> Share</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button>
|
||||
<MoreVerticalIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-extrabold text-contrast">Files</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div
|
||||
v-for="(file, index) in version.files"
|
||||
:key="index"
|
||||
class="flex gap-2 rounded-2xl bg-bg-raised p-4"
|
||||
>
|
||||
<div
|
||||
:class="`flex h-9 w-9 items-center justify-center rounded-full ${file.primary ? 'bg-brand-highlight text-brand' : 'bg-button-bg text-secondary'}`"
|
||||
>
|
||||
<FileIcon />
|
||||
</div>
|
||||
<div class="flex flex-grow flex-col">
|
||||
<span class="font-extrabold text-contrast">{{
|
||||
file.primary ? "Primary file" : "Supplementary resource"
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-secondary"
|
||||
>{{ file.filename }} • {{ formatBytes(file.size) }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-lg font-extrabold text-contrast">Dependencies</h2>
|
||||
<h2 class="text-lg font-extrabold text-contrast">Changes</h2>
|
||||
<div class="rounded-2xl bg-bg-raised px-6 py-4">
|
||||
<div
|
||||
class="markdown-body"
|
||||
v-html="renderHighlightedString(version.changelog ?? 'No changelog provided')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page__sidebar">
|
||||
<div class="padding-lg h-[250px] rounded-2xl bg-bg-raised"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
DownloadIcon,
|
||||
FileIcon,
|
||||
MoreVerticalIcon,
|
||||
ShareIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled, VersionChannelIndicator } from "@modrinth/ui";
|
||||
import { formatBytes, renderHighlightedString } from "@modrinth/utils";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
project: Project;
|
||||
versions: Version[];
|
||||
featuredVersions: Version[];
|
||||
members: User[];
|
||||
currentMember: User;
|
||||
dependencies: Dependency[];
|
||||
resetProject: Function;
|
||||
}>();
|
||||
|
||||
const version = computed(() => {
|
||||
let version: Version | undefined;
|
||||
|
||||
if (route.params.version === "latest") {
|
||||
let versionList = props.versions;
|
||||
if (route.query.loader) {
|
||||
versionList = versionList.filter((x) => x.loaders.includes(route.query.loader));
|
||||
}
|
||||
if (route.query.version) {
|
||||
versionList = versionList.filter((x) => x.game_versions.includes(route.query.version));
|
||||
}
|
||||
version = versionList.reduce((a, b) => (a.date_published > b.date_published ? a : b));
|
||||
} else {
|
||||
version = props.versions.find(
|
||||
(x) => x.id === route.params.version || x.displayUrlEnding === route.params.version,
|
||||
);
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: "Version not found",
|
||||
});
|
||||
}
|
||||
|
||||
return version;
|
||||
});
|
||||
|
||||
// const data = useNuxtApp();
|
||||
const route = useNativeRoute();
|
||||
|
||||
// const auth = await useAuth();
|
||||
// const tags = useTags();
|
||||
|
||||
const versionsListLink = computed(() => {
|
||||
if (router.options.history.state.back) {
|
||||
if (router.options.history.state.back.includes("/versions")) {
|
||||
return router.options.history.state.back;
|
||||
}
|
||||
}
|
||||
return `/${props.project.project_type}/${
|
||||
props.project.slug ? props.project.slug : props.project.id
|
||||
}/versions`;
|
||||
});
|
||||
|
||||
const hasBackLink = computed(
|
||||
() =>
|
||||
router.options.history.state.back && router.options.history.state.back.endsWith("/versions"),
|
||||
);
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
const messages = defineMessages({
|
||||
backToVersions: {
|
||||
id: "project.version.back-to-versions",
|
||||
defaultMessage: "Back to versions",
|
||||
},
|
||||
allVersions: {
|
||||
id: "project.version.all-versions",
|
||||
defaultMessage: "All versions",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-if="version" class="version-page">
|
||||
<ModalConfirm
|
||||
<ConfirmModal
|
||||
v-if="currentMember"
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete this version?"
|
||||
@@ -37,14 +37,18 @@
|
||||
open-direction="top"
|
||||
/>
|
||||
<div class="button-group">
|
||||
<button class="iconified-button" @click="$refs.modal_package_mod.hide()">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="createDataPackVersion">
|
||||
<RightArrowIcon />
|
||||
Begin packaging data pack
|
||||
</button>
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.modal_package_mod.hide()">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="createDataPackVersion">
|
||||
<RightArrowIcon />
|
||||
Begin packaging data pack
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -94,96 +98,102 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="isCreating" class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
:disabled="shouldPreventActions"
|
||||
@click="createVersion"
|
||||
>
|
||||
<PlusIcon aria-hidden="true" />
|
||||
Create
|
||||
</button>
|
||||
<nuxt-link
|
||||
v-if="auth.user"
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/versions`"
|
||||
class="iconified-button"
|
||||
>
|
||||
<CrossIcon aria-hidden="true" />
|
||||
Cancel
|
||||
</nuxt-link>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="shouldPreventActions" @click="createVersion">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
Create
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<nuxt-link
|
||||
v-if="auth.user"
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/versions`"
|
||||
>
|
||||
<CrossIcon aria-hidden="true" />
|
||||
Cancel
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-else-if="isEditing" class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
:disabled="shouldPreventActions"
|
||||
@click="saveEditedVersion"
|
||||
>
|
||||
<SaveIcon aria-hidden="true" />
|
||||
Save
|
||||
</button>
|
||||
<button class="iconified-button" @click="version.featured = !version.featured">
|
||||
<StarIcon aria-hidden="true" />
|
||||
<template v-if="!version.featured"> Feature version</template>
|
||||
<template v-else> Unfeature version</template>
|
||||
</button>
|
||||
<nuxt-link
|
||||
v-if="currentMember"
|
||||
class="action iconified-button"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||
>
|
||||
<CrossIcon aria-hidden="true" />
|
||||
Discard changes
|
||||
</nuxt-link>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="shouldPreventActions" @click="saveEditedVersion">
|
||||
<SaveIcon aria-hidden="true" />
|
||||
Save
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="version.featured = !version.featured">
|
||||
<StarIcon aria-hidden="true" />
|
||||
<template v-if="!version.featured"> Feature version</template>
|
||||
<template v-else> Unfeature version</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<nuxt-link
|
||||
v-if="currentMember"
|
||||
class="action"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||
>
|
||||
<CrossIcon aria-hidden="true" />
|
||||
Discard changes
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-else class="input-group">
|
||||
<a
|
||||
v-if="primaryFile"
|
||||
v-tooltip="primaryFile.filename + ' (' + $formatBytes(primaryFile.size) + ')'"
|
||||
:href="primaryFile.url"
|
||||
class="iconified-button brand-button"
|
||||
:aria-label="`Download ${primaryFile.filename}`"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Download
|
||||
</a>
|
||||
<nuxt-link v-if="!auth.user" class="iconified-button" to="/auth/sign-in">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</nuxt-link>
|
||||
<button v-else class="iconified-button" @click="() => reportVersion(version.id)">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
<nuxt-link
|
||||
v-if="currentMember"
|
||||
class="action iconified-button"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}/edit`"
|
||||
>
|
||||
<EditIcon aria-hidden="true" />
|
||||
Edit
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="
|
||||
currentMember &&
|
||||
version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))
|
||||
"
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_package_mod.show()"
|
||||
>
|
||||
<BoxIcon aria-hidden="true" />
|
||||
Package as mod
|
||||
</button>
|
||||
<button
|
||||
v-if="currentMember"
|
||||
class="iconified-button danger-button"
|
||||
@click="$refs.modal_confirm.show()"
|
||||
>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
Delete
|
||||
</button>
|
||||
<ButtonStyled v-if="primaryFile" color="brand">
|
||||
<a
|
||||
v-tooltip="primaryFile.filename + ' (' + $formatBytes(primaryFile.size) + ')'"
|
||||
:href="primaryFile.url"
|
||||
@click="emit('onDownload')"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Download
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="!auth.user">
|
||||
<nuxt-link to="/auth/sign-in">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button @click="() => reportVersion(version.id)">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<nuxt-link
|
||||
v-if="currentMember"
|
||||
class="action"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}/edit`"
|
||||
>
|
||||
<EditIcon aria-hidden="true" />
|
||||
Edit
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-if="
|
||||
currentMember &&
|
||||
version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))
|
||||
"
|
||||
@click="$refs.modal_package_mod.show()"
|
||||
>
|
||||
<BoxIcon aria-hidden="true" />
|
||||
Package as mod
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button v-if="currentMember" @click="$refs.modal_confirm.show()">
|
||||
<TrashIcon aria-hidden="true" />
|
||||
Delete
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-page__changelog universal-card">
|
||||
@@ -242,14 +252,12 @@
|
||||
{{ dependency.dependency_type }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="isEditing && project.project_type !== 'modpack'"
|
||||
class="iconified-button"
|
||||
@click="version.dependencies.splice(index, 1)"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove
|
||||
</button>
|
||||
<ButtonStyled v-if="isEditing && project.project_type !== 'modpack'">
|
||||
<button @click="version.dependencies.splice(index, 1)">
|
||||
<TrashIcon />
|
||||
Remove
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div
|
||||
v-for="(dependency, index) in deps.filter((x) => x.file_name)"
|
||||
@@ -297,13 +305,12 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="addDependency(dependencyAddMode, newDependencyId, newDependencyType)"
|
||||
>
|
||||
<PlusIcon />
|
||||
Add dependency
|
||||
</button>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="addDependency(dependencyAddMode, newDependencyId, newDependencyType)">
|
||||
<PlusIcon />
|
||||
Add dependency
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -371,31 +378,32 @@
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
<button
|
||||
v-if="isEditing"
|
||||
:disabled="primaryFile.hashes.sha1 === file.hashes.sha1"
|
||||
class="iconified-button raised-button"
|
||||
@click="
|
||||
() => {
|
||||
deleteFiles.push(file.hashes.sha1);
|
||||
version.files.splice(index, 1);
|
||||
oldFileTypes.splice(index, 1);
|
||||
}
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove
|
||||
</button>
|
||||
<a
|
||||
v-else
|
||||
:href="file.url"
|
||||
class="iconified-button raised-button"
|
||||
:title="`Download ${file.filename}`"
|
||||
tabindex="0"
|
||||
>
|
||||
<DownloadIcon />
|
||||
Download
|
||||
</a>
|
||||
<ButtonStyled v-if="isEditing">
|
||||
<button
|
||||
:disabled="primaryFile.hashes.sha1 === file.hashes.sha1"
|
||||
@click="
|
||||
() => {
|
||||
deleteFiles.push(file.hashes.sha1);
|
||||
version.files.splice(index, 1);
|
||||
oldFileTypes.splice(index, 1);
|
||||
}
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<a
|
||||
:href="file.url"
|
||||
class="raised-button"
|
||||
:title="`Download ${file.filename}`"
|
||||
tabindex="0"
|
||||
>
|
||||
<DownloadIcon />
|
||||
Download
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<template v-if="isEditing">
|
||||
<div v-for="(file, index) in newFiles" :key="index" class="file">
|
||||
@@ -417,18 +425,20 @@
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
<button
|
||||
class="iconified-button raised-button"
|
||||
@click="
|
||||
() => {
|
||||
newFiles.splice(index, 1);
|
||||
newFileTypes.splice(index, 1);
|
||||
}
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove
|
||||
</button>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
class="raised-button"
|
||||
@click="
|
||||
() => {
|
||||
newFiles.splice(index, 1);
|
||||
newFileTypes.splice(index, 1);
|
||||
}
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="additional-files">
|
||||
<h4>Upload additional files</h4>
|
||||
@@ -455,73 +465,100 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="version-page__metadata">
|
||||
<div class="universal-card full-width-inputs">
|
||||
<h3>Metadata</h3>
|
||||
<div>
|
||||
<h4>Release channel</h4>
|
||||
<Multiselect
|
||||
v-if="isEditing"
|
||||
v-model="version.version_type"
|
||||
class="input"
|
||||
placeholder="Select one"
|
||||
:options="['release', 'beta', 'alpha']"
|
||||
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
</div>
|
||||
<div class="normal-page__sidebar version-page__metadata">
|
||||
<AdPlaceholder />
|
||||
<div class="universal-card full-width-inputs">
|
||||
<h3>Metadata</h3>
|
||||
<div>
|
||||
<h4>Release channel</h4>
|
||||
<Multiselect
|
||||
v-if="isEditing"
|
||||
v-model="version.version_type"
|
||||
class="input"
|
||||
placeholder="Select one"
|
||||
:options="['release', 'beta', 'alpha']"
|
||||
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
<template v-else>
|
||||
<Badge
|
||||
v-if="version.version_type === 'release'"
|
||||
class="value"
|
||||
type="release"
|
||||
color="green"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="version.version_type === 'beta'"
|
||||
class="value"
|
||||
type="beta"
|
||||
color="orange"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="version.version_type === 'alpha'"
|
||||
class="value"
|
||||
type="alpha"
|
||||
color="red"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Version number</h4>
|
||||
<div v-if="isEditing" class="iconified-input">
|
||||
<label class="hidden" for="version-number">Version number</label>
|
||||
<HashIcon aria-hidden="true" />
|
||||
<input
|
||||
id="version-number"
|
||||
v-model="version.version_number"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
maxlength="54"
|
||||
/>
|
||||
<template v-else>
|
||||
<Badge
|
||||
v-if="version.version_type === 'release'"
|
||||
class="value"
|
||||
type="release"
|
||||
color="green"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="version.version_type === 'beta'"
|
||||
class="value"
|
||||
type="beta"
|
||||
color="orange"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="version.version_type === 'alpha'"
|
||||
class="value"
|
||||
type="alpha"
|
||||
color="red"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Version number</h4>
|
||||
<div v-if="isEditing" class="iconified-input">
|
||||
<label class="hidden" for="version-number">Version number</label>
|
||||
<HashIcon aria-hidden="true" />
|
||||
<input
|
||||
id="version-number"
|
||||
v-model="version.version_number"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
maxlength="54"
|
||||
/>
|
||||
</div>
|
||||
<span v-else>{{ version.version_number }}</span>
|
||||
</div>
|
||||
<div v-if="project.project_type !== 'resourcepack'">
|
||||
<h4>Loaders</h4>
|
||||
<Multiselect
|
||||
v-if="isEditing"
|
||||
v-model="version.loaders"
|
||||
<span v-else>{{ version.version_number }}</span>
|
||||
</div>
|
||||
<div v-if="project.project_type !== 'resourcepack'">
|
||||
<h4>Loaders</h4>
|
||||
<Multiselect
|
||||
v-if="isEditing"
|
||||
v-model="version.loaders"
|
||||
:options="
|
||||
tags.loaders
|
||||
.filter((x) =>
|
||||
x.supported_project_types.includes(project.actualProjectType.toLowerCase()),
|
||||
)
|
||||
.map((it) => it.name)
|
||||
"
|
||||
:custom-label="(value) => $formatCategory(value)"
|
||||
:loading="tags.loaders.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 loaders..."
|
||||
/>
|
||||
<Categories v-else :categories="version.loaders" :type="project.actualProjectType" />
|
||||
</div>
|
||||
<div>
|
||||
<h4>Game versions</h4>
|
||||
<template v-if="isEditing">
|
||||
<multiselect
|
||||
v-model="version.game_versions"
|
||||
:options="
|
||||
tags.loaders
|
||||
.filter((x) =>
|
||||
x.supported_project_types.includes(project.actualProjectType.toLowerCase()),
|
||||
)
|
||||
.map((it) => it.name)
|
||||
showSnapshots
|
||||
? tags.gameVersions.map((x) => x.version)
|
||||
: tags.gameVersions
|
||||
.filter((it) => it.version_type === 'release')
|
||||
.map((x) => x.version)
|
||||
"
|
||||
:custom-label="(value) => $formatCategory(value)"
|
||||
:loading="tags.loaders.length === 0"
|
||||
:loading="tags.gameVersions.length === 0"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
@@ -530,89 +567,63 @@
|
||||
:show-labels="false"
|
||||
:limit="6"
|
||||
:hide-selected="true"
|
||||
placeholder="Choose loaders..."
|
||||
placeholder="Choose versions..."
|
||||
/>
|
||||
<Checkbox
|
||||
v-model="showSnapshots"
|
||||
label="Show all versions"
|
||||
description="Show all versions"
|
||||
style="margin-top: 0.5rem"
|
||||
:border="false"
|
||||
/>
|
||||
</template>
|
||||
<span v-else>{{ $formatVersion(version.game_versions) }}</span>
|
||||
</div>
|
||||
<div v-if="!isEditing">
|
||||
<h4>Downloads</h4>
|
||||
<span>{{ version.downloads }}</span>
|
||||
</div>
|
||||
<div v-if="!isEditing">
|
||||
<h4>Publication date</h4>
|
||||
<span>
|
||||
{{ $dayjs(version.date_published).format("MMMM D, YYYY [at] h:mm A") }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!isEditing && version.author">
|
||||
<h4>Publisher</h4>
|
||||
<div
|
||||
class="team-member columns button-transparent"
|
||||
@click="$router.push('/user/' + version.author.user.username)"
|
||||
>
|
||||
<Avatar
|
||||
:src="version.author.avatar_url"
|
||||
:alt="version.author.user.username"
|
||||
size="sm"
|
||||
circle
|
||||
/>
|
||||
<Categories v-else :categories="version.loaders" :type="project.actualProjectType" />
|
||||
</div>
|
||||
<div>
|
||||
<h4>Game versions</h4>
|
||||
<template v-if="isEditing">
|
||||
<multiselect
|
||||
v-model="version.game_versions"
|
||||
:options="
|
||||
showSnapshots
|
||||
? tags.gameVersions.map((x) => x.version)
|
||||
: tags.gameVersions
|
||||
.filter((it) => it.version_type === 'release')
|
||||
.map((x) => x.version)
|
||||
"
|
||||
:loading="tags.gameVersions.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..."
|
||||
/>
|
||||
<Checkbox
|
||||
v-model="showSnapshots"
|
||||
label="Show all versions"
|
||||
description="Show all versions"
|
||||
style="margin-top: 0.5rem"
|
||||
:border="false"
|
||||
/>
|
||||
</template>
|
||||
<span v-else>{{ $formatVersion(version.game_versions) }}</span>
|
||||
</div>
|
||||
<div v-if="!isEditing">
|
||||
<h4>Downloads</h4>
|
||||
<span>{{ version.downloads }}</span>
|
||||
</div>
|
||||
<div v-if="!isEditing">
|
||||
<h4>Publication date</h4>
|
||||
<span>
|
||||
{{ $dayjs(version.date_published).format("MMMM D, YYYY [at] h:mm A") }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!isEditing && version.author">
|
||||
<h4>Publisher</h4>
|
||||
<div
|
||||
class="team-member columns button-transparent"
|
||||
@click="$router.push('/user/' + version.author.user.username)"
|
||||
>
|
||||
<Avatar
|
||||
:src="version.author.avatar_url"
|
||||
:alt="version.author.user.username"
|
||||
size="sm"
|
||||
circle
|
||||
/>
|
||||
|
||||
<div class="member-info">
|
||||
<nuxt-link :to="'/user/' + version.author.user.username" class="name">
|
||||
<p>
|
||||
{{ version.author.name }}
|
||||
</p>
|
||||
</nuxt-link>
|
||||
<p v-if="version.author.role" class="role">
|
||||
{{ version.author.role }}
|
||||
<div class="member-info">
|
||||
<nuxt-link :to="'/user/' + version.author.user.username" class="name">
|
||||
<p>
|
||||
{{ version.author.name }}
|
||||
</p>
|
||||
<p v-else-if="version.author_id === 'GVFjtWTf'" class="role">Archivist</p>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
<p v-if="version.author.role" class="role">
|
||||
{{ version.author.role }}
|
||||
</p>
|
||||
<p v-else-if="version.author_id === 'GVFjtWTf'" class="role">Archivist</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isEditing">
|
||||
<h4>Version ID</h4>
|
||||
<CopyCode :text="version.id" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isEditing">
|
||||
<h4>Version ID</h4>
|
||||
<CopyCode :text="version.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { MarkdownEditor } from "@modrinth/ui";
|
||||
import { ButtonStyled, ConfirmModal, MarkdownEditor } from "@modrinth/ui";
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
||||
import { inferVersionInfo } from "~/helpers/infer.js";
|
||||
@@ -626,7 +637,6 @@ import Badge from "~/components/ui/Badge.vue";
|
||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||
import Categories from "~/components/ui/search/Categories.vue";
|
||||
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
|
||||
import Chips from "~/components/ui/Chips.vue";
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
import FileInput from "~/components/ui/FileInput.vue";
|
||||
@@ -649,6 +659,7 @@ import RightArrowIcon from "~/assets/images/utils/right-arrow.svg?component";
|
||||
import Modal from "~/components/ui/Modal.vue";
|
||||
import ChevronRightIcon from "~/assets/images/utils/chevron-right.svg?component";
|
||||
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
MarkdownEditor,
|
||||
@@ -675,10 +686,12 @@ export default defineNuxtComponent({
|
||||
Badge,
|
||||
Breadcrumbs,
|
||||
CopyCode,
|
||||
ModalConfirm,
|
||||
Multiselect,
|
||||
BoxIcon,
|
||||
RightArrowIcon,
|
||||
ConfirmModal,
|
||||
ButtonStyled,
|
||||
AdPlaceholder,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
@@ -942,10 +955,7 @@ export default defineNuxtComponent({
|
||||
},
|
||||
getPreviousLink() {
|
||||
if (this.$router.options.history.state.back) {
|
||||
if (
|
||||
this.$router.options.history.state.back.includes("/changelog") ||
|
||||
this.$router.options.history.state.back.includes("/versions")
|
||||
) {
|
||||
if (this.$router.options.history.state.back.includes("/versions")) {
|
||||
return this.$router.options.history.state.back;
|
||||
}
|
||||
}
|
||||
@@ -955,9 +965,9 @@ export default defineNuxtComponent({
|
||||
},
|
||||
getPreviousLabel() {
|
||||
return this.$router.options.history.state.back &&
|
||||
this.$router.options.history.state.back.endsWith("/changelog")
|
||||
? "Changelog"
|
||||
: "Versions";
|
||||
this.$router.options.history.state.back.endsWith("/versions")
|
||||
? "Back to versions"
|
||||
: "All versions";
|
||||
},
|
||||
acceptFileFromProjectType,
|
||||
renderHighlightedString,
|
||||
@@ -1315,8 +1325,8 @@ export default defineNuxtComponent({
|
||||
"title" auto
|
||||
"changelog" auto
|
||||
"dependencies" auto
|
||||
"metadata" auto
|
||||
"files" auto
|
||||
"dummy" 1fr
|
||||
/ 1fr;
|
||||
|
||||
column-gap: var(--spacing-card-md);
|
||||
@@ -1480,8 +1490,9 @@ export default defineNuxtComponent({
|
||||
min-width: 235px;
|
||||
}
|
||||
|
||||
.iconified-button {
|
||||
.raised-button {
|
||||
margin-left: auto;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
&:not(:nth-child(2)) {
|
||||
@@ -1506,44 +1517,30 @@ export default defineNuxtComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.version-page__metadata {
|
||||
grid-area: metadata;
|
||||
|
||||
h4 {
|
||||
margin: 1rem 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.team-member {
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
.member-info {
|
||||
overflow: hidden;
|
||||
margin: auto 0 auto 0.75rem;
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.version-page {
|
||||
grid-template:
|
||||
"title title" auto
|
||||
"changelog metadata" auto
|
||||
"dependencies metadata" auto
|
||||
"files metadata" auto
|
||||
"dummy metadata" 1fr
|
||||
/ 1fr 20rem;
|
||||
.version-page__metadata {
|
||||
h4 {
|
||||
margin: 1rem 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.team-member {
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
.member-info {
|
||||
overflow: hidden;
|
||||
margin: auto 0 auto 0.75rem;
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,112 +1,296 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<div v-if="currentMember" class="card header-buttons">
|
||||
<section class="normal-page__content experimental-styles-within overflow-visible">
|
||||
<div
|
||||
v-if="currentMember && isPermission(currentMember?.permissions, 1 << 0)"
|
||||
class="card flex items-center gap-4"
|
||||
>
|
||||
<FileInput
|
||||
:max-size="524288000"
|
||||
:accept="acceptFileFromProjectType(project.project_type)"
|
||||
prompt="Upload a version"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!isPermission(currentMember?.permissions, 1 << 0)"
|
||||
class="btn btn-primary"
|
||||
@change="handleFiles"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
<span class="indicator">
|
||||
<span class="flex items-center gap-2">
|
||||
<InfoIcon /> Click to choose a file or drag one onto this page
|
||||
</span>
|
||||
<DropArea :accept="acceptFileFromProjectType(project.project_type)" @change="handleFiles" />
|
||||
</div>
|
||||
<VersionFilterControl :versions="props.versions" @switch-page="switchPage" />
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
class="pagination-before"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
<div v-if="filteredVersions.length > 0" id="all-versions" class="universal-card all-versions">
|
||||
<div class="header">
|
||||
<div />
|
||||
<div>Version</div>
|
||||
<div>Supports</div>
|
||||
<div>Stats</div>
|
||||
<div
|
||||
v-if="versions.length > 0"
|
||||
class="flex flex-col gap-4 rounded-2xl bg-bg-raised px-6 pb-8 pt-4 supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[1fr_min-content] sm:px-8 supports-[grid-template-columns:subgrid]:sm:grid-cols-[min-content_auto_auto_auto_min-content] supports-[grid-template-columns:subgrid]:xl:grid-cols-[min-content_auto_auto_auto_auto_auto_min-content]"
|
||||
>
|
||||
<div class="versions-grid-row">
|
||||
<div class="w-9 max-sm:hidden"></div>
|
||||
<div class="text-sm font-bold text-contrast max-sm:hidden">Name</div>
|
||||
<div
|
||||
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
|
||||
>
|
||||
Game version
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
|
||||
>
|
||||
Platforms
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
|
||||
>
|
||||
Published
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
|
||||
>
|
||||
Downloads
|
||||
</div>
|
||||
<div class="text-sm font-bold text-contrast max-sm:hidden xl:collapse xl:hidden">
|
||||
Compatibility
|
||||
</div>
|
||||
<div class="text-sm font-bold text-contrast max-sm:hidden xl:collapse xl:hidden">Stats</div>
|
||||
<div class="w-9 max-sm:hidden"></div>
|
||||
</div>
|
||||
<div
|
||||
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
||||
:key="version.id"
|
||||
class="version-button button-transparent"
|
||||
@click="
|
||||
$router.push(
|
||||
`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`,
|
||||
)
|
||||
"
|
||||
<template
|
||||
v-for="(version, index) in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
||||
:key="index"
|
||||
>
|
||||
<a
|
||||
v-tooltip="
|
||||
version.primaryFile.filename + ' (' + $formatBytes(version.primaryFile.size) + ')'
|
||||
"
|
||||
:href="version.primaryFile.url"
|
||||
class="download-button square-button brand-button"
|
||||
:class="version.version_type"
|
||||
:aria-label="`Download ${version.name}`"
|
||||
@click.stop="(event) => event.stopPropagation()"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
</a>
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||
class="version__title"
|
||||
>
|
||||
{{ version.name }}
|
||||
</nuxt-link>
|
||||
<div class="version__metadata">
|
||||
<VersionBadge v-if="version.version_type === 'release'" type="release" color="green" />
|
||||
<VersionBadge v-else-if="version.version_type === 'beta'" type="beta" color="orange" />
|
||||
<VersionBadge v-else-if="version.version_type === 'alpha'" type="alpha" color="red" />
|
||||
<span class="divider" />
|
||||
<span class="version_number">{{ version.version_number }}</span>
|
||||
<div
|
||||
:class="`versions-grid-row h-px w-full bg-button-bg ${index === 0 ? `max-sm:!hidden` : ``}`"
|
||||
></div>
|
||||
<div class="versions-grid-row group relative">
|
||||
<nuxt-link
|
||||
class="absolute inset-[calc(-1rem-2px)_-2rem] before:absolute before:inset-0 before:transition-all before:content-[''] hover:before:backdrop-brightness-110"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||
></nuxt-link>
|
||||
<div class="flex flex-col justify-center gap-2 sm:contents">
|
||||
<div class="flex flex-row items-center gap-2 sm:contents">
|
||||
<div class="self-center">
|
||||
<div class="pointer-events-none relative z-[1]">
|
||||
<VersionChannelIndicator :channel="version.version_type" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pointer-events-none relative z-[1] flex flex-col justify-center group-hover:underline"
|
||||
>
|
||||
<div class="font-bold text-contrast">{{ version.version_number }}</div>
|
||||
<div class="text-xs font-medium">{{ version.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center gap-2 sm:contents">
|
||||
<div class="flex flex-row flex-wrap items-center gap-1 xl:contents">
|
||||
<div class="flex items-center">
|
||||
<div class="tag-list">
|
||||
<div
|
||||
v-for="gameVersion in formatVersionsForDisplay(version.game_versions)"
|
||||
:key="`version-tag-${gameVersion}`"
|
||||
v-tooltip="`Toggle filter for ${gameVersion}`"
|
||||
class="tag-list__item z-[1] cursor-pointer hover:underline"
|
||||
@click="versionFilters.toggleFilters('gameVersion', version.game_versions)"
|
||||
>
|
||||
{{ gameVersion }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="tag-list">
|
||||
<div
|
||||
v-for="platform in version.loaders"
|
||||
:key="`platform-tag-${platform}`"
|
||||
v-tooltip="`Toggle filter for ${platform}`"
|
||||
:class="`tag-list__item z-[1] cursor-pointer hover:underline`"
|
||||
:style="`--_color: var(--color-platform-${platform})`"
|
||||
@click="versionFilters.toggleFilter('platform', platform)"
|
||||
>
|
||||
<svg v-html="tags.loaders.find((x) => x.name === platform).icon"></svg>
|
||||
{{ formatCategory(platform) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col justify-center gap-1 max-sm:flex-row max-sm:justify-start max-sm:gap-3 xl:contents"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none z-[1] flex items-center gap-1 text-nowrap font-medium xl:self-center"
|
||||
>
|
||||
<CalendarIcon class="xl:hidden" />
|
||||
{{ formatRelativeTime(version.date_published) }}
|
||||
</div>
|
||||
<div
|
||||
class="pointer-events-none z-[1] flex items-center gap-1 font-medium xl:self-center"
|
||||
>
|
||||
<DownloadIcon class="xl:hidden" />
|
||||
{{ formatCompactNumber(version.downloads) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start justify-end gap-1 sm:items-center">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<a
|
||||
v-tooltip="`Download`"
|
||||
:href="getPrimaryFile(version).url"
|
||||
class="z-[1] group-hover:!bg-brand group-hover:!text-brand-inverted"
|
||||
@click="emits('onDownload')"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
class="group-hover:!bg-button-bg"
|
||||
:options="[
|
||||
{
|
||||
id: 'download',
|
||||
color: 'primary',
|
||||
hoverFilled: true,
|
||||
link: getPrimaryFile(version).url,
|
||||
action: () => {
|
||||
emits('onDownload');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'new-tab',
|
||||
action: () => {},
|
||||
link: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`,
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
id: 'copy-link',
|
||||
action: () =>
|
||||
copyToClipboard(
|
||||
`https://modrinth.com/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'share',
|
||||
action: () => {},
|
||||
shown: false,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
action: () => reportVersion(version.id),
|
||||
},
|
||||
{ divider: true, shown: currentMember },
|
||||
{
|
||||
id: 'edit',
|
||||
link: `/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}/edit`,
|
||||
shown: currentMember,
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
action: () => {},
|
||||
shown: currentMember && false,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #download>
|
||||
<DownloadIcon />
|
||||
Download
|
||||
</template>
|
||||
<template #new-tab>
|
||||
<ExternalIcon />
|
||||
Open in new tab
|
||||
</template>
|
||||
<template #copy-link>
|
||||
<LinkIcon />
|
||||
Copy link
|
||||
</template>
|
||||
<template #share>
|
||||
<ShareIcon />
|
||||
Share
|
||||
</template>
|
||||
<template #report>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</template>
|
||||
<template #edit>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-if="showFiles" class="tag-list pointer-events-none relative z-[1] col-span-full">
|
||||
<div
|
||||
v-for="(file, fileIdx) in version.files"
|
||||
:key="`platform-tag-${fileIdx}`"
|
||||
:class="`flex items-center gap-1 text-wrap rounded-full bg-button-bg px-2 py-0.5 text-xs font-medium ${file.primary || fileIdx === 0 ? 'bg-brand-highlight text-contrast' : 'text-primary'}`"
|
||||
>
|
||||
<StarIcon v-if="file.primary || fileIdx === 0" class="shrink-0" />
|
||||
{{ file.filename }} - {{ formatBytes(file.size) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version__supports">
|
||||
<span>
|
||||
{{ version.loaders.map((x) => $formatCategory(x)).join(", ") }}
|
||||
</span>
|
||||
<span>{{ $formatVersion(version.game_versions) }}</span>
|
||||
</div>
|
||||
<div class="version__stats">
|
||||
<span>
|
||||
<strong>{{ $formatNumber(version.downloads) }}</strong>
|
||||
download<span v-if="version.downloads !== 1">s</span>
|
||||
</span>
|
||||
<span>
|
||||
Published on
|
||||
<strong>{{ $dayjs(version.date_published).format("MMM D, YYYY") }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
class="pagination-before"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
<div class="my-3 flex justify-end">
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
:link-function="(page) => `?page=${currentPage}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<div class="normal-page__sidebar">
|
||||
<AdPlaceholder />
|
||||
<VersionFilterControl
|
||||
ref="versionFilters"
|
||||
:versions="props.versions"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
||||
import DownloadIcon from "~/assets/images/utils/download.svg?component";
|
||||
import UploadIcon from "~/assets/images/utils/upload.svg?component";
|
||||
import InfoIcon from "~/assets/images/utils/info.svg?component";
|
||||
import VersionBadge from "~/components/ui/Badge.vue";
|
||||
import FileInput from "~/components/ui/FileInput.vue";
|
||||
import DropArea from "~/components/ui/DropArea.vue";
|
||||
import Pagination from "~/components/ui/Pagination.vue";
|
||||
import {
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
Pagination,
|
||||
VersionChannelIndicator,
|
||||
FileInput,
|
||||
} from "@modrinth/ui";
|
||||
import {
|
||||
StarIcon,
|
||||
CalendarIcon,
|
||||
DownloadIcon,
|
||||
MoreVerticalIcon,
|
||||
TrashIcon,
|
||||
ExternalIcon,
|
||||
LinkIcon,
|
||||
ShareIcon,
|
||||
EditIcon,
|
||||
ReportIcon,
|
||||
UploadIcon,
|
||||
InfoIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { formatBytes, formatCategory } from "@modrinth/utils";
|
||||
import { formatVersionsForDisplay } from "~/helpers/projects.js";
|
||||
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
|
||||
import DropArea from "~/components/ui/DropArea.vue";
|
||||
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
|
||||
const formatCompactNumber = useCompactNumber();
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
@@ -121,12 +305,6 @@ const props = defineProps({
|
||||
return [];
|
||||
},
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
@@ -135,30 +313,38 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const data = useNuxtApp();
|
||||
const tags = useTags();
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
|
||||
const title = `${props.project.title} - Versions`;
|
||||
const description = `Download and browse ${props.versions.length} ${
|
||||
props.project.title
|
||||
} versions. ${data.$formatNumber(props.project.downloads)} total downloads. Last updated ${data
|
||||
.$dayjs(props.project.updated)
|
||||
.format("MMM D, YYYY")}.`;
|
||||
const emits = defineEmits(["onDownload"]);
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: title,
|
||||
ogDescription: description,
|
||||
});
|
||||
|
||||
const router = useNativeRouter();
|
||||
const route = useNativeRoute();
|
||||
const router = useNativeRouter();
|
||||
|
||||
const currentPage = ref(Number(route.query.p ?? 1));
|
||||
const currentPage = ref(route.query.page ?? 1);
|
||||
|
||||
const showFiles = ref(false);
|
||||
|
||||
function switchPage(page) {
|
||||
currentPage.value = page;
|
||||
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
page: currentPage.value !== 1 ? currentPage.value : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getPrimaryFile(version) {
|
||||
return version.files.find((x) => x.primary) || version.files[0];
|
||||
}
|
||||
|
||||
const versionFilters = ref(null);
|
||||
const filteredVersions = computed(() => {
|
||||
const selectedGameVersions = getArrayOrString(route.query.g) ?? [];
|
||||
const selectedLoaders = getArrayOrString(route.query.l) ?? [];
|
||||
const selectedVersionTypes = getArrayOrString(route.query.c) ?? [];
|
||||
const selectedGameVersions = getArrayOrString(route.query.gameVersion) ?? [];
|
||||
const selectedLoaders = getArrayOrString(route.query.platform) ?? [];
|
||||
const selectedVersionTypes = getArrayOrString(route.query.type) ?? [];
|
||||
|
||||
return props.versions.filter(
|
||||
(projectVersion) =>
|
||||
@@ -173,17 +359,6 @@ const filteredVersions = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
function switchPage(page) {
|
||||
currentPage.value = page;
|
||||
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
p: currentPage.value !== 1 ? currentPage.value : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFiles(files) {
|
||||
await router.push({
|
||||
name: "type-id-version-version",
|
||||
@@ -197,144 +372,13 @@ async function handleFiles(files) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function copyToClipboard(text) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
.indicator {
|
||||
display: flex;
|
||||
gap: 0.5ch;
|
||||
align-items: center;
|
||||
color: var(--color-text-inactive);
|
||||
}
|
||||
}
|
||||
|
||||
.all-versions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template: "download title supports stats";
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1.25fr 1fr 1fr;
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: bold;
|
||||
justify-content: left;
|
||||
margin-inline: var(--spacing-card-md);
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
column-gap: var(--spacing-card-sm);
|
||||
|
||||
div:first-child {
|
||||
grid-area: download;
|
||||
}
|
||||
|
||||
div:nth-child(2) {
|
||||
grid-area: title;
|
||||
}
|
||||
|
||||
div:nth-child(3) {
|
||||
grid-area: supports;
|
||||
}
|
||||
|
||||
div:nth-child(4) {
|
||||
grid-area: stats;
|
||||
}
|
||||
}
|
||||
|
||||
.version-button {
|
||||
display: grid;
|
||||
grid-template:
|
||||
"download title supports stats"
|
||||
"download metadata supports stats"
|
||||
"download dummy supports stats";
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1.25fr 1fr 1fr;
|
||||
column-gap: var(--spacing-card-sm);
|
||||
justify-content: left;
|
||||
padding: var(--spacing-card-md);
|
||||
|
||||
.download-button {
|
||||
grid-area: download;
|
||||
}
|
||||
.version__title {
|
||||
grid-area: title;
|
||||
font-weight: bold;
|
||||
|
||||
svg {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
.version__metadata {
|
||||
grid-area: metadata;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-card-xs);
|
||||
margin-top: var(--spacing-card-xs);
|
||||
}
|
||||
.version__supports {
|
||||
grid-area: supports;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-xs);
|
||||
}
|
||||
.version__stats {
|
||||
grid-area: stats;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.all-versions {
|
||||
.header {
|
||||
grid-template: "download title";
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr;
|
||||
|
||||
div:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.version-button {
|
||||
grid-template: "download title" "download metadata" "download supports" "download stats";
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr;
|
||||
row-gap: var(--spacing-card-xs);
|
||||
|
||||
.version__supports {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
column-gap: var(--spacing-card-xs);
|
||||
}
|
||||
.version__metadata {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-card-md);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
.multiselect {
|
||||
flex: 1;
|
||||
}
|
||||
.checkbox-outer {
|
||||
min-width: fit-content;
|
||||
}
|
||||
<style scoped>
|
||||
.versions-grid-row {
|
||||
@apply grid grid-cols-[1fr_min-content] gap-4 supports-[grid-template-columns:subgrid]:col-span-full supports-[grid-template-columns:subgrid]:!grid-cols-subgrid sm:grid-cols-[min-content_1fr_1fr_1fr_min-content] xl:grid-cols-[min-content_1fr_1fr_1fr_1fr_1fr_min-content];
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1000,7 +1000,7 @@ useSeoMeta({
|
||||
center 4rem;
|
||||
background-size: cover;
|
||||
padding: 6rem 1rem 12rem 1rem;
|
||||
margin-top: -4rem;
|
||||
margin-top: -5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else-if="canEdit && isEditing === true">
|
||||
<PopoutMenu class="btn" position="bottom" direction="right">
|
||||
<PopoutMenu class="btn">
|
||||
<EditIcon /> {{ formatMessage(messages.editIconButton) }}
|
||||
<template #menu>
|
||||
<span class="icon-edit-menu">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<Button color="primary" @click="$refs.modal_creation.show()">
|
||||
<Button color="primary" @click="(event) => $refs.modal_creation.show(event)">
|
||||
<PlusIcon /> {{ formatMessage(messages.createNewButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -76,8 +76,8 @@ if (error.value) {
|
||||
});
|
||||
}
|
||||
|
||||
const openCreateOrgModal = () => {
|
||||
createOrgModal.value?.show();
|
||||
const openCreateOrgModal = (event) => {
|
||||
createOrgModal.value?.show(event);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="landing-hero">
|
||||
<ModrinthIcon />
|
||||
<ModrinthIcon class="modrinth-icon" />
|
||||
<h1 class="main-header">
|
||||
The place for Minecraft
|
||||
<div class="animate-strong">
|
||||
@@ -22,18 +22,19 @@
|
||||
community.
|
||||
</h2>
|
||||
<div class="button-group">
|
||||
<nuxt-link to="/mods" class="iconified-button brand-button"> Discover mods </nuxt-link>
|
||||
<nuxt-link
|
||||
v-if="!auth.user"
|
||||
to="/auth/sign-up"
|
||||
class="iconified-button outline-button"
|
||||
rel="noopener nofollow"
|
||||
>
|
||||
Sign up
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else to="/dashboard/projects" class="iconified-button outline-button">
|
||||
Go to dashboard
|
||||
</nuxt-link>
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<nuxt-link to="/mods"> <CompassIcon /> Discover mods </nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" type="outlined">
|
||||
<nuxt-link v-if="!auth.user" to="/auth/sign-up" rel="noopener nofollow">
|
||||
<LogInIcon />
|
||||
Sign up
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else to="/dashboard/projects">
|
||||
<DashboardIcon />
|
||||
Go to dashboard
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="users-section-outer">
|
||||
@@ -411,7 +412,7 @@
|
||||
viewBox="0 0 865 512"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="light-height"
|
||||
class="light-height modrinth-icon"
|
||||
>
|
||||
<g clip-path="url(#clip0_419_237)">
|
||||
<rect x="176" width="512" height="512" fill="url(#paint0_linear_419_237)" />
|
||||
@@ -455,7 +456,13 @@
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
v-else
|
||||
viewBox="0 0 512 512"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="modrinth-icon"
|
||||
>
|
||||
<g clip-path="url(#clip0_127_331)">
|
||||
<rect width="512" height="512" fill="url(#paint0_linear_127_331)" />
|
||||
<g style="mix-blend-mode: overlay">
|
||||
@@ -493,18 +500,22 @@
|
||||
Read more about <br />
|
||||
<strong class="main-header-strong">Modrinth</strong>
|
||||
</h2>
|
||||
<a
|
||||
href="https://blog.modrinth.com/?utm_source=website&utm_source=homepage&utm_campaign=newsletter"
|
||||
class="iconified-button brand-button"
|
||||
>
|
||||
Visit the blog
|
||||
</a>
|
||||
<ButtonStyled color="brand">
|
||||
<a
|
||||
href="https://blog.modrinth.com/?utm_source=website&utm_source=homepage&utm_campaign=newsletter"
|
||||
>
|
||||
<NewspaperIcon />
|
||||
Visit the blog
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { CompassIcon, LogInIcon, DashboardIcon, NewspaperIcon } from "@modrinth/assets";
|
||||
import SearchIcon from "~/assets/images/utils/search.svg?component";
|
||||
import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
|
||||
import ModrinthIcon from "~/assets/images/logo.svg?component";
|
||||
@@ -554,7 +565,7 @@ async function updateSearchProjects() {
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
|
||||
svg {
|
||||
.modrinth-icon {
|
||||
width: 13rem;
|
||||
height: 13rem;
|
||||
margin-bottom: 2.5rem;
|
||||
@@ -575,12 +586,6 @@ async function updateSearchProjects() {
|
||||
gap: 1.25rem;
|
||||
margin: 0 auto 5rem;
|
||||
justify-content: center;
|
||||
|
||||
.outline-button {
|
||||
color: var(--landing-color-heading);
|
||||
background: none;
|
||||
border: 1px var(--landing-color-heading) solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1044,7 +1049,7 @@ async function updateSearchProjects() {
|
||||
padding: 1rem 1rem 2rem 1rem;
|
||||
overflow: hidden;
|
||||
|
||||
svg {
|
||||
.modrinth-icon {
|
||||
z-index: 2;
|
||||
width: auto;
|
||||
height: 32rem;
|
||||
@@ -1201,11 +1206,6 @@ async function updateSearchProjects() {
|
||||
}
|
||||
}
|
||||
|
||||
.iconified-button {
|
||||
font-weight: 600;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 560px) {
|
||||
.landing-hero {
|
||||
h2 {
|
||||
@@ -1276,7 +1276,7 @@ async function updateSearchProjects() {
|
||||
font-size: 1.625rem;
|
||||
}
|
||||
|
||||
margin-top: -4rem;
|
||||
margin-top: -5rem;
|
||||
padding: 11.25rem 1rem 12rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ onMounted(() => {
|
||||
.main-hero {
|
||||
background: linear-gradient(360deg, rgba(199, 138, 255, 0.2) 10.92%, var(--color-bg) 100%),
|
||||
var(--color-accent-contrast);
|
||||
margin-top: -4rem;
|
||||
margin-top: -5rem;
|
||||
padding: 11.25rem 1rem 8rem;
|
||||
|
||||
display: flex;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'search-page': true,
|
||||
'normal-page': true,
|
||||
'alt-layout': cosmetics.searchLayout,
|
||||
}"
|
||||
class="new-page sidebar experimental-styles-within"
|
||||
:class="{ 'alt-layout': cosmetics.searchLayout }"
|
||||
>
|
||||
<Head>
|
||||
<Title>Search {{ projectType.display }}s - Modrinth</Title>
|
||||
@@ -12,206 +9,126 @@
|
||||
<aside
|
||||
:class="{
|
||||
'normal-page__sidebar': true,
|
||||
open: sidebarMenuOpen,
|
||||
}"
|
||||
aria-label="Filters"
|
||||
>
|
||||
<section class="card filters-card" role="presentation">
|
||||
<div class="sidebar-menu" :class="{ 'sidebar-menu_open': sidebarMenuOpen }">
|
||||
<AdPlaceholder />
|
||||
<section class="card gap-1" :class="{ 'max-lg:!hidden': !sidebarMenuOpen }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="iconified-input w-full">
|
||||
<label class="hidden" for="search">Search</label>
|
||||
<SearchIcon aria-hidden="true" />
|
||||
<input
|
||||
id="search"
|
||||
v-model="queryFilter"
|
||||
name="search"
|
||||
type="search"
|
||||
placeholder="Search filters..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
:disabled="
|
||||
onlyOpenSource === false &&
|
||||
selectedEnvironments.length === 0 &&
|
||||
selectedVersions.length === 0 &&
|
||||
facets.length === 0 &&
|
||||
orFacets.length === 0
|
||||
v-if="
|
||||
!(
|
||||
onlyOpenSource === false &&
|
||||
selectedEnvironments.length === 0 &&
|
||||
selectedVersions.length === 0 &&
|
||||
facets.length === 0 &&
|
||||
orFacets.length === 0 &&
|
||||
negativeFacets.length === 0
|
||||
)
|
||||
"
|
||||
class="iconified-button"
|
||||
v-tooltip="`Reset all filters`"
|
||||
class="btn icon-only"
|
||||
@click="clearFilters"
|
||||
>
|
||||
<ClearIcon aria-hidden="true" />
|
||||
Clear filters
|
||||
<FilterXIcon />
|
||||
</button>
|
||||
<section aria-label="Category filters">
|
||||
<div v-for="(categories, header) in categoriesMap" :key="header">
|
||||
<h3
|
||||
v-if="categories.filter((x) => x.project_type === projectType.actual).length > 0"
|
||||
class="sidebar-menu-heading"
|
||||
>
|
||||
{{ $formatCategoryHeader(header) }}
|
||||
</h3>
|
||||
|
||||
<template v-if="header === 'resolutions'">
|
||||
<SearchFilter
|
||||
v-for="category in categories.filter(
|
||||
(x) => x.project_type === projectType.actual,
|
||||
)"
|
||||
:key="category.name"
|
||||
:active-filters="orFacets"
|
||||
:display-name="$formatCategory(category.name)"
|
||||
:facet-name="`categories:'${encodeURIComponent(category.name)}'`"
|
||||
:icon="null"
|
||||
@toggle="toggleOrFacet"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<SearchFilter
|
||||
v-for="category in categories.filter(
|
||||
(x) => x.project_type === projectType.actual,
|
||||
)"
|
||||
:key="category.name"
|
||||
:active-filters="facets"
|
||||
:display-name="$formatCategory(category.name)"
|
||||
:facet-name="`categories:'${encodeURIComponent(category.name)}'`"
|
||||
:icon="category.icon"
|
||||
@toggle="toggleFacet"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
v-if="projectType.id !== 'resourcepack' && projectType.id !== 'datapack'"
|
||||
aria-label="Loader filters"
|
||||
>
|
||||
<h3
|
||||
v-if="
|
||||
tags.loaders.filter((x) => x.supported_project_types.includes(projectType.actual))
|
||||
.length > 0
|
||||
"
|
||||
class="sidebar-menu-heading"
|
||||
>
|
||||
Loaders
|
||||
</h3>
|
||||
<SearchFilter
|
||||
v-for="loader in tags.loaders.filter((x) => {
|
||||
if (projectType.id === 'mod') {
|
||||
return (
|
||||
tags.loaderData.modLoaders.includes(x.name) &&
|
||||
!tags.loaderData.hiddenModLoaders.includes(x.name)
|
||||
);
|
||||
} else if (projectType.id === 'plugin') {
|
||||
return tags.loaderData.pluginLoaders.includes(x.name);
|
||||
} else if (projectType.id === 'datapack') {
|
||||
return tags.loaderData.dataPackLoaders.includes(x.name);
|
||||
} else {
|
||||
return x.supported_project_types.includes(projectType.actual);
|
||||
}
|
||||
})"
|
||||
:key="loader.name"
|
||||
ref="loaderFilters"
|
||||
:active-filters="orFacets"
|
||||
:display-name="$formatCategory(loader.name)"
|
||||
:facet-name="`categories:'${encodeURIComponent(loader.name)}'`"
|
||||
:icon="loader.icon"
|
||||
@toggle="toggleOrFacet"
|
||||
/>
|
||||
<template v-if="projectType.id === 'mod' && showAllLoaders">
|
||||
<SearchFilter
|
||||
v-for="loader in tags.loaders.filter((x) => {
|
||||
return (
|
||||
tags.loaderData.modLoaders.includes(x.name) &&
|
||||
tags.loaderData.hiddenModLoaders.includes(x.name)
|
||||
);
|
||||
})"
|
||||
:key="loader.name"
|
||||
ref="loaderFilters"
|
||||
:active-filters="orFacets"
|
||||
:display-name="$formatCategory(loader.name)"
|
||||
:facet-name="`categories:'${encodeURIComponent(loader.name)}'`"
|
||||
:icon="loader.icon"
|
||||
@toggle="toggleOrFacet"
|
||||
/>
|
||||
</template>
|
||||
<Checkbox
|
||||
v-if="projectType.id === 'mod'"
|
||||
v-model="showAllLoaders"
|
||||
:label="showAllLoaders ? 'Less' : 'More'"
|
||||
description="Show all loaders"
|
||||
style="margin-bottom: 0.5rem"
|
||||
:border="false"
|
||||
:collapsing-toggle-style="true"
|
||||
/>
|
||||
</section>
|
||||
<section v-if="projectType.id === 'plugin'" aria-label="Platform loader filters">
|
||||
<h3
|
||||
v-if="
|
||||
tags.loaders.filter((x) => x.supported_project_types.includes(projectType.actual))
|
||||
.length > 0
|
||||
"
|
||||
class="sidebar-menu-heading"
|
||||
>
|
||||
Proxies
|
||||
</h3>
|
||||
<SearchFilter
|
||||
v-for="loader in tags.loaders.filter((x) =>
|
||||
tags.loaderData.pluginPlatformLoaders.includes(x.name),
|
||||
)"
|
||||
:key="loader.name"
|
||||
ref="platformFilters"
|
||||
:active-filters="orFacets"
|
||||
:display-name="$formatCategory(loader.name)"
|
||||
:facet-name="`categories:'${encodeURIComponent(loader.name)}'`"
|
||||
:icon="loader.icon"
|
||||
@toggle="toggleOrFacet"
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
v-if="!['resourcepack', 'plugin', 'shader', 'datapack'].includes(projectType.id)"
|
||||
aria-label="Environment filters"
|
||||
>
|
||||
<h3 class="sidebar-menu-heading">Environments</h3>
|
||||
<SearchFilter
|
||||
:active-filters="selectedEnvironments"
|
||||
display-name="Client"
|
||||
facet-name="client"
|
||||
@toggle="toggleEnv"
|
||||
>
|
||||
<ClientIcon aria-hidden="true" />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="selectedEnvironments"
|
||||
display-name="Server"
|
||||
facet-name="server"
|
||||
@toggle="toggleEnv"
|
||||
>
|
||||
<ServerIcon aria-hidden="true" />
|
||||
</SearchFilter>
|
||||
</section>
|
||||
<h3 class="sidebar-menu-heading">Minecraft versions</h3>
|
||||
<Checkbox
|
||||
v-model="showSnapshots"
|
||||
label="Show all versions"
|
||||
description="Show all versions"
|
||||
style="margin-bottom: 0.5rem"
|
||||
:border="false"
|
||||
/>
|
||||
<multiselect
|
||||
v-model="selectedVersions"
|
||||
:options="
|
||||
showSnapshots
|
||||
? tags.gameVersions.map((x) => x.version)
|
||||
: tags.gameVersions
|
||||
.filter((it) => it.version_type === 'release')
|
||||
.map((x) => x.version)
|
||||
</div>
|
||||
<div
|
||||
v-for="(categories, header, index) in filters"
|
||||
:key="header"
|
||||
:class="`border-0 border-b border-solid border-button-bg py-2 last:border-b-0`"
|
||||
>
|
||||
<button
|
||||
class="flex !w-full bg-transparent px-0 py-2 font-extrabold text-contrast transition-all active:scale-[0.98]"
|
||||
@click="
|
||||
() => {
|
||||
filterAccordions[index].isOpen
|
||||
? filterAccordions[index].close()
|
||||
: filterAccordions[index].open();
|
||||
}
|
||||
"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-search-on-select="false"
|
||||
:show-labels="false"
|
||||
:selectable="() => selectedVersions.length <= 6"
|
||||
placeholder="Choose versions..."
|
||||
@update:model-value="onSearchChange(1)"
|
||||
/>
|
||||
<h3 class="sidebar-menu-heading">Open source</h3>
|
||||
<Checkbox
|
||||
v-model="onlyOpenSource"
|
||||
label="Open source only"
|
||||
style="margin-bottom: 0.5rem"
|
||||
:border="false"
|
||||
@update:model-value="onSearchChange(1)"
|
||||
/>
|
||||
>
|
||||
<template v-if="header === 'gameVersion'"> Game versions </template>
|
||||
<template v-else>
|
||||
{{ $formatCategoryHeader(header) }}
|
||||
</template>
|
||||
<DropdownIcon
|
||||
class="ml-auto h-5 w-5 transition-transform"
|
||||
:class="{ 'rotate-180': filterAccordions[index]?.isOpen }"
|
||||
/>
|
||||
</button>
|
||||
<Accordion ref="filterAccordions" :open-by-default="true">
|
||||
<ScrollablePanel
|
||||
:class="{ 'h-[18rem]': categories.length >= 8 && header === 'gameVersion' }"
|
||||
:no-max-height="header !== 'gameVersion'"
|
||||
>
|
||||
<div class="mr-1 flex flex-col gap-1">
|
||||
<div v-for="category in categories" :key="category.name" class="group flex gap-1">
|
||||
<button
|
||||
:class="`flex !w-full items-center gap-2 truncate rounded-xl px-2 py-1 text-sm font-semibold transition-all active:scale-[0.98] ${filterSelected(category) ? 'bg-brand-highlight text-contrast hover:brightness-125' : negativeFilterSelected(category) ? 'bg-highlight-red text-contrast hover:brightness-125' : 'bg-transparent text-secondary hover:bg-button-bg'}`"
|
||||
@click="
|
||||
negativeFilterSelected(category)
|
||||
? toggleNegativeFilter(category)
|
||||
: toggleFilter(category)
|
||||
"
|
||||
>
|
||||
<ClientIcon v-if="category.name === 'client'" class="h-4 w-4" />
|
||||
<ServerIcon v-else-if="category.name === 'server'" class="h-4 w-4" />
|
||||
<div v-if="category.icon" class="h-4" v-html="category.icon" />
|
||||
<span class="truncate text-sm">{{ $formatCategory(category.name) }}</span>
|
||||
<BanIcon
|
||||
v-if="negativeFilterSelected(category)"
|
||||
:class="`ml-auto h-4 w-4 shrink-0 transition-opacity group-hover:opacity-100 ${negativeFilterSelected(category) ? '' : 'opacity-0'}`"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckIcon
|
||||
v-else
|
||||
:class="`ml-auto h-4 w-4 shrink-0 transition-opacity group-hover:opacity-100 ${filterSelected(category) ? '' : 'opacity-0'}`"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
(category.type === 'or' || category.type === 'normal') &&
|
||||
!negativeFilterSelected(category)
|
||||
"
|
||||
v-tooltip="negativeFilterSelected(category) ? 'Include' : 'Exclude'"
|
||||
class="hidden items-center justify-center gap-2 rounded-xl bg-transparent px-2 py-1 text-sm font-semibold text-secondary transition-all hover:bg-button-bg hover:text-red active:scale-[0.96] group-hover:flex"
|
||||
@click="toggleNegativeFilter(category)"
|
||||
>
|
||||
<BanIcon
|
||||
class="h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollablePanel>
|
||||
<Checkbox
|
||||
v-if="header === 'gameVersion'"
|
||||
v-model="showSnapshots"
|
||||
class="mx-2"
|
||||
:label="`Show all versions`"
|
||||
/>
|
||||
<Checkbox
|
||||
v-if="header === 'loaders' && projectType.id === 'mod'"
|
||||
v-model="showAllLoaders"
|
||||
class="mx-2"
|
||||
:label="`Show all loaders`"
|
||||
/>
|
||||
</Accordion>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
@@ -288,13 +205,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="pageCount"
|
||||
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
|
||||
class="pagination-before"
|
||||
@switch-page="onSearchChange"
|
||||
/>
|
||||
<LogoAnimated v-if="searchLoading && !noLoad" />
|
||||
<div v-else-if="results && results.hits && results.hits.length === 0" class="no-results">
|
||||
<p>No results found for your query!</p>
|
||||
@@ -332,38 +242,41 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<pagination
|
||||
:page="currentPage"
|
||||
:count="pageCount"
|
||||
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
|
||||
class="pagination-after"
|
||||
@switch-page="onSearchChangeToTop"
|
||||
/>
|
||||
<div class="pagination-after">
|
||||
<pagination
|
||||
:page="currentPage"
|
||||
:count="pageCount"
|
||||
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
|
||||
class="justify-end"
|
||||
@switch-page="onSearchChangeToTop"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Multiselect } from "vue-multiselect";
|
||||
import { Promotion } from "@modrinth/ui";
|
||||
import { Promotion, Pagination, ScrollablePanel, Checkbox } from "@modrinth/ui";
|
||||
import { BanIcon, DropdownIcon, CheckIcon, FilterXIcon } from "@modrinth/assets";
|
||||
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||
import Pagination from "~/components/ui/Pagination.vue";
|
||||
import SearchFilter from "~/components/ui/search/SearchFilter.vue";
|
||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
|
||||
|
||||
import ClientIcon from "~/assets/images/categories/client.svg?component";
|
||||
import ServerIcon from "~/assets/images/categories/server.svg?component";
|
||||
|
||||
import SearchIcon from "~/assets/images/utils/search.svg?component";
|
||||
import ClearIcon from "~/assets/images/utils/clear.svg?component";
|
||||
import FilterIcon from "~/assets/images/utils/filter.svg?component";
|
||||
import GridIcon from "~/assets/images/utils/grid.svg?component";
|
||||
import ListIcon from "~/assets/images/utils/list.svg?component";
|
||||
import ImageIcon from "~/assets/images/utils/image.svg?component";
|
||||
import Accordion from "~/components/ui/Accordion.vue";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
|
||||
const sidebarMenuOpen = ref(false);
|
||||
const showAllLoaders = ref(false);
|
||||
|
||||
const filterAccordions = ref([]);
|
||||
|
||||
const data = useNuxtApp();
|
||||
const route = useNativeRoute();
|
||||
|
||||
@@ -374,6 +287,7 @@ const auth = await useAuth();
|
||||
const query = ref("");
|
||||
const facets = ref([]);
|
||||
const orFacets = ref([]);
|
||||
const negativeFacets = ref([]);
|
||||
const selectedVersions = ref([]);
|
||||
const onlyOpenSource = ref(false);
|
||||
const showSnapshots = ref(false);
|
||||
@@ -413,6 +327,9 @@ if (route.query.f) {
|
||||
if (route.query.g) {
|
||||
orFacets.value = getArrayOrString(route.query.g);
|
||||
}
|
||||
if (route.query.nf) {
|
||||
negativeFacets.value = getArrayOrString(route.query.nf);
|
||||
}
|
||||
if (route.query.v) {
|
||||
selectedVersions.value = getArrayOrString(route.query.v);
|
||||
}
|
||||
@@ -477,6 +394,7 @@ const {
|
||||
if (
|
||||
facets.value.length > 0 ||
|
||||
orFacets.value.length > 0 ||
|
||||
negativeFacets.value.length > 0 ||
|
||||
selectedVersions.value.length > 0 ||
|
||||
selectedEnvironments.value.length > 0 ||
|
||||
projectType.value
|
||||
@@ -486,6 +404,10 @@ const {
|
||||
formattedFacets.push([facet]);
|
||||
}
|
||||
|
||||
for (const facet of negativeFacets.value) {
|
||||
formattedFacets.push([facet.replace(":", "!=")]);
|
||||
}
|
||||
|
||||
// loaders specifier
|
||||
if (orFacets.value.length > 0) {
|
||||
formattedFacets.push(orFacets.value);
|
||||
@@ -616,6 +538,10 @@ function getSearchUrl(offset, useObj) {
|
||||
queryItems.push(`g=${encodeURIComponent(orFacets.value)}`);
|
||||
obj.g = orFacets.value;
|
||||
}
|
||||
if (negativeFacets.value.length > 0) {
|
||||
queryItems.push(`nf=${encodeURIComponent(negativeFacets.value)}`);
|
||||
obj.nf = negativeFacets.value;
|
||||
}
|
||||
if (selectedVersions.value.length > 0) {
|
||||
queryItems.push(`v=${encodeURIComponent(selectedVersions.value)}`);
|
||||
obj.v = selectedVersions.value;
|
||||
@@ -654,103 +580,16 @@ function getSearchUrl(offset, useObj) {
|
||||
return useObj ? obj : url;
|
||||
}
|
||||
|
||||
const categoriesMap = computed(() => {
|
||||
const categories = {};
|
||||
|
||||
for (const category of data.$sortedCategories()) {
|
||||
if (categories[category.header]) {
|
||||
categories[category.header].push(category);
|
||||
} else {
|
||||
categories[category.header] = [category];
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(categories).reduce((obj, key) => {
|
||||
obj[key] = categories[key];
|
||||
return obj;
|
||||
}, {});
|
||||
});
|
||||
|
||||
function clearFilters() {
|
||||
for (const facet of [...facets.value]) {
|
||||
toggleFacet(facet, true);
|
||||
}
|
||||
for (const facet of [...orFacets.value]) {
|
||||
toggleOrFacet(facet, true);
|
||||
}
|
||||
|
||||
facets.value = [];
|
||||
orFacets.value = [];
|
||||
negativeFacets.value = [];
|
||||
onlyOpenSource.value = false;
|
||||
selectedVersions.value = [];
|
||||
selectedEnvironments.value = [];
|
||||
onSearchChange(1);
|
||||
}
|
||||
|
||||
function toggleFacet(elementName, doNotSendRequest = false) {
|
||||
const index = facets.value.indexOf(elementName);
|
||||
|
||||
if (index !== -1) {
|
||||
facets.value.splice(index, 1);
|
||||
} else {
|
||||
facets.value.push(elementName);
|
||||
}
|
||||
|
||||
if (!doNotSendRequest) {
|
||||
onSearchChange(1);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleOrFacet(elementName, doNotSendRequest) {
|
||||
const index = orFacets.value.indexOf(elementName);
|
||||
if (index !== -1) {
|
||||
orFacets.value.splice(index, 1);
|
||||
} else {
|
||||
if (elementName === "categories:purpur") {
|
||||
if (!orFacets.value.includes("categories:paper")) {
|
||||
orFacets.value.push("categories:paper");
|
||||
}
|
||||
if (!orFacets.value.includes("categories:spigot")) {
|
||||
orFacets.value.push("categories:spigot");
|
||||
}
|
||||
if (!orFacets.value.includes("categories:bukkit")) {
|
||||
orFacets.value.push("categories:bukkit");
|
||||
}
|
||||
} else if (elementName === "categories:paper") {
|
||||
if (!orFacets.value.includes("categories:spigot")) {
|
||||
orFacets.value.push("categories:spigot");
|
||||
}
|
||||
if (!orFacets.value.includes("categories:bukkit")) {
|
||||
orFacets.value.push("categories:bukkit");
|
||||
}
|
||||
} else if (elementName === "categories:spigot") {
|
||||
if (!orFacets.value.includes("categories:bukkit")) {
|
||||
orFacets.value.push("categories:bukkit");
|
||||
}
|
||||
} else if (elementName === "categories:waterfall") {
|
||||
if (!orFacets.value.includes("categories:bungeecord")) {
|
||||
orFacets.value.push("categories:bungeecord");
|
||||
}
|
||||
}
|
||||
orFacets.value.push(elementName);
|
||||
}
|
||||
|
||||
if (!doNotSendRequest) {
|
||||
onSearchChange(1);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleEnv(environment, sendRequest) {
|
||||
const index = selectedEnvironments.value.indexOf(environment);
|
||||
if (index !== -1) {
|
||||
selectedEnvironments.value.splice(index, 1);
|
||||
} else {
|
||||
selectedEnvironments.value.push(environment);
|
||||
}
|
||||
|
||||
if (!sendRequest) {
|
||||
onSearchChange(1);
|
||||
}
|
||||
}
|
||||
|
||||
function onSearchChangeToTop(newPageNumber) {
|
||||
if (import.meta.client) {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
@@ -796,6 +635,225 @@ function setClosestMaxResults() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const queryFilter = ref("");
|
||||
const filters = computed(() => {
|
||||
const filters = {};
|
||||
|
||||
if (projectType.value.id !== "resourcepack" && projectType.value.id !== "datapack") {
|
||||
const loaders = tags.value.loaders
|
||||
.filter((x) => {
|
||||
if (projectType.value.id === "mod" && !showAllLoaders.value) {
|
||||
return (
|
||||
tags.value.loaderData.modLoaders.includes(x.name) &&
|
||||
!tags.value.loaderData.hiddenModLoaders.includes(x.name)
|
||||
);
|
||||
} else if (projectType.value.id === "mod" && showAllLoaders.value) {
|
||||
return tags.value.loaderData.modLoaders.includes(x.name);
|
||||
} else if (projectType.value.id === "plugin") {
|
||||
return tags.value.loaderData.pluginLoaders.includes(x.name);
|
||||
} else if (projectType.value.id === "datapack") {
|
||||
return tags.value.loaderData.dataPackLoaders.includes(x.name);
|
||||
} else {
|
||||
return x.supported_project_types.includes(projectType.value.actual);
|
||||
}
|
||||
})
|
||||
.slice();
|
||||
|
||||
loaders.sort((a, b) => {
|
||||
const isAHidden = tags.value.loaderData.hiddenModLoaders.includes(a.name);
|
||||
const isBHidden = tags.value.loaderData.hiddenModLoaders.includes(b.name);
|
||||
|
||||
// Sort hidden mod loaders (true) after visible ones (false)
|
||||
if (isAHidden && !isBHidden) return 1;
|
||||
if (!isAHidden && isBHidden) return -1;
|
||||
return 0; // No sorting if both are hidden or both are visible
|
||||
});
|
||||
|
||||
if (loaders.length > 0) {
|
||||
filters.loaders = loaders.map((x) => ({
|
||||
icon: x.icon,
|
||||
name: x.name,
|
||||
type: "or",
|
||||
facet: `categories:${x.name}`,
|
||||
}));
|
||||
}
|
||||
|
||||
if (projectType.value.id === "plugin") {
|
||||
const platforms = tags.value.loaders.filter((x) =>
|
||||
tags.value.loaderData.pluginPlatformLoaders.includes(x.name),
|
||||
);
|
||||
|
||||
filters.platforms = platforms.map((x) => ({
|
||||
icon: x.icon,
|
||||
name: x.name,
|
||||
type: "or",
|
||||
facet: `categories:${x.name}`,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
filters.gameVersion = tags.value.gameVersions
|
||||
.filter((x) => (showSnapshots.value ? true : x.version_type === "release"))
|
||||
.map((x) => ({ name: x.version, type: "gameVersion" }));
|
||||
|
||||
if (!["resourcepack", "plugin", "shader", "datapack"].includes(projectType.value.id)) {
|
||||
filters.environment = [
|
||||
{ name: "client", type: "env" },
|
||||
{ name: "server", type: "env" },
|
||||
];
|
||||
}
|
||||
|
||||
for (const category of data.$sortedCategories()) {
|
||||
if (category.project_type === projectType.value.actual) {
|
||||
const parsedCategory = {
|
||||
name: category.name,
|
||||
icon: category.icon,
|
||||
facet: `categories:${category.name}`,
|
||||
type: category.header === "resolutions" ? "or" : "normal",
|
||||
};
|
||||
|
||||
if (filters[category.header]) {
|
||||
filters[category.header].push(parsedCategory);
|
||||
} else {
|
||||
filters[category.header] = [parsedCategory];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filters.license = [{ name: "Open source only", type: "license" }];
|
||||
|
||||
const filteredObj = {};
|
||||
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
const filters = queryFilter.value
|
||||
? value.filter((x) => x.name.toLowerCase().includes(queryFilter.value.toLowerCase()))
|
||||
: value;
|
||||
|
||||
if (filters.length > 0) {
|
||||
filteredObj[key] = filters;
|
||||
}
|
||||
}
|
||||
|
||||
return filteredObj;
|
||||
});
|
||||
|
||||
function filterSelected(filter) {
|
||||
if (filter.type === "or") {
|
||||
return orFacets.value.includes(filter.facet);
|
||||
} else if (filter.type === "normal") {
|
||||
return facets.value.includes(filter.facet);
|
||||
} else if (filter.type === "env") {
|
||||
return selectedEnvironments.value.includes(filter.name);
|
||||
} else if (filter.type === "gameVersion") {
|
||||
return selectedVersions.value.includes(filter.name);
|
||||
} else if (filter.type === "license") {
|
||||
return onlyOpenSource.value;
|
||||
}
|
||||
}
|
||||
|
||||
function negativeFilterSelected(filter) {
|
||||
if (filter.type === "or" || filter.type === "normal") {
|
||||
return negativeFacets.value.includes(filter.facet);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleNegativeFilter(filter) {
|
||||
const elementName = filter.facet;
|
||||
|
||||
if (filterSelected(filter)) {
|
||||
if (filter.type === "or") {
|
||||
const index = orFacets.value.indexOf(elementName);
|
||||
orFacets.value.splice(index, 1);
|
||||
} else if (filter.type === "normal") {
|
||||
const index = facets.value.indexOf(elementName);
|
||||
facets.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.type === "or" || filter.type === "normal") {
|
||||
const index = negativeFacets.value.indexOf(elementName);
|
||||
if (index !== -1) {
|
||||
negativeFacets.value.splice(index, 1);
|
||||
} else {
|
||||
negativeFacets.value.push(elementName);
|
||||
}
|
||||
}
|
||||
|
||||
onSearchChange(1);
|
||||
}
|
||||
|
||||
function toggleFilter(filter, doNotSendRequest) {
|
||||
const elementName = filter.facet;
|
||||
|
||||
if (negativeFilterSelected(filter)) {
|
||||
const index = negativeFacets.value.indexOf(elementName);
|
||||
negativeFacets.value.splice(index, 1);
|
||||
}
|
||||
|
||||
if (filter.type === "or") {
|
||||
const index = orFacets.value.indexOf(elementName);
|
||||
if (index !== -1) {
|
||||
orFacets.value.splice(index, 1);
|
||||
} else {
|
||||
if (elementName === "categories:purpur") {
|
||||
if (!orFacets.value.includes("categories:paper")) {
|
||||
orFacets.value.push("categories:paper");
|
||||
}
|
||||
if (!orFacets.value.includes("categories:spigot")) {
|
||||
orFacets.value.push("categories:spigot");
|
||||
}
|
||||
if (!orFacets.value.includes("categories:bukkit")) {
|
||||
orFacets.value.push("categories:bukkit");
|
||||
}
|
||||
} else if (elementName === "categories:paper") {
|
||||
if (!orFacets.value.includes("categories:spigot")) {
|
||||
orFacets.value.push("categories:spigot");
|
||||
}
|
||||
if (!orFacets.value.includes("categories:bukkit")) {
|
||||
orFacets.value.push("categories:bukkit");
|
||||
}
|
||||
} else if (elementName === "categories:spigot") {
|
||||
if (!orFacets.value.includes("categories:bukkit")) {
|
||||
orFacets.value.push("categories:bukkit");
|
||||
}
|
||||
} else if (elementName === "categories:waterfall") {
|
||||
if (!orFacets.value.includes("categories:bungeecord")) {
|
||||
orFacets.value.push("categories:bungeecord");
|
||||
}
|
||||
}
|
||||
orFacets.value.push(elementName);
|
||||
}
|
||||
} else if (filter.type === "normal") {
|
||||
const index = facets.value.indexOf(elementName);
|
||||
|
||||
if (index !== -1) {
|
||||
facets.value.splice(index, 1);
|
||||
} else {
|
||||
facets.value.push(elementName);
|
||||
}
|
||||
} else if (filter.type === "env") {
|
||||
const index = selectedEnvironments.value.indexOf(filter.name);
|
||||
if (index !== -1) {
|
||||
selectedEnvironments.value.splice(index, 1);
|
||||
} else {
|
||||
selectedEnvironments.value.push(filter.name);
|
||||
}
|
||||
} else if (filter.type === "gameVersion") {
|
||||
const index = selectedVersions.value.indexOf(filter.name);
|
||||
if (index !== -1) {
|
||||
selectedVersions.value.splice(index, 1);
|
||||
} else {
|
||||
selectedVersions.value.push(filter.name);
|
||||
}
|
||||
} else if (filter.type === "license") {
|
||||
onlyOpenSource.value = !onlyOpenSource.value;
|
||||
}
|
||||
|
||||
if (!doNotSendRequest) {
|
||||
onSearchChange(1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -816,12 +874,6 @@ function setClosestMaxResults() {
|
||||
@media screen and (min-width: 1024px) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Hide on mobile unless open
|
||||
display: none;
|
||||
&.open {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.filters-card {
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<div v-if="false" class="adjacent-input small">
|
||||
<label for="modrinth-app-promos">
|
||||
<span class="label__title">
|
||||
{{ formatMessage(toggleFeatures.hideModrinthAppPromosTitle) }}
|
||||
@@ -190,10 +190,10 @@
|
||||
<div class="adjacent-input small">
|
||||
<label for="search-layout-toggle">
|
||||
<span class="label__title">
|
||||
{{ formatMessage(toggleFeatures.rightAlignedSearchSidebarTitle) }}
|
||||
{{ formatMessage(toggleFeatures.leftAlignedSearchSidebarTitle) }}
|
||||
</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(toggleFeatures.rightAlignedSearchSidebarDescription) }}
|
||||
{{ formatMessage(toggleFeatures.leftAlignedSearchSidebarDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -206,10 +206,10 @@
|
||||
<div class="adjacent-input small">
|
||||
<label for="project-layout-toggle">
|
||||
<span class="label__title">
|
||||
{{ formatMessage(toggleFeatures.rightAlignedProjectSidebarTitle) }}
|
||||
{{ formatMessage(toggleFeatures.leftAlignedProjectSidebarTitle) }}
|
||||
</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(toggleFeatures.rightAlignedProjectSidebarDescription) }}
|
||||
{{ formatMessage(toggleFeatures.leftAlignedProjectSidebarDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -368,21 +368,21 @@ const toggleFeatures = defineMessages({
|
||||
defaultMessage:
|
||||
'Hides the "Get Modrinth App" buttons from primary navigation. The Modrinth App page can still be found on the landing page or in the footer.',
|
||||
},
|
||||
rightAlignedSearchSidebarTitle: {
|
||||
id: "settings.display.sidebar.right-aligned-search-sidebar.title",
|
||||
defaultMessage: "Right-aligned search sidebar",
|
||||
leftAlignedSearchSidebarTitle: {
|
||||
id: "settings.display.sidebar.Left-aligned-search-sidebar.title",
|
||||
defaultMessage: "Left-aligned search sidebar",
|
||||
},
|
||||
rightAlignedSearchSidebarDescription: {
|
||||
id: "settings.display.sidebar.right-aligned-search-sidebar.description",
|
||||
defaultMessage: "Aligns the search filters sidebar to the right of the search results.",
|
||||
leftAlignedSearchSidebarDescription: {
|
||||
id: "settings.display.sidebar.left-aligned-search-sidebar.description",
|
||||
defaultMessage: "Aligns the search filters sidebar to the left of the search results.",
|
||||
},
|
||||
rightAlignedProjectSidebarTitle: {
|
||||
id: "settings.display.sidebar.right-aligned-project-sidebar.title",
|
||||
defaultMessage: "Right-aligned project sidebar",
|
||||
leftAlignedProjectSidebarTitle: {
|
||||
id: "settings.display.sidebar.left-aligned-project-sidebar.title",
|
||||
defaultMessage: "Left-aligned project sidebar",
|
||||
},
|
||||
rightAlignedProjectSidebarDescription: {
|
||||
id: "settings.display.sidebar.right-aligned-project-sidebar.description",
|
||||
defaultMessage: "Aligns the project details sidebar to the right of the page's content.",
|
||||
leftAlignedProjectSidebarDescription: {
|
||||
id: "settings.display.sidebar.left-aligned-project-sidebar.description",
|
||||
defaultMessage: "Aligns the project details sidebar to the left of the page's content.",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,169 +1,81 @@
|
||||
<template>
|
||||
<div v-if="user">
|
||||
<div v-if="user" class="experimental-styles-within">
|
||||
<ModalCreation ref="modal_creation" />
|
||||
<CollectionCreateModal ref="modal_collection_creation" />
|
||||
<div class="user-header-wrapper">
|
||||
<div class="user-header">
|
||||
<Avatar :src="user.avatar_url" size="md" circle :alt="user.username" />
|
||||
<h1 class="username">
|
||||
{{ user.username }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<div class="card sidebar">
|
||||
<h1 class="mobile-username">
|
||||
{{ user.username }}
|
||||
</h1>
|
||||
<div class="card__overlay">
|
||||
<NuxtLink
|
||||
v-if="auth.user && auth.user.id === user.id"
|
||||
to="/settings/profile"
|
||||
class="iconified-button"
|
||||
>
|
||||
<EditIcon />
|
||||
{{ formatMessage(commonMessages.editButton) }}
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-else-if="auth.user"
|
||||
class="iconified-button"
|
||||
@click="() => reportUser(user.id)"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.profileReportButton) }}
|
||||
</button>
|
||||
<nuxt-link v-else class="iconified-button" to="/auth/sign-in">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.profileReportButton) }}
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="sidebar__item">
|
||||
<Badge v-if="tags.staffRoles.includes(user.role)" :type="user.role" />
|
||||
<Badge v-else-if="isPermission(user.badges, 1 << 0)" type="plus" />
|
||||
<Badge v-else-if="projects.length > 0" type="creator" />
|
||||
</div>
|
||||
<span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span>
|
||||
<hr class="card-divider" />
|
||||
<div class="primary-stat">
|
||||
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<IntlFormatted
|
||||
:message-id="messages.profileDownloadsStats"
|
||||
:values="{ count: formatCompactNumber(sumDownloads) }"
|
||||
>
|
||||
<template #stat="{ children }">
|
||||
<span class="primary-stat__counter">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
<div class="primary-stat">
|
||||
<HeartIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<IntlFormatted
|
||||
:message-id="messages.profileProjectsFollowersStats"
|
||||
:values="{ count: formatCompactNumber(sumFollows) }"
|
||||
>
|
||||
<template #stat="{ children }">
|
||||
<span class="primary-stat__counter">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<SunriseIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span
|
||||
v-tooltip="
|
||||
formatMessage(commonMessages.dateAtTimeTooltip, {
|
||||
date: new Date(user.created),
|
||||
time: new Date(user.created),
|
||||
})
|
||||
"
|
||||
class="secondary-stat__text date"
|
||||
>
|
||||
{{
|
||||
formatMessage(messages.profileJoinedAt, { ago: formatRelativeTime(user.created) })
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<UserIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span class="secondary-stat__text">
|
||||
<IntlFormatted :message-id="messages.profileUserId">
|
||||
<template #~id>
|
||||
<CopyCode :text="user.id" />
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
</div>
|
||||
<template v-if="organizations.length > 0">
|
||||
<hr class="card-divider" />
|
||||
<div class="stats-block__item">
|
||||
<IntlFormatted :message-id="messages.profileOrganizations" />
|
||||
<div class="organizations-grid">
|
||||
<nuxt-link
|
||||
v-for="org in organizations"
|
||||
:key="org.id"
|
||||
v-tooltip="org.name"
|
||||
class="organization"
|
||||
:to="`/organization/${org.slug}`"
|
||||
>
|
||||
<Avatar :src="org.icon_url" :alt="'Icon for ' + org.name" size="xs" />
|
||||
</nuxt-link>
|
||||
<div class="new-page sidebar">
|
||||
<div class="normal-page__header pt-4">
|
||||
<div
|
||||
class="mb-4 grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<Avatar :src="user.avatar_url" :alt="user.username" size="96px" circle />
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
|
||||
{{ user.username }}
|
||||
</h1>
|
||||
</div>
|
||||
<p class="m-0 line-clamp-2 max-w-[40rem]">
|
||||
{{
|
||||
user.bio
|
||||
? user.bio
|
||||
: projects.length === 0
|
||||
? "A Modrinth user."
|
||||
: "A Modrinth creator."
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center gap-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<ButtonStyled size="large">
|
||||
<NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
|
||||
<EditIcon />
|
||||
{{ formatMessage(commonMessages.editButton) }}
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'manage-projects',
|
||||
action: () => navigateTo('/dashboard/projects'),
|
||||
hoverOnly: true,
|
||||
shown: auth.user && auth.user.id === user.id,
|
||||
},
|
||||
{ divider: true, shown: auth.user && auth.user.id === user.id },
|
||||
{
|
||||
id: 'report',
|
||||
action: () => reportUser(user.id),
|
||||
color: 'red',
|
||||
hoverOnly: true,
|
||||
},
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #manage-projects>
|
||||
<BoxIcon />
|
||||
{{ formatMessage(messages.profileManageProjectsButton) }}
|
||||
</template>
|
||||
<template #report>
|
||||
<ReportIcon />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</template>
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
|
||||
<NavTabs :links="navLinks" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<Promotion v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0)" :external="false" />
|
||||
<nav class="navigation-card">
|
||||
<NavRow
|
||||
:links="[
|
||||
{
|
||||
label: formatMessage(commonMessages.allProjectType),
|
||||
href: `/user/${user.username}`,
|
||||
},
|
||||
...projectTypes.map((x) => {
|
||||
return {
|
||||
label: formatMessage(getProjectTypeMessage(x, true)),
|
||||
href: `/user/${user.username}/${x}s`,
|
||||
};
|
||||
}),
|
||||
]"
|
||||
/>
|
||||
<div class="input-group">
|
||||
<NuxtLink
|
||||
v-if="auth.user && auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
to="/dashboard/projects"
|
||||
>
|
||||
<SettingsIcon />
|
||||
{{ formatMessage(messages.profileManageProjectsButton) }}
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-if="route.params.projectType !== 'collections'"
|
||||
v-tooltip="
|
||||
formatMessage(commonMessages[`${cosmetics.searchDisplayMode.user}InputView`])
|
||||
"
|
||||
:aria-label="
|
||||
formatMessage(commonMessages[`${cosmetics.searchDisplayMode.user}InputView`])
|
||||
"
|
||||
class="square-button"
|
||||
@click="cycleSearchDisplayMode()"
|
||||
>
|
||||
<GridIcon v-if="cosmetics.searchDisplayMode.user === 'grid'" />
|
||||
<ImageIcon v-else-if="cosmetics.searchDisplayMode.user === 'gallery'" />
|
||||
<ListIcon v-else />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div v-if="projects.length > 0">
|
||||
<div
|
||||
v-if="route.params.projectType !== 'collections'"
|
||||
@@ -268,7 +180,10 @@
|
||||
<span v-if="auth.user && auth.user.id === user.id" class="preserve-lines text">
|
||||
<IntlFormatted :message-id="messages.profileNoCollectionsAuthLabel">
|
||||
<template #create-link="{ children }">
|
||||
<a class="link" @click.prevent="$refs.modal_collection_creation.show()">
|
||||
<a
|
||||
class="link"
|
||||
@click.prevent="(event) => $refs.modal_collection_creation.show(event)"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
@@ -277,33 +192,135 @@
|
||||
<span v-else class="text">{{ formatMessage(messages.profileNoCollectionsLabel) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page__sidebar">
|
||||
<AdPlaceholder />
|
||||
<div class="card flex-card">
|
||||
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileDetails) }}</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<BoxIcon aria-hidden="true" class="stroke-[3] text-secondary" />
|
||||
<div class="text-secondary">
|
||||
<IntlFormatted
|
||||
:message-id="messages.profileProjectsStats"
|
||||
:values="{ count: formatCompactNumber(projects.length) }"
|
||||
>
|
||||
<template #stat="{ children }">
|
||||
<span class="font-bold text-primary">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<DownloadIcon aria-hidden="true" class="stroke-[3] text-secondary" />
|
||||
<div class="text-secondary">
|
||||
<IntlFormatted
|
||||
:message-id="messages.profileDownloadsStats"
|
||||
:values="{ count: formatCompactNumber(sumDownloads) }"
|
||||
>
|
||||
<template #stat="{ children }">
|
||||
<span class="font-bold text-primary">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<HeartIcon aria-hidden="true" class="text-secondary *:stroke-[3]" />
|
||||
<div class="text-secondary">
|
||||
<IntlFormatted
|
||||
:message-id="messages.profileProjectsFollowersStats"
|
||||
:values="{ count: formatCompactNumber(sumFollows) }"
|
||||
>
|
||||
<template #stat="{ children }">
|
||||
<span class="font-bold text-primary">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<CalendarIcon aria-hidden="true" class="text-secondary *:stroke-[3]" />
|
||||
<div class="text-secondary">
|
||||
<IntlFormatted
|
||||
:message-id="messages.profileJoinedAt"
|
||||
:values="{ ago: formatRelativeTime(user.created) }"
|
||||
>
|
||||
<template #date="{ children }">
|
||||
<span class="font-bold text-primary">
|
||||
<component :is="() => children" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="organizations.length > 0" class="card flex-card">
|
||||
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<nuxt-link
|
||||
v-for="org in organizations"
|
||||
:key="org.id"
|
||||
v-tooltip="org.name"
|
||||
class="organization"
|
||||
:to="`/organization/${org.slug}`"
|
||||
>
|
||||
<Avatar :src="org.icon_url" :alt="'Icon for ' + org.name" size="3rem" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="badges.length > 0" class="card flex-card">
|
||||
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileBadges) }}</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="badge in badges" :key="badge">
|
||||
<StaffBadge v-if="badge === 'staff'" class="h-14 w-14" />
|
||||
<ModBadge v-else-if="badge === 'mod'" class="h-14 w-14" />
|
||||
<nuxt-link v-else-if="badge === 'plus'" to="/plus">
|
||||
<PlusBadge class="h-14 w-14" />
|
||||
</nuxt-link>
|
||||
<TenMClubBadge v-else-if="badge === '10m-club'" class="h-14 w-14" />
|
||||
<EarlyAdopterBadge v-else-if="badge === 'early-adopter'" class="h-14 w-14" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { LibraryIcon, BoxIcon, LinkIcon, LockIcon, XIcon } from "@modrinth/assets";
|
||||
import { Promotion } from "@modrinth/ui";
|
||||
import {
|
||||
LibraryIcon,
|
||||
BoxIcon,
|
||||
LinkIcon,
|
||||
LockIcon,
|
||||
XIcon,
|
||||
CalendarIcon,
|
||||
DownloadIcon,
|
||||
ClipboardCopyIcon,
|
||||
MoreVerticalIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { OverflowMenu, ButtonStyled } from "@modrinth/ui";
|
||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import { reportUser } from "~/utils/report-helpers.ts";
|
||||
|
||||
import StaffBadge from "~/assets/images/badges/staff.svg?component";
|
||||
import ModBadge from "~/assets/images/badges/mod.svg?component";
|
||||
import PlusBadge from "~/assets/images/badges/plus.svg?component";
|
||||
import TenMClubBadge from "~/assets/images/badges/10m-club.svg?component";
|
||||
import EarlyAdopterBadge from "~/assets/images/badges/early-adopter.svg?component";
|
||||
|
||||
import ReportIcon from "~/assets/images/utils/report.svg?component";
|
||||
import SunriseIcon from "~/assets/images/utils/sunrise.svg?component";
|
||||
import DownloadIcon from "~/assets/images/utils/download.svg?component";
|
||||
import SettingsIcon from "~/assets/images/utils/settings.svg?component";
|
||||
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
|
||||
import UserIcon from "~/assets/images/utils/user.svg?component";
|
||||
import EditIcon from "~/assets/images/utils/edit.svg?component";
|
||||
import HeartIcon from "~/assets/images/utils/heart.svg?component";
|
||||
import GridIcon from "~/assets/images/utils/grid.svg?component";
|
||||
import ListIcon from "~/assets/images/utils/list.svg?component";
|
||||
import ImageIcon from "~/assets/images/utils/image.svg?component";
|
||||
import WorldIcon from "~/assets/images/utils/world.svg?component";
|
||||
import ModalCreation from "~/components/ui/ModalCreation.vue";
|
||||
import NavRow from "~/components/ui/NavRow.vue";
|
||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
|
||||
const data = useNuxtApp();
|
||||
const route = useNativeRoute();
|
||||
@@ -319,28 +336,41 @@ const formatCompactNumber = useCompactNumber();
|
||||
const formatRelativeTime = useRelativeTime();
|
||||
|
||||
const messages = defineMessages({
|
||||
profileProjectsStats: {
|
||||
id: "profile.stats.projects",
|
||||
defaultMessage:
|
||||
"{count, plural, one {<stat>{count}</stat> project} other {<stat>{count}</stat> projects}}",
|
||||
},
|
||||
profileDownloadsStats: {
|
||||
id: "profile.stats.downloads",
|
||||
defaultMessage:
|
||||
"{count, plural, one {<stat>{count}</stat> download} other {<stat>{count}</stat> downloads}}",
|
||||
"{count, plural, one {<stat>{count}</stat> project download} other {<stat>{count}</stat> project downloads}}",
|
||||
},
|
||||
profileProjectsFollowersStats: {
|
||||
id: "profile.stats.projects-followers",
|
||||
defaultMessage:
|
||||
"{count, plural, one {<stat>{count}</stat> follower} other {<stat>{count}</stat> followers}} of projects",
|
||||
"{count, plural, one {<stat>{count}</stat> project follower} other {<stat>{count}</stat> project followers}}",
|
||||
},
|
||||
profileJoinedAt: {
|
||||
id: "profile.joined-at",
|
||||
defaultMessage: "Joined {ago}",
|
||||
defaultMessage: "Joined <date>{ago}</date>",
|
||||
},
|
||||
profileUserId: {
|
||||
id: "profile.user-id",
|
||||
defaultMessage: "User ID: {id}",
|
||||
},
|
||||
profileDetails: {
|
||||
id: "profile.label.details",
|
||||
defaultMessage: "Details",
|
||||
},
|
||||
profileOrganizations: {
|
||||
id: "profile.label.organizations",
|
||||
defaultMessage: "Organizations",
|
||||
},
|
||||
profileBadges: {
|
||||
id: "profile.label.badges",
|
||||
defaultMessage: "Badges",
|
||||
},
|
||||
profileManageProjectsButton: {
|
||||
id: "profile.button.manage-projects",
|
||||
defaultMessage: "Manage projects",
|
||||
@@ -353,10 +383,6 @@ const messages = defineMessages({
|
||||
id: "profile.meta.description-with-bio",
|
||||
defaultMessage: "{bio} - Download {username}'s projects on Modrinth",
|
||||
},
|
||||
profileReportButton: {
|
||||
id: "profile.button.report",
|
||||
defaultMessage: "Report",
|
||||
},
|
||||
profileNoProjectsLabel: {
|
||||
id: "profile.label.no-projects",
|
||||
defaultMessage: "This user has no projects!",
|
||||
@@ -485,12 +511,64 @@ const sumFollows = computed(() => {
|
||||
return sum;
|
||||
});
|
||||
|
||||
function cycleSearchDisplayMode() {
|
||||
cosmetics.value.searchDisplayMode.user = data.$cycleValue(
|
||||
cosmetics.value.searchDisplayMode.user,
|
||||
tags.value.projectViewModes,
|
||||
);
|
||||
const badges = computed(() => {
|
||||
const badges = [];
|
||||
|
||||
if (user.value.role === "admin") {
|
||||
badges.push("staff");
|
||||
}
|
||||
|
||||
if (user.value.role === "moderator") {
|
||||
badges.push("mod");
|
||||
}
|
||||
|
||||
if (isPermission(user.value.badges, 1 << 0)) {
|
||||
badges.push("plus");
|
||||
}
|
||||
|
||||
if (sumDownloads.value > 10000000) {
|
||||
badges.push("10m-club");
|
||||
}
|
||||
|
||||
if (
|
||||
isPermission(user.value.badges, 1 << 1) ||
|
||||
isPermission(user.value.badges, 1 << 2) ||
|
||||
isPermission(user.value.badges, 1 << 3)
|
||||
) {
|
||||
badges.push("early-adopter");
|
||||
}
|
||||
|
||||
if (isPermission(user.value.badges, 1 << 4)) {
|
||||
badges.push("alpha-tester");
|
||||
}
|
||||
|
||||
if (isPermission(user.value.badges, 1 << 5)) {
|
||||
badges.push("contributor");
|
||||
}
|
||||
|
||||
if (isPermission(user.value.badges, 1 << 6)) {
|
||||
badges.push("translator");
|
||||
}
|
||||
|
||||
return badges;
|
||||
});
|
||||
|
||||
async function copyId() {
|
||||
await navigator.clipboard.writeText(project.value.id);
|
||||
}
|
||||
|
||||
const navLinks = computed(() => [
|
||||
{
|
||||
label: formatMessage(commonMessages.allProjectType),
|
||||
href: `/user/${user.value.username}`,
|
||||
},
|
||||
...projectTypes.value.map((x) => {
|
||||
return {
|
||||
label: formatMessage(getProjectTypeMessage(x, true)),
|
||||
href: `/user/${user.value.username}/${x}s`,
|
||||
};
|
||||
}),
|
||||
]);
|
||||
</script>
|
||||
<script>
|
||||
export default defineNuxtComponent({
|
||||
@@ -499,16 +577,6 @@ export default defineNuxtComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.organizations-grid {
|
||||
// 5 wide
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
grid-gap: var(--gap-sm);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.collections-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@@ -579,107 +647,7 @@ export default defineNuxtComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.user-header-wrapper {
|
||||
display: flex;
|
||||
margin: 0 auto -1.5rem;
|
||||
max-width: 80rem;
|
||||
|
||||
.user-header {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-username {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 501px) {
|
||||
.mobile-username {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-header-wrapper .user-header .username {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding-top: 2.5rem;
|
||||
}
|
||||
|
||||
.sidebar__item:not(:last-child) {
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.profile-picture {
|
||||
border-radius: var(--size-rounded-lg);
|
||||
height: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.bio {
|
||||
display: block;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.secondary-stat {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.secondary-stat__icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.secondary-stat__text {
|
||||
margin-left: 0.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.date {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
input {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.sidebar {
|
||||
padding-top: 3rem;
|
||||
}
|
||||
.normal-page__header {
|
||||
grid-area: header;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user