Sidebar refinements (#2306)

* Begin sidebar refinement, change back to left as default

* New filters proof of concept

* Hide if only one option

* Version filters

* Update changelog page

* Use new cosmetic variable for sidebar position

* Fix safari issue and change defaults to left filters, right sidebars

* Fix download modal on safari and firefox

* Add date published tooltip to versions page

* Improve selection consistency

* Fix lint and extract i18n

* Remove unnecessary observer options
This commit is contained in:
Prospector
2024-08-26 16:53:27 -07:00
committed by GitHub
parent 656c5b61cc
commit 2dd8d5a119
22 changed files with 965 additions and 779 deletions

View File

@@ -149,6 +149,20 @@
<span class="text-lg font-extrabold text-contrast"> Settings </span>
</template>
</NewModal>
<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>
<div
class="over-the-top-download-animation"
:class="{ 'animation-hidden': !overTheTopDownloadAnimation }"
@@ -232,7 +246,6 @@
class="accordion-with-bg"
@on-open="
() => {
gameVersionFilterInput.focus();
if (platformAccordion) {
platformAccordion.close();
}
@@ -402,7 +415,8 @@
!filteredAlpha
"
>
No versions available for {{ currentGameVersion }} and {{ currentPlatform }}.
No versions available for {{ currentGameVersion }} and
{{ formatCategory(currentPlatform) }}.
</p>
</AutomaticAccordion>
</div>
@@ -410,10 +424,9 @@
</NewModal>
<CollectionCreateModal ref="modal_collection" :project-ids="[project.id]" />
<div
class="new-page"
class="new-page sidebar"
:class="{
sidebar: !route.name.endsWith('gallery') && !route.name.endsWith('moderation'),
'alt-layout': cosmetics.projectLayout,
'alt-layout': cosmetics.leftContentLayout,
}"
>
<div class="normal-page__header relative my-4">
@@ -438,7 +451,7 @@
</p>
<div class="mt-auto flex flex-wrap gap-4">
<div
class="flex items-center gap-3 border-0 border-r border-solid border-button-bg pr-4"
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4"
>
<DownloadIcon class="h-6 w-6 text-secondary" />
<span class="font-semibold">
@@ -446,14 +459,14 @@
</span>
</div>
<div
class="flex items-center gap-3 border-0 border-solid border-button-bg pr-4 md:border-r"
class="flex items-center gap-2 border-0 border-solid border-button-bg pr-4 md:border-r"
>
<HeartIcon class="h-6 w-6 text-secondary" />
<span class="font-semibold">
{{ $formatNumber(project.followers) }}
</span>
</div>
<div class="hidden items-center gap-3 md:flex">
<div class="hidden items-center gap-2 md:flex">
<TagsIcon class="h-6 w-6 text-secondary" />
<div class="flex flex-wrap gap-2">
<div
@@ -650,25 +663,327 @@
{{ project.title }} has been archived. {{ project.title }} will not receive any further
updates unless the author decides to unarchive the project.
</MessageBanner>
<div class="overflow-x-auto">
<NavTabs :links="navLinks" class="mt-4" />
</div>
<div class="normal-page__sidebar">
<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 v-if="project.project_type !== 'resourcepack'">
<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
v-if="
(project.actualProjectType === 'mod' || project.project_type === 'modpack') &&
!(project.client_side === 'unsupported' && project.server_side === 'unsupported') &&
!(project.client_side === 'unknown' && project.server_side === 'unknown')
"
>
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
<div class="status-list">
<div
v-if="
(project.client_side === 'required' && project.server_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional')
"
class="status-list__item"
>
<ClientIcon aria-hidden="true" />
Client-side
</div>
<div
v-if="
(project.server_side === 'required' && project.client_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional')
"
class="status-list__item"
>
<ServerIcon aria-hidden="true" />
Server-side
</div>
<div v-if="false" class="status-list__item">
<UserIcon aria-hidden="true" />
Singleplayer
</div>
<div
v-if="project.client_side === 'required' && project.server_side === 'required'"
class="status-list__item"
>
<MonitorSmartphoneIcon aria-hidden="true" />
Client and server
</div>
<div
v-else-if="
project.client_side === 'optional' ||
(project.client_side === 'required' && project.server_side === 'optional') ||
project.server_side === 'optional' ||
(project.server_side === 'required' && project.client_side === 'optional')
"
class="status-list__item"
>
<MonitorSmartphoneIcon aria-hidden="true" />
Client and server <span class="text-sm">(optional)</span>
</div>
</div>
</section>
</div>
<AdPlaceholder
v-if="
(!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus) &&
tags.approvedStatuses.includes(project.status)
"
/>
<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>
<NuxtPage
v-model:project="project"
v-model:versions="versions"
v-model:featured-versions="featuredVersions"
v-model:members="members"
v-model:all-members="allMembers"
v-model:dependencies="dependencies"
v-model:organization="organization"
:current-member="currentMember"
:reset-project="resetProject"
:reset-organization="resetOrganization"
:reset-members="resetMembers"
:route="route"
@on-download="triggerDownloadAnimation"
/>
<div class="normal-page__content">
<div class="overflow-x-auto">
<NavTabs :links="navLinks" class="mb-4" />
</div>
<NuxtPage
v-model:project="project"
v-model:versions="versions"
v-model:featured-versions="featuredVersions"
v-model:members="members"
v-model:all-members="allMembers"
v-model:dependencies="dependencies"
v-model:organization="organization"
:current-member="currentMember"
:reset-project="resetProject"
:reset-organization="resetOrganization"
:reset-members="resetMembers"
:route="route"
@on-download="triggerDownloadAnimation"
/>
</div>
</div>
<ModerationChecklist
v-if="auth.user && tags.staffRoles.includes(auth.user.role) && showModerationChecklist"
@@ -703,6 +1018,23 @@ import {
UsersIcon,
VersionIcon,
WrenchIcon,
ClientIcon,
BookTextIcon,
MonitorSmartphoneIcon,
WikiIcon,
DiscordIcon,
CalendarIcon,
KoFiIcon,
BuyMeACoffeeIcon,
IssuesIcon,
UserIcon,
PayPalIcon,
ServerIcon,
PatreonIcon,
CrownIcon,
OpenCollectiveIcon,
CodeIcon,
CurrencyIcon,
} from "@modrinth/assets";
import {
Avatar,
@@ -713,7 +1045,7 @@ import {
PopoutMenu,
ScrollablePanel,
} from "@modrinth/ui";
import { formatCategory, isRejected, isStaff, isUnderReview } from "@modrinth/utils";
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
import dayjs from "dayjs";
import Badge from "~/components/ui/Badge.vue";
import NavTabs from "~/components/ui/NavTabs.vue";
@@ -730,6 +1062,8 @@ import Accordion from "~/components/ui/Accordion.vue";
import ModrinthIcon from "~/assets/images/utils/modrinth.svg?component";
import VersionSummary from "~/components/ui/VersionSummary.vue";
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
import { getVersionsToDisplay } from "~/helpers/projects.js";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
const data = useNuxtApp();
const route = useNativeRoute();
@@ -738,6 +1072,7 @@ const auth = await useAuth();
const user = await useUser();
const tags = useTags();
const flags = useFeatureFlags();
const cosmetics = useCosmetics();
const { formatMessage } = useVIntl();
@@ -789,6 +1124,152 @@ const gameVersionAccordion = ref();
const platformAccordion = ref();
const getModrinthAppAccordion = ref();
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: "Supported 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(() =>
project.value.published ? formatRelativeTime(project.value.published) : "unknown",
);
const submittedDate = computed(() =>
project.value.queued ? formatRelativeTime(project.value.queued) : "unknown",
);
const publishedDate = computed(() =>
project.value.approved ? formatRelativeTime(project.value.approved) : "unknown",
);
const updatedDate = computed(() =>
project.value.updated ? formatRelativeTime(project.value.updated) : "unknown",
);
const licenseIdDisplay = computed(() => {
const id = project.value.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/${project.value.license.id}`);
licenseText.value = text.body || "License text could not be retrieved.";
} catch {
licenseText.value = "License text could not be retrieved.";
}
}
const filteredVersions = computed(() => {
return versions.value.filter(
(x) =>
@@ -830,9 +1311,9 @@ const messages = defineMessages({
id: "project.stats.followers-label",
defaultMessage: "follower{count, plural, one {} other {s}}",
},
aboutTab: {
id: "project.about.title",
defaultMessage: "About",
descriptionTab: {
id: "project.description.title",
defaultMessage: "Description",
},
galleryTab: {
id: "project.gallery.title",
@@ -1223,7 +1704,7 @@ const navLinks = computed(() => {
return [
{
label: formatMessage(messages.aboutTab),
label: formatMessage(messages.descriptionTab),
href: projectUrl,
},
{

View File

@@ -1,5 +1,15 @@
<template>
<div class="content">
<div class="mb-3 flex">
<VersionFilterControl :versions="props.versions" @switch-page="switchPage" />
<Pagination
:page="currentPage"
:count="Math.ceil(filteredVersions.length / 20)"
class="ml-auto mt-auto"
:link-function="(page) => `?page=${page}`"
@switch-page="switchPage"
/>
</div>
<div class="card changelog-wrapper">
<div
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
@@ -57,15 +67,6 @@
@switch-page="switchPage"
/>
</div>
<div class="normal-page__sidebar">
<AdPlaceholder
v-if="
(!auth.user || !isPermission(auth.user.badges, 1 << 0)) &&
tags.approvedStatuses.includes(project.status)
"
/>
<VersionFilterControl :versions="props.versions" @switch-page="switchPage" />
</div>
</template>
<script setup>
import { Pagination } from "@modrinth/ui";
@@ -73,7 +74,6 @@ import { DownloadIcon } from "@modrinth/assets";
import { renderHighlightedString } from "~/helpers/highlight.js";
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
const props = defineProps({
project: {
@@ -96,9 +96,6 @@ const props = defineProps({
},
});
const auth = await useAuth();
const tags = useTags();
const title = `${props.project.title} - Changelog`;
const description = `View the changelog of ${props.project.title}'s ${props.versions.length} versions.`;
@@ -114,9 +111,9 @@ const route = useNativeRoute();
const currentPage = ref(Number(route.query.page ?? 1));
const filteredVersions = computed(() => {
const selectedGameVersions = getArrayOrString(route.query.gameVersion) ?? [];
const selectedLoaders = getArrayOrString(route.query.platform) ?? [];
const selectedVersionTypes = getArrayOrString(route.query.type) ?? [];
const selectedGameVersions = getArrayOrString(route.query.g) ?? [];
const selectedLoaders = getArrayOrString(route.query.l) ?? [];
const selectedVersionTypes = getArrayOrString(route.query.c) ?? [];
return props.versions.filter(
(projectVersion) =>

View File

@@ -1,18 +1,4 @@
<template>
<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"
@@ -20,333 +6,12 @@
v-html="renderHighlightedString(project.body || '')"
/>
</section>
<div class="normal-page__sidebar">
<AdPlaceholder
v-if="
(!auth.user || !isPermission(auth.user.badges, 1 << 0)) &&
tags.approvedStatuses.includes(project.status)
"
/>
<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 v-if="project.project_type !== 'resourcepack'">
<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
v-if="
(project.actualProjectType === 'mod' || project.project_type === 'modpack') &&
!(project.client_side === 'unsupported' && project.server_side === 'unsupported') &&
!(project.client_side === 'unknown' && project.server_side === 'unknown')
"
>
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
<div class="status-list">
<div
v-if="
(project.client_side === 'required' && project.server_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional')
"
class="status-list__item"
>
<ClientIcon aria-hidden="true" />
Client-side
</div>
<div
v-if="
(project.server_side === 'required' && project.client_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional')
"
class="status-list__item"
>
<ServerIcon aria-hidden="true" />
Server-side
</div>
<div v-if="false" class="status-list__item">
<UserIcon aria-hidden="true" />
Singleplayer
</div>
<div
v-if="project.client_side === 'required' && project.server_side === 'required'"
class="status-list__item"
>
<MonitorSmartphoneIcon aria-hidden="true" />
Client and server
</div>
<div
v-else-if="
project.client_side === 'optional' ||
(project.client_side === 'required' && project.server_side === 'optional') ||
project.server_side === 'optional' ||
(project.server_side === 'required' && project.client_side === 'optional')
"
class="status-list__item"
>
<MonitorSmartphoneIcon aria-hidden="true" />
Client and server <span class="text-sm">(optional)</span>
</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 setup>
import {
CalendarIcon,
IssuesIcon,
WikiIcon,
OpenCollectiveIcon,
DiscordIcon,
ScaleIcon,
KoFiIcon,
BookTextIcon,
PayPalIcon,
CrownIcon,
BuyMeACoffeeIcon,
CurrencyIcon,
PatreonIcon,
HeartIcon,
VersionIcon,
ExternalIcon,
CodeIcon,
UserIcon,
ServerIcon,
ClientIcon,
MonitorSmartphoneIcon,
} from "@modrinth/assets";
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({
defineProps({
project: {
type: Object,
default() {
@@ -372,153 +37,4 @@ const props = defineProps({
},
},
});
const auth = await useAuth();
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: "Supported 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>

View File

@@ -471,7 +471,7 @@
<div class="normal-page__sidebar version-page__metadata">
<AdPlaceholder
v-if="
(!auth.user || !isPermission(auth.user.badges, 1 << 0)) &&
(!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus) &&
tags.approvedStatuses.includes(project.status)
"
/>
@@ -749,6 +749,7 @@ export default defineNuxtComponent({
const auth = await useAuth();
const tags = useTags();
const flags = useFeatureFlags();
const path = route.name.split("-");
const mode = path[path.length - 1];
@@ -896,6 +897,7 @@ export default defineNuxtComponent({
return {
auth,
tags,
flags,
fileTypes: ref(fileTypes),
oldFileTypes: ref(oldFileTypes),
isCreating: ref(isCreating),

View File

@@ -1,5 +1,5 @@
<template>
<section class="normal-page__content experimental-styles-within overflow-visible">
<section class="experimental-styles-within overflow-visible">
<div
v-if="currentMember && isPermission(currentMember?.permissions, 1 << 0)"
class="card flex items-center gap-4"
@@ -19,6 +19,20 @@
</span>
<DropArea :accept="acceptFileFromProjectType(project.project_type)" @change="handleFiles" />
</div>
<div class="mb-3 flex flex-wrap gap-2">
<VersionFilterControl
ref="versionFilters"
:versions="props.versions"
@switch-page="switchPage"
/>
<Pagination
:page="currentPage"
class="ml-auto mt-auto"
:count="Math.ceil(filteredVersions.length / 20)"
:link-function="(page) => `?page=${currentPage}`"
@switch-page="switchPage"
/>
</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]"
@@ -69,8 +83,12 @@
<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 class="relative z-[1] cursor-pointer">
<VersionChannelIndicator
v-tooltip="`Toggle filter for ${version.version_type}`"
:channel="version.version_type"
@click="versionFilters.toggleFilter('channel', version.version_type)"
/>
</div>
</div>
<div
@@ -115,7 +133,13 @@
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"
v-tooltip="
formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(version.date_published),
time: new Date(version.date_published),
})
"
class="z-[1] flex cursor-help items-center gap-1 text-nowrap font-medium xl:self-center"
>
<CalendarIcon class="xl:hidden" />
{{ formatRelativeTime(version.date_published) }}
@@ -232,7 +256,10 @@
</OverflowMenu>
</ButtonStyled>
</div>
<div v-if="showFiles" class="tag-list pointer-events-none relative z-[1] col-span-full">
<div
v-if="flags.showVersionFilesInTable"
class="tag-list pointer-events-none relative z-[1] col-span-full"
>
<div
v-for="(file, fileIdx) in version.files"
:key="`platform-tag-${fileIdx}`"
@@ -254,19 +281,6 @@
/>
</div>
</section>
<div class="normal-page__sidebar">
<AdPlaceholder
v-if="
(!auth.user || !isPermission(auth.user.badges, 1 << 0)) &&
tags.approvedStatuses.includes(project.status)
"
/>
<VersionFilterControl
ref="versionFilters"
:versions="props.versions"
@switch-page="switchPage"
/>
</div>
</template>
<script setup>
@@ -296,9 +310,9 @@ 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 { formatMessage } = useVIntl();
const props = defineProps({
project: {
@@ -321,8 +335,8 @@ const props = defineProps({
},
});
const auth = await useAuth();
const tags = useTags();
const flags = useFeatureFlags();
const formatRelativeTime = useRelativeTime();
const emits = defineEmits(["onDownload"]);
@@ -332,8 +346,6 @@ const router = useNativeRouter();
const currentPage = ref(route.query.page ?? 1);
const showFiles = ref(false);
function switchPage(page) {
currentPage.value = page;
@@ -349,22 +361,30 @@ function getPrimaryFile(version) {
return version.files.find((x) => x.primary) || version.files[0];
}
const selectedGameVersions = computed(() => {
return getArrayOrString(route.query.g) ?? [];
});
const selectedPlatforms = computed(() => {
return getArrayOrString(route.query.l) ?? [];
});
const selectedVersionChannels = computed(() => {
return getArrayOrString(route.query.c) ?? [];
});
const versionFilters = ref(null);
const filteredVersions = computed(() => {
const selectedGameVersions = getArrayOrString(route.query.gameVersion) ?? [];
const selectedLoaders = getArrayOrString(route.query.platform) ?? [];
const selectedVersionTypes = getArrayOrString(route.query.type) ?? [];
return props.versions.filter(
(projectVersion) =>
(selectedGameVersions.length === 0 ||
selectedGameVersions.some((gameVersion) =>
(selectedGameVersions.value.length === 0 ||
selectedGameVersions.value.some((gameVersion) =>
projectVersion.game_versions.includes(gameVersion),
)) &&
(selectedLoaders.length === 0 ||
selectedLoaders.some((loader) => projectVersion.loaders.includes(loader))) &&
(selectedVersionTypes.length === 0 ||
selectedVersionTypes.includes(projectVersion.version_type)),
(selectedPlatforms.value.length === 0 ||
selectedPlatforms.value.some((loader) => projectVersion.loaders.includes(loader))) &&
(selectedVersionChannels.value.length === 0 ||
selectedVersionChannels.value.includes(projectVersion.version_type)),
);
});

View File

@@ -248,7 +248,9 @@
</div>
</template>
</div>
<AdPlaceholder v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0)" />
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
</div>
<div class="normal-page__content">
<nav class="navigation-card">
@@ -480,6 +482,7 @@ const route = useNativeRoute();
const auth = await useAuth();
const cosmetics = useCosmetics();
const tags = useTags();
const flags = useFeatureFlags();
const isEditing = ref(false);

View File

@@ -109,7 +109,9 @@
</div>
</div>
<AdPlaceholder v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0)" />
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
<div class="creator-list universal-card">
<div class="title-and-link">
@@ -253,6 +255,7 @@ const user = await useUser();
const cosmetics = useCosmetics();
const route = useNativeRoute();
const tags = useTags();
const flags = useFeatureFlags();
let orgId = useRouteId();

View File

@@ -1,7 +1,7 @@
<template>
<div
class="new-page sidebar experimental-styles-within"
:class="{ 'alt-layout': cosmetics.searchLayout }"
:class="{ 'alt-layout': !cosmetics.rightSearchLayout }"
>
<Head>
<Title>Search {{ projectType.display }}s - Modrinth</Title>
@@ -12,7 +12,9 @@
}"
aria-label="Filters"
>
<AdPlaceholder v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0)" />
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
<section class="card gap-1" :class="{ 'max-lg:!hidden': !sidebarMenuOpen }">
<div class="flex items-center gap-2">
<div class="iconified-input w-full">
@@ -202,6 +204,14 @@
</button>
</div>
</div>
<pagination
v-if="false"
:page="currentPage"
:count="pageCount"
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="mb-3 justify-end"
@switch-page="onSearchChangeToTop"
/>
<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>
@@ -279,6 +289,7 @@ const route = useNativeRoute();
const cosmetics = useCosmetics();
const tags = useTags();
const flags = useFeatureFlags();
const auth = await useAuth();
const query = ref("");

View File

@@ -190,15 +190,15 @@
<div class="adjacent-input small">
<label for="search-layout-toggle">
<span class="label__title">
{{ formatMessage(toggleFeatures.leftAlignedSearchSidebarTitle) }}
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.leftAlignedSearchSidebarDescription) }}
{{ formatMessage(toggleFeatures.rightAlignedFiltersSidebarDescription) }}
</span>
</label>
<input
id="search-layout-toggle"
v-model="cosmetics.searchLayout"
v-model="cosmetics.rightSearchLayout"
class="switch stylized-toggle"
type="checkbox"
/>
@@ -206,15 +206,15 @@
<div class="adjacent-input small">
<label for="project-layout-toggle">
<span class="label__title">
{{ formatMessage(toggleFeatures.leftAlignedProjectSidebarTitle) }}
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarTitle) }}
</span>
<span class="label__description">
{{ formatMessage(toggleFeatures.leftAlignedProjectSidebarDescription) }}
{{ formatMessage(toggleFeatures.leftAlignedContentSidebarDescription) }}
</span>
</label>
<input
id="project-layout-toggle"
v-model="cosmetics.projectLayout"
v-model="cosmetics.leftContentLayout"
class="switch stylized-toggle"
type="checkbox"
/>
@@ -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.',
},
leftAlignedSearchSidebarTitle: {
id: "settings.display.sidebar.Left-aligned-search-sidebar.title",
defaultMessage: "Left-aligned search sidebar",
rightAlignedFiltersSidebarTitle: {
id: "settings.display.sidebar.right-aligned-filters-sidebar.title",
defaultMessage: "Right-aligned filters sidebar on search pages",
},
leftAlignedSearchSidebarDescription: {
id: "settings.display.sidebar.left-aligned-search-sidebar.description",
defaultMessage: "Aligns the search filters sidebar to the left of the search results.",
rightAlignedFiltersSidebarDescription: {
id: "settings.display.sidebar.right-aligned-filters-sidebar.description",
defaultMessage: "Aligns the filters sidebar to the right of the search results.",
},
leftAlignedProjectSidebarTitle: {
id: "settings.display.sidebar.left-aligned-project-sidebar.title",
defaultMessage: "Left-aligned project sidebar",
leftAlignedContentSidebarTitle: {
id: "settings.display.sidebar.left-aligned-content-sidebar.title",
defaultMessage: "Left-aligned sidebar on content pages",
},
leftAlignedProjectSidebarDescription: {
id: "settings.display.sidebar.left-aligned-project-sidebar.description",
defaultMessage: "Aligns the project details sidebar to the left of the page's content.",
leftAlignedContentSidebarDescription: {
id: "settings.display.sidebar.right-aligned-content-sidebar.description",
defaultMessage: "Aligns the sidebar to the left of the page's content.",
},
});

View File

@@ -2,7 +2,7 @@
<div v-if="user" class="experimental-styles-within">
<ModalCreation ref="modal_creation" />
<CollectionCreateModal ref="modal_collection_creation" />
<div class="new-page sidebar">
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
<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]"
@@ -72,11 +72,11 @@
</div>
</div>
</div>
</div>
<div class="normal-page__content">
<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">
<div v-if="projects.length > 0">
<div
v-if="route.params.projectType !== 'collections'"
@@ -194,7 +194,6 @@
</div>
</div>
<div class="normal-page__sidebar">
<AdPlaceholder v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0)" />
<div class="card flex-card">
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileDetails) }}</h2>
<div class="flex items-center gap-2">
@@ -258,6 +257,9 @@
</div>
</div>
</div>
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
<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">
@@ -328,6 +330,7 @@ const route = useNativeRoute();
const auth = await useAuth();
const cosmetics = useCosmetics();
const tags = useTags();
const flags = useFeatureFlags();
const vintl = useVIntl();
const { formatMessage } = vintl;