You've already forked AstralRinth
forked from didirus/AstralRinth
Moderation messages are superseded by threads, so there is no need to have the ModalModeration anymore, it would only confuse new moderators
1383 lines
41 KiB
Vue
1383 lines
41 KiB
Vue
<template>
|
|
<div v-if="$route.name.startsWith('type-id-settings')" class="normal-page">
|
|
<div class="normal-page__sidebar">
|
|
<aside class="universal-card">
|
|
<Breadcrumbs
|
|
current-title="Settings"
|
|
:link-stack="[
|
|
{ href: `/dashboard/projects`, label: 'Projects' },
|
|
{
|
|
href: `/${project.project_type}/${project.slug ? project.slug : project.id}`,
|
|
label: project.title,
|
|
allowTrimming: true,
|
|
},
|
|
]"
|
|
/>
|
|
<div class="settings-header">
|
|
<Avatar
|
|
:src="project.icon_url"
|
|
:alt="project.title"
|
|
size="sm"
|
|
class="settings-header__icon"
|
|
/>
|
|
<div class="settings-header__text">
|
|
<h1 class="wrap-as-needed">
|
|
{{ project.title }}
|
|
</h1>
|
|
<Badge :type="project.status" />
|
|
</div>
|
|
</div>
|
|
<h2>Project settings</h2>
|
|
<NavStack>
|
|
<NavStackItem
|
|
:link="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
|
label="General"
|
|
>
|
|
<SettingsIcon />
|
|
</NavStackItem>
|
|
<NavStackItem
|
|
:link="`/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/settings/tags`"
|
|
label="Tags"
|
|
>
|
|
<CategoriesIcon />
|
|
</NavStackItem>
|
|
<NavStackItem
|
|
:link="`/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/settings/description`"
|
|
label="Description"
|
|
>
|
|
<DescriptionIcon />
|
|
</NavStackItem>
|
|
<NavStackItem
|
|
:link="`/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/settings/license`"
|
|
label="License"
|
|
>
|
|
<LicenseIcon />
|
|
</NavStackItem>
|
|
<NavStackItem
|
|
:link="`/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/settings/links`"
|
|
label="Links"
|
|
>
|
|
<LinksIcon />
|
|
</NavStackItem>
|
|
<NavStackItem
|
|
:link="`/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/settings/members`"
|
|
label="Members"
|
|
>
|
|
<UsersIcon />
|
|
</NavStackItem>
|
|
<h3>Upload</h3>
|
|
<NavStackItem
|
|
:link="`/${project.project_type}/${project.slug ? project.slug : project.id}/gallery`"
|
|
label="Gallery"
|
|
chevron
|
|
>
|
|
<GalleryIcon />
|
|
</NavStackItem>
|
|
<NavStackItem
|
|
:link="`/${project.project_type}/${project.slug ? project.slug : project.id}/versions`"
|
|
label="Versions"
|
|
chevron
|
|
>
|
|
<VersionIcon />
|
|
</NavStackItem>
|
|
</NavStack>
|
|
</aside>
|
|
</div>
|
|
<div class="normal-page__content">
|
|
<ProjectMemberHeader
|
|
v-if="currentMember"
|
|
:project="project"
|
|
:versions="versions"
|
|
:current-member="currentMember"
|
|
:is-settings="$route.name.startsWith('type-id-settings')"
|
|
:route-name="$route.name"
|
|
:set-processing="setProcessing"
|
|
:collapsed="collapsedChecklist"
|
|
:toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
|
|
:all-members="allMembers"
|
|
:update-members="updateMembers"
|
|
:auth="auth"
|
|
:tags="tags"
|
|
/>
|
|
<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"
|
|
:current-member="currentMember"
|
|
:patch-project="patchProject"
|
|
:patch-icon="patchIcon"
|
|
:update-icon="resetProject"
|
|
:route="route"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<Head>
|
|
<Title> {{ project.title }} - Minecraft {{ projectTypeDisplay }} </Title>
|
|
<Meta name="og:title" :content="`${project.title} - Minecraft ${projectTypeDisplay}`" />
|
|
<Meta
|
|
name="description"
|
|
:content="`${project.description} - Download the Minecraft ${projectTypeDisplay} ${
|
|
project.title
|
|
} by ${members.find((x) => x.role === 'Owner').user.username} on Modrinth`"
|
|
/>
|
|
<Meta
|
|
name="apple-mobile-web-app-title"
|
|
:content="`${project.title} - Minecraft ${projectTypeDisplay}`"
|
|
/>
|
|
<Meta name="og:description" :content="project.description" />
|
|
<Meta
|
|
name="og:image"
|
|
:content="project.icon_url ? project.icon_url : 'https://cdn.modrinth.com/placeholder.png'"
|
|
/>
|
|
<Meta
|
|
name="robots"
|
|
:content="
|
|
project.status === 'approved' || project.status === 'archived' ? 'all' : 'noindex'
|
|
"
|
|
/>
|
|
</Head>
|
|
<Modal ref="modalLicense" :header="project.license.name ? project.license.name : 'License'">
|
|
<div class="modal-license">
|
|
<div class="markdown-body" v-html="renderString(licenseText)" />
|
|
</div>
|
|
</Modal>
|
|
<ModalReport
|
|
v-if="auth.user"
|
|
ref="modal_project_report"
|
|
:item-id="project.id"
|
|
item-type="project"
|
|
/>
|
|
<div
|
|
:class="{
|
|
'normal-page': true,
|
|
'alt-layout': cosmetics.projectLayout,
|
|
}"
|
|
>
|
|
<div class="normal-page__sidebar">
|
|
<div
|
|
class="header project__header base-card padding-0"
|
|
:class="{ 'has-featured-image': featuredGalleryImage }"
|
|
>
|
|
<nuxt-link
|
|
class="project__gallery"
|
|
tabindex="-1"
|
|
:to="
|
|
'/' +
|
|
project.project_type +
|
|
'/' +
|
|
(project.slug ? project.slug : project.id) +
|
|
'/gallery'
|
|
"
|
|
>
|
|
<img
|
|
v-if="featuredGalleryImage"
|
|
:src="featuredGalleryImage.url"
|
|
:alt="
|
|
featuredGalleryImage.description
|
|
? featuredGalleryImage.description
|
|
: featuredGalleryImage.title
|
|
"
|
|
/>
|
|
</nuxt-link>
|
|
<div class="project__header__content universal-card full-width-inputs">
|
|
<Avatar
|
|
:src="project.icon_url"
|
|
:alt="project.title"
|
|
size="md"
|
|
class="project__icon"
|
|
no-shadow
|
|
/>
|
|
<h1 class="title">
|
|
{{ project.title }}
|
|
</h1>
|
|
<nuxt-link
|
|
class="title-link project-type"
|
|
:to="`/${$getProjectTypeForUrl(project.actualProjectType, project.loaders)}s`"
|
|
>
|
|
<BoxIcon />
|
|
<span>{{
|
|
$formatProjectType(
|
|
$getProjectTypeForDisplay(project.actualProjectType, project.loaders)
|
|
)
|
|
}}</span>
|
|
</nuxt-link>
|
|
<p class="description">
|
|
{{ project.description }}
|
|
</p>
|
|
<Categories
|
|
:categories="project.categories.concat(project.additional_categories)"
|
|
:type="project.actualProjectType"
|
|
class="categories"
|
|
>
|
|
<Badge
|
|
v-if="auth.user && currentMember"
|
|
:type="project.status"
|
|
class="status-badge"
|
|
/>
|
|
<EnvironmentIndicator
|
|
:client-side="project.client_side"
|
|
:server-side="project.server_side"
|
|
:type="project.project_type"
|
|
/>
|
|
</Categories>
|
|
<hr class="card-divider" />
|
|
<div class="primary-stat">
|
|
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
|
|
<div class="primary-stat__text">
|
|
<span class="primary-stat__counter">
|
|
{{ $formatNumber(project.downloads) }}
|
|
</span>
|
|
download<span v-if="project.downloads !== 1">s</span>
|
|
</div>
|
|
</div>
|
|
<div class="primary-stat">
|
|
<HeartIcon class="primary-stat__icon" aria-hidden="true" />
|
|
<div class="primary-stat__text">
|
|
<span class="primary-stat__counter">
|
|
{{ $formatNumber(project.followers) }}
|
|
</span>
|
|
follower<span v-if="project.followers !== 1">s</span>
|
|
</div>
|
|
</div>
|
|
<div class="dates">
|
|
<div
|
|
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
|
|
class="date"
|
|
>
|
|
<CalendarIcon aria-hidden="true" />
|
|
<span class="label">Created</span>
|
|
<span class="value">{{ fromNow(project.published) }}</span>
|
|
</div>
|
|
<div
|
|
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
|
|
class="date"
|
|
>
|
|
<UpdateIcon aria-hidden="true" />
|
|
<span class="label">Updated</span>
|
|
<span class="value">{{ fromNow(project.updated) }}</span>
|
|
</div>
|
|
<div
|
|
v-if="project.status === 'processing' && project.queued"
|
|
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')"
|
|
class="date"
|
|
>
|
|
<QueuedIcon aria-hidden="true" />
|
|
<span class="label">Submitted</span>
|
|
<span class="value">{{ fromNow(project.queued) }}</span>
|
|
</div>
|
|
</div>
|
|
<hr class="card-divider" />
|
|
<div class="input-group">
|
|
<template v-if="auth.user">
|
|
<button class="iconified-button" @click="$refs.modal_project_report.show()">
|
|
<ReportIcon aria-hidden="true" />
|
|
Report
|
|
</button>
|
|
<button
|
|
v-if="!user.follows.find((x) => x.id === project.id)"
|
|
class="iconified-button"
|
|
@click="userFollowProject(project)"
|
|
>
|
|
<HeartIcon aria-hidden="true" />
|
|
Follow
|
|
</button>
|
|
<button
|
|
v-if="user.follows.find((x) => x.id === project.id)"
|
|
class="iconified-button"
|
|
@click="userUnfollowProject(project)"
|
|
>
|
|
<HeartIcon fill="currentColor" aria-hidden="true" />
|
|
Unfollow
|
|
</button>
|
|
</template>
|
|
<template v-else>
|
|
<a class="iconified-button" :href="getAuthUrl()" rel="noopener nofollow">
|
|
<ReportIcon aria-hidden="true" />
|
|
Report
|
|
</a>
|
|
<a class="iconified-button" :href="getAuthUrl()" rel="noopener nofollow">
|
|
<HeartIcon aria-hidden="true" />
|
|
Follow
|
|
</a>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="currentMember && project.moderator_message"
|
|
class="universal-card moderation-card"
|
|
>
|
|
<h2 class="card-header">Message from the moderators:</h2>
|
|
<div v-if="project.moderator_message.body">
|
|
<p v-if="project.moderator_message.message" class="mod-message__title">
|
|
{{ project.moderator_message.message }}
|
|
</p>
|
|
</div>
|
|
<div
|
|
class="markdown-body"
|
|
v-html="
|
|
renderString(
|
|
project.moderator_message.body
|
|
? project.moderator_message.body
|
|
: project.moderator_message.message
|
|
)
|
|
"
|
|
/>
|
|
<div class="buttons status-buttons">
|
|
<button
|
|
v-if="tags.approvedStatuses.includes(project.status)"
|
|
class="iconified-button"
|
|
@click="clearMessage"
|
|
>
|
|
<ClearIcon />
|
|
Clear message
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<section class="normal-page__content">
|
|
<ProjectMemberHeader
|
|
v-if="currentMember"
|
|
:project="project"
|
|
:versions="versions"
|
|
:current-member="currentMember"
|
|
:is-settings="$route.name.startsWith('type-id-settings')"
|
|
:route-name="$route.name"
|
|
:set-processing="setProcessing"
|
|
:collapsed="collapsedChecklist"
|
|
:toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
|
|
:all-members="allMembers"
|
|
:update-members="updateMembers"
|
|
:auth="auth"
|
|
:tags="tags"
|
|
/>
|
|
<div v-else-if="project.status === 'withheld'" class="card warning" aria-label="Warning">
|
|
{{ project.title }} has been removed from search by Modrinth's moderators. Please use
|
|
{{ project.title }} at your own risk.
|
|
</div>
|
|
<div v-if="project.status === 'archived'" class="card warning" aria-label="Warning">
|
|
{{ project.title }} has been archived. {{ project.title }} will not receive any further
|
|
updates unless the author decides to unarchive the project.
|
|
</div>
|
|
<div
|
|
v-if="project.project_type === 'modpack'"
|
|
class="card information"
|
|
aria-label="Information"
|
|
>
|
|
To install {{ project.title }}, visit
|
|
<a href="https://docs.modrinth.com/docs/modpacks/playing_modpacks/" :target="$external()"
|
|
>our documentation</a
|
|
>
|
|
which provides instructions on using
|
|
<a href="https://atlauncher.com/about" :target="$external()" rel="noopener"> ATLauncher</a
|
|
>, <a href="https://multimc.org/" :target="$external()" rel="noopener">MultiMC</a>, and
|
|
<a href="https://prismlauncher.org" :target="$external()" rel="noopener">
|
|
Prism Launcher</a
|
|
>.
|
|
</div>
|
|
<Promotion v-if="tags.approvedStatuses.includes(project.status)" />
|
|
<div class="navigation-card">
|
|
<NavRow
|
|
:links="[
|
|
{
|
|
label: 'Description',
|
|
href: `/${project.project_type}/${project.slug ? project.slug : project.id}`,
|
|
},
|
|
{
|
|
label: 'Gallery',
|
|
href: `/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/gallery`,
|
|
shown: project.gallery.length > 0 || !!currentMember,
|
|
},
|
|
{
|
|
label: 'Changelog',
|
|
href: `/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/changelog`,
|
|
shown: versions.length > 0,
|
|
},
|
|
{
|
|
label: 'Versions',
|
|
href: `/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/versions`,
|
|
shown: versions.length > 0 || !!currentMember,
|
|
},
|
|
{
|
|
label: 'Moderation',
|
|
href: `/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/moderation`,
|
|
shown: !!currentMember,
|
|
},
|
|
]"
|
|
/>
|
|
<div v-if="auth.user && currentMember" class="input-group">
|
|
<nuxt-link
|
|
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
|
class="iconified-button"
|
|
>
|
|
<SettingsIcon /> Settings
|
|
</nuxt-link>
|
|
</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"
|
|
:current-member="currentMember"
|
|
:route="route"
|
|
/>
|
|
</section>
|
|
<div class="card normal-page__info">
|
|
<template
|
|
v-if="
|
|
project.issues_url ||
|
|
project.source_url ||
|
|
project.wiki_url ||
|
|
project.discord_url ||
|
|
project.donation_urls.length > 0
|
|
"
|
|
>
|
|
<h2 class="card-header">External resources</h2>
|
|
<div class="links">
|
|
<a
|
|
v-if="project.issues_url"
|
|
:href="project.issues_url"
|
|
class="title"
|
|
:target="$external()"
|
|
rel="noopener nofollow ugc"
|
|
>
|
|
<IssuesIcon aria-hidden="true" />
|
|
<span>Issues</span>
|
|
</a>
|
|
<a
|
|
v-if="project.source_url"
|
|
:href="project.source_url"
|
|
class="title"
|
|
:target="$external()"
|
|
rel="noopener nofollow ugc"
|
|
>
|
|
<CodeIcon aria-hidden="true" />
|
|
<span>Source</span>
|
|
</a>
|
|
<a
|
|
v-if="project.wiki_url"
|
|
:href="project.wiki_url"
|
|
class="title"
|
|
:target="$external()"
|
|
rel="noopener nofollow ugc"
|
|
>
|
|
<WikiIcon aria-hidden="true" />
|
|
<span>Wiki</span>
|
|
</a>
|
|
<a
|
|
v-if="project.discord_url"
|
|
:href="project.discord_url"
|
|
:target="$external()"
|
|
rel="noopener nofollow ugc"
|
|
>
|
|
<DiscordIcon class="shrink" aria-hidden="true" />
|
|
<span>Discord</span>
|
|
</a>
|
|
<a
|
|
v-for="(donation, index) in project.donation_urls"
|
|
:key="index"
|
|
:href="donation.url"
|
|
:target="$external()"
|
|
rel="noopener nofollow ugc"
|
|
>
|
|
<BuyMeACoffeeLogo 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'" />
|
|
<UnknownIcon v-else />
|
|
<span v-if="donation.id === 'bmac'">Buy Me a Coffee</span>
|
|
<span v-else-if="donation.id === 'patreon'">Patreon</span>
|
|
<span v-else-if="donation.id === 'paypal'">PayPal</span>
|
|
<span v-else-if="donation.id === 'ko-fi'">Ko-fi</span>
|
|
<span v-else-if="donation.id === 'github'">GitHub Sponsors</span>
|
|
<span v-else>Donate</span>
|
|
</a>
|
|
</div>
|
|
<hr class="card-divider" />
|
|
</template>
|
|
<template v-if="featuredVersions.length > 0">
|
|
<div class="featured-header">
|
|
<h2 class="card-header">Featured versions</h2>
|
|
<nuxt-link
|
|
v-if="$route.name !== 'type-id-versions' && (versions.length > 0 || currentMember)"
|
|
:to="`/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/versions#all-versions`"
|
|
class="goto-link"
|
|
>
|
|
See all
|
|
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
|
|
</nuxt-link>
|
|
</div>
|
|
<div
|
|
v-for="version in featuredVersions"
|
|
:key="version.id"
|
|
class="featured-version button-transparent"
|
|
@click="
|
|
$router.push(
|
|
`/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/version/${encodeURI(version.displayUrlEnding)}`
|
|
)
|
|
"
|
|
>
|
|
<a
|
|
v-tooltip="
|
|
version.primaryFile.filename + ' (' + $formatBytes(version.primaryFile.size) + ')'
|
|
"
|
|
:href="version.primaryFile.url"
|
|
class="download square-button brand-button"
|
|
:aria-label="`Download ${version.name}`"
|
|
@click.stop="(event) => event.stopPropagation()"
|
|
>
|
|
<DownloadIcon aria-hidden="true" />
|
|
</a>
|
|
<div class="info">
|
|
<nuxt-link
|
|
:to="`/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}/version/${encodeURI(version.displayUrlEnding)}`"
|
|
class="top"
|
|
>
|
|
{{ version.name }}
|
|
</nuxt-link>
|
|
<div v-if="version.game_versions.length > 0" class="game-version item">
|
|
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
|
|
{{ $formatVersion(version.game_versions) }}
|
|
</div>
|
|
<Badge v-if="version.version_type === 'release'" type="release" color="green" />
|
|
<Badge v-else-if="version.version_type === 'beta'" type="beta" color="orange" />
|
|
<Badge v-else-if="version.version_type === 'alpha'" type="alpha" color="red" />
|
|
</div>
|
|
</div>
|
|
<hr class="card-divider" />
|
|
</template>
|
|
<h2 class="card-header">Project members</h2>
|
|
<nuxt-link
|
|
v-for="member in members"
|
|
:key="member.user.id"
|
|
class="team-member columns button-transparent"
|
|
:to="'/user/' + member.user.username"
|
|
>
|
|
<Avatar :src="member.avatar_url" :alt="member.username" size="sm" circle />
|
|
|
|
<div class="member-info">
|
|
<p class="name">{{ member.name }}</p>
|
|
<p class="role">
|
|
{{ member.role }}
|
|
</p>
|
|
</div>
|
|
</nuxt-link>
|
|
<hr class="card-divider" />
|
|
<h2 class="card-header">Technical information</h2>
|
|
<div class="infos">
|
|
<div class="info">
|
|
<div class="key">License</div>
|
|
<div class="value lowercase">
|
|
<a
|
|
v-if="project.license.url"
|
|
class="text-link"
|
|
:href="project.license.url"
|
|
rel="noopener nofollow ugc"
|
|
>
|
|
{{ licenseIdDisplay }}
|
|
</a>
|
|
<span
|
|
v-else-if="
|
|
project.license.id === 'LicenseRef-All-Rights-Reserved' ||
|
|
!project.license.id.includes('LicenseRef')
|
|
"
|
|
class="text-link"
|
|
@click="getLicenseData()"
|
|
>
|
|
{{ licenseIdDisplay }}
|
|
</span>
|
|
<span v-else>{{ licenseIdDisplay }}</span>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="
|
|
project.project_type !== 'resourcepack' &&
|
|
project.project_type !== 'plugin' &&
|
|
project.project_type !== 'shader' &&
|
|
project.project_type !== 'datapack'
|
|
"
|
|
class="info"
|
|
>
|
|
<div class="key">Client side</div>
|
|
<div class="value">
|
|
{{ project.client_side }}
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="
|
|
project.project_type !== 'resourcepack' &&
|
|
project.project_type !== 'plugin' &&
|
|
project.project_type !== 'shader' &&
|
|
project.project_type !== 'datapack'
|
|
"
|
|
class="info"
|
|
>
|
|
<div class="key">Server side</div>
|
|
<div class="value">
|
|
{{ project.server_side }}
|
|
</div>
|
|
</div>
|
|
<div class="info">
|
|
<div class="key">Project ID</div>
|
|
<div class="value lowercase">
|
|
<CopyCode :text="project.id" />
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<a
|
|
v-if="
|
|
config.public.apiBaseUrl.startsWith('https://api.modrinth.com') &&
|
|
config.public.siteUrl !== 'https://modrinth.com'
|
|
"
|
|
class="iconified-button"
|
|
:href="`https://modrinth.com/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}`"
|
|
rel="noopener nofollow"
|
|
target="_blank"
|
|
>
|
|
<ExternalIcon aria-hidden="true" />
|
|
View on modrinth.com
|
|
</a>
|
|
<a
|
|
v-else-if="
|
|
config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com') &&
|
|
config.public.siteUrl !== 'https://staging.modrinth.com'
|
|
"
|
|
class="iconified-button"
|
|
:href="`https://staging.modrinth.com/${project.project_type}/${
|
|
project.slug ? project.slug : project.id
|
|
}`"
|
|
rel="noopener nofollow"
|
|
target="_blank"
|
|
>
|
|
<ExternalIcon aria-hidden="true" />
|
|
View on staging.modrinth.com
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script setup>
|
|
import CalendarIcon from '~/assets/images/utils/calendar.svg'
|
|
import ClearIcon from '~/assets/images/utils/clear.svg'
|
|
import DownloadIcon from '~/assets/images/utils/download.svg'
|
|
import UpdateIcon from '~/assets/images/utils/updated.svg'
|
|
import QueuedIcon from '~/assets/images/utils/list-end.svg'
|
|
import CodeIcon from '~/assets/images/sidebar/mod.svg'
|
|
import ExternalIcon from '~/assets/images/utils/external.svg'
|
|
import ReportIcon from '~/assets/images/utils/report.svg'
|
|
import HeartIcon from '~/assets/images/utils/heart.svg'
|
|
import IssuesIcon from '~/assets/images/utils/issues.svg'
|
|
import WikiIcon from '~/assets/images/utils/wiki.svg'
|
|
import DiscordIcon from '~/assets/images/external/discord.svg'
|
|
import BuyMeACoffeeLogo from '~/assets/images/external/bmac.svg'
|
|
import PatreonIcon from '~/assets/images/external/patreon.svg'
|
|
import KoFiIcon from '~/assets/images/external/kofi.svg'
|
|
import PayPalIcon from '~/assets/images/external/paypal.svg'
|
|
import OpenCollectiveIcon from '~/assets/images/external/opencollective.svg'
|
|
import UnknownIcon from '~/assets/images/utils/unknown-donation.svg'
|
|
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
|
import BoxIcon from '~/assets/images/utils/box.svg'
|
|
import Promotion from '~/components/ads/Promotion.vue'
|
|
import Badge from '~/components/ui/Badge.vue'
|
|
import Categories from '~/components/ui/search/Categories.vue'
|
|
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator.vue'
|
|
import Modal from '~/components/ui/Modal.vue'
|
|
import ModalReport from '~/components/ui/ModalReport.vue'
|
|
import NavRow from '~/components/ui/NavRow.vue'
|
|
import CopyCode from '~/components/ui/CopyCode.vue'
|
|
import Avatar from '~/components/ui/Avatar.vue'
|
|
import NavStack from '~/components/ui/NavStack.vue'
|
|
import NavStackItem from '~/components/ui/NavStackItem.vue'
|
|
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
|
|
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
|
import UsersIcon from '~/assets/images/utils/users.svg'
|
|
import CategoriesIcon from '~/assets/images/utils/tags.svg'
|
|
import DescriptionIcon from '~/assets/images/utils/align-left.svg'
|
|
import LinksIcon from '~/assets/images/utils/link.svg'
|
|
import LicenseIcon from '~/assets/images/utils/copyright.svg'
|
|
import GalleryIcon from '~/assets/images/utils/image.svg'
|
|
import VersionIcon from '~/assets/images/utils/version.svg'
|
|
import { renderString } from '~/helpers/parse.js'
|
|
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
|
|
|
const data = useNuxtApp()
|
|
const route = useRoute()
|
|
const config = useRuntimeConfig()
|
|
|
|
const auth = await useAuth()
|
|
const user = await useUser()
|
|
const cosmetics = useCosmetics()
|
|
const tags = useTags()
|
|
|
|
if (
|
|
!route.params.id ||
|
|
!(
|
|
tags.value.projectTypes.find((x) => x.id === route.params.type) ||
|
|
route.params.type === 'project'
|
|
)
|
|
) {
|
|
throw createError({
|
|
fatal: true,
|
|
statusCode: 404,
|
|
message: 'The page could not be found',
|
|
})
|
|
}
|
|
|
|
let project, allMembers, dependencies, featuredVersions, versions
|
|
try {
|
|
;[
|
|
{ data: project },
|
|
{ data: allMembers },
|
|
{ data: dependencies },
|
|
{ data: featuredVersions },
|
|
{ data: versions },
|
|
] = await Promise.all([
|
|
useAsyncData(`project/${route.params.id}`, () => useBaseFetch(`project/${route.params.id}`), {
|
|
transform: (project) => {
|
|
if (project) {
|
|
project.actualProjectType = JSON.parse(JSON.stringify(project.project_type))
|
|
project.project_type = data.$getProjectTypeForUrl(
|
|
project.project_type,
|
|
project.loaders,
|
|
tags.value
|
|
)
|
|
|
|
if (process.client && history.state && history.state.overrideProjectType) {
|
|
project.project_type = history.state.overrideProjectType
|
|
}
|
|
}
|
|
|
|
return project
|
|
},
|
|
}),
|
|
useAsyncData(
|
|
`project/${route.params.id}/members`,
|
|
() => useBaseFetch(`project/${route.params.id}/members`),
|
|
{
|
|
transform: (members) => {
|
|
members.forEach((it, index) => {
|
|
members[index].avatar_url = it.user.avatar_url
|
|
members[index].name = it.user.username
|
|
})
|
|
|
|
return members
|
|
},
|
|
}
|
|
),
|
|
useAsyncData(`project/${route.params.id}/dependencies`, () =>
|
|
useBaseFetch(`project/${route.params.id}/dependencies`)
|
|
),
|
|
useAsyncData(`project/${route.params.id}/version?featured=true`, () =>
|
|
useBaseFetch(`project/${route.params.id}/version?featured=true`)
|
|
),
|
|
useAsyncData(`project/${route.params.id}/version`, () =>
|
|
useBaseFetch(`project/${route.params.id}/version`)
|
|
),
|
|
])
|
|
|
|
versions = shallowRef(toRaw(versions))
|
|
featuredVersions = shallowRef(toRaw(featuredVersions))
|
|
} catch (error) {
|
|
throw createError({
|
|
fatal: true,
|
|
statusCode: 404,
|
|
message: 'Project not found',
|
|
})
|
|
}
|
|
|
|
if (!project.value) {
|
|
throw createError({
|
|
fatal: true,
|
|
statusCode: 404,
|
|
message: 'Project not found',
|
|
})
|
|
}
|
|
|
|
if (project.value.project_type !== route.params.type || route.params.id !== project.value.slug) {
|
|
let path = route.fullPath.split('/')
|
|
path.splice(0, 3)
|
|
path = path.filter((x) => x)
|
|
|
|
await navigateTo(
|
|
`/${project.value.project_type}/${project.value.slug}${
|
|
path.length > 0 ? `/${path.join('/')}` : ''
|
|
}`,
|
|
{ redirectCode: 301, replace: true }
|
|
)
|
|
}
|
|
|
|
const members = ref(allMembers.value.filter((x) => x.accepted))
|
|
const currentMember = ref(
|
|
auth.value.user ? allMembers.value.find((x) => x.user.id === auth.value.user.id) : null
|
|
)
|
|
|
|
if (
|
|
!currentMember.value &&
|
|
auth.value.user &&
|
|
tags.value.staffRoles.includes(auth.value.user.role)
|
|
) {
|
|
currentMember.value = {
|
|
team_id: project.team_id,
|
|
user: auth.value.user,
|
|
role: auth.value.role,
|
|
permissions: auth.value.user.role === 'admin' ? 1023 : 12,
|
|
accepted: true,
|
|
payouts_split: 0,
|
|
avatar_url: auth.value.user.avatar_url,
|
|
name: auth.value.user.username,
|
|
}
|
|
}
|
|
|
|
versions.value = data.$computeVersions(versions.value, allMembers.value)
|
|
|
|
// Q: Why do this instead of computing the versions of featuredVersions?
|
|
// A: It will incorrectly generate the version slugs because it doesn't have the full context of
|
|
// all the versions. For example, if version 1.1.0 for Forge is featured but 1.1.0 for Fabric
|
|
// is not, but the Fabric one was uploaded first, the Forge version would link to the Fabric
|
|
/// version
|
|
const featuredIds = featuredVersions.value.map((x) => x.id)
|
|
featuredVersions.value = versions.value.filter((version) => featuredIds.includes(version.id))
|
|
|
|
featuredVersions.value.sort((a, b) => {
|
|
const aLatest = a.game_versions[a.game_versions.length - 1]
|
|
const bLatest = b.game_versions[b.game_versions.length - 1]
|
|
const gameVersions = tags.value.gameVersions.map((e) => e.version)
|
|
return gameVersions.indexOf(aLatest) - gameVersions.indexOf(bLatest)
|
|
})
|
|
|
|
const projectTypeDisplay = computed(() =>
|
|
data.$formatProjectType(
|
|
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders)
|
|
)
|
|
)
|
|
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
|
|
}
|
|
})
|
|
const featuredGalleryImage = computed(() => project.value.gallery.find((img) => img.featured))
|
|
|
|
async function resetProject() {
|
|
const newProject = await useBaseFetch(`project/${project.value.id}`)
|
|
|
|
newProject.actualProjectType = JSON.parse(JSON.stringify(newProject.project_type))
|
|
|
|
newProject.project_type = data.$getProjectTypeForUrl(newProject.project_type, newProject.loaders)
|
|
|
|
project.value = newProject
|
|
}
|
|
|
|
async function clearMessage() {
|
|
startLoading()
|
|
|
|
try {
|
|
await useBaseFetch(`project/${project.value.id}`, {
|
|
method: 'PATCH',
|
|
body: {
|
|
moderation_message: null,
|
|
moderation_message_body: null,
|
|
},
|
|
})
|
|
|
|
project.value.moderator_message = null
|
|
} catch (err) {
|
|
data.$notify({
|
|
group: 'main',
|
|
title: 'An error occurred',
|
|
text: err.data.description,
|
|
type: 'error',
|
|
})
|
|
}
|
|
|
|
stopLoading()
|
|
}
|
|
|
|
async function setProcessing() {
|
|
startLoading()
|
|
|
|
try {
|
|
await useBaseFetch(`project/${project.value.id}`, {
|
|
method: 'PATCH',
|
|
body: {
|
|
status: 'processing',
|
|
},
|
|
})
|
|
|
|
project.value.status = 'processing'
|
|
} catch (err) {
|
|
data.$notify({
|
|
group: 'main',
|
|
title: 'An error occurred',
|
|
text: err.data.description,
|
|
type: 'error',
|
|
})
|
|
}
|
|
|
|
stopLoading()
|
|
}
|
|
|
|
const modalLicense = ref(null)
|
|
const licenseText = ref('')
|
|
async function getLicenseData() {
|
|
try {
|
|
const text = await useBaseFetch(`tag/license/${project.value.license.id}`)
|
|
licenseText.value = text.body
|
|
} catch {
|
|
licenseText.value = 'License text could not be retrieved.'
|
|
}
|
|
|
|
modalLicense.value.show()
|
|
}
|
|
|
|
async function patchProject(resData, quiet = false) {
|
|
let result = false
|
|
startLoading()
|
|
|
|
try {
|
|
await useBaseFetch(`project/${project.value.id}`, {
|
|
method: 'PATCH',
|
|
body: resData,
|
|
})
|
|
|
|
for (const key in resData) {
|
|
project.value[key] = resData[key]
|
|
}
|
|
|
|
if (resData.license_id) {
|
|
project.value.license.id = resData.license_id
|
|
}
|
|
if (resData.license_url) {
|
|
project.value.license.url = resData.license_url
|
|
}
|
|
|
|
result = true
|
|
if (!quiet) {
|
|
data.$notify({
|
|
group: 'main',
|
|
title: 'Project updated',
|
|
text: 'Your project has been updated.',
|
|
type: 'success',
|
|
})
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}
|
|
} catch (err) {
|
|
data.$notify({
|
|
group: 'main',
|
|
title: 'An error occurred',
|
|
text: err.data.description,
|
|
type: 'error',
|
|
})
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}
|
|
|
|
stopLoading()
|
|
|
|
return result
|
|
}
|
|
|
|
async function patchIcon(icon) {
|
|
let result = false
|
|
startLoading()
|
|
|
|
try {
|
|
await useBaseFetch(
|
|
`project/${project.value.id}/icon?ext=${
|
|
icon.type.split('/')[icon.type.split('/').length - 1]
|
|
}`,
|
|
{
|
|
method: 'PATCH',
|
|
body: icon,
|
|
}
|
|
)
|
|
await resetProject()
|
|
result = true
|
|
data.$notify({
|
|
group: 'main',
|
|
title: 'Project icon updated',
|
|
text: "Your project's icon has been updated.",
|
|
type: 'success',
|
|
})
|
|
} catch (err) {
|
|
data.$notify({
|
|
group: 'main',
|
|
title: 'An error occurred',
|
|
text: err.data.description,
|
|
type: 'error',
|
|
})
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}
|
|
|
|
stopLoading()
|
|
return result
|
|
}
|
|
|
|
async function updateMembers() {
|
|
allMembers.value = await useAsyncData(
|
|
`project/${route.params.id}/members`,
|
|
() => useBaseFetch(`project/${route.params.id}/members`),
|
|
{
|
|
transform: (members) => {
|
|
members.forEach((it, index) => {
|
|
members[index].avatar_url = it.user.avatar_url
|
|
members[index].name = it.user.username
|
|
})
|
|
|
|
return members
|
|
},
|
|
}
|
|
)
|
|
}
|
|
|
|
const collapsedChecklist = ref(false)
|
|
</script>
|
|
<style lang="scss" scoped>
|
|
.header {
|
|
grid-area: header;
|
|
.title {
|
|
overflow-wrap: break-word;
|
|
margin: var(--spacing-card-xs) 0;
|
|
color: var(--color-text-dark);
|
|
font-size: var(--font-size-xl);
|
|
}
|
|
|
|
.project-type {
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
|
|
svg {
|
|
vertical-align: top;
|
|
margin-right: 0.25em;
|
|
}
|
|
|
|
&:hover,
|
|
&:focus-visible {
|
|
span {
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
}
|
|
|
|
.description {
|
|
line-height: 1.3;
|
|
overflow-wrap: break-word;
|
|
|
|
margin-top: var(--spacing-card-sm);
|
|
margin-bottom: 0.5rem;
|
|
font-size: var(--font-size-nm);
|
|
}
|
|
|
|
.categories {
|
|
margin: 0.25rem 0;
|
|
color: var(--color-text-secondary);
|
|
font-size: var(--font-size-nm);
|
|
}
|
|
|
|
.dates {
|
|
margin: 0.75rem 0;
|
|
|
|
.date {
|
|
color: var(--color-text-secondary);
|
|
font-size: var(--font-size-nm);
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 0.25rem;
|
|
cursor: default;
|
|
|
|
.label {
|
|
margin-right: 0.25rem;
|
|
}
|
|
|
|
svg {
|
|
height: 1rem;
|
|
margin-right: 0.25rem;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.project__header {
|
|
overflow: hidden;
|
|
.project__gallery {
|
|
display: none;
|
|
}
|
|
&.has-featured-image {
|
|
.project__gallery {
|
|
display: inline-block;
|
|
width: 100%;
|
|
height: 10rem;
|
|
background-color: var(--color-button-bg-active);
|
|
img {
|
|
width: 100%;
|
|
height: 10rem;
|
|
object-fit: cover;
|
|
}
|
|
}
|
|
.project__icon {
|
|
margin-top: calc(-3rem - var(--spacing-card-lg) - 4px);
|
|
margin-left: -4px;
|
|
z-index: 1;
|
|
border: 4px solid var(--color-raised-bg);
|
|
border-bottom: none;
|
|
}
|
|
}
|
|
.project__header__content {
|
|
margin: 0;
|
|
background: none;
|
|
border-radius: unset;
|
|
}
|
|
}
|
|
|
|
.project-info {
|
|
height: auto;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-header {
|
|
font-size: 1.125rem;
|
|
font-weight: bold;
|
|
color: var(--color-heading);
|
|
margin-top: var(--spacing-card-md);
|
|
margin-bottom: 0.3rem;
|
|
width: fit-content;
|
|
}
|
|
|
|
.featured-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
|
|
.card-header {
|
|
height: 23px;
|
|
}
|
|
|
|
.goto-link {
|
|
margin-bottom: 0.3rem;
|
|
}
|
|
}
|
|
|
|
.featured-version {
|
|
display: flex;
|
|
flex-direction: row;
|
|
padding: 0.5rem;
|
|
|
|
.download {
|
|
height: 2.5rem;
|
|
width: 2.5rem;
|
|
margin-right: 0.75rem;
|
|
|
|
svg {
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
}
|
|
}
|
|
|
|
.info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.top {
|
|
font-weight: bold;
|
|
word-wrap: break-word;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
}
|
|
}
|
|
|
|
.links {
|
|
a {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
border-radius: 1rem;
|
|
|
|
svg,
|
|
img {
|
|
height: 1rem;
|
|
width: 1rem;
|
|
}
|
|
|
|
span {
|
|
margin-left: 0.25rem;
|
|
text-decoration: underline;
|
|
line-height: 2rem;
|
|
}
|
|
|
|
&:focus-visible,
|
|
&:hover {
|
|
svg,
|
|
img,
|
|
span {
|
|
color: var(--color-heading);
|
|
}
|
|
}
|
|
|
|
&:active {
|
|
svg,
|
|
img,
|
|
span {
|
|
color: var(--color-text-dark);
|
|
}
|
|
}
|
|
|
|
&:not(:last-child)::after {
|
|
content: '•';
|
|
margin: 0 0.25rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
.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;
|
|
}
|
|
}
|
|
}
|
|
|
|
.infos {
|
|
.info {
|
|
display: flex;
|
|
margin: 0.5rem 0;
|
|
|
|
.key {
|
|
font-weight: bold;
|
|
color: var(--color-text-secondary);
|
|
width: 40%;
|
|
}
|
|
|
|
.value {
|
|
width: 50%;
|
|
|
|
&::first-letter {
|
|
text-transform: capitalize;
|
|
}
|
|
|
|
&.lowercase {
|
|
&::first-letter {
|
|
text-transform: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
.uppercase {
|
|
text-transform: uppercase;
|
|
}
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 550px) {
|
|
.title a {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 800px) {
|
|
.project-navigation {
|
|
display: block;
|
|
overflow-x: auto;
|
|
overflow-wrap: break-word;
|
|
overflow-y: hidden;
|
|
}
|
|
}
|
|
|
|
@media screen and (min-width: 1024px) {
|
|
.content {
|
|
max-width: calc(1280px - 21rem);
|
|
}
|
|
}
|
|
|
|
.status-buttons {
|
|
margin-top: var(--spacing-card-sm);
|
|
}
|
|
|
|
.mod-message__title {
|
|
font-weight: bold;
|
|
margin-bottom: var(--spacing-card-xs);
|
|
font-size: 1.125rem;
|
|
}
|
|
|
|
.modal-license {
|
|
padding: var(--spacing-card-bg);
|
|
}
|
|
.settings-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: var(--spacing-card-sm);
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-card-bg);
|
|
|
|
.settings-header__icon {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.settings-header__text {
|
|
h1 {
|
|
font-size: var(--font-size-md);
|
|
margin-top: 0;
|
|
margin-bottom: var(--spacing-card-sm);
|
|
}
|
|
}
|
|
}
|
|
|
|
.normal-page__sidebar .mod-button {
|
|
margin-top: var(--spacing-card-sm);
|
|
}
|
|
</style>
|