You've already forked AstralRinth
forked from didirus/AstralRinth
Add initial support for the v2 of the API (Still WIP) (#250)
* Functionally implement modpacks * Add LogoAnimated to logo license * Fix eslint errors * Add `z-index: 20` to user dropdown (#287) * Fix pages not working, add changelog page, redesign versions page * Update theme colors, add OLED theme, update some project creation text. (#292) * Update theme colors, add OLED theme, update some project creation text. * Make summary normal text color * Update favicons, update logos to use dynamic colors, updated filters panel a bit * Update wording from #250 * Version page rework * Manually apply some commits from master, other minor v2 fixes (#296) * Homepage styling improvements (#285) * Add border radius to video + example code colors * Change color + allow overflow scroll * Minor v2 fixes - Makes multiple loaders display correctly (used to be `Fabric,Forge` is now `Fabric, Forge` - Fix oopses in #292 - Allow .jar and .zip in file prompt - Apply 30cbd3a6c372940d1e86cc8134d0dfc7e8e5ee9c to pages/create/project.vue - Display `fabric, forge` instead of broken icons on pages/create/project.vue * Markdown styling fixes (#268) * Add table color variables (+ prettier fixes) * Add details and table styling to .markdown-body * Add indexing meta value depending on the status of the mod. (#261) * General UI Improvement (again) (#255) * Add and fix some stuff * Add warning when leaving to `mod/create` * Fix mods/create not working * Fix a bug & add improvements to a couple moderation aspects (#278) This PR fixes reports on the moderation dashboard going to `/dashboard/mod/_id` instead of to `/mod/_id`. It also allows the ability for moderators to unlist mods in the queue from the frontend instead of having to do it via the backend.  Unlisted mods should have the ability to resubmit for approval, so I've also changed "Submit for Review" to "Submit for approval", allowing unlisted mods to do that as well.  * Add project guidelines to Terms page (#275) * Add project guidelines to Terms page This adds the project guidelines as outlined [here](https://discord.com/channels/734077874708938864/734077874708938867/806556531491471368). NOTE: I've made a few tweaks in wording to accommodate this format, so this is not an exact copy. * Move rules to its own page * Allow users to login from search page when it is rendered serverside (#272) * Change `this.$route.fullPath` → `this.$route.path` * Closes modrinth/knossos#256 * Wrap mod icon and title in link (#273) * Wrap mod icon and title in link * Fixes #218 * Editor's note Skipped #249 (search was rewritten), #266 (couldn't figure out how to apply it), #270 (didn't seem to apply properly), #252 (manually merged in with #292), #262 (superceded by #270), #282, #271, #277, #283, and #281 (those five didn't get wiped) Co-authored-by: venashial <venashial.levo@aleeas.com> Co-authored-by: Redblueflame <contact@redblueflame.com> Co-authored-by: Johan Novak <wickedtree@wickedtree.codes> * SSR descriptions, version edit page * Working version editing + dependency management (besides files) * Version create page, file functionality * Fix some issues with the version page * More versions page fixes * Project gallery * Box shadows, user profile page, WIP header * Finish user dashboard * Finish search and fix minor issues * Moderator page + messages, notifications page * Fix dropdown menu, fix XSS, fix team members page * Change doc url on main page (#309) * Re-Fix docs url (#313) * Clean up. Part 1: Fix immediate problems (#316) * Clean up tabs and cards CSS a little * Fix project page; Remove bad styles from search * Yeet and flatten lots of styles; fix font sizes * Restyle search; fix moderation * Fix profile page * Remove injected SCSS entirely * Fix a mobile layout overflowing * Apiv2-support fixes (#320) * Fix member user_id -> user.id * Fix incorrect report redirect * Change theme switcher from button to multiselect * Fix remaining items Co-authored-by: Jai A <jaiagr+gpg@pm.me> * Fix bugs * Full mobile support, update create project page, fix various bugs * New Dark Mode brand colors (#325) * Use "color-brand-hover" for auth-prompt when hover over * New dark mode brand colors * Fix new version featured bug * Remove old home page, other fixes * Fix error when merging * Fix prettier error :( Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: venashial <venashial.levo@aleeas.com> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com> Co-authored-by: Emma <emmaffle@modrinth.com> Co-authored-by: Johan Novak <wickedtree@wickedtree.codes> Co-authored-by: Jai A <jaiagr@pm.me> Co-authored-by: Mysterious_Dev <40738104+Mysterious-Dev@users.noreply.github.com> Co-authored-by: Mikhail Oleynikov <contact@falseresync.ru> Co-authored-by: Christian Popov <30723811+Xrey274@users.noreply.github.com>
This commit is contained in:
977
pages/_type/_id.vue
Normal file
977
pages/_type/_id.vue
Normal file
@@ -0,0 +1,977 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<section class="project-info">
|
||||
<div class="header card">
|
||||
<nuxt-link
|
||||
:to="
|
||||
'/' +
|
||||
project.project_type +
|
||||
'/' +
|
||||
(project.slug ? project.slug : project.id)
|
||||
"
|
||||
>
|
||||
<img
|
||||
class="icon"
|
||||
:src="
|
||||
project.icon_url
|
||||
? project.icon_url
|
||||
: 'https://cdn.modrinth.com/placeholder.svg?inline'
|
||||
"
|
||||
alt="project - icon"
|
||||
/></nuxt-link>
|
||||
<nuxt-link
|
||||
:to="
|
||||
'/' +
|
||||
project.project_type +
|
||||
'/' +
|
||||
(project.slug ? project.slug : project.id)
|
||||
"
|
||||
>
|
||||
<h1 class="title">{{ project.title }}</h1>
|
||||
</nuxt-link>
|
||||
<div
|
||||
v-if="
|
||||
project.client_side === 'optional' &&
|
||||
project.server_side === 'optional'
|
||||
"
|
||||
class="side-descriptor"
|
||||
>
|
||||
<InfoIcon />
|
||||
Universal {{ project.project_type }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(project.client_side === 'optional' ||
|
||||
project.client_side === 'required') &&
|
||||
(project.server_side === 'optional' ||
|
||||
project.server_side === 'unsupported')
|
||||
"
|
||||
class="side-descriptor"
|
||||
>
|
||||
<InfoIcon />
|
||||
Client {{ project.project_type }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
(project.server_side === 'optional' ||
|
||||
project.server_side === 'required') &&
|
||||
(project.client_side === 'optional' ||
|
||||
project.client_side === 'unsupported')
|
||||
"
|
||||
class="side-descriptor"
|
||||
>
|
||||
<InfoIcon />
|
||||
Server {{ project.project_type }}
|
||||
</div>
|
||||
<p class="description">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
<Categories :categories="project.categories" class="categories" />
|
||||
<hr />
|
||||
<div class="stats">
|
||||
<span class="stat">{{ formatNumber(project.downloads) }}</span>
|
||||
<span class="label">downloads</span>
|
||||
<span class="stat">{{ formatNumber(project.followers) }}</span>
|
||||
<span class="label">followers</span>
|
||||
</div>
|
||||
<div class="dates">
|
||||
<div class="date">
|
||||
<CalendarIcon />
|
||||
<span class="label">Created</span>
|
||||
<span class="value">{{
|
||||
$dayjs(project.published).fromNow()
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="date">
|
||||
<UpdateIcon />
|
||||
<span class="label">Updated</span>
|
||||
<span class="value">{{ $dayjs(project.updated).fromNow() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="buttons">
|
||||
<nuxt-link
|
||||
v-if="$auth.user"
|
||||
:to="`/create/report?id=${project.id}&t=project`"
|
||||
class="iconified-button"
|
||||
>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="
|
||||
$auth.user && !$user.follows.find((x) => x.id === project.id)
|
||||
"
|
||||
class="iconified-button"
|
||||
@click="$store.dispatch('user/followProject', project)"
|
||||
>
|
||||
<FollowIcon />
|
||||
Follow
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
$auth.user && $user.follows.find((x) => x.id === project.id)
|
||||
"
|
||||
class="iconified-button"
|
||||
@click="$store.dispatch('user/unfollowProject', project)"
|
||||
>
|
||||
<FollowIcon fill="currentColor" />
|
||||
Unfollow
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
currentMember &&
|
||||
(project.status === 'processing' ||
|
||||
(project.moderator_message &&
|
||||
(project.moderator_message.message ||
|
||||
project.moderator_message.body)))
|
||||
"
|
||||
class="card"
|
||||
>
|
||||
<h3>Project status</h3>
|
||||
<div class="status-info"></div>
|
||||
<p>
|
||||
Your project is currently:
|
||||
<VersionBadge
|
||||
v-if="project.status === 'approved'"
|
||||
color="green"
|
||||
:type="project.status"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="
|
||||
project.status === 'processing' || project.status === 'archived'
|
||||
"
|
||||
color="yellow"
|
||||
:type="project.status"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="project.status === 'rejected'"
|
||||
color="red"
|
||||
:type="project.status"
|
||||
/>
|
||||
<VersionBadge v-else color="gray" :type="project.status" />
|
||||
</p>
|
||||
<div class="message">
|
||||
<p v-if="project.status === 'processing'">
|
||||
Your project is currently not viewable by people who are not part
|
||||
of your team. Please wait for our moderators to manually review
|
||||
your project to see if it abides by our project rules!
|
||||
</p>
|
||||
<p v-if="project.status === 'draft'">
|
||||
Your project is currently not viewable by people who are not part
|
||||
of your team. If your project is ready for review, click the
|
||||
button below to make your mod public!
|
||||
</p>
|
||||
<p v-if="project.moderator_message">
|
||||
{{ project.moderator_message.message }}
|
||||
</p>
|
||||
<div
|
||||
v-if="project.moderator_message && project.moderator_message.body"
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="$xss($md.render(project.moderator_message.body))"
|
||||
></div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button
|
||||
v-if="
|
||||
project.status !== 'processing' && project.status !== 'approved'
|
||||
"
|
||||
class="iconified-button"
|
||||
@click="submitForReview"
|
||||
>
|
||||
Resubmit for approval
|
||||
</button>
|
||||
<button
|
||||
v-if="project.status === 'approved'"
|
||||
class="iconified-button"
|
||||
@click="clearMessage"
|
||||
>
|
||||
Clear message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<template
|
||||
v-if="
|
||||
project.issues_url ||
|
||||
project.source_url ||
|
||||
project.wiki_url ||
|
||||
project.discord_url
|
||||
"
|
||||
>
|
||||
<h3>External resources</h3>
|
||||
<div class="links">
|
||||
<a
|
||||
v-if="project.issues_url"
|
||||
:href="project.issues_url"
|
||||
class="title"
|
||||
target="_blank"
|
||||
>
|
||||
<IssuesIcon />
|
||||
<span>Issues</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="project.source_url"
|
||||
:href="project.source_url"
|
||||
class="title"
|
||||
target="_blank"
|
||||
>
|
||||
<CodeIcon />
|
||||
<span>Source</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="project.wiki_url"
|
||||
:href="project.wiki_url"
|
||||
class="title"
|
||||
target="_blank"
|
||||
>
|
||||
<WikiIcon />
|
||||
<span>Wiki</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="project.discord_url"
|
||||
:href="project.discord_url"
|
||||
target="_blank"
|
||||
>
|
||||
<DiscordIcon
|
||||
v-if="$colorMode.value === 'light'"
|
||||
class="shrink"
|
||||
/>
|
||||
<DiscordIconWhite v-else class="shrink" />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
<a
|
||||
v-for="(donation, index) in project.donation_urls"
|
||||
:key="index"
|
||||
:href="donation.url"
|
||||
target="_blank"
|
||||
>
|
||||
<BuyMeACoffeeLogo
|
||||
v-if="donation.id === 'bmac' && $colorMode.value === 'light'"
|
||||
/>
|
||||
<BuyMeACoffeeLogoWhite
|
||||
v-else-if="
|
||||
donation.id === 'bmac' && $colorMode.value === 'dark'
|
||||
"
|
||||
/>
|
||||
<img
|
||||
v-else-if="
|
||||
donation.id === 'patreon' && $colorMode.value === 'light'
|
||||
"
|
||||
class="shrink"
|
||||
alt="patreon"
|
||||
src="~/assets/images/external/patreon.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="
|
||||
donation.id === 'patreon' && $colorMode.value === 'dark'
|
||||
"
|
||||
class="shrink"
|
||||
alt="patreon"
|
||||
src="~/assets/images/external/patreon-white.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="
|
||||
donation.id === 'paypal' && $colorMode.value === 'light'
|
||||
"
|
||||
class="shrink"
|
||||
alt="paypal"
|
||||
src="~/assets/images/external/paypal.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="
|
||||
donation.id === 'paypal' && $colorMode.value === 'dark'
|
||||
"
|
||||
class="shrink"
|
||||
alt="paypal"
|
||||
src="~/assets/images/external/paypal-white.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="
|
||||
donation.id === 'ko-fi' && $colorMode.value === 'light'
|
||||
"
|
||||
alt="kofi"
|
||||
src="~/assets/images/external/kofi.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="
|
||||
donation.id === 'ko-fi' && $colorMode.value === 'dark'
|
||||
"
|
||||
alt="kofi"
|
||||
src="~/assets/images/external/kofi-white.png"
|
||||
/>
|
||||
<FollowIcon 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 />
|
||||
</template>
|
||||
<template v-if="featuredVersions.length > 0">
|
||||
<h3>Featured versions</h3>
|
||||
<div
|
||||
v-for="version in featuredVersions"
|
||||
:key="version.id"
|
||||
class="featured-version"
|
||||
>
|
||||
<a
|
||||
:href="findPrimary(version).url"
|
||||
class="download"
|
||||
@click.prevent="
|
||||
downloadFile(
|
||||
findPrimary(version).hashes.sha1,
|
||||
findPrimary(version).url
|
||||
)
|
||||
"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</a>
|
||||
<div class="info">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURIComponent(version.version_number)}`"
|
||||
class="top"
|
||||
>
|
||||
{{ version.name }}
|
||||
</nuxt-link>
|
||||
<div
|
||||
v-if="version.game_versions.length > 0"
|
||||
class="game-version item"
|
||||
>
|
||||
{{
|
||||
version.loaders
|
||||
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
|
||||
.join(', ')
|
||||
}}
|
||||
{{ version.game_versions[version.game_versions.length - 1] }}
|
||||
</div>
|
||||
<VersionBadge
|
||||
v-if="version.version_type === 'release'"
|
||||
type="release"
|
||||
color="green"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="version.version_type === 'beta'"
|
||||
type="beta"
|
||||
color="yellow"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="version.version_type === 'alpha'"
|
||||
type="alpha"
|
||||
color="red"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</template>
|
||||
<h3>Project members</h3>
|
||||
<div
|
||||
v-for="member in members"
|
||||
:key="member.user.id"
|
||||
class="team-member columns"
|
||||
>
|
||||
<img :src="member.avatar_url" alt="profile-picture" />
|
||||
<div class="member-info">
|
||||
<nuxt-link :to="'/user/' + member.user.username" class="name">
|
||||
<p>{{ member.name }}</p>
|
||||
</nuxt-link>
|
||||
<p class="role">{{ member.role }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<h3>Technical information</h3>
|
||||
<div class="infos">
|
||||
<div class="info">
|
||||
<div class="key">License</div>
|
||||
<div class="value uppercase">
|
||||
<a class="text-link" :href="project.license.url || null">{{
|
||||
project.license.id
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="key">Client side</div>
|
||||
<div class="value">
|
||||
{{ project.client_side }}
|
||||
</div>
|
||||
</div>
|
||||
<div 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">
|
||||
{{ project.id }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Advertisement
|
||||
v-if="project.status === 'approved' || project.status === 'unlisted'"
|
||||
type="square"
|
||||
small-screen="destroy"
|
||||
/>
|
||||
</section>
|
||||
<div class="content">
|
||||
<div class="project-main">
|
||||
<div class="card styled-tabs">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}`"
|
||||
class="tab"
|
||||
exact
|
||||
>
|
||||
<span>Description</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-if="project.gallery.length > 0 || currentMember"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/gallery`"
|
||||
class="tab"
|
||||
>
|
||||
<span>Gallery</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-if="project.versions.length > 0"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/changelog`"
|
||||
class="tab"
|
||||
>
|
||||
<span>Changelog</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-if="project.versions.length > 0"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/versions`"
|
||||
class="tab"
|
||||
>
|
||||
<span>Versions</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-if="currentMember"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
class="tab"
|
||||
>
|
||||
<span>Settings</span>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<Advertisement
|
||||
v-if="
|
||||
project.status === 'approved' || project.status === 'unlisted'
|
||||
"
|
||||
type="banner"
|
||||
small-screen="square"
|
||||
ethical-ads-small
|
||||
ethical-ads-big
|
||||
/>
|
||||
<div class="project-content">
|
||||
<NuxtChild
|
||||
:project.sync="project"
|
||||
:versions.sync="versions"
|
||||
:featured-versions.sync="featuredVersions"
|
||||
:members.sync="members"
|
||||
:current-member="currentMember"
|
||||
:all-members.sync="allMembers"
|
||||
:dependencies.sync="dependencies"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import UpdateIcon from '~/assets/images/utils/updated.svg?inline'
|
||||
import CodeIcon from '~/assets/images/sidebar/mod.svg?inline'
|
||||
import ReportIcon from '~/assets/images/utils/report.svg?inline'
|
||||
import FollowIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import InfoIcon from '~/assets/images/utils/info.svg?inline'
|
||||
import IssuesIcon from '~/assets/images/utils/issues.svg?inline'
|
||||
import WikiIcon from '~/assets/images/utils/wiki.svg?inline'
|
||||
import DiscordIcon from '~/assets/images/external/discord.svg?inline'
|
||||
import DiscordIconWhite from '~/assets/images/external/discord-white.svg?inline'
|
||||
import BuyMeACoffeeLogo from '~/assets/images/external/bmac.svg?inline'
|
||||
import BuyMeACoffeeLogoWhite from '~/assets/images/external/bmac-white.svg?inline'
|
||||
import UnknownIcon from '~/assets/images/utils/unknown.svg?inline'
|
||||
|
||||
import Advertisement from '~/components/ads/Advertisement'
|
||||
import VersionBadge from '~/components/ui/Badge'
|
||||
import Categories from '~/components/ui/search/Categories'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VersionBadge,
|
||||
Advertisement,
|
||||
IssuesIcon,
|
||||
DownloadIcon,
|
||||
CalendarIcon,
|
||||
UpdateIcon,
|
||||
CodeIcon,
|
||||
ReportIcon,
|
||||
FollowIcon,
|
||||
InfoIcon,
|
||||
WikiIcon,
|
||||
DiscordIcon,
|
||||
DiscordIconWhite,
|
||||
BuyMeACoffeeLogo,
|
||||
BuyMeACoffeeLogoWhite,
|
||||
UnknownIcon,
|
||||
Categories,
|
||||
},
|
||||
async asyncData(data) {
|
||||
const projectTypes = ['mod', 'modpack']
|
||||
|
||||
try {
|
||||
if (
|
||||
!data.params.id ||
|
||||
!projectTypes.includes(data.params.type.toLowerCase())
|
||||
) {
|
||||
data.error({
|
||||
statusCode: 404,
|
||||
message: 'The page could not be found',
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const [project, members, dependencies, versions, featuredVersions] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`project/${data.params.id}`, data.$auth.headers),
|
||||
data.$axios.get(
|
||||
`project/${data.params.id}/members`,
|
||||
data.$auth.headers
|
||||
),
|
||||
data.$axios.get(`project/${data.params.id}/dependencies`),
|
||||
data.$axios.get(`project/${data.params.id}/version`),
|
||||
data.$axios.get(`project/${data.params.id}/version?featured=true`),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
if (project.project_type !== data.params.type) {
|
||||
data.error({
|
||||
statusCode: 404,
|
||||
message: 'Project not found',
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
members.forEach((it, index) => {
|
||||
members[index].avatar_url = it.user.avatar_url
|
||||
members[index].name = it.user.username
|
||||
})
|
||||
|
||||
const currentMember = data.$auth.user
|
||||
? members.find((x) => x.user.id === data.$auth.user.id)
|
||||
: null
|
||||
|
||||
if (project.body_url && !project.body) {
|
||||
project.body = (await data.$axios.get(project.body_url)).data
|
||||
}
|
||||
|
||||
return {
|
||||
project,
|
||||
versions,
|
||||
featuredVersions,
|
||||
members: members.filter((x) => x.accepted),
|
||||
allMembers: members,
|
||||
currentMember,
|
||||
dependencies,
|
||||
}
|
||||
} catch {
|
||||
data.error({
|
||||
statusCode: 404,
|
||||
message: 'Project not found',
|
||||
})
|
||||
}
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: `${this.project.title} - ${
|
||||
this.project.project_type.charAt(0).toUpperCase() +
|
||||
this.project.project_type.slice(1)
|
||||
}s - Modrinth`,
|
||||
meta: [
|
||||
{
|
||||
hid: 'og:type',
|
||||
name: 'og:type',
|
||||
content: 'website',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: this.project.title,
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: this.project.title,
|
||||
},
|
||||
{
|
||||
hid: 'og:description',
|
||||
name: 'og:description',
|
||||
content: this.project.description,
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: `${this.project.title}: ${this.project.description} View other minecraft mods on Modrinth today! Modrinth is a new and modern Minecraft modding platform supporting both the Forge and Fabric mod loaders.`,
|
||||
},
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/${this.project.project_type}/${this.project.id}`,
|
||||
},
|
||||
{
|
||||
hid: 'og:image',
|
||||
name: 'og:image',
|
||||
content: this.project.icon_url
|
||||
? this.project.icon_url
|
||||
: 'https://cdn.modrinth.com/placeholder.png',
|
||||
},
|
||||
{
|
||||
hid: 'robots',
|
||||
name: 'robots',
|
||||
content: this.project.status !== 'approved' ? 'noindex' : 'all',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatNumber(x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
},
|
||||
findPrimary(version) {
|
||||
let file = version.files.find((x) => x.primary)
|
||||
|
||||
if (!file) {
|
||||
file = version.files[0]
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
file = { url: `/project/${this.project.id}/version/${version.id}` }
|
||||
}
|
||||
|
||||
return file
|
||||
},
|
||||
async downloadFile(hash, url) {
|
||||
await this.$axios.get(`version_file/${hash}/download`)
|
||||
|
||||
const elem = document.createElement('a')
|
||||
elem.download = hash
|
||||
elem.href = url
|
||||
elem.click()
|
||||
},
|
||||
async clearMessage() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.patch(
|
||||
`project/${this.currentProject.id}`,
|
||||
{
|
||||
moderation_message: null,
|
||||
moderation_message_body: null,
|
||||
},
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
this.project.moderator_message = null
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async submitForReview() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.patch(
|
||||
`project/${this.currentProject.id}`,
|
||||
{
|
||||
status: 'processing',
|
||||
},
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
this.project.status = 'processing'
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
hr {
|
||||
background-color: var(--color-divider);
|
||||
border: none;
|
||||
color: var(--color-divider);
|
||||
height: 1px;
|
||||
margin: var(--spacing-card-bg) 0;
|
||||
}
|
||||
.header {
|
||||
.icon {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
object-fit: contain;
|
||||
border-radius: var(--size-rounded-icon);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0.25rem 0;
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.side-descriptor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-text-dark);
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
svg {
|
||||
height: 1.25rem;
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
button,
|
||||
a {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-nm);
|
||||
}
|
||||
|
||||
.categories {
|
||||
margin: 0.25rem 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-nm);
|
||||
}
|
||||
|
||||
.stats {
|
||||
.stat {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: bold;
|
||||
}
|
||||
.label {
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
.label {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-info {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
min-width: 21rem;
|
||||
max-width: 21rem;
|
||||
margin-right: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
color: var(--color-heading);
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.featured-version {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: var(--spacing-card-md);
|
||||
.download {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
border-radius: 1.5rem;
|
||||
color: var(--color-brand-inverted);
|
||||
background-color: var(--color-brand);
|
||||
margin-right: var(--spacing-card-sm);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-brand-hover);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1.5rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.top {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg,
|
||||
img,
|
||||
span {
|
||||
color: var(--color-link);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: '•';
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.team-member {
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
img {
|
||||
border-radius: var(--size-rounded-icon);
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
.member-info {
|
||||
overflow: hidden;
|
||||
margin: auto 0 auto 0.5rem;
|
||||
.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%;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
163
pages/_type/_id/changelog.vue
Normal file
163
pages/_type/_id/changelog.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="content card">
|
||||
<div v-for="version in versions" :key="version.id">
|
||||
<div class="version-header">
|
||||
<span :class="'circle ' + version.version_type" />
|
||||
<div class="version-header-text">
|
||||
<h2 class="name">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURIComponent(version.version_number)}`"
|
||||
>{{ version.name }}</nuxt-link
|
||||
>
|
||||
</h2>
|
||||
<span v-if="members.find((x) => x.user.id === version.author_id)">
|
||||
by
|
||||
<nuxt-link
|
||||
class="text-link"
|
||||
:to="
|
||||
'/user/' +
|
||||
members.find((x) => x.user.id === version.author_id).user
|
||||
.username
|
||||
"
|
||||
>{{
|
||||
members.find((x) => x.user.id === version.author_id).user
|
||||
.username
|
||||
}}</nuxt-link
|
||||
>
|
||||
</span>
|
||||
<span>
|
||||
on {{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span
|
||||
>
|
||||
</div>
|
||||
<a
|
||||
:href="$parent.findPrimary(version).url"
|
||||
class="iconified-button download"
|
||||
@click.prevent="
|
||||
$parent.downloadFile(
|
||||
$parent.findPrimary(version).hashes.sha1,
|
||||
$parent.findPrimary(version).url
|
||||
)
|
||||
"
|
||||
>
|
||||
<DownloadIcon />
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-highlightjs
|
||||
:class="'markdown-body ' + version.version_type"
|
||||
v-html="
|
||||
version.changelog
|
||||
? $xss($md.render(version.changelog))
|
||||
: 'No changelog specified.'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DownloadIcon,
|
||||
},
|
||||
auth: false,
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
max-width: calc(100% - (2 * var(--spacing-card-lg)));
|
||||
}
|
||||
|
||||
.version-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.circle {
|
||||
min-width: 0.75rem;
|
||||
min-height: 0.75rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.25rem;
|
||||
|
||||
&.alpha {
|
||||
background-color: var(--color-badge-red-bg);
|
||||
}
|
||||
|
||||
&.release {
|
||||
background-color: var(--color-badge-green-bg);
|
||||
}
|
||||
|
||||
&.beta {
|
||||
background-color: var(--color-badge-yellow-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.version-header-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin: 0 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
h2,
|
||||
span {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.download {
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
margin: 0.5rem 0.5rem 1rem calc(0.375rem - 1px);
|
||||
padding-left: 1.275rem;
|
||||
border-left: 2px solid var(--color-text);
|
||||
|
||||
&.alpha {
|
||||
border-left-color: var(--color-badge-red-bg);
|
||||
}
|
||||
|
||||
&.release {
|
||||
border-left-color: var(--color-badge-green-bg);
|
||||
}
|
||||
|
||||
&.beta {
|
||||
border-left-color: var(--color-badge-yellow-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
782
pages/_type/_id/edit.vue
Normal file
782
pages/_type/_id/edit.vue
Normal file
@@ -0,0 +1,782 @@
|
||||
<template>
|
||||
<div class="page-contents">
|
||||
<header class="card columns">
|
||||
<h3 class="column-grow-1">Edit project</h3>
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}`"
|
||||
class="iconified-button column"
|
||||
>
|
||||
Back
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="
|
||||
project.status === 'rejected' ||
|
||||
project.status === 'draft' ||
|
||||
project.status === 'unlisted'
|
||||
"
|
||||
title="Submit for approval"
|
||||
class="iconified-button column"
|
||||
:disabled="!$nuxt.$loading"
|
||||
@click="saveProjectReview"
|
||||
>
|
||||
Submit for approval
|
||||
</button>
|
||||
<button
|
||||
title="Save"
|
||||
class="iconified-button brand-button-colors column"
|
||||
:disabled="!$nuxt.$loading"
|
||||
@click="saveProject"
|
||||
>
|
||||
<CheckIcon />
|
||||
Save
|
||||
</button>
|
||||
</header>
|
||||
<section class="card essentials">
|
||||
<h3>Name</h3>
|
||||
<label>
|
||||
<span>
|
||||
Be creative! Generic project names will be harder to search for.
|
||||
</span>
|
||||
<input
|
||||
v-model="newProject.title"
|
||||
type="text"
|
||||
placeholder="Enter the name"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<h3>Summary</h3>
|
||||
<label>
|
||||
<span>
|
||||
Give a short description of your project that will appear on search
|
||||
pages.
|
||||
</span>
|
||||
<input
|
||||
v-model="newProject.description"
|
||||
type="text"
|
||||
placeholder="Enter the summary"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<h3>Categories</h3>
|
||||
<label>
|
||||
<span>
|
||||
Select up to 3 categories that will help others find your project.
|
||||
</span>
|
||||
<Multiselect
|
||||
id="categories"
|
||||
v-model="newProject.categories"
|
||||
:options="
|
||||
$tag.categories
|
||||
.filter((x) => x.project_type === project.project_type)
|
||||
.map((it) => it.name)
|
||||
"
|
||||
:loading="$tag.categories.length === 0"
|
||||
:multiple="true"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:show-labels="false"
|
||||
:max="3"
|
||||
:limit="6"
|
||||
:hide-selected="true"
|
||||
placeholder="Choose categories"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<h3>Vanity URL (slug)</h3>
|
||||
<label>
|
||||
<span>
|
||||
Set this to something that will looks nice in your project's URL.
|
||||
</span>
|
||||
<input
|
||||
id="name"
|
||||
v-model="newProject.slug"
|
||||
type="text"
|
||||
placeholder="Enter the vanity URL slug"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section class="card project-icon rows">
|
||||
<h3>Icon</h3>
|
||||
<img
|
||||
:src="
|
||||
previewImage
|
||||
? previewImage
|
||||
: newProject.icon_url && !iconChanged
|
||||
? newProject.icon_url
|
||||
: 'https://cdn.modrinth.com/placeholder.svg'
|
||||
"
|
||||
alt="preview-image"
|
||||
/>
|
||||
<file-input
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image"
|
||||
prompt="Choose image or drag it here"
|
||||
@change="showPreviewImage"
|
||||
:disabled="(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS"
|
||||
/>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
icon = null
|
||||
previewImage = null
|
||||
iconChanged = true
|
||||
"
|
||||
:disabled="(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS"
|
||||
>
|
||||
<TrashIcon />
|
||||
Reset icon
|
||||
</button>
|
||||
</section>
|
||||
<section class="card game-sides">
|
||||
<h3>Supported environments</h3>
|
||||
<div class="columns">
|
||||
<span> Let others know what environments your project supports. </span>
|
||||
<div class="labeled-control">
|
||||
<h3>Client</h3>
|
||||
<Multiselect
|
||||
v-model="clientSideType"
|
||||
placeholder="Select one"
|
||||
:options="sideTypes"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled-control">
|
||||
<h3>Server</h3>
|
||||
<Multiselect
|
||||
v-model="serverSideType"
|
||||
placeholder="Select one"
|
||||
:options="sideTypes"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card description">
|
||||
<h3>
|
||||
<label
|
||||
for="body"
|
||||
title="You can type an extended description of your project here."
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
</h3>
|
||||
<span>
|
||||
You can type an extended description of your mod here. This editor
|
||||
supports Markdown. Its syntax can be found
|
||||
<a
|
||||
href="https://guides.github.com/features/mastering-markdown/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-link"
|
||||
>here</a
|
||||
>.
|
||||
</span>
|
||||
<ThisOrThat
|
||||
v-model="bodyViewMode"
|
||||
class="separator"
|
||||
:items="['source', 'preview']"
|
||||
/>
|
||||
<div class="edit-wrapper">
|
||||
<div v-if="bodyViewMode === 'source'" class="textarea-wrapper">
|
||||
<textarea
|
||||
id="body"
|
||||
v-model="newProject.body"
|
||||
:disabled="(currentMember.permissions & EDIT_BODY) !== EDIT_BODY"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="bodyViewMode === 'preview'"
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="
|
||||
newProject.body
|
||||
? $xss($md.render(newProject.body))
|
||||
: 'No body specified.'
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card extra-links">
|
||||
<div class="title">
|
||||
<h3>External links</h3>
|
||||
</div>
|
||||
<label
|
||||
title="A place for users to report bugs, issues, and concerns about your project."
|
||||
>
|
||||
<span>Issue tracker</span>
|
||||
<input
|
||||
v-model="newProject.issues_url"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
title="A page/repository containing the source code for your project"
|
||||
>
|
||||
<span>Source code</span>
|
||||
<input
|
||||
v-model="newProject.source_url"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
title="A page containing information, documentation, and help for the project."
|
||||
>
|
||||
<span>Wiki page</span>
|
||||
<input
|
||||
v-model="newProject.wiki_url"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label title="An invitation link to your Discord server.">
|
||||
<span>Discord invite</span>
|
||||
<input
|
||||
v-model="newProject.discord_url"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section class="card license">
|
||||
<div class="title">
|
||||
<h3>License</h3>
|
||||
</div>
|
||||
<label>
|
||||
<span>
|
||||
It is very important to choose a proper license for your mod. You may
|
||||
choose one from our list or provide a URL to a custom license.
|
||||
<br />
|
||||
Confused? See our
|
||||
<a
|
||||
href="https://blog.modrinth.com/licensing-guide/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-link"
|
||||
>
|
||||
licensing guide</a
|
||||
>
|
||||
for more information.
|
||||
</span>
|
||||
<div class="input-group">
|
||||
<Multiselect
|
||||
v-model="license"
|
||||
placeholder="Select one"
|
||||
track-by="short"
|
||||
label="name"
|
||||
:options="$tag.licenses"
|
||||
:searchable="true"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
<input
|
||||
v-model="license_url"
|
||||
type="url"
|
||||
placeholder="License URL"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</section>
|
||||
<section class="card donations">
|
||||
<div class="title">
|
||||
<h3>Donation links</h3>
|
||||
<button
|
||||
title="Add a link"
|
||||
class="iconified-button"
|
||||
:disabled="false"
|
||||
@click="
|
||||
donationPlatforms.push({})
|
||||
donationLinks.push('')
|
||||
"
|
||||
>
|
||||
<PlusIcon />
|
||||
Add a link
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="(item, index) in donationPlatforms" :key="index">
|
||||
<label title="The donation link.">
|
||||
<span>Donation Link</span>
|
||||
<input
|
||||
v-model="donationLinks[index]"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
/>
|
||||
</label>
|
||||
<label title="The donation platform of the link.">
|
||||
<span>Donation Platform</span>
|
||||
<Multiselect
|
||||
v-model="donationPlatforms[index]"
|
||||
placeholder="Select one"
|
||||
track-by="short"
|
||||
label="name"
|
||||
:options="$tag.donationPlatforms"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
donationPlatforms.splice(index, 1)
|
||||
donationLinks.splice(index, 1)
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove Link
|
||||
</button>
|
||||
<hr
|
||||
v-if="
|
||||
donationPlatforms.length > 0 &&
|
||||
index !== donationPlatforms.length - 1
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
||||
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
import ThisOrThat from '~/components/ui/ThisOrThat'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FileInput,
|
||||
ThisOrThat,
|
||||
Multiselect,
|
||||
TrashIcon,
|
||||
CheckIcon,
|
||||
PlusIcon,
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (
|
||||
this.isEditing &&
|
||||
!window.confirm('Are you sure that you want to leave without saving?')
|
||||
) {
|
||||
return
|
||||
}
|
||||
next()
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newProject: {},
|
||||
|
||||
clientSideType: '',
|
||||
serverSideType: '',
|
||||
|
||||
license: { short: '', name: '' },
|
||||
license_url: '',
|
||||
|
||||
donationPlatforms: [],
|
||||
donationLinks: [],
|
||||
|
||||
isProcessing: false,
|
||||
previewImage: null,
|
||||
compiledBody: '',
|
||||
|
||||
icon: null,
|
||||
iconChanged: false,
|
||||
|
||||
sideTypes: ['Required', 'Optional', 'Unsupported'],
|
||||
|
||||
isEditing: true,
|
||||
bodyViewMode: 'source',
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.newProject = this.project
|
||||
|
||||
this.newProject.license.short = this.newProject.license.id
|
||||
|
||||
if (this.newProject.donation_urls) {
|
||||
for (const platform of this.newProject.donation_urls) {
|
||||
this.donationPlatforms.push({
|
||||
short: platform.id,
|
||||
name: platform.platform,
|
||||
})
|
||||
this.donationLinks.push(platform.url)
|
||||
}
|
||||
}
|
||||
|
||||
this.license = {
|
||||
short: this.newProject.license.id,
|
||||
name: this.newProject.license.name,
|
||||
}
|
||||
|
||||
this.license_url = this.newProject.license.url
|
||||
|
||||
this.clientSideType =
|
||||
this.newProject.client_side.charAt(0) +
|
||||
this.newProject.client_side.slice(1)
|
||||
this.serverSideType =
|
||||
this.newProject.server_side.charAt(0) +
|
||||
this.newProject.server_side.slice(1)
|
||||
},
|
||||
watch: {
|
||||
license(newValue, oldValue) {
|
||||
if (newValue == null) {
|
||||
this.license_url = ''
|
||||
return
|
||||
}
|
||||
|
||||
switch (newValue.short) {
|
||||
case 'custom':
|
||||
this.license_url = ''
|
||||
break
|
||||
default:
|
||||
this.license_url = `https://cdn.modrinth.com/licenses/${newValue.short}.txt`
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
function preventLeave(e) {
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
||||
window.addEventListener('beforeunload', preventLeave)
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
window.removeEventListener('beforeunload', preventLeave)
|
||||
})
|
||||
},
|
||||
created() {
|
||||
this.UPLOAD_VERSION = 1 << 0
|
||||
this.DELETE_VERSION = 1 << 1
|
||||
this.EDIT_DETAILS = 1 << 2
|
||||
this.EDIT_BODY = 1 << 3
|
||||
this.MANAGE_INVITES = 1 << 4
|
||||
this.REMOVE_MEMBER = 1 << 5
|
||||
this.EDIT_MEMBER = 1 << 6
|
||||
this.DELETE_MOD = 1 << 7
|
||||
},
|
||||
methods: {
|
||||
async saveProjectReview() {
|
||||
this.isProcessing = true
|
||||
await this.saveProject()
|
||||
},
|
||||
async saveProject() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
const data = {
|
||||
title: this.newProject.title,
|
||||
description: this.newProject.description,
|
||||
body: this.newProject.body,
|
||||
categories: this.newProject.categories,
|
||||
issues_url: this.newProject.issues_url,
|
||||
source_url: this.newProject.source_url,
|
||||
wiki_url: this.newProject.wiki_url,
|
||||
license_url: this.license_url,
|
||||
discord_url: this.newProject.discord_url,
|
||||
license_id: this.license.short,
|
||||
client_side: this.clientSideType.toLowerCase(),
|
||||
server_side: this.serverSideType.toLowerCase(),
|
||||
slug: this.newProject.slug,
|
||||
license: this.license.short,
|
||||
donation_urls: this.donationPlatforms.map((it, index) => {
|
||||
return {
|
||||
id: it.short,
|
||||
platform: it.name,
|
||||
url: this.donationLinks[index],
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
if (this.isProcessing) {
|
||||
data.status = 'processing'
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`project/${this.newProject.id}`,
|
||||
data,
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
if (this.iconChanged) {
|
||||
await this.$axios.patch(
|
||||
`project/${this.newProject.id}/icon?ext=${
|
||||
this.icon.type.split('/')[this.icon.type.split('/').length - 1]
|
||||
}`,
|
||||
this.icon,
|
||||
this.$auth.headers
|
||||
)
|
||||
}
|
||||
|
||||
this.$emit('update:featuredVersions', this.newProject)
|
||||
|
||||
this.isEditing = false
|
||||
|
||||
await this.$router.replace(
|
||||
`/${this.project.project_type}/${
|
||||
this.newProject.slug ? this.newProject.slug : this.newProject.id
|
||||
}`
|
||||
)
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
showPreviewImage(files) {
|
||||
const reader = new FileReader()
|
||||
this.iconChanged = true
|
||||
this.icon = files[0]
|
||||
reader.readAsDataURL(this.icon)
|
||||
|
||||
reader.onload = (event) => {
|
||||
this.previewImage = event.target.result
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
flex: 2;
|
||||
padding-right: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
input,
|
||||
.multiselect,
|
||||
.input-group {
|
||||
flex: 3;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
* {
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
textarea {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
resize: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.page-contents {
|
||||
display: grid;
|
||||
grid-template:
|
||||
'header header header' auto
|
||||
'essentials essentials project-icon' auto
|
||||
'game-sides game-sides game-sides' auto
|
||||
'description description description' auto
|
||||
'extra-links extra-links extra-links' auto
|
||||
'license license license' auto
|
||||
'donations donations donations' auto
|
||||
'footer footer footer' auto
|
||||
/ 4fr 1fr 2fr;
|
||||
column-gap: var(--spacing-card-md);
|
||||
row-gap: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
header {
|
||||
grid-area: header;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
h3 {
|
||||
margin: auto 0;
|
||||
color: var(--color-text-dark);
|
||||
font-weight: var(--font-weight-extrabold);
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
section.essentials {
|
||||
grid-area: essentials;
|
||||
}
|
||||
|
||||
section.project-icon {
|
||||
grid-area: project-icon;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: var(--size-rounded-lg);
|
||||
}
|
||||
|
||||
.iconified-button {
|
||||
width: 9rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
section.game-sides {
|
||||
grid-area: game-sides;
|
||||
|
||||
.columns {
|
||||
flex-wrap: wrap;
|
||||
|
||||
span {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.labeled-control {
|
||||
flex: 2;
|
||||
margin-left: var(--spacing-card-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.description {
|
||||
grid-area: description;
|
||||
|
||||
.separator {
|
||||
margin: var(--spacing-card-sm) 0;
|
||||
}
|
||||
|
||||
.edit-wrapper * {
|
||||
min-height: 10rem;
|
||||
max-height: 40rem;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
overflow-y: auto;
|
||||
padding: 0 var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
|
||||
section.extra-links {
|
||||
grid-area: extra-links;
|
||||
|
||||
label {
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-card-sm);
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.license {
|
||||
grid-area: license;
|
||||
|
||||
label {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
|
||||
section.donations {
|
||||
grid-area: donations;
|
||||
|
||||
label {
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-card-sm);
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-area: footer;
|
||||
}
|
||||
|
||||
.choose-image {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
416
pages/_type/_id/gallery.vue
Normal file
416
pages/_type/_id/gallery.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="buttons">
|
||||
<button
|
||||
v-if="currentMember"
|
||||
class="iconified-button"
|
||||
@click="
|
||||
newGalleryItems.push({
|
||||
title: '',
|
||||
description: '',
|
||||
featured: false,
|
||||
url: '',
|
||||
})
|
||||
"
|
||||
>
|
||||
<UploadIcon />
|
||||
Upload
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
newGalleryItems.length > 0 ||
|
||||
editGalleryIndexes.length > 0 ||
|
||||
deleteGalleryUrls.length > 0
|
||||
"
|
||||
class="action brand-button-colors iconified-button"
|
||||
@click="saveGallery"
|
||||
>
|
||||
<CheckIcon />
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
newGalleryItems.length > 0 ||
|
||||
editGalleryIndexes.length > 0 ||
|
||||
deleteGalleryUrls.length > 0
|
||||
"
|
||||
class="action iconified-button"
|
||||
@click="resetGallery"
|
||||
>
|
||||
<TrashIcon />
|
||||
Discard Changes
|
||||
</button>
|
||||
</div>
|
||||
<div class="items">
|
||||
<div
|
||||
v-for="(item, index) in gallery"
|
||||
:key="index"
|
||||
class="card gallery-item"
|
||||
>
|
||||
<img
|
||||
:src="
|
||||
item.url
|
||||
? item.url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
:alt="item.title ? item.title : 'gallery-image'"
|
||||
/>
|
||||
<div class="gallery-body">
|
||||
<div v-if="editGalleryIndexes.includes(index)" class="gallery-info">
|
||||
<input
|
||||
v-model="item.title"
|
||||
type="text"
|
||||
placeholder="Enter the title..."
|
||||
/>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea
|
||||
id="body"
|
||||
v-model="item.description"
|
||||
placeholder="Enter the description..."
|
||||
/>
|
||||
</div>
|
||||
<Checkbox v-model="item.featured" label="Featured" />
|
||||
</div>
|
||||
<div v-else class="gallery-info">
|
||||
<h2 v-if="item.title">{{ item.title }}</h2>
|
||||
<p v-if="item.description">{{ item.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-bottom">
|
||||
<div class="gallery-created">
|
||||
<CalendarIcon />
|
||||
{{ $dayjs(item.created).format('MMMM D, YYYY') }}
|
||||
</div>
|
||||
<div v-if="currentMember" class="gallery-buttons">
|
||||
<button
|
||||
v-if="editGalleryIndexes.includes(index)"
|
||||
class="iconified-button"
|
||||
@click="
|
||||
editGalleryIndexes.splice(editGalleryIndexes.indexOf(index), 1)
|
||||
gallery[index] = JSON.parse(
|
||||
JSON.stringify(project.gallery[index])
|
||||
)
|
||||
"
|
||||
>
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="iconified-button"
|
||||
@click="editGalleryIndexes.push(index)"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
deleteGalleryUrls.push(item.url)
|
||||
gallery.splice(index, 1)
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, index) in newGalleryItems"
|
||||
:key="index + 'new'"
|
||||
class="card gallery-item"
|
||||
>
|
||||
<img
|
||||
:src="
|
||||
newGalleryItems[index].preview
|
||||
? newGalleryItems[index].preview
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
:alt="item.title ? item.title : 'gallery-image'"
|
||||
/>
|
||||
<div class="gallery-body">
|
||||
<div class="gallery-info">
|
||||
<input
|
||||
v-model="item.title"
|
||||
type="text"
|
||||
placeholder="Enter the title..."
|
||||
/>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea
|
||||
id="body"
|
||||
v-model="item.description"
|
||||
placeholder="Enter the description..."
|
||||
/>
|
||||
</div>
|
||||
<Checkbox v-model="item.featured" label="Featured" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-bottom">
|
||||
<div class="gallery-buttons">
|
||||
<SmartFileInput
|
||||
accept="image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp"
|
||||
prompt="Upload"
|
||||
@change="(files) => showPreviewImage(files, index)"
|
||||
/>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="newGalleryItems.splice(index, 1)"
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
|
||||
import SmartFileInput from '~/components/ui/SmartFileInput'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CalendarIcon,
|
||||
UploadIcon,
|
||||
Checkbox,
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
CheckIcon,
|
||||
SmartFileInput,
|
||||
CrossIcon,
|
||||
},
|
||||
auth: false,
|
||||
beforeRouteLeave(to, from, next) {
|
||||
this.resetGallery()
|
||||
|
||||
next()
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
gallery: [],
|
||||
newGalleryItems: [],
|
||||
editGalleryIndexes: [],
|
||||
deleteGalleryUrls: [],
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.gallery = JSON.parse(JSON.stringify(this.project.gallery))
|
||||
},
|
||||
methods: {
|
||||
showPreviewImage(files, index) {
|
||||
const reader = new FileReader()
|
||||
this.newGalleryItems[index].icon = files[0]
|
||||
|
||||
if (this.newGalleryItems[index].icon instanceof Blob) {
|
||||
reader.readAsDataURL(this.newGalleryItems[index].icon)
|
||||
|
||||
reader.onload = (event) => {
|
||||
this.newGalleryItems[index].preview = event.target.result
|
||||
|
||||
// TODO: Find an alternative for this!
|
||||
this.$forceUpdate()
|
||||
}
|
||||
}
|
||||
},
|
||||
async saveGallery() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
for (const item of this.newGalleryItems) {
|
||||
let url = `project/${this.project.id}/gallery?ext=${
|
||||
item.icon
|
||||
? item.icon.type.split('/')[item.icon.type.split('/').length - 1]
|
||||
: null
|
||||
}&featured=${item.featured}`
|
||||
|
||||
if (item.title) url += `&title=${item.title}`
|
||||
if (item.description) url += `&description=${item.description}`
|
||||
|
||||
await this.$axios.post(url, item.icon, this.$auth.headers)
|
||||
}
|
||||
|
||||
for (const index of this.editGalleryIndexes) {
|
||||
const item = this.gallery[index]
|
||||
|
||||
let url = `project/${
|
||||
this.project.id
|
||||
}/gallery?url=${encodeURIComponent(item.url)}&featured=${
|
||||
item.featured
|
||||
}`
|
||||
|
||||
if (item.title) url += `&title=${item.title}`
|
||||
if (item.description) url += `&description=${item.description}`
|
||||
|
||||
await this.$axios.patch(url, {}, this.$auth.headers)
|
||||
}
|
||||
|
||||
for (const url of this.deleteGalleryUrls) {
|
||||
await this.$axios.delete(
|
||||
`project/${this.project.id}/gallery?url=${encodeURIComponent(url)}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
}
|
||||
|
||||
const project = (
|
||||
await this.$axios.get(
|
||||
`project/${this.project.id}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
).data
|
||||
this.$emit('update:project', project)
|
||||
this.gallery = JSON.parse(JSON.stringify(project.gallery))
|
||||
|
||||
this.deleteGalleryUrls = []
|
||||
this.editGalleryIndexes = []
|
||||
this.newGalleryItems = []
|
||||
} catch (err) {
|
||||
const description = err.response.data.description
|
||||
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: description,
|
||||
type: 'error',
|
||||
})
|
||||
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
resetGallery() {
|
||||
this.newGalleryItems = []
|
||||
this.editGalleryIndexes = []
|
||||
this.deleteGalleryUrls = []
|
||||
this.gallery = JSON.parse(JSON.stringify(this.project.gallery))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.buttons {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
background-color: var(--color-raised-bg);
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg);
|
||||
}
|
||||
|
||||
&.brand-button-colors {
|
||||
background-color: var(--color-brand);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-brand-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: var(--spacing-card-md);
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
|
||||
min-height: 10rem;
|
||||
object-fit: cover;
|
||||
|
||||
border-radius: var(--size-rounded-card);
|
||||
}
|
||||
|
||||
.gallery-body {
|
||||
flex-grow: 1;
|
||||
width: calc(100% - 2 * var(--spacing-card-md));
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-md);
|
||||
|
||||
textarea {
|
||||
border-radius: var(--size-rounded-sm);
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: calc(14rem - 2rem - 4px);
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.gallery-info {
|
||||
h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-bottom {
|
||||
width: calc(100% - 2 * var(--spacing-card-md));
|
||||
padding: 0 var(--spacing-card-md) var(--spacing-card-sm)
|
||||
var(--spacing-card-md);
|
||||
|
||||
.gallery-created {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-icon);
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-buttons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
27
pages/_type/_id/index.vue
Normal file
27
pages/_type/_id/index.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div
|
||||
v-highlightjs
|
||||
class="markdown-body card"
|
||||
v-html="$xss($md.render(project.body))"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.markdown-body {
|
||||
max-width: calc(100% - (2 * var(--spacing-card-lg)));
|
||||
}
|
||||
</style>
|
||||
634
pages/_type/_id/settings.vue
Normal file
634
pages/_type/_id/settings.vue
Normal file
@@ -0,0 +1,634 @@
|
||||
<template>
|
||||
<div>
|
||||
<ConfirmPopup
|
||||
ref="delete_popup"
|
||||
title="Are you sure you want to delete this project?"
|
||||
description="If you proceed, all versions and any attached data will be removed from our servers. This may break other projects, so be careful."
|
||||
:has-to-type="true"
|
||||
:confirmation-text="project.title"
|
||||
proceed-label="Delete project"
|
||||
@proceed="deleteProject"
|
||||
/>
|
||||
<div class="card">
|
||||
<h3>General</h3>
|
||||
</div>
|
||||
<section class="card">
|
||||
<h3>Edit project</h3>
|
||||
<label>
|
||||
<span> This leads you to a page where you can edit your project. </span>
|
||||
<nuxt-link class="iconified-button" to="edit">Edit</nuxt-link>
|
||||
</label>
|
||||
<h3>Create version</h3>
|
||||
<label>
|
||||
<span>
|
||||
This leads to a page where you can create a version for your project.
|
||||
</span>
|
||||
<nuxt-link
|
||||
class="iconified-button"
|
||||
to="version/create"
|
||||
:disabled="
|
||||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
||||
"
|
||||
>Create version</nuxt-link
|
||||
>
|
||||
</label>
|
||||
<h3>Delete project</h3>
|
||||
<label>
|
||||
<span>
|
||||
Removes your project from Modrinth's servers and search. Clicking on
|
||||
this will delete your project, so be extra careful!
|
||||
</span>
|
||||
<div
|
||||
class="iconified-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
||||
"
|
||||
@click="showPopup"
|
||||
>
|
||||
Delete project
|
||||
</div>
|
||||
</label>
|
||||
</section>
|
||||
<div class="card columns team-invite">
|
||||
<h3>Team members</h3>
|
||||
<div
|
||||
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||
class="column"
|
||||
>
|
||||
<input
|
||||
id="username"
|
||||
v-model="currentUsername"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
/>
|
||||
<label for="username" class="hidden">Username</label>
|
||||
<button
|
||||
class="iconified-button brand-button-colors column"
|
||||
@click="inviteTeamMember"
|
||||
>
|
||||
<PlusIcon />
|
||||
Invite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(member, index) in allTeamMembers"
|
||||
:key="member.user.id"
|
||||
class="card member"
|
||||
:class="{ open: openTeamMembers.includes(member.user.id) }"
|
||||
>
|
||||
<div class="member-header">
|
||||
<div class="info">
|
||||
<img :src="member.avatar_url" :alt="member.name" />
|
||||
<div class="text">
|
||||
<h4>{{ member.name }}</h4>
|
||||
<h3>{{ member.role }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-buttons">
|
||||
<Badge v-if="member.accepted" type="accepted" color="green" />
|
||||
<Badge v-else type="pending" color="yellow" />
|
||||
<button
|
||||
class="dropdown-icon"
|
||||
@click="
|
||||
openTeamMembers.indexOf(member.user.id) === -1
|
||||
? openTeamMembers.push(member.user.id)
|
||||
: (openTeamMembers = openTeamMembers.filter(
|
||||
(it) => it !== member.user.id
|
||||
))
|
||||
"
|
||||
>
|
||||
<DropdownIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="main-info">
|
||||
<label>
|
||||
Role:
|
||||
<input
|
||||
v-model="allTeamMembers[index].role"
|
||||
type="text"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<h3>Permissions</h3>
|
||||
<div class="permissions">
|
||||
<Checkbox
|
||||
:value="
|
||||
(member.permissions & UPLOAD_VERSION) === UPLOAD_VERSION ||
|
||||
member.role === 'Owner'
|
||||
"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
||||
"
|
||||
label="Upload version"
|
||||
@input="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
(member.permissions & DELETE_VERSION) === DELETE_VERSION ||
|
||||
member.role === 'Owner'
|
||||
"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & DELETE_VERSION) !== DELETE_VERSION
|
||||
"
|
||||
label="Delete version"
|
||||
@input="allTeamMembers[index].permissions ^= DELETE_VERSION"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
(member.permissions & EDIT_DETAILS) === EDIT_DETAILS ||
|
||||
member.role === 'Owner'
|
||||
"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
label="Edit details"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_DETAILS"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
(member.permissions & EDIT_BODY) === EDIT_BODY ||
|
||||
member.role === 'Owner'
|
||||
"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & EDIT_BODY) !== EDIT_BODY
|
||||
"
|
||||
label="Edit body"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_BODY"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
(member.permissions & MANAGE_INVITES) === MANAGE_INVITES ||
|
||||
member.role === 'Owner'
|
||||
"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES
|
||||
"
|
||||
label="Manage invites"
|
||||
@input="allTeamMembers[index].permissions ^= MANAGE_INVITES"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
(member.permissions & REMOVE_MEMBER) === REMOVE_MEMBER ||
|
||||
member.role === 'Owner'
|
||||
"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
|
||||
"
|
||||
label="Remove member"
|
||||
@input="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
(member.permissions & EDIT_MEMBER) === EDIT_MEMBER ||
|
||||
member.role === 'Owner'
|
||||
"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
label="Edit member"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_MEMBER"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
(member.permissions & DELETE_PROJECT) === DELETE_PROJECT ||
|
||||
member.role === 'Owner'
|
||||
"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
||||
"
|
||||
label="Delete project"
|
||||
@input="allTeamMembers[index].permissions ^= DELETE_PROJECT"
|
||||
/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button
|
||||
class="iconified-button"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
@click="removeTeamMember(index)"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove member
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
member.role !== 'Owner' &&
|
||||
currentMember.role === 'Owner' &&
|
||||
member.accepted
|
||||
"
|
||||
class="iconified-button"
|
||||
@click="transferOwnership(index)"
|
||||
>
|
||||
<UserIcon />
|
||||
Transfer ownership
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button-colors"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
@click="updateTeamMember(index)"
|
||||
>
|
||||
<CheckIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmPopup from '~/components/ui/ConfirmPopup'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
|
||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DropdownIcon,
|
||||
ConfirmPopup,
|
||||
Checkbox,
|
||||
Badge,
|
||||
PlusIcon,
|
||||
CheckIcon,
|
||||
TrashIcon,
|
||||
UserIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
allMembers: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentUsername: '',
|
||||
openTeamMembers: [],
|
||||
allTeamMembers: [],
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.allTeamMembers = this.allMembers
|
||||
},
|
||||
created() {
|
||||
this.UPLOAD_VERSION = 1 << 0
|
||||
this.DELETE_VERSION = 1 << 1
|
||||
this.EDIT_DETAILS = 1 << 2
|
||||
this.EDIT_BODY = 1 << 3
|
||||
this.MANAGE_INVITES = 1 << 4
|
||||
this.REMOVE_MEMBER = 1 << 5
|
||||
this.EDIT_MEMBER = 1 << 6
|
||||
this.DELETE_PROJECT = 1 << 7
|
||||
},
|
||||
methods: {
|
||||
async inviteTeamMember() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
const user = (await this.$axios.get(`user/${this.currentUsername}`))
|
||||
.data
|
||||
|
||||
const data = {
|
||||
user_id: user.id,
|
||||
}
|
||||
|
||||
await this.$axios.post(
|
||||
`team/${this.project.team}/members`,
|
||||
data,
|
||||
this.$auth.headers
|
||||
)
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async removeTeamMember(index) {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async updateTeamMember(index) {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
const data = {
|
||||
permissions: this.allTeamMembers[index].permissions,
|
||||
role: this.allTeamMembers[index].role,
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
||||
data,
|
||||
this.$auth.headers
|
||||
)
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async transferOwnership(index) {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.patch(
|
||||
`team/${this.project.team}/owner`,
|
||||
{
|
||||
user_id: this.allTeamMembers[index].user.id,
|
||||
},
|
||||
this.$auth.headers
|
||||
)
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
showPopup() {
|
||||
if (
|
||||
(this.currentMember.permissions & this.DELETE_MOD) ===
|
||||
this.DELETE_MOD
|
||||
) {
|
||||
this.$refs.delete_popup.show()
|
||||
}
|
||||
},
|
||||
async deleteProject() {
|
||||
await this.$axios.delete(`project/${this.project.id}`, this.$auth.headers)
|
||||
await this.$store.dispatch('user/fetchProjects')
|
||||
await this.$router.push(`/user/${this.$auth.user.username}`)
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Action Success',
|
||||
text: 'Your _type has been successfully deleted.',
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
async updateMembers() {
|
||||
this.allTeamMembers = (
|
||||
await this.$axios.get(
|
||||
`team/${this.project.team}/members`,
|
||||
this.$auth.headers
|
||||
)
|
||||
).data.map((it) => ({
|
||||
avatar_url: it.user.avatar_url,
|
||||
name: it.user.username,
|
||||
...it,
|
||||
}))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.member {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
.member-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.info {
|
||||
display: flex;
|
||||
img {
|
||||
border-radius: var(--size-rounded-icon);
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
.text {
|
||||
margin: auto 0 auto 0.5rem;
|
||||
h4 {
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
h3 {
|
||||
text-transform: uppercase;
|
||||
margin-top: 0.1rem;
|
||||
margin-bottom: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-extrabold);
|
||||
letter-spacing: 0.02rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.side-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.dropdown-icon {
|
||||
margin-left: 1rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-dark);
|
||||
background-color: unset;
|
||||
transition: 150ms ease transform;
|
||||
padding: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: none;
|
||||
|
||||
.main-info {
|
||||
margin-bottom: var(--spacing-card-lg);
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
label {
|
||||
align-items: center;
|
||||
input {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.permissions {
|
||||
margin: 1rem 0;
|
||||
max-width: 45rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: 0.5rem;
|
||||
|
||||
label {
|
||||
flex-direction: row;
|
||||
input {
|
||||
flex: none;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
.member-header {
|
||||
.dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: unset;
|
||||
margin: var(--spacing-card-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
&:disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
flex: 2;
|
||||
padding-right: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 3;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
div,
|
||||
a {
|
||||
display: block;
|
||||
text-align: center;
|
||||
height: fit-content;
|
||||
flex: 1;
|
||||
@media screen and (max-width: 1024px) {
|
||||
margin: 0.5rem 0 1rem 0;
|
||||
}
|
||||
}
|
||||
div:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.team-invite {
|
||||
@media screen and (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
margin-right: 0.5rem;
|
||||
|
||||
&:first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1020
pages/_type/_id/version.vue
Normal file
1020
pages/_type/_id/version.vue
Normal file
File diff suppressed because it is too large
Load Diff
8
pages/_type/_id/version/_version/edit.vue
Normal file
8
pages/_type/_id/version/_version/edit.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
30
pages/_type/_id/version/_version/index.vue
Normal file
30
pages/_type/_id/version/_version/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
head: {
|
||||
title: 'Mods - Modrinth',
|
||||
meta: [
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: 'Mods',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Mods',
|
||||
},
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/mods`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
8
pages/_type/_id/version/create.vue
Normal file
8
pages/_type/_id/version/create.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
241
pages/_type/_id/versions.vue
Normal file
241
pages/_type/_id/versions.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div class="content card">
|
||||
<nuxt-link
|
||||
v-if="currentMember"
|
||||
to="version/create"
|
||||
class="iconified-button new-version"
|
||||
>
|
||||
<UploadIcon />
|
||||
Upload
|
||||
</nuxt-link>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Version</th>
|
||||
<th>Supports</th>
|
||||
<th>Stats</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="version in versions" :key="version.id">
|
||||
<td>
|
||||
<a
|
||||
:href="$parent.findPrimary(version).url"
|
||||
class="download-button"
|
||||
@click.prevent="
|
||||
$parent.downloadFile(
|
||||
$parent.findPrimary(version).hashes.sha1,
|
||||
$parent.findPrimary(version).url
|
||||
)
|
||||
"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="info">
|
||||
<div class="top">
|
||||
<nuxt-link
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/version/${encodeURIComponent(version.version_number)}`"
|
||||
>
|
||||
{{ version.name }}
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<VersionBadge
|
||||
v-if="version.version_type === 'release'"
|
||||
type="release"
|
||||
color="green"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="version.version_type === 'beta'"
|
||||
type="beta"
|
||||
color="yellow"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="version.version_type === 'alpha'"
|
||||
type="alpha"
|
||||
color="red"
|
||||
/>
|
||||
<span class="divider" />
|
||||
<span class="version_number">{{ version.version_number }}</span>
|
||||
</div>
|
||||
<div class="mobile-info">
|
||||
<p>
|
||||
{{
|
||||
version.loaders
|
||||
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
|
||||
.join(', ') +
|
||||
' ' +
|
||||
version.game_versions[version.game_versions.length - 1]
|
||||
}}
|
||||
</p>
|
||||
<p></p>
|
||||
<p>
|
||||
<strong>{{ $parent.formatNumber(version.downloads) }}</strong>
|
||||
downloads
|
||||
</p>
|
||||
<p>
|
||||
Published on
|
||||
<strong>{{
|
||||
$dayjs(version.date_published).format('MMM D, YYYY')
|
||||
}}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<p>
|
||||
{{
|
||||
version.loaders
|
||||
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
|
||||
.join(', ')
|
||||
}}
|
||||
</p>
|
||||
<p>{{ version.game_versions[version.game_versions.length - 1] }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>
|
||||
<span>{{ $parent.formatNumber(version.downloads) }}</span>
|
||||
downloads
|
||||
</p>
|
||||
<p>
|
||||
Published on
|
||||
<span>{{
|
||||
$dayjs(version.date_published).format('MMM D, YYYY')
|
||||
}}</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import VersionBadge from '~/components/ui/Badge'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UploadIcon,
|
||||
DownloadIcon,
|
||||
VersionBadge,
|
||||
},
|
||||
auth: false,
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
max-width: calc(100% - (2 * var(--spacing-card-lg)));
|
||||
}
|
||||
|
||||
.new-version {
|
||||
max-width: 5.25rem;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 0.75rem;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: var(--font-size-md);
|
||||
|
||||
&:nth-child(3),
|
||||
&:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
td:nth-child(2) {
|
||||
padding-right: 2rem;
|
||||
min-width: 13.875rem;
|
||||
.top {
|
||||
font-weight: bold;
|
||||
}
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
.divider {
|
||||
width: 0.25rem;
|
||||
height: 0.25rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin: 0 0.25rem;
|
||||
background-color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-info {
|
||||
p {
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
td:nth-child(3) {
|
||||
display: none;
|
||||
width: 100%;
|
||||
p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
}
|
||||
td:nth-child(4) {
|
||||
display: none;
|
||||
min-width: 15rem;
|
||||
p {
|
||||
margin: 0.25rem 0;
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
table {
|
||||
tr {
|
||||
th:nth-child(3),
|
||||
td:nth-child(3),
|
||||
th:nth-child(4),
|
||||
td:nth-child(4) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user