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>
|
||||
@@ -1,43 +1,46 @@
|
||||
<template>
|
||||
<div class="page-contents">
|
||||
<header class="columns">
|
||||
<h3 class="column-grow-1">Edit Mod</h3>
|
||||
<header class="card columns">
|
||||
<h3 class="column-grow-1">Edit project</h3>
|
||||
<nuxt-link
|
||||
:to="'/mod/' + (mod.slug ? mod.slug : mod.id)"
|
||||
class="button column"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}`"
|
||||
class="iconified-button column"
|
||||
>
|
||||
Back
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="
|
||||
mod.status === 'rejected' ||
|
||||
mod.status === 'draft' ||
|
||||
mod.status === 'unlisted'
|
||||
project.status === 'rejected' ||
|
||||
project.status === 'draft' ||
|
||||
project.status === 'unlisted'
|
||||
"
|
||||
title="Submit for approval"
|
||||
class="button column"
|
||||
class="iconified-button column"
|
||||
:disabled="!$nuxt.$loading"
|
||||
@click="saveModReview"
|
||||
@click="saveProjectReview"
|
||||
>
|
||||
Submit for approval
|
||||
</button>
|
||||
<button
|
||||
title="Save"
|
||||
class="brand-button column"
|
||||
class="iconified-button brand-button-colors column"
|
||||
:disabled="!$nuxt.$loading"
|
||||
@click="saveMod"
|
||||
@click="saveProject"
|
||||
>
|
||||
<CheckIcon />
|
||||
Save
|
||||
</button>
|
||||
</header>
|
||||
<section class="essentials">
|
||||
<section class="card essentials">
|
||||
<h3>Name</h3>
|
||||
<label>
|
||||
<span>
|
||||
Be creative. TechCraft v7 won't be searchable and won't be clicked on.
|
||||
Be creative! Generic project names will be harder to search for.
|
||||
</span>
|
||||
<input
|
||||
v-model="mod.title"
|
||||
v-model="newProject.title"
|
||||
type="text"
|
||||
placeholder="Enter the name"
|
||||
:disabled="
|
||||
@@ -48,10 +51,11 @@
|
||||
<h3>Summary</h3>
|
||||
<label>
|
||||
<span>
|
||||
Give a quick summary of your mod. This will appear in search.
|
||||
Give a short description of your project that will appear on search
|
||||
pages.
|
||||
</span>
|
||||
<input
|
||||
v-model="mod.description"
|
||||
v-model="newProject.description"
|
||||
type="text"
|
||||
placeholder="Enter the summary"
|
||||
:disabled="
|
||||
@@ -62,13 +66,17 @@
|
||||
<h3>Categories</h3>
|
||||
<label>
|
||||
<span>
|
||||
Select up to 3 categories. These will help others find your mod.
|
||||
Select up to 3 categories that will help others find your project.
|
||||
</span>
|
||||
<multiselect
|
||||
<Multiselect
|
||||
id="categories"
|
||||
v-model="mod.categories"
|
||||
:options="availableCategories"
|
||||
:loading="availableCategories.length === 0"
|
||||
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"
|
||||
@@ -87,77 +95,59 @@
|
||||
<h3>Vanity URL (slug)</h3>
|
||||
<label>
|
||||
<span>
|
||||
Set this to something pretty, so your mod's URL can be more readable.
|
||||
Set this to something that will looks nice in your project's URL.
|
||||
</span>
|
||||
<input
|
||||
id="name"
|
||||
v-model="mod.slug"
|
||||
v-model="newProject.slug"
|
||||
type="text"
|
||||
placeholder="Enter the vanity URL's last bit"
|
||||
placeholder="Enter the vanity URL slug"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section class="mod-icon rows">
|
||||
<section class="card project-icon rows">
|
||||
<h3>Icon</h3>
|
||||
<div class="columns row-grow-1">
|
||||
<div class="rows row-grow-1">
|
||||
<file-input
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image"
|
||||
prompt="Choose an image or drag it here"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
@change="showPreviewImage"
|
||||
/>
|
||||
<ul class="row-grow-1">
|
||||
<li>Must be a square</li>
|
||||
<li>Minimum size is 100x100</li>
|
||||
<li>Acceptable formats are PNG, JPEG, GIF, and WEBP</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="rows">
|
||||
<img
|
||||
:src="
|
||||
previewImage
|
||||
? previewImage
|
||||
: mod.icon_url && !iconChanged
|
||||
? mod.icon_url
|
||||
: 'https://cdn.modrinth.com/placeholder.svg'
|
||||
"
|
||||
alt="preview-image"
|
||||
/>
|
||||
<button
|
||||
class="button"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
@click="
|
||||
icon = null
|
||||
previewImage = null
|
||||
iconChanged = true
|
||||
"
|
||||
>
|
||||
Reset icon
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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="game-sides">
|
||||
<section class="card game-sides">
|
||||
<h3>Supported environments</h3>
|
||||
<div class="columns">
|
||||
<span>
|
||||
Let others know if your mod is for clients, servers, or both. For
|
||||
example, Lithium would be optional for both sides, whereas Sodium
|
||||
would be required on the client and unsupported on the server.
|
||||
</span>
|
||||
<span> Let others know what environments your project supports. </span>
|
||||
<div class="labeled-control">
|
||||
<h3>Client</h3>
|
||||
<Multiselect
|
||||
v-model="mod.client_side"
|
||||
v-model="clientSideType"
|
||||
placeholder="Select one"
|
||||
:options="sideTypes"
|
||||
:searchable="false"
|
||||
@@ -172,7 +162,7 @@
|
||||
<div class="labeled-control">
|
||||
<h3>Server</h3>
|
||||
<Multiselect
|
||||
v-model="mod.server_side"
|
||||
v-model="serverSideType"
|
||||
placeholder="Select one"
|
||||
:options="sideTypes"
|
||||
:searchable="false"
|
||||
@@ -186,11 +176,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="description">
|
||||
<section class="card description">
|
||||
<h3>
|
||||
<label
|
||||
for="body"
|
||||
title="You can type an extended description of your mod here."
|
||||
title="You can type an extended description of your project here."
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
@@ -202,41 +192,45 @@
|
||||
href="https://guides.github.com/features/mastering-markdown/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-link"
|
||||
>here</a
|
||||
>.
|
||||
</span>
|
||||
<div class="columns">
|
||||
<div class="textarea-wrapper">
|
||||
<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="mod.body"
|
||||
v-model="newProject.body"
|
||||
:disabled="(currentMember.permissions & EDIT_BODY) !== EDIT_BODY"
|
||||
></textarea>
|
||||
/>
|
||||
</div>
|
||||
<div v-compiled-markdown="mod.body" class="markdown-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="extra-links">
|
||||
<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 mod."
|
||||
title="A place for users to report bugs, issues, and concerns about your project."
|
||||
>
|
||||
<span>Issue tracker</span>
|
||||
<input
|
||||
v-model="mod.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 mod.">
|
||||
<span>Source code</span>
|
||||
<input
|
||||
v-model="mod.source_url"
|
||||
v-model="newProject.issues_url"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="
|
||||
@@ -245,11 +239,21 @@
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
title="A page containing information, documentation, and help for the mod."
|
||||
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="mod.wiki_url"
|
||||
v-model="newProject.wiki_url"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="
|
||||
@@ -260,7 +264,7 @@
|
||||
<label title="An invitation link to your Discord server.">
|
||||
<span>Discord invite</span>
|
||||
<input
|
||||
v-model="mod.discord_url"
|
||||
v-model="newProject.discord_url"
|
||||
type="url"
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="
|
||||
@@ -269,7 +273,7 @@
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section class="license">
|
||||
<section class="card license">
|
||||
<div class="title">
|
||||
<h3>License</h3>
|
||||
</div>
|
||||
@@ -283,6 +287,7 @@
|
||||
href="https://blog.modrinth.com/licensing-guide/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-link"
|
||||
>
|
||||
licensing guide</a
|
||||
>
|
||||
@@ -294,7 +299,7 @@
|
||||
placeholder="Select one"
|
||||
track-by="short"
|
||||
label="name"
|
||||
:options="availableLicenses"
|
||||
:options="$tag.licenses"
|
||||
:searchable="true"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
@@ -313,19 +318,19 @@
|
||||
</div>
|
||||
</label>
|
||||
</section>
|
||||
<!--
|
||||
<section class="donations">
|
||||
<section class="card donations">
|
||||
<div class="title">
|
||||
<h3>Donation links</h3>
|
||||
<button
|
||||
title="Add a link"
|
||||
class="button"
|
||||
class="iconified-button"
|
||||
:disabled="false"
|
||||
@click="
|
||||
donationPlatforms.push({})
|
||||
donationLinks.push('')
|
||||
"
|
||||
>
|
||||
<PlusIcon />
|
||||
Add a link
|
||||
</button>
|
||||
</div>
|
||||
@@ -345,39 +350,51 @@
|
||||
placeholder="Select one"
|
||||
track-by="short"
|
||||
label="name"
|
||||
:options="availableDonationPlatforms"
|
||||
:options="$tag.donationPlatforms"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
class="button"
|
||||
class="iconified-button"
|
||||
@click="
|
||||
donationPlatforms.splice(index, 1)
|
||||
donationLinks.splice(index, 1)
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove Link
|
||||
</button>
|
||||
<hr />
|
||||
<hr
|
||||
v-if="
|
||||
donationPlatforms.length > 0 &&
|
||||
index !== donationPlatforms.length - 1
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
|
||||
import getDifferences from '~/libs/getDifferences'
|
||||
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 (
|
||||
@@ -389,6 +406,12 @@ export default {
|
||||
next()
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
@@ -396,77 +419,19 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
async asyncData(data) {
|
||||
try {
|
||||
const [
|
||||
savedMod,
|
||||
availableCategories,
|
||||
availableLoaders,
|
||||
availableGameVersions,
|
||||
availableLicenses,
|
||||
availableDonationPlatforms,
|
||||
] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`mod/${data.params.id}`, data.$auth.headers),
|
||||
data.$axios.get(`tag/category`),
|
||||
data.$axios.get(`tag/loader`),
|
||||
data.$axios.get(`tag/game_version`),
|
||||
data.$axios.get(`tag/license`),
|
||||
data.$axios.get(`tag/donation_platform`),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
savedMod.license = {
|
||||
short: savedMod.license.id,
|
||||
name: savedMod.license.name,
|
||||
url: savedMod.license.url,
|
||||
}
|
||||
|
||||
if (savedMod.body_url && !savedMod.body) {
|
||||
savedMod.body = (await data.$axios.get(savedMod.body_url)).data
|
||||
}
|
||||
|
||||
/*
|
||||
const donationPlatforms = []
|
||||
const donationLinks = []
|
||||
|
||||
if (savedMod.donation_urls) {
|
||||
for (const platform of savedMod.donation_urls) {
|
||||
donationPlatforms.push({
|
||||
short: platform.id,
|
||||
name: platform.platform,
|
||||
})
|
||||
donationLinks.push(platform.url)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
availableLicenses.sort((a, b) => a.name.localeCompare(b.name))
|
||||
return {
|
||||
savedMod,
|
||||
mod: { ...savedMod },
|
||||
availableCategories,
|
||||
availableLoaders,
|
||||
availableGameVersions,
|
||||
availableLicenses,
|
||||
license: {
|
||||
short: savedMod.license.id,
|
||||
name: savedMod.license.name,
|
||||
},
|
||||
license_url: savedMod.license.url,
|
||||
availableDonationPlatforms,
|
||||
// donationPlatforms,
|
||||
// donationLinks,
|
||||
}
|
||||
} catch {
|
||||
data.error({
|
||||
statusCode: 404,
|
||||
message: 'Mod not found',
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newProject: {},
|
||||
|
||||
clientSideType: '',
|
||||
serverSideType: '',
|
||||
|
||||
license: { short: '', name: '' },
|
||||
license_url: '',
|
||||
|
||||
donationPlatforms: [],
|
||||
donationLinks: [],
|
||||
|
||||
isProcessing: false,
|
||||
previewImage: null,
|
||||
compiledBody: '',
|
||||
@@ -474,11 +439,41 @@ export default {
|
||||
icon: null,
|
||||
iconChanged: false,
|
||||
|
||||
sideTypes: ['required', 'optional', 'unsupported'],
|
||||
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) {
|
||||
@@ -506,8 +501,6 @@ export default {
|
||||
})
|
||||
},
|
||||
created() {
|
||||
this.$emit('update:link-bar', [['Edit', 'edit']])
|
||||
|
||||
this.UPLOAD_VERSION = 1 << 0
|
||||
this.DELETE_VERSION = 1 << 1
|
||||
this.EDIT_DETAILS = 1 << 2
|
||||
@@ -518,34 +511,29 @@ export default {
|
||||
this.DELETE_MOD = 1 << 7
|
||||
},
|
||||
methods: {
|
||||
async saveModReview() {
|
||||
async saveProjectReview() {
|
||||
this.isProcessing = true
|
||||
await this.saveMod()
|
||||
await this.saveProject()
|
||||
},
|
||||
async saveMod() {
|
||||
async saveProject() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
const modChanges = getDifferences(this.savedMod, this.mod)
|
||||
|
||||
try {
|
||||
const data = {
|
||||
...({ title: modChanges.title } || {}),
|
||||
...({ description: modChanges.description } || {}),
|
||||
...({ body: modChanges.body } || {}),
|
||||
...({ categories: modChanges.categories } || {}),
|
||||
...({ issues_url: modChanges.issues_url } || {}),
|
||||
...({ source_url: modChanges.source_url } || {}),
|
||||
...({ wiki_url: modChanges.wiki_url } || {}),
|
||||
...({ license_url: modChanges.license_url } || {}),
|
||||
...({ discord_url: modChanges.discord_url } || {}),
|
||||
...({ license_id: modChanges.license_id } || {}),
|
||||
...({ client_side: modChanges.client_side } || {}),
|
||||
...({ server_side: modChanges.server_side } || {}),
|
||||
...({ slug: modChanges.slug } || {}),
|
||||
...(modChanges.license
|
||||
? { license: modChanges.license.short } || {}
|
||||
: {}),
|
||||
/*
|
||||
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,
|
||||
@@ -553,18 +541,21 @@ export default {
|
||||
url: this.donationLinks[index],
|
||||
}
|
||||
}),
|
||||
*/
|
||||
}
|
||||
|
||||
if (this.isProcessing) {
|
||||
data.status = 'processing'
|
||||
}
|
||||
|
||||
await this.$axios.patch(`mod/${this.mod.id}`, data, this.$auth.headers)
|
||||
await this.$axios.patch(
|
||||
`project/${this.newProject.id}`,
|
||||
data,
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
if (this.iconChanged) {
|
||||
await this.$axios.patch(
|
||||
`mod/${this.mod.id}/icon?ext=${
|
||||
`project/${this.newProject.id}/icon?ext=${
|
||||
this.icon.type.split('/')[this.icon.type.split('/').length - 1]
|
||||
}`,
|
||||
this.icon,
|
||||
@@ -572,14 +563,15 @@ export default {
|
||||
)
|
||||
}
|
||||
|
||||
this.isEditing = false
|
||||
this.savedMod = this.mod
|
||||
this.$emit('update:featuredVersions', this.newProject)
|
||||
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Changes saved',
|
||||
type: 'success',
|
||||
})
|
||||
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',
|
||||
@@ -609,11 +601,13 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
* {
|
||||
display: inline;
|
||||
}
|
||||
.button {
|
||||
margin-left: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,25 +652,20 @@ label {
|
||||
.page-contents {
|
||||
display: grid;
|
||||
grid-template:
|
||||
'header header header' auto
|
||||
'advert advert advert' auto
|
||||
'essentials essentials essentials' auto
|
||||
'mod-icon mod-icon mod-icon' auto
|
||||
'game-sides game-sides game-sides' auto
|
||||
'description description description' auto
|
||||
'versions versions versions' auto
|
||||
'extra-links extra-links extra-links' auto
|
||||
'license license license' auto
|
||||
'donations donations donations' auto
|
||||
'footer footer footer' auto
|
||||
/ 4fr 1fr 4fr;
|
||||
'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 {
|
||||
@extend %card;
|
||||
|
||||
grid-area: header;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
@@ -691,28 +680,22 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
.advert {
|
||||
grid-area: advert;
|
||||
}
|
||||
|
||||
section {
|
||||
@extend %card;
|
||||
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
section.essentials {
|
||||
grid-area: essentials;
|
||||
}
|
||||
|
||||
section.mod-icon {
|
||||
grid-area: mod-icon;
|
||||
section.project-icon {
|
||||
grid-area: project-icon;
|
||||
|
||||
img {
|
||||
align-self: flex-start;
|
||||
max-width: 6.08rem;
|
||||
margin-left: var(--spacing-card-lg);
|
||||
border-radius: var(--size-rounded-icon);
|
||||
max-width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: var(--size-rounded-lg);
|
||||
}
|
||||
|
||||
.iconified-button {
|
||||
width: 9rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,15 +719,13 @@ section.game-sides {
|
||||
section.description {
|
||||
grid-area: description;
|
||||
|
||||
& > .columns {
|
||||
align-items: stretch;
|
||||
.separator {
|
||||
margin: var(--spacing-card-sm) 0;
|
||||
}
|
||||
|
||||
.edit-wrapper * {
|
||||
min-height: 10rem;
|
||||
max-height: 40rem;
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
@@ -795,8 +776,7 @@ section.donations {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--color-link);
|
||||
.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>
|
||||
@@ -2,55 +2,55 @@
|
||||
<div>
|
||||
<ConfirmPopup
|
||||
ref="delete_popup"
|
||||
title="Are you sure you want to delete this mod?"
|
||||
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="mod.title"
|
||||
proceed-label="Delete mod"
|
||||
@proceed="deleteMod"
|
||||
:confirmation-text="project.title"
|
||||
proceed-label="Delete project"
|
||||
@proceed="deleteProject"
|
||||
/>
|
||||
<div class="section-header columns">
|
||||
<h3 class="column-grow-1">General</h3>
|
||||
<div class="card">
|
||||
<h3>General</h3>
|
||||
</div>
|
||||
<section>
|
||||
<h3>Edit mod</h3>
|
||||
<section class="card">
|
||||
<h3>Edit project</h3>
|
||||
<label>
|
||||
<span>This leads you to a page where you can edit your mod.</span>
|
||||
<nuxt-link class="button" to="edit">Edit</nuxt-link>
|
||||
<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 mod.
|
||||
This leads to a page where you can create a version for your project.
|
||||
</span>
|
||||
<nuxt-link
|
||||
class="button"
|
||||
to="newversion"
|
||||
class="iconified-button"
|
||||
to="version/create"
|
||||
:disabled="
|
||||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
||||
"
|
||||
>Create version</nuxt-link
|
||||
>
|
||||
</label>
|
||||
<h3>Delete mod</h3>
|
||||
<h3>Delete project</h3>
|
||||
<label>
|
||||
<span>
|
||||
Clicking on this WILL delete your mod. Do not click on this unless you
|
||||
want your mod deleted. If you delete your mod, all versions and any
|
||||
attached data will be removed from our servers. This may break other
|
||||
projects, so be careful!
|
||||
Removes your project from Modrinth's servers and search. Clicking on
|
||||
this will delete your project, so be extra careful!
|
||||
</span>
|
||||
<div
|
||||
class="button"
|
||||
:disabled="(currentMember.permissions & DELETE_MOD) !== DELETE_MOD"
|
||||
class="iconified-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
||||
"
|
||||
@click="showPopup"
|
||||
>
|
||||
Delete mod
|
||||
Delete project
|
||||
</div>
|
||||
</label>
|
||||
</section>
|
||||
<div class="section-header columns team-invite">
|
||||
<h3 class="column-grow-1">Team members</h3>
|
||||
<div class="card columns team-invite">
|
||||
<h3>Team members</h3>
|
||||
<div
|
||||
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||
class="column"
|
||||
@@ -62,16 +62,20 @@
|
||||
placeholder="Username"
|
||||
/>
|
||||
<label for="username" class="hidden">Username</label>
|
||||
<button class="brand-button column" @click="inviteTeamMember">
|
||||
<button
|
||||
class="iconified-button brand-button-colors column"
|
||||
@click="inviteTeamMember"
|
||||
>
|
||||
<PlusIcon />
|
||||
Invite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(member, index) in allMembers"
|
||||
:key="member.user_id"
|
||||
class="member"
|
||||
:class="{ open: openTeamMembers.includes(member.user_id) }"
|
||||
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">
|
||||
@@ -82,15 +86,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-buttons">
|
||||
<span v-if="member.accepted" class="badge green">Accepted</span>
|
||||
<span v-else class="badge yellow">Pending</span>
|
||||
<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.indexOf(member.user.id) === -1
|
||||
? openTeamMembers.push(member.user.id)
|
||||
: (openTeamMembers = openTeamMembers.filter(
|
||||
(it) => it !== member.user_id
|
||||
(it) => it !== member.user.id
|
||||
))
|
||||
"
|
||||
>
|
||||
@@ -103,7 +107,7 @@
|
||||
<label>
|
||||
Role:
|
||||
<input
|
||||
v-model="allMembers[index].role"
|
||||
v-model="allTeamMembers[index].role"
|
||||
type="text"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
@@ -125,7 +129,7 @@
|
||||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
||||
"
|
||||
label="Upload version"
|
||||
@input="allMembers[index].permissions ^= UPLOAD_VERSION"
|
||||
@input="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
@@ -138,7 +142,7 @@
|
||||
(currentMember.permissions & DELETE_VERSION) !== DELETE_VERSION
|
||||
"
|
||||
label="Delete version"
|
||||
@input="allMembers[index].permissions ^= DELETE_VERSION"
|
||||
@input="allTeamMembers[index].permissions ^= DELETE_VERSION"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
@@ -151,7 +155,7 @@
|
||||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||
"
|
||||
label="Edit details"
|
||||
@input="allMembers[index].permissions ^= EDIT_DETAILS"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_DETAILS"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
@@ -164,7 +168,7 @@
|
||||
(currentMember.permissions & EDIT_BODY) !== EDIT_BODY
|
||||
"
|
||||
label="Edit body"
|
||||
@input="allMembers[index].permissions ^= EDIT_BODY"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_BODY"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
@@ -177,7 +181,7 @@
|
||||
(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES
|
||||
"
|
||||
label="Manage invites"
|
||||
@input="allMembers[index].permissions ^= MANAGE_INVITES"
|
||||
@input="allTeamMembers[index].permissions ^= MANAGE_INVITES"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
@@ -190,7 +194,7 @@
|
||||
(currentMember.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
|
||||
"
|
||||
label="Remove member"
|
||||
@input="allMembers[index].permissions ^= REMOVE_MEMBER"
|
||||
@input="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
@@ -202,39 +206,54 @@
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
label="Edit member"
|
||||
@input="allMembers[index].permissions ^= EDIT_MEMBER"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_MEMBER"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="
|
||||
(member.permissions & DELETE_MOD) === DELETE_MOD ||
|
||||
(member.permissions & DELETE_PROJECT) === DELETE_PROJECT ||
|
||||
member.role === 'Owner'
|
||||
"
|
||||
:disabled="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||
(currentMember.permissions & DELETE_MOD) !== DELETE_MOD
|
||||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
||||
"
|
||||
label="Delete mod"
|
||||
@input="allMembers[index].permissions ^= DELETE_MOD"
|
||||
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="
|
||||
member.role === 'Owner' ||
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
@click="updateTeamMember(index)"
|
||||
>
|
||||
<CheckIcon />
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
@@ -246,13 +265,27 @@
|
||||
<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 },
|
||||
components: {
|
||||
DropdownIcon,
|
||||
ConfirmPopup,
|
||||
Checkbox,
|
||||
Badge,
|
||||
PlusIcon,
|
||||
CheckIcon,
|
||||
TrashIcon,
|
||||
UserIcon,
|
||||
},
|
||||
props: {
|
||||
mod: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
@@ -275,11 +308,13 @@ export default {
|
||||
return {
|
||||
currentUsername: '',
|
||||
openTeamMembers: [],
|
||||
allTeamMembers: [],
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.allTeamMembers = this.allMembers
|
||||
},
|
||||
created() {
|
||||
this.$emit('update:link-bar', [['Settings', 'settings']])
|
||||
|
||||
this.UPLOAD_VERSION = 1 << 0
|
||||
this.DELETE_VERSION = 1 << 1
|
||||
this.EDIT_DETAILS = 1 << 2
|
||||
@@ -287,7 +322,7 @@ export default {
|
||||
this.MANAGE_INVITES = 1 << 4
|
||||
this.REMOVE_MEMBER = 1 << 5
|
||||
this.EDIT_MEMBER = 1 << 6
|
||||
this.DELETE_MOD = 1 << 7
|
||||
this.DELETE_PROJECT = 1 << 7
|
||||
},
|
||||
methods: {
|
||||
async inviteTeamMember() {
|
||||
@@ -302,11 +337,11 @@ export default {
|
||||
}
|
||||
|
||||
await this.$axios.post(
|
||||
`team/${this.mod.team}/members`,
|
||||
`team/${this.project.team}/members`,
|
||||
data,
|
||||
this.$auth.headers
|
||||
)
|
||||
await this.$router.go(null)
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
@@ -323,10 +358,10 @@ export default {
|
||||
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`team/${this.mod.team}/members/${this.allMembers[index].user_id}`,
|
||||
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
await this.$router.go(null)
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
@@ -343,16 +378,39 @@ export default {
|
||||
|
||||
try {
|
||||
const data = {
|
||||
permissions: this.allMembers[index].permissions,
|
||||
role: this.allMembers[index].role,
|
||||
permissions: this.allTeamMembers[index].permissions,
|
||||
role: this.allTeamMembers[index].role,
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`team/${this.mod.team}/members/${this.allMembers[index].user_id}`,
|
||||
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
||||
data,
|
||||
this.$auth.headers
|
||||
)
|
||||
await this.$router.go(null)
|
||||
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',
|
||||
@@ -372,35 +430,35 @@ export default {
|
||||
this.$refs.delete_popup.show()
|
||||
}
|
||||
},
|
||||
async deleteMod() {
|
||||
await this.$axios.delete(`mod/${this.mod.id}`, this.$auth.headers)
|
||||
await this.$router.push('/dashboard/projects')
|
||||
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 mod has been successfully deleted.',
|
||||
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>
|
||||
.section-header {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
h3 {
|
||||
margin: auto 0;
|
||||
color: var(--color-text-dark);
|
||||
font-weight: var(--font-weight-extrabold);
|
||||
}
|
||||
}
|
||||
|
||||
.member {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
.member-header {
|
||||
@@ -496,8 +554,6 @@ button {
|
||||
}
|
||||
|
||||
section {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
label {
|
||||
@@ -515,6 +571,7 @@ section {
|
||||
|
||||
div,
|
||||
a {
|
||||
display: block;
|
||||
text-align: center;
|
||||
height: fit-content;
|
||||
flex: 1;
|
||||
@@ -535,20 +592,43 @@ section {
|
||||
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>
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>About</h1>
|
||||
<p>
|
||||
Founded in 2020, Modrinth was created to provide modders with an open
|
||||
and intuitive platform to publish their mods on.
|
||||
and intuitive platform to publish their projects on.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
@@ -56,17 +56,11 @@
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
<m-footer class="footer" centered />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MFooter from '~/components/layout/MFooter'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MFooter,
|
||||
},
|
||||
auth: false,
|
||||
head: {
|
||||
title: 'About - Modrinth',
|
||||
@@ -103,11 +97,6 @@ export default {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.container {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--color-link);
|
||||
|
||||
1465
pages/create/project.vue
Normal file
1465
pages/create/project.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,25 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<header class="columns">
|
||||
<header class="card columns">
|
||||
<h3 class="column-grow-1">File a report</h3>
|
||||
<button
|
||||
title="Create"
|
||||
class="brand-button column"
|
||||
:disabled="!this.$nuxt.$loading"
|
||||
class="brand-button-colors iconified-button column"
|
||||
:disabled="!$nuxt.$loading"
|
||||
@click="createReport"
|
||||
>
|
||||
<PlusIcon />
|
||||
Create
|
||||
</button>
|
||||
</header>
|
||||
<section class="info">
|
||||
<section class="card info">
|
||||
<h3>Item ID</h3>
|
||||
<label>
|
||||
<span>
|
||||
The ID of the item you are reporting. For example, the item ID of a
|
||||
mod would be its mod ID, found on the right side of that mod's page
|
||||
under "Project ID".
|
||||
project would be its project ID, found on the right side of that
|
||||
project's page under "Project ID".
|
||||
</span>
|
||||
<input v-model="itemId" type="text" placeholder="Enter the item ID" />
|
||||
</label>
|
||||
@@ -28,7 +29,7 @@
|
||||
<multiselect
|
||||
id="item-type"
|
||||
v-model="itemType"
|
||||
:options="['mod', 'version', 'user']"
|
||||
:options="['project', 'version', 'user']"
|
||||
:multiple="false"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
@@ -54,7 +55,7 @@
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section class="description">
|
||||
<section class="card description">
|
||||
<h3>
|
||||
<label
|
||||
for="body"
|
||||
@@ -70,14 +71,25 @@
|
||||
href="https://guides.github.com/features/mastering-markdown/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-link"
|
||||
>here</a
|
||||
>.
|
||||
</span>
|
||||
<div class="columns">
|
||||
<div class="textarea-wrapper">
|
||||
<textarea id="body" v-model="body"></textarea>
|
||||
<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="body" />
|
||||
</div>
|
||||
<div v-compiled-markdown="body" class="markdown-body"></div>
|
||||
<div
|
||||
v-if="bodyViewMode === 'preview'"
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="body ? $xss($md.render(body)) : 'No body specified.'"
|
||||
></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -86,14 +98,15 @@
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import ThisOrThat from '~/components/ui/ThisOrThat'
|
||||
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
},
|
||||
fetch() {
|
||||
if (this.$route.query.id) this.itemId = this.$route.query.id
|
||||
if (this.$route.query.t) this.itemType = this.$route.query.t
|
||||
ThisOrThat,
|
||||
PlusIcon,
|
||||
},
|
||||
async asyncData(data) {
|
||||
const reportTypes = (await data.$axios.get(`tag/report_type`)).data
|
||||
@@ -109,9 +122,15 @@ export default {
|
||||
reportType: '',
|
||||
body: '',
|
||||
|
||||
bodyViewMode: 'source',
|
||||
|
||||
reportTypes: ['aaaa'],
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
if (this.$route.query.id) this.itemId = this.$route.query.id
|
||||
if (this.$route.query.t) this.itemType = this.$route.query.t
|
||||
},
|
||||
methods: {
|
||||
async createReport() {
|
||||
this.$nuxt.$loading.start()
|
||||
@@ -126,7 +145,31 @@ export default {
|
||||
|
||||
await this.$axios.post('report', data, this.$auth.headers)
|
||||
|
||||
await this.$router.replace(`/${this.itemType}/${this.itemId}`)
|
||||
switch (this.itemType) {
|
||||
case 'version': {
|
||||
const version = (await this.$axios.get(`version/${this.itemId}`))
|
||||
.data
|
||||
const project = (
|
||||
await this.$axios.get(`project/${version.project_id}`)
|
||||
).data
|
||||
await this.$router.replace(
|
||||
`/${project.project_type}/${project.slug || project.id}/version/${
|
||||
this.itemId
|
||||
}`
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'project': {
|
||||
const project = (await this.$axios.get(`project/${this.itemId}`))
|
||||
.data
|
||||
await this.$router.replace(
|
||||
`/${project.project_type}/${project.slug || project.id}`
|
||||
)
|
||||
break
|
||||
}
|
||||
default:
|
||||
await this.$router.replace(`/${this.itemType}/${this.itemId}`)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
@@ -194,10 +237,7 @@ label {
|
||||
}
|
||||
|
||||
header {
|
||||
@extend %card;
|
||||
|
||||
grid-area: header;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
h3 {
|
||||
margin: auto 0;
|
||||
@@ -210,12 +250,6 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
@extend %card;
|
||||
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
section.info {
|
||||
grid-area: info;
|
||||
}
|
||||
@@ -223,15 +257,13 @@ section.info {
|
||||
section.description {
|
||||
grid-area: description;
|
||||
|
||||
& > .columns {
|
||||
align-items: stretch;
|
||||
.separator {
|
||||
margin: var(--spacing-card-sm) 0;
|
||||
}
|
||||
|
||||
.edit-wrapper * {
|
||||
min-height: 10rem;
|
||||
max-height: 40rem;
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
@@ -239,4 +271,8 @@ section.description {
|
||||
padding: 0 var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,107 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<div class="sidebar-l">
|
||||
<div v-if="$auth.user != null" class="card page-nav">
|
||||
<nuxt-link :to="'/dashboard/projects'" class="tab">
|
||||
<ModIcon />
|
||||
My mods
|
||||
</nuxt-link>
|
||||
<nuxt-link :to="'/dashboard/notifications'" class="tab">
|
||||
<NotificationsIcon />
|
||||
Notifications
|
||||
<div v-if="this.$user.notifications.count > 0" class="notif-count">
|
||||
{{ this.$user.notifications.count }}
|
||||
</div>
|
||||
</nuxt-link>
|
||||
<nuxt-link :to="'/dashboard/follows'" class="tab">
|
||||
<FollowIcon />
|
||||
Followed mods
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-if="
|
||||
$auth.user.role === 'admin' || $auth.user.role === 'moderator'
|
||||
"
|
||||
:to="'/dashboard/moderation'"
|
||||
class="tab"
|
||||
>
|
||||
<ModerationIcon />
|
||||
Moderation
|
||||
</nuxt-link>
|
||||
<nuxt-link :to="'/dashboard/settings'" class="tab">
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</nuxt-link>
|
||||
<nuxt-link :to="'/dashboard/privacy'" class="tab">
|
||||
<ShieldIcon />
|
||||
Privacy settings
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div v-else class="card page-nav">
|
||||
<a :href="authUrl" class="tab">
|
||||
<UserIcon />
|
||||
Log in
|
||||
</a>
|
||||
<nuxt-link :to="'/dashboard/privacy'" class="tab">
|
||||
<SettingsIcon />
|
||||
Privacy settings
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<m-footer class="footer" hide-small />
|
||||
</div>
|
||||
<div class="content">
|
||||
<NuxtChild />
|
||||
<m-footer class="footer" hide-big centered />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ModIcon from '~/assets/images/sidebar/mod.svg?inline'
|
||||
import ModerationIcon from '~/assets/images/sidebar/admin.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/sidebar/settings.svg?inline'
|
||||
import NotificationsIcon from '~/assets/images/sidebar/notifications.svg?inline'
|
||||
import FollowIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
import ShieldIcon from '~/assets/images/utils/shield.svg?inline'
|
||||
|
||||
import MFooter from '~/components/layout/MFooter'
|
||||
|
||||
export default {
|
||||
name: 'DashboardPage',
|
||||
components: {
|
||||
ModIcon,
|
||||
ModerationIcon,
|
||||
SettingsIcon,
|
||||
NotificationsIcon,
|
||||
FollowIcon,
|
||||
UserIcon,
|
||||
ShieldIcon,
|
||||
MFooter,
|
||||
},
|
||||
computed: {
|
||||
authUrl() {
|
||||
return `${this.$axios.defaults.baseURL}auth/init?url=${this.$store.app.$config.utils.domain}${this.$route.fullPath}`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hideSmall {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.notif-count {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
background-color: rgba(180, 180, 180, 0.4);
|
||||
border-radius: 2rem;
|
||||
padding: 0.1rem 0.35rem;
|
||||
margin: 0 0.2rem 0 auto;
|
||||
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,129 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="section-header">
|
||||
<h3 class="column-grow-1">Followed mods</h3>
|
||||
</div>
|
||||
<div v-if="mods.length !== 0">
|
||||
<ModCard
|
||||
v-for="(mod, index) in mods"
|
||||
:id="mod.id"
|
||||
:key="mod.id"
|
||||
:author="mod.author"
|
||||
:author-url="mod.author_url"
|
||||
:categories="mod.categories"
|
||||
:created-at="mod.published"
|
||||
:description="mod.description"
|
||||
:downloads="mod.downloads.toString()"
|
||||
:edit-mode="true"
|
||||
:icon-url="mod.icon_url"
|
||||
:is-modrinth="true"
|
||||
:latest-version="mod.latest_version"
|
||||
:name="mod.title"
|
||||
:page-url="mod.page_url"
|
||||
:updated-at="mod.updated"
|
||||
>
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="button column unfav-button iconified-button"
|
||||
@click="unfavMod(index)"
|
||||
>
|
||||
<FollowIcon />
|
||||
Unfollow
|
||||
</button>
|
||||
</div>
|
||||
</ModCard>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<FollowIllustration class="icon"></FollowIllustration>
|
||||
<br />
|
||||
<span class="text"
|
||||
>You don't have any followed mods. <br />
|
||||
Why don't you <nuxt-link class="link" to="/mods">search</nuxt-link> for
|
||||
new ones?</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModCard from '~/components/ui/ProjectCard'
|
||||
import FollowIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import FollowIllustration from '~/assets/images/illustrations/follow_illustration.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModCard,
|
||||
FollowIcon,
|
||||
FollowIllustration,
|
||||
},
|
||||
async asyncData(data) {
|
||||
const res = await data.$axios.get(
|
||||
`user/${data.$auth.user.id}/follows`,
|
||||
data.$auth.headers
|
||||
)
|
||||
|
||||
const mods = (
|
||||
await data.$axios.get(`mods?ids=${JSON.stringify(res.data)}`)
|
||||
).data.sort((a, b) => a.title > b.title)
|
||||
|
||||
return {
|
||||
mods,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async unfavMod(index) {
|
||||
await this.$axios.delete(
|
||||
`mod/${this.mods[index].id}/follow`,
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
this.mods.splice(index, 1)
|
||||
},
|
||||
},
|
||||
head: {
|
||||
title: 'Followed Mods - Modrinth',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button {
|
||||
margin: 0.25rem 2rem 0.25rem 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.unfav-button {
|
||||
margin-left: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,192 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="section-header">
|
||||
<h3 class="column-grow-1">Mods</h3>
|
||||
</div>
|
||||
<div v-if="mods.length !== 0">
|
||||
<ModCard
|
||||
v-for="(mod, index) in mods"
|
||||
:id="mod.id"
|
||||
:key="mod.id"
|
||||
:author="mod.author"
|
||||
:author-url="mod.author_url"
|
||||
:categories="mod.categories"
|
||||
:created-at="mod.published"
|
||||
:description="mod.description"
|
||||
:downloads="mod.downloads.toString()"
|
||||
:edit-mode="true"
|
||||
:icon-url="mod.icon_url"
|
||||
:is-modrinth="true"
|
||||
:latest-version="mod.latest_version"
|
||||
:name="mod.title"
|
||||
:page-url="mod.page_url"
|
||||
:status="mod.status"
|
||||
:updated-at="mod.updated"
|
||||
>
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="button column approve"
|
||||
@click="changeModStatus(mod.id, 'approved', index)"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
class="button column unlist"
|
||||
@click="changeModStatus(mod.id, 'unlisted', index)"
|
||||
>
|
||||
Unlist
|
||||
</button>
|
||||
<button
|
||||
class="button column reject"
|
||||
@click="changeModStatus(mod.id, 'rejected', index)"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</ModCard>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<Security class="icon"></Security>
|
||||
<br />
|
||||
<span class="text">You are up-to-date!</span>
|
||||
</div>
|
||||
<div class="section-header">
|
||||
<h3 class="column-grow-1">Reports</h3>
|
||||
</div>
|
||||
<div v-if="reports.length !== 0">
|
||||
<div v-for="(report, index) in reports" :key="report.id" class="report">
|
||||
<div class="header">
|
||||
<h5 class="title">
|
||||
Report for {{ report.item_type }}
|
||||
<nuxt-link
|
||||
:to="
|
||||
'/' + report.item_type + '/' + report.item_id.replace(/\W/g, '')
|
||||
"
|
||||
>{{ report.item_id }}
|
||||
</nuxt-link>
|
||||
</h5>
|
||||
<p
|
||||
v-tooltip="
|
||||
$dayjs(report.created).format(
|
||||
'[Created at] YYYY-MM-DD [at] HH:mm A'
|
||||
)
|
||||
"
|
||||
class="date"
|
||||
>
|
||||
Created {{ $dayjs(report.created).fromNow() }}
|
||||
</p>
|
||||
<button class="delete iconified-button" @click="deleteReport(index)">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-compiled-markdown="report.body"
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<Security class="icon"></Security>
|
||||
<br />
|
||||
<span class="text">You are up-to-date!</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModCard from '~/components/ui/ProjectCard'
|
||||
import Security from '~/assets/images/illustrations/security.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModCard,
|
||||
Security,
|
||||
},
|
||||
async asyncData(data) {
|
||||
const mods = (await data.$axios.get(`moderation/mods`, data.$auth.headers))
|
||||
.data
|
||||
|
||||
const reports = (await data.$axios.get(`report`, data.$auth.headers)).data
|
||||
|
||||
return {
|
||||
mods,
|
||||
reports,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async changeModStatus(id, status, index) {
|
||||
await this.$axios.patch(
|
||||
`mod/${id}`,
|
||||
{
|
||||
status,
|
||||
},
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
this.mods.splice(index, 1)
|
||||
},
|
||||
async deleteReport(index) {
|
||||
await this.$axios.delete(
|
||||
`report/${this.reports[index].id}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
this.reports.splice(index, 1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button {
|
||||
margin: 0 5rem 0.5rem auto;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.report {
|
||||
@extend %card-spaced-b;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-lg);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.iconified-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,167 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="section-header columns">
|
||||
<h3 class="column-grow-1">My notifications</h3>
|
||||
</div>
|
||||
<div v-if="notifications.length !== 0">
|
||||
<div
|
||||
v-for="(notification, notificationIndex) in notifications"
|
||||
:key="notification.id"
|
||||
class="notification columns"
|
||||
>
|
||||
<div class="text">
|
||||
<nuxt-link :to="'/' + notification.link" class="top-wrapper">
|
||||
<h3 class="title">
|
||||
{{ notification.title }}
|
||||
</h3>
|
||||
<p
|
||||
v-tooltip="
|
||||
$dayjs(notification.created).format(
|
||||
'[Created at] YYYY-MM-DD [at] HH:mm A'
|
||||
)
|
||||
"
|
||||
class="date"
|
||||
>
|
||||
Notified {{ $dayjs(notification.created).fromNow() }}
|
||||
</p>
|
||||
</nuxt-link>
|
||||
<p class="description">
|
||||
{{ notification.text }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="notification.actions.length > 0" class="actions">
|
||||
<button
|
||||
v-for="(action, actionIndex) in notification.actions"
|
||||
:key="actionIndex"
|
||||
@click="performAction(notification, notificationIndex, actionIndex)"
|
||||
>
|
||||
{{ action.title }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="actions">
|
||||
<button @click="performAction(notification, notificationIndex, null)">
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<UpToDate class="icon"></UpToDate>
|
||||
<br />
|
||||
<span class="text">You are up-to-date!</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UpToDate,
|
||||
},
|
||||
async asyncData(data) {
|
||||
const notifications = (
|
||||
await data.$axios.get(
|
||||
`user/${data.$auth.user.id}/notifications`,
|
||||
data.$auth.headers
|
||||
)
|
||||
).data.sort((a, b) => new Date(b.created) - new Date(a.created))
|
||||
|
||||
return {
|
||||
notifications,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async performAction(notification, notificationIndex, actionIndex) {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
if (actionIndex !== null) {
|
||||
const config = {
|
||||
method: notification.actions[
|
||||
actionIndex
|
||||
].action_route[0].toLowerCase(),
|
||||
url: `${notification.actions[actionIndex].action_route[1]}`,
|
||||
headers: {
|
||||
Authorization: this.$auth.token,
|
||||
},
|
||||
}
|
||||
|
||||
await this.$axios(config)
|
||||
}
|
||||
|
||||
await this.$axios.delete(
|
||||
`notification/${notification.id}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
this.notifications.splice(notificationIndex, 1)
|
||||
this.$store.dispatch('user/fetchNotifications', { force: true })
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
},
|
||||
head: {
|
||||
title: 'Notifications - Modrinth',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notification {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-lg);
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.text {
|
||||
.top-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0 0.5rem 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="section-header columns">
|
||||
<h3 class="column-grow-1">My mods</h3>
|
||||
<nuxt-link class="brand-button column" to="/mod/create">
|
||||
Create a mod
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div v-if="mods.length !== 0">
|
||||
<ModCard
|
||||
v-for="mod in mods"
|
||||
:id="mod.slug ? mod.slug : mod.id"
|
||||
:key="mod.id"
|
||||
:author="mod.author"
|
||||
:name="mod.title"
|
||||
:description="mod.description"
|
||||
:latest-version="mod.latest_version"
|
||||
:created-at="mod.published"
|
||||
:updated-at="mod.updated"
|
||||
:downloads="mod.downloads.toString()"
|
||||
:icon-url="mod.icon_url"
|
||||
:author-url="mod.author_url"
|
||||
:page-url="mod.page_url"
|
||||
:categories="mod.categories"
|
||||
:edit-mode="true"
|
||||
:status="mod.status"
|
||||
:is-modrinth="true"
|
||||
>
|
||||
<nuxt-link
|
||||
class="button column edit-button"
|
||||
:to="'/mod/' + mod.id + '/settings'"
|
||||
>
|
||||
Settings
|
||||
</nuxt-link>
|
||||
</ModCard>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<UpToDate class="icon"></UpToDate><br />
|
||||
<span class="text"
|
||||
>You don't have any mods.<br />
|
||||
Would you like to
|
||||
<nuxt-link class="link" to="/mod/create">create one</nuxt-link>?</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModCard from '~/components/ui/ProjectCard'
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModCard,
|
||||
UpToDate,
|
||||
},
|
||||
async asyncData(data) {
|
||||
let res = await data.$axios.get(
|
||||
`user/${data.$auth.user.id}/mods`,
|
||||
data.$auth.headers
|
||||
)
|
||||
|
||||
res = await data.$axios.get(
|
||||
`mods?ids=${JSON.stringify(res.data)}`,
|
||||
data.$auth.headers
|
||||
)
|
||||
|
||||
return {
|
||||
mods: res.data,
|
||||
}
|
||||
},
|
||||
head: {
|
||||
title: 'My Mods - Modrinth',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mod-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
align-self: flex-end;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
// .buttonse {
|
||||
// margin-left: 4.5rem;
|
||||
// padding: 0.5rem 2rem 0.5rem 2rem;
|
||||
// }
|
||||
</style>
|
||||
@@ -1,242 +0,0 @@
|
||||
/* eslint-disable vue/attribute-hyphenation */
|
||||
<template>
|
||||
<div>
|
||||
<ConfirmPopup
|
||||
ref="delete_popup"
|
||||
title="Are you sure you want to delete your account?"
|
||||
description="If you proceed, your user and all attached data will be removed from our
|
||||
servers. This cannot be reversed, so be careful!"
|
||||
proceed-label="Delete account"
|
||||
:confirmation-text="username"
|
||||
:has-to-type="true"
|
||||
@proceed="deleteAccount"
|
||||
/>
|
||||
<div class="section-header columns">
|
||||
<h3 class="column-grow-1">Settings</h3>
|
||||
<button class="brand-button column" @click="editProfile">Save</button>
|
||||
</div>
|
||||
<section>
|
||||
<h3>Username</h3>
|
||||
<label>
|
||||
<span>
|
||||
The username used on Modrinth to identify yourself. This must be
|
||||
unique.
|
||||
</span>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
placeholder="Enter your username"
|
||||
/>
|
||||
</label>
|
||||
<h3>Name</h3>
|
||||
<label>
|
||||
<span>
|
||||
Your display name on your Modrinth profile. This does not have to be
|
||||
unique, can be set to anything, and is optional.
|
||||
</span>
|
||||
<input v-model="name" type="text" placeholder="Enter your name" />
|
||||
</label>
|
||||
<h3>Email</h3>
|
||||
<label>
|
||||
<span>
|
||||
The email for your account. This is private information which is not
|
||||
exposed in any API routes or on your profile. It is also optional.
|
||||
</span>
|
||||
<input v-model="email" type="email" placeholder="Enter your email" />
|
||||
</label>
|
||||
<h3>Bio</h3>
|
||||
<label>
|
||||
<span>
|
||||
A description of yourself which other users can see on your profile.
|
||||
</span>
|
||||
<input v-model="bio" type="text" placeholder="Enter your bio" />
|
||||
</label>
|
||||
</section>
|
||||
<section class="pad-maker">
|
||||
<h3>Theme</h3>
|
||||
<label>
|
||||
<span
|
||||
>Change the global site theme of Modrinth. You can choose between
|
||||
light mode and dark mode. You can switch it using this button or
|
||||
anywhere by accessing the theme switcher in the navigation bar
|
||||
dropdown.</span
|
||||
>
|
||||
<input
|
||||
type="button"
|
||||
class="button pad-rem"
|
||||
value="Change theme"
|
||||
@click="changeTheme"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section class="pad-maker">
|
||||
<h3>Authorization token</h3>
|
||||
<label>
|
||||
<span>
|
||||
Your authorization token can be used with the Modrinth API, the
|
||||
Minotaur Gradle plugin, and other applications that interact with
|
||||
Modrinth's API. Be sure to keep this secret!
|
||||
</span>
|
||||
<input
|
||||
type="button"
|
||||
class="button pad-rem"
|
||||
value="Copy to clipboard"
|
||||
@click="copyToken"
|
||||
/>
|
||||
</label>
|
||||
<h3>Revoke your token</h3>
|
||||
<label>
|
||||
<span
|
||||
>This will log you out of Modrinth, and you will have to log in again
|
||||
to access Modrinth with a new token.</span
|
||||
>
|
||||
<input
|
||||
type="button"
|
||||
class="button"
|
||||
value="Revoke token"
|
||||
@click="gotoRevoke"
|
||||
/>
|
||||
</label>
|
||||
<h3>Delete your account</h3>
|
||||
<label>
|
||||
<span
|
||||
>Clicking on this WILL delete your account. Do not click on this
|
||||
unless you want your account deleted. If you delete your account, all
|
||||
attached data, including projects, will be removed from our servers.
|
||||
This cannot be reversed, so be careful!</span
|
||||
>
|
||||
<input
|
||||
value="Delete Account"
|
||||
type="button"
|
||||
class="button"
|
||||
@click="showPopup"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmPopup from '~/components/ui/ConfirmPopup'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmPopup,
|
||||
},
|
||||
fetch() {
|
||||
this.username = this.$auth.user.username
|
||||
this.name = this.$auth.user.name
|
||||
this.email = this.$auth.user.email
|
||||
this.bio = this.$auth.user.bio
|
||||
this.token = this.$auth.token
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
name: '',
|
||||
email: '',
|
||||
bio: '',
|
||||
token: '',
|
||||
confirm_delete: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeTheme() {
|
||||
this.$colorMode.preference =
|
||||
this.$colorMode.value === 'dark' ? 'light' : 'dark'
|
||||
},
|
||||
gotoRevoke() {
|
||||
this.$router.replace('/dashboard/misc/revoke-token')
|
||||
},
|
||||
async copyToken() {
|
||||
await navigator.clipboard.writeText(this.token)
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Copied to clipboard.',
|
||||
text: 'Copied your Modrinth token to the clipboard.',
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
async editProfile() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
const data = {
|
||||
username: this.username,
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
bio: this.bio,
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}`,
|
||||
data,
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
})
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async deleteAccount() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`user/${this.$auth.user.id}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
showPopup() {
|
||||
this.$refs.delete_popup.show()
|
||||
},
|
||||
},
|
||||
head: {
|
||||
title: 'Settings - Modrinth',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pad-rem {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pad-maker {
|
||||
margin-top: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.save-btn-div {
|
||||
overflow: hidden;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
float: right;
|
||||
}
|
||||
|
||||
section {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
}
|
||||
</style>
|
||||
498
pages/index.vue
498
pages/index.vue
@@ -1,451 +1,109 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="main-hero columns">
|
||||
<div class="left">
|
||||
<h1 class="typewriter">
|
||||
{{ currentText }}<span aria-hidden="true"></span>
|
||||
</h1>
|
||||
<h1>modding platform</h1>
|
||||
</div>
|
||||
<div class="right columns">
|
||||
<img class="char" src="~/assets/images/logo.svg" alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="slanted-hero columns">
|
||||
<div class="left hero">
|
||||
<h3>Conveniently modern</h3>
|
||||
<h1>A redefined search interface</h1>
|
||||
<p>
|
||||
We've implemented <span>fast and adaptable</span> search algorithms so
|
||||
you don't have to wait, while creating a responsive interface that
|
||||
makes sense. Modrinth is full of elegant mod discovery and a platform
|
||||
which just works.
|
||||
</p>
|
||||
</div>
|
||||
<div class="right hero-image">
|
||||
<video loading="lazy" loop muted autoplay>
|
||||
<source src="~/assets/images/search.webm" />
|
||||
<source src="~/assets/images/search.mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
<div class="default-hero columns">
|
||||
<div class="left hero">
|
||||
<h3>Built for developers</h3>
|
||||
<h1>The world's most modder-friendly platform</h1>
|
||||
<p>
|
||||
Modrinth intends to give back to the community, not leech from it.
|
||||
That's why we plan to give creators <span>one hundred percent</span>
|
||||
of the ad revenue earned on their project pages back to them, while
|
||||
creating easy to use tools for every modder to publish their mods on
|
||||
the Modrinth platform.
|
||||
</p>
|
||||
<p>
|
||||
<span>Note: This is currently not implemented.</span> There is no ETA
|
||||
for when it will be.
|
||||
</p>
|
||||
</div>
|
||||
<div class="right columns workflow">
|
||||
<div>
|
||||
<h3>Code</h3>
|
||||
<svg
|
||||
style="background-color: #1a202c"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="16 18 22 12 16 6"></polyline>
|
||||
<polyline points="8 6 2 12 8 18"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="char" alt="logo"><RightArrowIcon /></span>
|
||||
<div>
|
||||
<h3>Build</h3>
|
||||
<svg
|
||||
style="background-color: #127183"
|
||||
viewBox="0 0 256 188"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M242.08678,11.8947553 C226.620445,-3.57845046 201.673638,-4.00992041 185.681477,10.9191919 C184.914067,11.6431863 184.467166,12.6433945 184.439851,13.6980695 C184.445493,14.7482934 184.858879,15.7552589 185.592789,16.5065097 L190.647982,21.739077 C192.011075,23.0964262 194.163868,23.2474994 195.703174,22.0938274 C199.713048,19.0675791 204.603476,17.4374363 209.627124,17.4525105 C219.052991,17.4112768 227.56987,23.0684934 231.186492,31.7730153 C234.803114,40.4775372 232.802822,50.5045093 226.123015,57.1549851 C193.89986,89.3781401 150.886382,-0.905819089 53.270916,45.5369117 C49.9136349,47.1062801 47.3653193,50.0077326 46.2423493,53.5394732 C45.1193793,57.0712139 45.5240245,60.9116046 47.3584105,64.1317416 L64.0908011,93.0734561 C67.7236263,99.3391772 75.708598,101.536031 82.0352554,98.0103982 L82.4491308,97.7738979 L82.1239429,98.0103982 L89.5441374,93.8420818 C97.8137349,88.7071757 105.627223,82.8717856 112.898534,76.4001905 C114.43778,75.0798243 116.710106,75.0798243 118.249352,76.4001905 L118.249352,76.4001905 C119.158022,77.1129332 119.691473,78.2016086 119.697915,79.3564433 C119.719135,80.4788344 119.255955,81.5559976 118.426727,82.312696 C110.76814,89.1108483 102.528036,95.2241548 93.8011413,100.582338 L93.5646411,100.582338 L86.1444467,104.721092 C83.0080654,106.498631 79.4617275,107.425978 75.8566871,107.411282 C68.2794178,107.411872 61.2698464,103.395151 57.4392324,96.8574596 L41.6232802,69.5121216 C11.2330018,90.9745166 -7.21401537,132.332493 2.68943136,184.805979 C3.05238462,186.613115 4.630317,187.91984 6.47343489,187.939608 L24.5065767,187.939608 C26.4460384,187.939608 28.0814398,186.494195 28.3201428,184.569479 C29.9827135,171.36939 41.2081637,161.469113 54.5125422,161.469113 C67.8169207,161.469113 79.0423709,171.36939 80.7049416,184.569479 C80.9574458,186.498798 82.6022985,187.939608 84.5480702,187.939608 L102.108212,187.939608 C104.047673,187.939608 105.683075,186.494195 105.921778,184.569479 C107.637793,171.405162 118.853268,161.556778 132.128958,161.556778 C145.404648,161.556778 156.620124,171.405162 158.336139,184.569479 C158.574842,186.494195 160.210243,187.939608 162.149705,187.939608 L179.502909,187.939608 C181.602533,187.939608 183.313735,186.254979 183.346037,184.155604 C183.759913,159.707393 190.352356,131.622992 209.154124,117.551229 C274.280372,68.8321835 257.163669,27.060332 242.08678,11.8947553 Z M175.65978,85.5941366 L163.243464,79.3564433 L163.243464,79.3564433 C163.229874,75.6716754 165.795264,72.4793993 169.39663,71.69974 C172.997995,70.9200808 176.653993,72.7654893 178.165574,76.1259689 C179.677154,79.4864485 178.632425,83.4462961 175.65978,85.6236991 L175.65978,85.5941366 Z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
<svg
|
||||
style="background-color: #6a4fba"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
style="background-color: #e14329"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="char" alt="logo"><RightArrowIcon /></span>
|
||||
<div>
|
||||
<h3>Publish</h3>
|
||||
<svg
|
||||
style="background-color: #4d9227"
|
||||
viewBox="0 0 512 514"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M503.16 323.56C514.55 281.47 515.32 235.91 503.2 190.76C466.57 54.2299 326.04 -26.8001 189.33 9.77991C83.8101 38.0199 11.3899 128.07 0.689941 230.47H43.99C54.29 147.33 113.74 74.7298 199.75 51.7098C306.05 23.2598 415.13 80.6699 453.17 181.38L411.03 192.65C391.64 145.8 352.57 111.45 306.3 96.8198L298.56 140.66C335.09 154.13 364.72 184.5 375.56 224.91C391.36 283.8 361.94 344.14 308.56 369.17L320.09 412.16C390.25 383.21 432.4 310.3 422.43 235.14L464.41 223.91C468.91 252.62 467.35 281.16 460.55 308.07L503.16 323.56Z"
|
||||
fill="#fff"
|
||||
<div class="page">
|
||||
<div class="cover">
|
||||
<img
|
||||
class="cover-image"
|
||||
src="~/assets/images/landing.svg"
|
||||
width="100%"
|
||||
alt="cover-image"
|
||||
/>
|
||||
<div class="text">
|
||||
<h1>Discover, Play, and Create Minecraft content</h1>
|
||||
<h3>
|
||||
Find enjoyable and quality content through our open-source modding
|
||||
platform built for the community. Create stuff, get paid*, and deploy
|
||||
your project with our fully documented API!
|
||||
</h3>
|
||||
<form action="/mods">
|
||||
<div class="iconified-input">
|
||||
<label class="hidden" for="q">Search Mods</label>
|
||||
<SearchIcon />
|
||||
<input
|
||||
id="q"
|
||||
type="search"
|
||||
name="q"
|
||||
placeholder="Search mods..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<path
|
||||
d="M321.99 504.22C185.27 540.8 44.7501 459.77 8.11011 323.24C3.84011 307.31 1.17 291.33 0 275.46H43.27C44.36 287.37 46.4699 299.35 49.6799 311.29C53.0399 323.8 57.45 335.75 62.79 347.07L101.38 323.92C98.1299 316.42 95.39 308.6 93.21 300.47C69.17 210.87 122.41 118.77 212.13 94.7601C229.13 90.2101 246.23 88.4401 262.93 89.1501L255.19 133C244.73 133.05 234.11 134.42 223.53 137.25C157.31 154.98 118.01 222.95 135.75 289.09C136.85 293.16 138.13 297.13 139.59 300.99L188.94 271.38L174.07 231.95L220.67 184.08L279.57 171.39L296.62 192.38L269.47 219.88L245.79 227.33L228.87 244.72L237.16 267.79C237.16 267.79 253.95 285.63 253.98 285.64L277.7 279.33L294.58 260.79L331.44 249.12L342.42 273.82L304.39 320.45L240.66 340.63L212.08 308.81L162.26 338.7C187.8 367.78 226.2 383.93 266.01 380.56L277.54 423.55C218.13 431.41 160.1 406.82 124.05 361.64L85.6399 384.68C136.25 451.17 223.84 484.11 309.61 461.16C371.35 444.64 419.4 402.56 445.42 349.38L488.06 364.88C457.17 431.16 398.22 483.82 321.99 504.22Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="char" alt="logo"><RightArrowIcon /></span>
|
||||
<div>
|
||||
<h3>Earn</h3>
|
||||
<svg
|
||||
style="background-color: #f3b433"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="12" y1="1" x2="12" y2="23"></line>
|
||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<button class="iconified-button brand-button-colors" type="submit">
|
||||
<RightArrowIcon />
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slanted-hero columns">
|
||||
<div class="left hero">
|
||||
<h3>Easy to implement</h3>
|
||||
<h1>Backed by an open source API</h1>
|
||||
<p>
|
||||
Modrinth's code is fully open source, licensed under the GNU AGPL.
|
||||
We've created a high-performance Rust-based backend that is
|
||||
<span
|
||||
><a href="https://github.com/modrinth/labrinth/wiki"
|
||||
>fully documented</a
|
||||
></span
|
||||
>
|
||||
for all kinds of tools to use. Our team is dedicated to maintaining an
|
||||
open source ecosystem for all Modrinth applications.
|
||||
</p>
|
||||
</div>
|
||||
<div class="right hero-image less-margin">
|
||||
<pre v-highlightjs>
|
||||
<code class="javascript example-code">const fetch = require('node-fetch');
|
||||
fetch('https://api.modrinth.com/api/v1/mod').then(res => res.json()).then(data => {
|
||||
console.log(data);
|
||||
// hits: [Object {author: "mezz", author_url: …, …}, …]
|
||||
// limit: 10
|
||||
// offset: 0
|
||||
/// total_hits: 19440
|
||||
});</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<m-footer class="footer" centered padded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MFooter from '~/components/layout/MFooter'
|
||||
import RightArrowIcon from '~/assets/images/right-arrow.svg?inline'
|
||||
import SearchIcon from '~/assets/images/utils/search.svg?inline'
|
||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MFooter,
|
||||
SearchIcon,
|
||||
RightArrowIcon,
|
||||
},
|
||||
auth: false,
|
||||
data() {
|
||||
return {
|
||||
currentText: 'Open source',
|
||||
increasing: true,
|
||||
texts: ['Open source', 'Easy to use', 'Developer focused', 'API Based'],
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
|
||||
this.startNext(0)
|
||||
},
|
||||
methods: {
|
||||
startNext(i) {
|
||||
const startIndex = i % this.texts.length
|
||||
|
||||
this.typeWriter(this.texts[startIndex], 0, () => {
|
||||
this.startNext(startIndex + 1)
|
||||
})
|
||||
},
|
||||
typeWriter(text, i, callback) {
|
||||
if (!this.increasing && i <= 0) {
|
||||
this.increasing = true
|
||||
setTimeout(callback, 300 + Math.random() * 50)
|
||||
return
|
||||
}
|
||||
|
||||
const step = this.increasing ? 1 : -1
|
||||
|
||||
if (i >= text.length && this.increasing) {
|
||||
this.increasing = false
|
||||
|
||||
setTimeout(
|
||||
() => this.typeWriter(text, i + step, callback),
|
||||
1300 + Math.random() * 500
|
||||
)
|
||||
} else {
|
||||
this.currentText = text.substring(0, i + step)
|
||||
const speed = this.increasing ? 140 : 50
|
||||
setTimeout(
|
||||
() => this.typeWriter(text, i + step, callback),
|
||||
speed + Math.random() * 20
|
||||
)
|
||||
}
|
||||
},
|
||||
return {}
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.left,
|
||||
.right {
|
||||
width: 50%;
|
||||
}
|
||||
.page {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
|
||||
.main-hero {
|
||||
margin-top: 100px;
|
||||
height: 600px;
|
||||
|
||||
.left {
|
||||
padding-top: 75px;
|
||||
padding-left: 100px;
|
||||
@media screen and (min-width: 1500px) {
|
||||
padding-left: 15%;
|
||||
.cover {
|
||||
img {
|
||||
border-radius: var(--size-rounded-lg);
|
||||
width: 100%;
|
||||
height: calc(75vh - var(--size-navbar-height));
|
||||
object-fit: cover;
|
||||
object-position: 10% 12.5%;
|
||||
}
|
||||
|
||||
.typewriter {
|
||||
display: inline-block;
|
||||
color: var(--color-brand);
|
||||
.text {
|
||||
position: absolute;
|
||||
top: calc(10vh + var(--size-navbar-height));
|
||||
width: 30rem;
|
||||
//max-width: 25%;
|
||||
padding-left: 6rem;
|
||||
|
||||
span {
|
||||
border-right: 0.15em solid var(--color-brand);
|
||||
animation: caret 1s steps(1) infinite;
|
||||
color: #fff;
|
||||
|
||||
@keyframes caret {
|
||||
50% {
|
||||
border-color: transparent;
|
||||
}
|
||||
h2 {
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #fff;
|
||||
margin: 1rem 0;
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #fff;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: normal;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.iconified-button {
|
||||
margin-left: 0.25rem;
|
||||
padding: 1.25rem 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 4em;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
padding-left: 20%;
|
||||
|
||||
.char {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-left: 15%;
|
||||
width: 30%;
|
||||
z-index: 1;
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: bolder;
|
||||
color: var(--color-brand);
|
||||
}
|
||||
h1 {
|
||||
font-size: 46px;
|
||||
font-weight: 520;
|
||||
}
|
||||
p {
|
||||
line-height: 25px;
|
||||
letter-spacing: 0.2px;
|
||||
color: var(--color-text);
|
||||
|
||||
span {
|
||||
color: var(--color-brand);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
&.left {
|
||||
@media screen and (min-width: 2048px) {
|
||||
max-width: 15vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
padding-left: 10%;
|
||||
padding-top: 75px;
|
||||
padding-right: 10%;
|
||||
|
||||
img,
|
||||
video {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.slanted-hero {
|
||||
background: var(--color-raised-bg);
|
||||
height: 500px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
background: inherit;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: -1;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
&:before {
|
||||
top: 0;
|
||||
transform: skewY(5deg);
|
||||
@media screen and (min-width: 2048px) {
|
||||
transform: skewY(2deg);
|
||||
}
|
||||
transform-origin: 100% 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
bottom: 0;
|
||||
transform: skewY(-5deg);
|
||||
@media screen and (min-width: 2048px) {
|
||||
transform: skewY(-2deg);
|
||||
}
|
||||
transform-origin: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.default-hero {
|
||||
margin-top: 200px;
|
||||
height: 700px;
|
||||
}
|
||||
|
||||
.workflow {
|
||||
padding-top: 75px;
|
||||
padding-left: 10%;
|
||||
div {
|
||||
margin: 0 20px;
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
svg {
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
margin-top: 10px;
|
||||
padding: 25px;
|
||||
}
|
||||
}
|
||||
img {
|
||||
color: var(--color-text);
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
border-radius: var(--size-rounded-lg);
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #171719;
|
||||
border-radius: var(--size-rounded-lg);
|
||||
overflow-x: auto;
|
||||
|
||||
.example-code {
|
||||
color: #cecece;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 150px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.hero {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1300px) {
|
||||
.workflow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.left {
|
||||
padding-left: 0 !important;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
h1,
|
||||
h3,
|
||||
p {
|
||||
padding: 0 5vw;
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-top: 100px;
|
||||
text-align: center;
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1300px) and (max-width: 1500px) {
|
||||
.hero {
|
||||
margin-left: 10%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>Privacy Policy</h1>
|
||||
|
||||
<p>
|
||||
@@ -177,17 +177,11 @@
|
||||
to its Terms and Conditions.
|
||||
</p>
|
||||
</div>
|
||||
<m-footer class="footer" centered />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MFooter from '~/components/layout/MFooter'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MFooter,
|
||||
},
|
||||
auth: false,
|
||||
head: {
|
||||
title: 'Privacy - Modrinth',
|
||||
@@ -224,11 +218,6 @@ export default {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.container {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--color-link);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>Project Rules</h1>
|
||||
|
||||
<p>
|
||||
@@ -94,17 +94,11 @@
|
||||
the Modrinth moderators' discretion.
|
||||
</p>
|
||||
</div>
|
||||
<m-footer class="footer" centered />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MFooter from '~/components/layout/MFooter'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MFooter,
|
||||
},
|
||||
auth: false,
|
||||
head: {
|
||||
title: 'Rules - Modrinth',
|
||||
@@ -141,11 +135,6 @@ export default {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.container {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--color-link);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>Terms and Conditions</h1>
|
||||
|
||||
<h2>1. Terms</h2>
|
||||
@@ -157,17 +157,11 @@
|
||||
<nuxt-link to="/legal/rules">Project Rules</nuxt-link>.
|
||||
</p>
|
||||
</div>
|
||||
<m-footer class="footer" centered />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MFooter from '~/components/layout/MFooter'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MFooter,
|
||||
},
|
||||
auth: false,
|
||||
head: {
|
||||
title: 'Terms - Modrinth',
|
||||
@@ -204,11 +198,6 @@ export default {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.container {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--color-link);
|
||||
|
||||
@@ -1,833 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<div class="icon">
|
||||
<nuxt-link :to="'/mod/' + (mod.slug ? mod.slug : mod.id)">
|
||||
<img
|
||||
:src="
|
||||
mod.icon_url
|
||||
? mod.icon_url
|
||||
: 'https://cdn.modrinth.com/placeholder.svg?inline'
|
||||
"
|
||||
alt="mod - icon"
|
||||
/></nuxt-link>
|
||||
</div>
|
||||
<div class="info">
|
||||
<nuxt-link :to="'/mod/' + (mod.slug ? mod.slug : mod.id)">
|
||||
<h1 class="title">{{ mod.title }}</h1>
|
||||
</nuxt-link>
|
||||
<p class="description">
|
||||
{{ mod.description }}
|
||||
</p>
|
||||
<div class="alt-nav">
|
||||
<p>
|
||||
<nuxt-link to="/mods"> Mods </nuxt-link>
|
||||
>
|
||||
<nuxt-link :to="'/mod/' + (mod.slug ? mod.slug : mod.id)">{{
|
||||
mod.title
|
||||
}}</nuxt-link>
|
||||
<span v-if="linkBar.length > 0"> ></span>
|
||||
<nuxt-link
|
||||
v-for="(link, index) in linkBar"
|
||||
:key="index"
|
||||
:to="/mod/ + (mod.slug ? mod.slug : mod.id) + '/' + link[1]"
|
||||
>{{ link[0] }}
|
||||
<span v-if="index !== linkBar.length - 1"> > </span>
|
||||
</nuxt-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<nuxt-link
|
||||
v-if="this.$auth.user"
|
||||
:to="`/report/create?id=${mod.id}&t=mod`"
|
||||
class="iconified-button"
|
||||
>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="userFollows && !userFollows.includes(mod.id)"
|
||||
class="iconified-button"
|
||||
@click="followMod"
|
||||
>
|
||||
<FollowIcon />
|
||||
Follow
|
||||
</button>
|
||||
<button
|
||||
v-if="userFollows && userFollows.includes(mod.id)"
|
||||
class="iconified-button"
|
||||
@click="unfollowMod"
|
||||
>
|
||||
<FollowIcon fill="currentColor" />
|
||||
Unfollow
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Advertisement
|
||||
v-if="mod.status === 'approved' || mod.status === 'unlisted'"
|
||||
type="banner"
|
||||
small-screen="square"
|
||||
ethical-ads-small
|
||||
ethical-ads-big
|
||||
/>
|
||||
<div class="mod-navigation">
|
||||
<div class="tabs">
|
||||
<nuxt-link
|
||||
:to="'/mod/' + (mod.slug ? mod.slug : mod.id)"
|
||||
class="tab"
|
||||
>
|
||||
<span>Description</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
:to="'/mod/' + (mod.slug ? mod.slug : mod.id) + '/versions'"
|
||||
class="tab"
|
||||
>
|
||||
<span>Versions</span>
|
||||
</nuxt-link>
|
||||
<a
|
||||
v-if="mod.wiki_url"
|
||||
:href="mod.wiki_url"
|
||||
target="_blank"
|
||||
class="tab"
|
||||
>
|
||||
<ExternalIcon />
|
||||
<span>Wiki</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="mod.issues_url"
|
||||
:href="mod.issues_url"
|
||||
target="_blank"
|
||||
class="tab"
|
||||
>
|
||||
<ExternalIcon />
|
||||
<span>Issues</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="mod.source_url"
|
||||
:href="mod.source_url"
|
||||
target="_blank"
|
||||
class="tab"
|
||||
>
|
||||
<ExternalIcon />
|
||||
<span>Source</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="mod.discord_url"
|
||||
:href="mod.discord_url"
|
||||
target="_blank"
|
||||
class="tab"
|
||||
>
|
||||
<ExternalIcon />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
<nuxt-link
|
||||
v-if="currentMember"
|
||||
:to="'/mod/' + mod.id + '/settings'"
|
||||
class="tab settings-tab"
|
||||
>
|
||||
<SettingsIcon />
|
||||
<span>Settings</span>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mod-content">
|
||||
<NuxtChild
|
||||
:mod="mod"
|
||||
:versions="versions"
|
||||
:featured-versions="featuredVersions"
|
||||
:members="members"
|
||||
:current-member="currentMember"
|
||||
:all-members="allMembers"
|
||||
:link-bar.sync="linkBar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<section class="mod-info">
|
||||
<div class="mod-stats section">
|
||||
<div class="stat">
|
||||
<DownloadIcon />
|
||||
<div class="info">
|
||||
<h4>Downloads</h4>
|
||||
<p class="value">{{ formatNumber(mod.downloads) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<CalendarIcon />
|
||||
<div class="info">
|
||||
<h4>Created</h4>
|
||||
<p
|
||||
v-tooltip="
|
||||
$dayjs(mod.published).format(
|
||||
'[Created on] YYYY-MM-DD [at] HH:mm A'
|
||||
)
|
||||
"
|
||||
class="value"
|
||||
>
|
||||
{{ formatTime(mod.published) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<TagIcon />
|
||||
<div class="info">
|
||||
<h4>Available For</h4>
|
||||
<p class="value">
|
||||
{{
|
||||
versions[0]
|
||||
? versions[0].game_versions[0]
|
||||
? versions[0].game_versions[
|
||||
versions[0].game_versions.length - 1
|
||||
]
|
||||
: 'None'
|
||||
: 'None'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<EditIcon />
|
||||
<div class="info">
|
||||
<h4>Updated</h4>
|
||||
<p
|
||||
v-tooltip="
|
||||
$dayjs(mod.updated).format(
|
||||
'[Updated on] YYYY-MM-DD [at] HH:mm A'
|
||||
)
|
||||
"
|
||||
class="value"
|
||||
>
|
||||
{{ formatTime(mod.updated) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<ClientIcon />
|
||||
<div class="info">
|
||||
<h4>Client Side</h4>
|
||||
<p class="value capitalize">{{ mod.client_side }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<ServerIcon />
|
||||
<div class="info">
|
||||
<h4>Server Side</h4>
|
||||
<p class="value capitalize">{{ mod.server_side }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<FileTextIcon />
|
||||
<div class="info">
|
||||
<h4>License</h4>
|
||||
<p v-tooltip="mod.license.name" class="value ellipsis">
|
||||
<a :href="mod.license.url || null">
|
||||
{{ mod.license.id.toUpperCase() }}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<CodeIcon />
|
||||
<div class="info">
|
||||
<h4>Project ID</h4>
|
||||
<p class="value">{{ mod.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Categories :categories="mod.categories.concat(mod.loaders)" />
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>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_id">
|
||||
<h4>{{ member.name }}</h4>
|
||||
</nuxt-link>
|
||||
<h3>{{ member.role }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="featuredVersions.length > 0" class="section">
|
||||
<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">
|
||||
<div class="top">
|
||||
<span
|
||||
v-if="version.version_type === 'release'"
|
||||
class="badge green"
|
||||
>
|
||||
Release
|
||||
</span>
|
||||
<span
|
||||
v-if="version.version_type === 'beta'"
|
||||
class="badge yellow"
|
||||
>
|
||||
Beta
|
||||
</span>
|
||||
<span v-if="version.version_type === 'alpha'" class="badge red">
|
||||
Alpha
|
||||
</span>
|
||||
<h4 class="title">
|
||||
<nuxt-link
|
||||
:to="
|
||||
'/mod/' +
|
||||
(mod.slug ? mod.slug : mod.id) +
|
||||
'/version/' +
|
||||
version.id
|
||||
"
|
||||
>
|
||||
{{ version.name }}
|
||||
</nuxt-link>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<span class="version-number limit-text-width">
|
||||
{{ version.version_number }} ·
|
||||
</span>
|
||||
<FabricIcon
|
||||
v-if="version.loaders.includes('fabric')"
|
||||
class="loader"
|
||||
/>
|
||||
<ForgeIcon
|
||||
v-if="version.loaders.includes('forge')"
|
||||
class="loader"
|
||||
/>
|
||||
<span
|
||||
v-if="version.game_versions.length > 0"
|
||||
class="game-version limit-text-width"
|
||||
>
|
||||
·
|
||||
{{ version.game_versions[version.game_versions.length - 1] }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="mod.donation_urls && mod.donation_urls.length > 0"
|
||||
class="section"
|
||||
>
|
||||
<h3>Donation Links</h3>
|
||||
<div
|
||||
v-for="(item, index) in mod.donation_urls"
|
||||
:key="index"
|
||||
class="links"
|
||||
>
|
||||
<a :href="item.url" class="link">
|
||||
<ExternalIcon />
|
||||
{{ item.platform }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Advertisement
|
||||
v-if="mod.status === 'approved' || mod.status === 'unlisted'"
|
||||
type="square"
|
||||
small-screen="destroy"
|
||||
/>
|
||||
<m-footer class="footer" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Categories from '~/components/ui/search/Categories'
|
||||
import MFooter from '~/components/layout/MFooter'
|
||||
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import TagIcon from '~/assets/images/utils/tag.svg?inline'
|
||||
import ClientIcon from '~/assets/images/utils/client.svg?inline'
|
||||
import ServerIcon from '~/assets/images/utils/server.svg?inline'
|
||||
import FileTextIcon from '~/assets/images/utils/file-text.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 ExternalIcon from '~/assets/images/utils/external.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
|
||||
import ForgeIcon from '~/assets/images/categories/forge.svg?inline'
|
||||
import FabricIcon from '~/assets/images/categories/fabric.svg?inline'
|
||||
import Advertisement from '~/components/ads/Advertisement'
|
||||
|
||||
export default {
|
||||
name: 'ModPage',
|
||||
components: {
|
||||
Advertisement,
|
||||
MFooter,
|
||||
Categories,
|
||||
ExternalIcon,
|
||||
SettingsIcon,
|
||||
ForgeIcon,
|
||||
FabricIcon,
|
||||
DownloadIcon,
|
||||
CalendarIcon,
|
||||
EditIcon,
|
||||
TagIcon,
|
||||
ClientIcon,
|
||||
ServerIcon,
|
||||
FileTextIcon,
|
||||
CodeIcon,
|
||||
ReportIcon,
|
||||
FollowIcon,
|
||||
},
|
||||
async asyncData(data) {
|
||||
try {
|
||||
const mod = (
|
||||
await data.$axios.get(`mod/${data.params.id}`, data.$auth.headers)
|
||||
).data
|
||||
|
||||
const [members, versions, featuredVersions, userFollows] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`team/${mod.team}/members`, data.$auth.headers),
|
||||
data.$axios.get(`mod/${mod.id}/version`),
|
||||
data.$axios.get(`mod/${mod.id}/version?featured=true`),
|
||||
data.$axios.get(
|
||||
data.$auth.user
|
||||
? `user/${data.$auth.user.id}/follows`
|
||||
: `https://api.modrinth.com`,
|
||||
data.$auth.headers
|
||||
),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
const users = (
|
||||
await data.$axios.get(
|
||||
`users?ids=${JSON.stringify(members.map((it) => it.user_id))}`,
|
||||
data.$auth.headers
|
||||
)
|
||||
).data
|
||||
|
||||
users.forEach((it) => {
|
||||
const index = members.findIndex((x) => x.user_id === it.id)
|
||||
members[index].avatar_url = it.avatar_url
|
||||
members[index].name = it.username
|
||||
})
|
||||
|
||||
const currentMember = data.$auth.user
|
||||
? members.find((x) => x.user_id === data.$auth.user.id)
|
||||
: null
|
||||
|
||||
if (mod.body_url && !mod.body) {
|
||||
mod.body = (await data.$axios.get(mod.body_url)).data
|
||||
}
|
||||
|
||||
return {
|
||||
mod,
|
||||
versions,
|
||||
featuredVersions,
|
||||
members: members.filter((x) => x.accepted),
|
||||
allMembers: members,
|
||||
currentMember,
|
||||
userFollows: userFollows.name ? null : userFollows,
|
||||
linkBar: [],
|
||||
}
|
||||
} catch {
|
||||
data.error({
|
||||
statusCode: 404,
|
||||
message: 'Mod not found',
|
||||
})
|
||||
}
|
||||
},
|
||||
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: `/mod/${this.mod.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 followMod() {
|
||||
await this.$axios.post(
|
||||
`mod/${this.mod.id}/follow`,
|
||||
{},
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
this.userFollows.push(this.mod.id)
|
||||
},
|
||||
async unfollowMod() {
|
||||
await this.$axios.delete(`mod/${this.mod.id}/follow`, this.$auth.headers)
|
||||
|
||||
this.userFollows.splice(this.userFollows.indexOf(this.mod.id), 1)
|
||||
},
|
||||
formatTime(date) {
|
||||
let defaultMessage = this.$dayjs(date).fromNow()
|
||||
if (defaultMessage.length > 13) {
|
||||
defaultMessage = defaultMessage.replace('minutes', 'min.')
|
||||
}
|
||||
return defaultMessage
|
||||
},
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.mod.title + ' - Modrinth',
|
||||
meta: [
|
||||
{
|
||||
hid: 'og:type',
|
||||
name: 'og:type',
|
||||
content: 'website',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: this.mod.title,
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: this.mod.title,
|
||||
},
|
||||
{
|
||||
hid: 'og:description',
|
||||
name: 'og:description',
|
||||
content: this.mod.description,
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: `${this.mod.title}: ${this.mod.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/mod/${this.mod.id}`,
|
||||
},
|
||||
{
|
||||
hid: 'og:image',
|
||||
name: 'og:image',
|
||||
content: this.mod.icon_url
|
||||
? this.mod.icon_url
|
||||
: 'https://cdn.modrinth.com/placeholder.png',
|
||||
},
|
||||
{
|
||||
hid: 'robots',
|
||||
name: 'robots',
|
||||
content: this.mod.status !== 'approved' ? 'noindex' : 'all',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
|
||||
@extend %card-spaced-b;
|
||||
.icon {
|
||||
margin: unset 0;
|
||||
height: 6.08rem;
|
||||
@media screen and (min-width: 1024px) {
|
||||
align-self: flex-start;
|
||||
}
|
||||
img {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
border-radius: var(--size-rounded-icon);
|
||||
}
|
||||
}
|
||||
.info {
|
||||
@extend %column;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
height: calc(100% - 0.2rem);
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
.description {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
.alt-nav {
|
||||
margin-top: auto;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttons {
|
||||
@extend %row;
|
||||
|
||||
button,
|
||||
a {
|
||||
margin: 0;
|
||||
padding: 0.2rem 0.5rem;
|
||||
display: flex;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
@media screen and (min-width: 1024px) {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-bottom: 1rem;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
padding: 1rem;
|
||||
grid-gap: 1rem;
|
||||
text-align: left;
|
||||
|
||||
.buttons {
|
||||
align-self: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mod-navigation {
|
||||
@extend %card-spaced-b;
|
||||
padding: 0 1rem;
|
||||
|
||||
.tabs {
|
||||
overflow-x: auto;
|
||||
padding: 0;
|
||||
|
||||
.tab {
|
||||
padding: 0;
|
||||
margin: 0.9rem 0.5rem 0.8rem 0.5rem;
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-tab {
|
||||
@media screen and (min-width: 1024px) {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mod-info {
|
||||
height: auto;
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
width: 30rem;
|
||||
margin-left: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: var(--spacing-card-sm);
|
||||
@extend %card-spaced-b;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@extend %large-label;
|
||||
}
|
||||
|
||||
.mod-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0;
|
||||
p {
|
||||
max-width: 6rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: '';
|
||||
margin: 3px;
|
||||
}
|
||||
.stat {
|
||||
width: 8.5rem;
|
||||
margin: 0.75rem;
|
||||
@extend %stat;
|
||||
|
||||
svg {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.team-member {
|
||||
margin-left: 5px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
img {
|
||||
border-radius: var(--size-rounded-icon);
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
.member-info {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
margin: auto 0 auto 0.5rem;
|
||||
h4 {
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
h3 {
|
||||
margin-top: 0.1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.featured-version {
|
||||
@extend %row;
|
||||
padding-top: var(--spacing-card-sm);
|
||||
padding-bottom: var(--spacing-card-sm);
|
||||
.download {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
width: 2.25rem;
|
||||
border-radius: 2rem;
|
||||
background-color: var(--color-button-bg);
|
||||
margin-right: var(--spacing-card-sm);
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
}
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
margin: auto;
|
||||
}
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.info {
|
||||
@extend %column;
|
||||
font-size: var(--font-size-xs);
|
||||
.top {
|
||||
@extend %row;
|
||||
.badge {
|
||||
font-size: var(--font-size-xs);
|
||||
margin-right: var(--spacing-card-sm);
|
||||
}
|
||||
.title {
|
||||
margin: auto 0;
|
||||
}
|
||||
}
|
||||
.bottom {
|
||||
margin-top: 0.25rem;
|
||||
@extend %row;
|
||||
.loader {
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
padding-bottom: calc(0.25rem - 3px);
|
||||
border-bottom: 3px solid var(--color-brand-disabled);
|
||||
color: var(--color-text-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.limit-text-width {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.title a {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.mod-navigation {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
overflow-wrap: break-word;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
/*
|
||||
@media screen and (max-width: 1400px) {
|
||||
.mod-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
*/
|
||||
</style>
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<div v-compiled-markdown="mod.body" v-highlightjs class="markdown-body"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
props: {
|
||||
mod: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$emit('update:link-bar', [['Description', '']])
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.markdown-body {
|
||||
padding: 1rem;
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
background: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
}
|
||||
</style>
|
||||
@@ -1,316 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="new-version">
|
||||
<div class="controls">
|
||||
<button
|
||||
class="brand-button"
|
||||
title="Create version"
|
||||
@click="createVersion"
|
||||
>
|
||||
Create version
|
||||
</button>
|
||||
</div>
|
||||
<div class="main">
|
||||
<h3>Name</h3>
|
||||
<label>
|
||||
<span>
|
||||
This is what users will see first. If not specified, this will
|
||||
default to the version number.
|
||||
</span>
|
||||
<input
|
||||
v-model="createdVersion.version_title"
|
||||
type="text"
|
||||
placeholder="Enter the name"
|
||||
/>
|
||||
</label>
|
||||
<h3>Number</h3>
|
||||
<label>
|
||||
<span>
|
||||
This is how your version will appear in mod lists and URLs.
|
||||
</span>
|
||||
<input
|
||||
v-model="createdVersion.version_number"
|
||||
type="text"
|
||||
placeholder="Enter the number"
|
||||
/>
|
||||
</label>
|
||||
<h3>Channel</h3>
|
||||
<label>
|
||||
<span>
|
||||
It is important to notify players and modpack makers whether the
|
||||
version is stable or if it's still in development.
|
||||
</span>
|
||||
<multiselect
|
||||
v-model="createdVersion.release_channel"
|
||||
placeholder="Select one"
|
||||
:options="['release', 'beta', 'alpha']"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
</label>
|
||||
<h3>Mod loaders</h3>
|
||||
<label>
|
||||
<span>Mark all mod loaders this version works with.</span>
|
||||
<multiselect
|
||||
v-model="createdVersion.loaders"
|
||||
:options="selectableLoaders"
|
||||
:loading="selectableLoaders.length === 0"
|
||||
:multiple="true"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="false"
|
||||
:show-labels="false"
|
||||
:limit="6"
|
||||
:hide-selected="true"
|
||||
placeholder="Choose loaders..."
|
||||
/>
|
||||
</label>
|
||||
<h3>Minecraft versions</h3>
|
||||
<label>
|
||||
<span>Mark all Minecraft versions this mod version supports.</span>
|
||||
<multiselect
|
||||
v-model="createdVersion.game_versions"
|
||||
:options="selectableVersions"
|
||||
:loading="selectableVersions.length === 0"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:show-labels="false"
|
||||
:limit="6"
|
||||
:hide-selected="true"
|
||||
placeholder="Choose versions..."
|
||||
/>
|
||||
</label>
|
||||
<h3>Files</h3>
|
||||
<label>
|
||||
<span>
|
||||
You should upload a single JAR file. However, you are allowed to
|
||||
upload multiple.
|
||||
</span>
|
||||
<FileInput
|
||||
accept=".jar,application/java-archive,application/x-java-archive"
|
||||
multiple
|
||||
prompt="Choose files or drag them here"
|
||||
@change="updateVersionFiles"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="changelog">
|
||||
<h3>Changelog</h3>
|
||||
<span>
|
||||
Tell players and modpack makers what's new. It supports the same
|
||||
Markdown as the description, but it is advised not to be too creative
|
||||
with the changelogs.
|
||||
</span>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea v-model="createdVersion.version_body"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
FileInput,
|
||||
},
|
||||
props: {
|
||||
mod: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
async asyncData(data) {
|
||||
try {
|
||||
const [selectableLoaders, selectableVersions] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`tag/loader`),
|
||||
data.$axios.get(`tag/game_version`),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
return {
|
||||
selectableLoaders,
|
||||
selectableVersions,
|
||||
}
|
||||
} catch {
|
||||
data.error({
|
||||
statusCode: 404,
|
||||
message: 'Unable to fetch versions and loaders',
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
createdVersion: {},
|
||||
isEditing: true,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$emit('update:link-bar', [['New Version', 'newversion']])
|
||||
},
|
||||
mounted() {
|
||||
function preventLeave(e) {
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
||||
window.addEventListener('beforeunload', preventLeave)
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
window.removeEventListener('beforeunload', preventLeave)
|
||||
})
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (
|
||||
this.isEditing &&
|
||||
!window.confirm('Are you sure that you want to leave without saving?')
|
||||
) {
|
||||
return
|
||||
}
|
||||
next()
|
||||
},
|
||||
methods: {
|
||||
async createVersion() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
const formData = new FormData()
|
||||
if (!this.createdVersion.version_title) {
|
||||
this.createdVersion.version_title = this.createdVersion.version_number
|
||||
}
|
||||
this.createdVersion.mod_id = this.mod.id
|
||||
this.createdVersion.dependencies = []
|
||||
this.createdVersion.featured = false
|
||||
formData.append('data', JSON.stringify(this.createdVersion))
|
||||
if (this.createdVersion.raw_files) {
|
||||
for (let i = 0; i < this.createdVersion.raw_files.length; i++) {
|
||||
formData.append(
|
||||
this.createdVersion.file_parts[i],
|
||||
new Blob([this.createdVersion.raw_files[i]]),
|
||||
this.createdVersion.raw_files[i].name
|
||||
)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const data = (
|
||||
await this.$axios({
|
||||
url: 'version',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: this.$auth.token,
|
||||
},
|
||||
})
|
||||
).data
|
||||
|
||||
this.isEditing = false
|
||||
await this.$router.push(
|
||||
`/mod/${this.mod.slug ? this.mod.slug : data.mod_id}/version/${
|
||||
data.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()
|
||||
},
|
||||
updateVersionFiles(files) {
|
||||
this.createdVersion.raw_files = files
|
||||
|
||||
const newFileParts = []
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
newFileParts.push(files[i].name.concat('-' + i))
|
||||
}
|
||||
|
||||
this.createdVersion.file_parts = newFileParts
|
||||
},
|
||||
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()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.textarea-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
textarea {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
resize: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.new-version {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
display: grid;
|
||||
grid-template:
|
||||
'controls controls' auto
|
||||
'main changelog' auto
|
||||
/ 5fr 4fr;
|
||||
column-gap: var(--spacing-card-md);
|
||||
|
||||
.controls {
|
||||
grid-area: controls;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.main {
|
||||
grid-area: main;
|
||||
}
|
||||
|
||||
.changelog {
|
||||
grid-area: changelog;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.textarea-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
flex: 2;
|
||||
padding-right: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
input,
|
||||
.multiselect,
|
||||
.input-group {
|
||||
flex: 3;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,287 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="new-version">
|
||||
<div class="controls">
|
||||
<button class="brand-button" title="Save version" @click="saveVersion">
|
||||
Save version
|
||||
</button>
|
||||
</div>
|
||||
<div class="main">
|
||||
<h3>Name</h3>
|
||||
<label>
|
||||
<span>
|
||||
This is what users will see first. If not specified, this will
|
||||
default to the version number.
|
||||
</span>
|
||||
<input
|
||||
v-model="version.name"
|
||||
type="text"
|
||||
placeholder="Enter the name"
|
||||
/>
|
||||
</label>
|
||||
<h3>Number</h3>
|
||||
<label>
|
||||
<span>
|
||||
This is how your version will appear in mod lists and URLs.
|
||||
</span>
|
||||
<input
|
||||
v-model="version.version_number"
|
||||
type="text"
|
||||
placeholder="Enter the number"
|
||||
/>
|
||||
</label>
|
||||
<h3>Channel</h3>
|
||||
<label>
|
||||
<span>
|
||||
It is important to notify players and modpack makers whether the
|
||||
version is stable or if it's still in development.
|
||||
</span>
|
||||
<multiselect
|
||||
v-model="version.version_type"
|
||||
placeholder="Select one"
|
||||
:options="['release', 'beta', 'alpha']"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
</label>
|
||||
<h3>Mod loaders</h3>
|
||||
<label>
|
||||
<span>Mark all mod loaders this version works with.</span>
|
||||
<multiselect
|
||||
v-model="version.loaders"
|
||||
:options="selectableLoaders"
|
||||
:loading="selectableLoaders.length === 0"
|
||||
:multiple="true"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="false"
|
||||
:show-labels="false"
|
||||
:limit="6"
|
||||
:hide-selected="true"
|
||||
placeholder="Choose loaders..."
|
||||
/>
|
||||
</label>
|
||||
<h3>Minecraft versions</h3>
|
||||
<label>
|
||||
<span>Mark all Minecraft versions this mod version supports.</span>
|
||||
<multiselect
|
||||
v-model="version.game_versions"
|
||||
:options="selectableVersions"
|
||||
:loading="selectableVersions.length === 0"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:show-labels="false"
|
||||
:limit="6"
|
||||
:hide-selected="true"
|
||||
placeholder="Choose versions..."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="changelog">
|
||||
<h3>Changelog</h3>
|
||||
<span>
|
||||
Tell players and modpack makers what's new. It supports the same
|
||||
Markdown as the description, but it is advised not to be too creative
|
||||
with the changelogs.
|
||||
</span>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea v-model="version.changelog"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
},
|
||||
auth: false,
|
||||
props: {
|
||||
mod: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [{}]
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
},
|
||||
async fetch() {
|
||||
this.version = this.versions.find(
|
||||
(x) => x.id === this.$route.params.version
|
||||
)
|
||||
|
||||
if (!this.version.changelog && this.version.changelog_url) {
|
||||
this.version.changelog = (
|
||||
await this.$axios.get(this.version.changelog_url)
|
||||
).data
|
||||
}
|
||||
},
|
||||
async asyncData(data) {
|
||||
try {
|
||||
const [selectableLoaders, selectableVersions] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`tag/loader`),
|
||||
data.$axios.get(`tag/game_version`),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
return {
|
||||
selectableLoaders,
|
||||
selectableVersions,
|
||||
}
|
||||
} catch {
|
||||
data.error({
|
||||
statusCode: 404,
|
||||
message: 'Unable to fetch versions or loaders',
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
version: {},
|
||||
isEditing: true,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$emit('update:link-bar', [
|
||||
['Versions', 'versions'],
|
||||
[this.version.name, 'versions/' + this.version.id],
|
||||
['Edit Version', 'versions/' + this.version.id + '/edit'],
|
||||
])
|
||||
function preventLeave(e) {
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
||||
window.addEventListener('beforeunload', preventLeave)
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
window.removeEventListener('beforeunload', preventLeave)
|
||||
})
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (
|
||||
this.isEditing &&
|
||||
!window.confirm('Are you sure that you want to leave without saving?')
|
||||
) {
|
||||
return
|
||||
}
|
||||
next()
|
||||
},
|
||||
methods: {
|
||||
async saveVersion() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.patch(
|
||||
`version/${this.version.id}`,
|
||||
this.version,
|
||||
this.$auth.headers
|
||||
)
|
||||
this.isEditing = false
|
||||
await this.$router.replace(
|
||||
`/mod/${this.mod.slug ? this.mod.slug : this.mod.id}/version/${
|
||||
this.version.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()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.textarea-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
textarea {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
resize: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.new-version {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
display: grid;
|
||||
grid-template:
|
||||
'controls controls' auto
|
||||
'main changelog' auto
|
||||
/ 5fr 4fr;
|
||||
column-gap: var(--spacing-card-md);
|
||||
|
||||
.controls {
|
||||
grid-area: controls;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.main {
|
||||
grid-area: main;
|
||||
}
|
||||
|
||||
.changelog {
|
||||
grid-area: changelog;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.textarea-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
flex: 2;
|
||||
padding-right: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
input,
|
||||
.multiselect,
|
||||
.input-group {
|
||||
flex: 3;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,432 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<ConfirmPopup
|
||||
ref="delete_file_popup"
|
||||
title="Are you sure you want to delete this file?"
|
||||
description="This will remove this file forever (like really forever)"
|
||||
:has-to-type="false"
|
||||
proceed-label="Delete file"
|
||||
@proceed="deleteFile(popup_data)"
|
||||
/>
|
||||
<ConfirmPopup
|
||||
ref="delete_version_popup"
|
||||
title="Are you sure you want to delete this version?"
|
||||
description="This will remove this version forever (like really forever), and if some mods depends on this version, it won't work anymore."
|
||||
:has-to-type="false"
|
||||
proceed-label="Delete version"
|
||||
@proceed="deleteVersion()"
|
||||
/>
|
||||
<div class="version">
|
||||
<div class="version-header">
|
||||
<h4>{{ version.name }}</h4>
|
||||
<span v-if="version.version_type === 'release'" class="badge green">
|
||||
Release
|
||||
</span>
|
||||
<span v-if="version.version_type === 'beta'" class="badge yellow">
|
||||
Beta
|
||||
</span>
|
||||
<span v-if="version.version_type === 'alpha'" class="badge red">
|
||||
Alpha
|
||||
</span>
|
||||
<span>
|
||||
{{ version.version_number }}
|
||||
</span>
|
||||
<Categories :categories="version.loaders" />
|
||||
<div class="buttons">
|
||||
<nuxt-link
|
||||
v-if="this.$auth.user"
|
||||
:to="`/report/create?id=${version.id}&t=version`"
|
||||
class="action iconified-button"
|
||||
>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="currentMember"
|
||||
class="action iconified-button"
|
||||
@click="deleteVersionPopup"
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</button>
|
||||
<nuxt-link
|
||||
v-if="currentMember"
|
||||
class="action iconified-button"
|
||||
:to="version.id + '/edit'"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</nuxt-link>
|
||||
<a
|
||||
v-if="primaryFile"
|
||||
:href="primaryFile.url"
|
||||
class="action iconified-button download"
|
||||
@click.prevent="
|
||||
$parent.downloadFile(primaryFile.hashes.sha1, primaryFile.url)
|
||||
"
|
||||
>
|
||||
<DownloadIcon />
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<DownloadIcon />
|
||||
<div class="info">
|
||||
<h4>Downloads</h4>
|
||||
<p class="value">{{ version.downloads }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<CalendarIcon />
|
||||
<div class="info">
|
||||
<h4>Created</h4>
|
||||
<p
|
||||
v-tooltip="
|
||||
$dayjs(version.date_published).format(
|
||||
'[Created on] YYYY-MM-DD [at] HH:mm A'
|
||||
)
|
||||
"
|
||||
class="value"
|
||||
>
|
||||
{{ $dayjs(version.date_published).fromNow() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<TagIcon />
|
||||
<div class="info">
|
||||
<h4>Available for</h4>
|
||||
<p class="value">
|
||||
{{
|
||||
version.game_versions ? version.game_versions.join(', ') : ''
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-compiled-markdown="version.changelog ? version.changelog : ''"
|
||||
class="markdown-body"
|
||||
></div>
|
||||
<div class="files">
|
||||
<div v-for="file in version.files" :key="file.hashes.sha1" class="file">
|
||||
<div class="text-wrapper">
|
||||
<p>{{ file.filename }}</p>
|
||||
<div v-if="currentMember" class="actions">
|
||||
<button @click="deleteFilePopup(file.hashes.sha1)">
|
||||
Delete file
|
||||
</button>
|
||||
<button @click="makePrimary(file.hashes.sha1)">
|
||||
Make primary
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
:href="file.url"
|
||||
@click.prevent="$parent.downloadFile(file.hashes.sha1, file.url)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<FileInput
|
||||
v-if="currentMember"
|
||||
accept=".jar,application/java-archive,application/x-java-archive"
|
||||
multiple
|
||||
prompt="Choose files or drag them here"
|
||||
class="file-input"
|
||||
@change="addFiles"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ConfirmPopup from '~/components/ui/ConfirmPopup'
|
||||
|
||||
import Categories from '~/components/ui/search/Categories'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import TagIcon from '~/assets/images/utils/tag.svg?inline'
|
||||
import ReportIcon from '~/assets/images/utils/report.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FileInput,
|
||||
Categories,
|
||||
DownloadIcon,
|
||||
CalendarIcon,
|
||||
TagIcon,
|
||||
TrashIcon,
|
||||
EditIcon,
|
||||
ReportIcon,
|
||||
ConfirmPopup,
|
||||
},
|
||||
auth: false,
|
||||
props: {
|
||||
mod: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [{}]
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
},
|
||||
async fetch() {
|
||||
this.version = this.versions.find(
|
||||
(x) => x.id === this.$route.params.version
|
||||
)
|
||||
|
||||
this.primaryFile = this.version.files.find((file) => file.primary)
|
||||
|
||||
if (!this.primaryFile) {
|
||||
this.primaryFile = this.version.files[0]
|
||||
}
|
||||
|
||||
if (!this.version.changelog && this.version.changelog_url) {
|
||||
this.version.changelog = (
|
||||
await this.$axios.get(this.version.changelog_url)
|
||||
).data
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
primaryFile: {},
|
||||
version: {},
|
||||
filesToUpload: [],
|
||||
popup_data: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$emit('update:link-bar', [
|
||||
['Versions', 'versions'],
|
||||
[this.version.name, 'version/' + this.version.id],
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
deleteFilePopup(hash) {
|
||||
this.popup_data = hash
|
||||
this.$refs.delete_file_popup.show()
|
||||
},
|
||||
async deleteFile(hash) {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
await this.$axios.delete(`version_file/${hash}`, this.$auth.headers)
|
||||
|
||||
await this.$router.go(null)
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async makePrimary(hash) {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
await this.$axios.patch(
|
||||
`version/${this.version.id}`,
|
||||
{
|
||||
primary_file: ['sha1', hash],
|
||||
},
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
await this.$router.go(null)
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async addFiles(files) {
|
||||
this.filesToUpload = files
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
this.filesToUpload[i].multipartName = files[i].name.concat('-' + i)
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('data', JSON.stringify({}))
|
||||
|
||||
for (const fileToUpload of this.filesToUpload) {
|
||||
formData.append(
|
||||
fileToUpload.multipartName,
|
||||
new Blob([fileToUpload]),
|
||||
fileToUpload.name
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$axios({
|
||||
url: `version/${this.version.id}/file`,
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: this.$auth.token,
|
||||
},
|
||||
})
|
||||
|
||||
await this.$router.go(null)
|
||||
} 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()
|
||||
},
|
||||
deleteVersionPopup() {
|
||||
this.$refs.delete_version_popup.show()
|
||||
},
|
||||
async deleteVersion() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
await this.$axios.delete(`version/${this.version.id}`, this.$auth.headers)
|
||||
|
||||
await this.$router.replace(`/mod/${this.mod.id}`)
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.version {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
background: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
padding: 1rem;
|
||||
|
||||
.version-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h4,
|
||||
span {
|
||||
margin: auto 0.5rem auto 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.action:not(:first-child) {
|
||||
margin: 0 0 0 0.5rem;
|
||||
}
|
||||
}
|
||||
.download {
|
||||
background-color: var(--color-brand);
|
||||
color: white;
|
||||
&:hover {
|
||||
background-color: var(--color-brand-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.files {
|
||||
display: flex;
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: var(--size-rounded-control);
|
||||
border: 1px solid var(--color-divider);
|
||||
|
||||
.text-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem;
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-height: 3rem;
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
width: 2.5rem;
|
||||
height: auto;
|
||||
background-color: var(--color-button-bg);
|
||||
color: var(--color-button-text);
|
||||
border-radius: 0 var(--size-rounded-control) var(--size-rounded-control)
|
||||
0;
|
||||
|
||||
svg {
|
||||
vertical-align: center;
|
||||
height: 30px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
color: var(--color-button-text-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0.5rem 0;
|
||||
.stat {
|
||||
margin-right: 0.75rem;
|
||||
@extend %stat;
|
||||
|
||||
svg {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-input {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,235 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Version</th>
|
||||
<th>Mod Loader</th>
|
||||
<th>Minecraft Version</th>
|
||||
<th>Status</th>
|
||||
<th>Downloads</th>
|
||||
<th>Date Published</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="version in versions" :key="version.id">
|
||||
<td>
|
||||
<a
|
||||
:href="$parent.findPrimary(version).url"
|
||||
class="download"
|
||||
@click.prevent="
|
||||
$parent.downloadFile(
|
||||
$parent.findPrimary(version).hashes.sha1,
|
||||
$parent.findPrimary(version).url
|
||||
)
|
||||
"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<nuxt-link
|
||||
:to="
|
||||
'/mod/' +
|
||||
(mod.slug ? mod.slug : mod.id) +
|
||||
'/version/' +
|
||||
version.id
|
||||
"
|
||||
>
|
||||
{{ version.name ? version.name : version.version_number }}
|
||||
</nuxt-link>
|
||||
</td>
|
||||
<td>
|
||||
<nuxt-link
|
||||
:to="
|
||||
'/mod/' +
|
||||
(mod.slug ? mod.slug : mod.id) +
|
||||
'/version/' +
|
||||
version.id
|
||||
"
|
||||
>
|
||||
{{ version.version_number }}
|
||||
</nuxt-link>
|
||||
</td>
|
||||
<td>
|
||||
<FabricIcon v-if="version.loaders.includes('fabric')" />
|
||||
<ForgeIcon v-if="version.loaders.includes('forge')" />
|
||||
</td>
|
||||
<td>{{ version.game_versions.join(', ') }}</td>
|
||||
<td>
|
||||
<span v-if="version.version_type === 'release'" class="badge green">
|
||||
Release
|
||||
</span>
|
||||
<span v-if="version.version_type === 'beta'" class="badge yellow">
|
||||
Beta
|
||||
</span>
|
||||
<span v-if="version.version_type === 'alpha'" class="badge red">
|
||||
Alpha
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ version.downloads }}</td>
|
||||
<td>{{ $dayjs(version.date_published).format('YYYY-MM-DD') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="new-version">
|
||||
<nuxt-link v-if="currentMember" to="newversion" class="button">
|
||||
New Version
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import ForgeIcon from '~/assets/images/categories/forge.svg?inline'
|
||||
import FabricIcon from '~/assets/images/categories/fabric.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ForgeIcon,
|
||||
FabricIcon,
|
||||
DownloadIcon,
|
||||
},
|
||||
auth: false,
|
||||
props: {
|
||||
mod: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$emit('update:link-bar', [['Versions', 'versions']])
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
background: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
|
||||
* {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr:not(:last-child),
|
||||
tr:first-child {
|
||||
th,
|
||||
td {
|
||||
border-bottom: 1px solid var(--color-divider);
|
||||
}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
&:first-child {
|
||||
text-align: center;
|
||||
width: 7%;
|
||||
.download {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.25rem;
|
||||
width: 2.25rem;
|
||||
border-radius: 2rem;
|
||||
background-color: var(--color-button-bg);
|
||||
margin-right: var(--spacing-card-sm);
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg-hover);
|
||||
}
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2),
|
||||
&:nth-child(5) {
|
||||
padding-left: 0;
|
||||
width: 12%;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--color-heading);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.02rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
td {
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
img {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-version {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
th,
|
||||
td {
|
||||
&:nth-child(7) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
th,
|
||||
td {
|
||||
&:nth-child(8) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
th,
|
||||
td {
|
||||
&:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
th,
|
||||
td {
|
||||
&:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1051
pages/mod/create.vue
1051
pages/mod/create.vue
File diff suppressed because it is too large
Load Diff
350
pages/moderation.vue
Normal file
350
pages/moderation.vue
Normal file
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<Popup v-if="currentProject" :show-popup="true">
|
||||
<div class="moderation-popup">
|
||||
<h2>Moderation Form</h2>
|
||||
<p>
|
||||
Both of these fields are optional, but can be used to communicate
|
||||
problems with a project team members. The body supports markdown
|
||||
formatting!
|
||||
</p>
|
||||
<div class="status">
|
||||
<span>New Project Status: </span>
|
||||
<Badge
|
||||
v-if="currentProject.newStatus === 'approved'"
|
||||
color="green"
|
||||
:type="currentProject.newStatus"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="
|
||||
currentProject.newStatus === 'processing' ||
|
||||
currentProject.newStatus === 'archived'
|
||||
"
|
||||
color="yellow"
|
||||
:type="currentProject.newStatus"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="currentProject.newStatus === 'rejected'"
|
||||
color="red"
|
||||
:type="currentProject.newStatus"
|
||||
/>
|
||||
<Badge v-else color="gray" :type="currentProject.newStatus" />
|
||||
</div>
|
||||
<input
|
||||
v-model="currentProject.moderation_message"
|
||||
type="text"
|
||||
placeholder="Enter the message..."
|
||||
/>
|
||||
<h3>Body</h3>
|
||||
<ThisOrThat v-model="bodyViewMode" :items="['source', 'preview']" />
|
||||
<div v-if="bodyViewMode === 'source'" class="textarea-wrapper">
|
||||
<textarea
|
||||
id="body"
|
||||
v-model="currentProject.moderation_message_body"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="bodyViewMode === 'preview'"
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="$xss($md.render(currentProject.moderation_message_body))"
|
||||
></div>
|
||||
<div class="buttons">
|
||||
<button class="iconified-button" @click="currentProject = null">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button-colors"
|
||||
@click="saveProject"
|
||||
>
|
||||
<CheckIcon />
|
||||
Save project status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
<div class="page-contents">
|
||||
<div class="content">
|
||||
<h1>Moderation</h1>
|
||||
<ThisOrThat
|
||||
v-model="selectedType"
|
||||
class="card"
|
||||
:items="moderationTypes"
|
||||
/>
|
||||
<div class="projects">
|
||||
<ProjectCard
|
||||
v-for="project in selectedType !== 'all'
|
||||
? projects.filter((x) => x.project_type === selectedType)
|
||||
: projects"
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
:name="project.title"
|
||||
:description="project.description"
|
||||
:created-at="project.published"
|
||||
:updated-at="project.updated"
|
||||
:icon-url="project.icon_url"
|
||||
:categories="project.categories"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:type="project.project_type"
|
||||
>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="setProjectStatus(project, 'approved')"
|
||||
>
|
||||
<CheckIcon />
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="setProjectStatus(project, 'unlisted')"
|
||||
>
|
||||
<UnlistIcon />
|
||||
Unlist
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="setProjectStatus(project, 'rejected')"
|
||||
>
|
||||
<CrossIcon />
|
||||
Reject
|
||||
</button>
|
||||
</ProjectCard>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedType === 'report' || selectedType === 'all'"
|
||||
class="reports"
|
||||
>
|
||||
<div
|
||||
v-for="(report, index) in reports"
|
||||
:key="report.id"
|
||||
class="report card"
|
||||
>
|
||||
<div class="header">
|
||||
<h5 class="title">
|
||||
Report for {{ report.item_type }}
|
||||
<nuxt-link
|
||||
:to="
|
||||
'/' +
|
||||
report.item_type +
|
||||
'/' +
|
||||
report.item_id.replace(/\W/g, '')
|
||||
"
|
||||
>{{ report.item_id }}
|
||||
</nuxt-link>
|
||||
</h5>
|
||||
<p
|
||||
v-tooltip="
|
||||
$dayjs(report.created).format(
|
||||
'[Created at] YYYY-MM-DD [at] HH:mm A'
|
||||
)
|
||||
"
|
||||
class="date"
|
||||
>
|
||||
Created {{ $dayjs(report.created).fromNow() }}
|
||||
</p>
|
||||
<button
|
||||
class="delete iconified-button"
|
||||
@click="deleteReport(index)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="$xss($md.render(report.body))"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="reports.length === 0 && projects.length === 0" class="error">
|
||||
<Security class="icon"></Security>
|
||||
<br />
|
||||
<span class="text">You are up-to-date!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ThisOrThat from '~/components/ui/ThisOrThat'
|
||||
import ProjectCard from '~/components/ui/ProjectCard'
|
||||
import Popup from '~/components/ui/Popup'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import UnlistIcon from '~/assets/images/utils/eye-off.svg?inline'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import Security from '~/assets/images/illustrations/security.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Moderation',
|
||||
components: {
|
||||
ThisOrThat,
|
||||
ProjectCard,
|
||||
CheckIcon,
|
||||
CrossIcon,
|
||||
UnlistIcon,
|
||||
Popup,
|
||||
Badge,
|
||||
Security,
|
||||
},
|
||||
async asyncData(data) {
|
||||
const [projects, reports] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`moderation/projects`, data.$auth.headers),
|
||||
data.$axios.get(`report`, data.$auth.headers),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
return {
|
||||
projects,
|
||||
reports,
|
||||
selectedType: 'all',
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bodyViewMode: 'source',
|
||||
currentProject: null,
|
||||
}
|
||||
},
|
||||
head: {
|
||||
title: 'Moderation - Modrinth',
|
||||
},
|
||||
computed: {
|
||||
moderationTypes() {
|
||||
const obj = { all: true }
|
||||
|
||||
for (const project of this.projects) {
|
||||
obj[project.project_type] = true
|
||||
}
|
||||
|
||||
if (this.reports.length > 0) {
|
||||
obj.report = true
|
||||
}
|
||||
|
||||
return Object.keys(obj)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setProjectStatus(project, status) {
|
||||
project.moderation_message = ''
|
||||
project.moderation_message_body = ''
|
||||
project.newStatus = status
|
||||
|
||||
this.currentProject = project
|
||||
},
|
||||
async saveProject() {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.patch(
|
||||
`project/${this.currentProject.id}`,
|
||||
{
|
||||
moderation_message: this.currentProject.moderation_message
|
||||
? this.currentProject.moderation_message
|
||||
: null,
|
||||
moderation_message_body: this.currentProject.moderation_message_body
|
||||
? this.currentProject.moderation_message_body
|
||||
: null,
|
||||
status: this.currentProject.newStatus,
|
||||
},
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
this.projects.splice(
|
||||
this.projects.findIndex((x) => this.currentProject.id === x.id),
|
||||
1
|
||||
)
|
||||
this.currentProject = null
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async deleteReport(index) {
|
||||
this.$nuxt.$loading.start()
|
||||
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`report/${this.reports[index].id}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
this.reports.splice(index, 1)
|
||||
} 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>
|
||||
.moderation-popup {
|
||||
width: 480px;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
span {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
:first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--color-text-dark);
|
||||
margin: 0 0 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.report {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
.title {
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0 0.5rem 0 0;
|
||||
}
|
||||
.iconified-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.page-contents {
|
||||
max-width: calc(1280px - 20rem) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<h2>Modpacks</h2>
|
||||
<div class="columns">
|
||||
<div class="column-grow-4">
|
||||
<section id="search-pagination">
|
||||
<div class="iconified-input column-grow-2">
|
||||
<input
|
||||
id="search"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search modpacks"
|
||||
/>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="pagination column-grow-1">pagination</div>
|
||||
</section>
|
||||
</div>
|
||||
<section class="column-grow-1">
|
||||
<h3>Filters</h3>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
head: {
|
||||
title: 'Packs - Modrinth',
|
||||
meta: [
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: 'Packs',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Packs',
|
||||
},
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/modpacks`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#search-pagination {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
908
pages/mods.vue
908
pages/mods.vue
@@ -1,908 +0,0 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<div class="content">
|
||||
<section class="search-nav">
|
||||
<div class="iconified-input column-grow-2">
|
||||
<label class="hidden" for="search">Search Mods</label>
|
||||
<input
|
||||
id="search"
|
||||
v-model="query"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search..."
|
||||
autocomplete="off"
|
||||
@input="onSearchChange(1)"
|
||||
/>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
<div class="sort-paginate">
|
||||
<div class="labeled-control">
|
||||
<h3>Sort By</h3>
|
||||
<Multiselect
|
||||
v-model="sortType"
|
||||
class="sort-types"
|
||||
placeholder="Select one"
|
||||
track-by="display"
|
||||
label="display"
|
||||
:options="sortTypes"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@input="onSearchChange(1)"
|
||||
>
|
||||
<template slot="singleLabel" slot-scope="{ option }">{{
|
||||
option.display
|
||||
}}</template>
|
||||
</Multiselect>
|
||||
</div>
|
||||
<div class="labeled-control per-page">
|
||||
<h3>Per Page</h3>
|
||||
<Multiselect
|
||||
v-model="maxResults"
|
||||
class="max-results"
|
||||
placeholder="Select one"
|
||||
:options="[5, 10, 15, 20, 50, 100]"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@input="onSearchChange(currentPage)"
|
||||
>
|
||||
</Multiselect>
|
||||
</div>
|
||||
<div class="labeled-control mobile-filters-button">
|
||||
<h3>Filters</h3>
|
||||
<button @click="toggleFiltersMenu">Open</button>
|
||||
</div>
|
||||
</div>
|
||||
<pagination
|
||||
:current-page="currentPage"
|
||||
:pages="pages"
|
||||
@switch-page="onSearchChange"
|
||||
></pagination>
|
||||
</section>
|
||||
<div class="results column-grow-4">
|
||||
<Advertisement
|
||||
type="banner"
|
||||
small-screen="square"
|
||||
ethical-ads-small
|
||||
ethical-ads-big
|
||||
/>
|
||||
<div v-if="$fetchState.pending" class="no-results">
|
||||
<LogoAnimated />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<SearchResult
|
||||
v-for="(result, index) in results"
|
||||
:id="result.slug ? result.slug : result.mod_id.split('-')[1]"
|
||||
:key="result.mod_id"
|
||||
:author="result.author"
|
||||
:name="result.title"
|
||||
:description="result.description"
|
||||
:latest-version="result.latest_version"
|
||||
:created-at="result.date_created"
|
||||
:updated-at="result.date_modified"
|
||||
:downloads="result.downloads.toString()"
|
||||
:icon-url="result.icon_url"
|
||||
:author-url="result.author_url"
|
||||
:page-url="result.page_url"
|
||||
:categories="result.categories"
|
||||
:is-ad="index === -1"
|
||||
:is-modrinth="result.host === 'modrinth'"
|
||||
/>
|
||||
<div v-if="results.length === 0" class="no-results">
|
||||
<p>No results found for your query!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section v-if="pages.length > 1" class="search-bottom">
|
||||
<div class="per-page labeled-control">
|
||||
<h3>Per Page</h3>
|
||||
<Multiselect
|
||||
v-model="maxResults"
|
||||
class="max-results"
|
||||
placeholder="Select one"
|
||||
:options="[5, 10, 15, 20, 50, 100]"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@input="onSearchChange(currentPage)"
|
||||
>
|
||||
</Multiselect>
|
||||
</div>
|
||||
<pagination
|
||||
:current-page="currentPage"
|
||||
:pages="pages"
|
||||
@switch-page="onSearchChangeToTop"
|
||||
></pagination>
|
||||
</section>
|
||||
<m-footer class="footer" hide-big centered />
|
||||
</div>
|
||||
<section ref="filters" class="filters">
|
||||
<div class="filters-wrapper">
|
||||
<section class="filter-group">
|
||||
<div class="filter-clear-button">
|
||||
<h3>Categories</h3>
|
||||
<div class="columns">
|
||||
<button class="filter-button-done" @click="toggleFiltersMenu">
|
||||
Close
|
||||
</button>
|
||||
<button class="iconified-button" @click="clearFilters">
|
||||
<ExitIcon />
|
||||
Clear filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Technology"
|
||||
facet-name="categories:technology"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<TechCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Adventure"
|
||||
facet-name="categories:adventure"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<AdventureCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Magic"
|
||||
facet-name="categories:magic"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<MagicCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Utility"
|
||||
facet-name="categories:utility"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<UtilityCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Decoration"
|
||||
facet-name="categories:decoration"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<DecorationCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Library"
|
||||
facet-name="categories:library"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<LibraryCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Cursed"
|
||||
facet-name="categories:cursed"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<CursedCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="World generation"
|
||||
facet-name="categories:worldgen"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<WorldGenCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Storage"
|
||||
facet-name="categories:storage"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<StorageCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Food"
|
||||
facet-name="categories:food"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<FoodCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Equipment"
|
||||
facet-name="categories:equipment"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<EquipmentCategory />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Miscellaneous"
|
||||
facet-name="categories:misc"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<MiscCategory />
|
||||
</SearchFilter>
|
||||
<h3>Mod Loaders</h3>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Fabric"
|
||||
facet-name="categories:fabric"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<FabricLoader />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="facets"
|
||||
display-name="Forge"
|
||||
facet-name="categories:forge"
|
||||
@toggle="toggleFacet"
|
||||
>
|
||||
<ForgeLoader />
|
||||
</SearchFilter>
|
||||
<h3>Environments</h3>
|
||||
<SearchFilter
|
||||
:active-filters="selectedEnvironments"
|
||||
display-name="Client"
|
||||
facet-name="client"
|
||||
@toggle="toggleEnv"
|
||||
>
|
||||
<ClientSide />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="selectedEnvironments"
|
||||
display-name="Server"
|
||||
facet-name="server"
|
||||
@toggle="toggleEnv"
|
||||
>
|
||||
<ServerSide />
|
||||
</SearchFilter>
|
||||
<h3>Minecraft versions</h3>
|
||||
<Checkbox
|
||||
v-model="showSnapshots"
|
||||
label="Include snapshots"
|
||||
style="margin-bottom: 0.5rem"
|
||||
:border="false"
|
||||
@input="reloadVersions"
|
||||
/>
|
||||
</section>
|
||||
<Multiselect
|
||||
v-model="selectedVersions"
|
||||
:options="versions"
|
||||
:loading="versions.length === 0"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:show-labels="false"
|
||||
:limit="6"
|
||||
:hide-selected="true"
|
||||
placeholder="Choose versions..."
|
||||
@input="onSearchChange(1)"
|
||||
></Multiselect>
|
||||
<h3>Licenses</h3>
|
||||
<Multiselect
|
||||
v-model="displayLicense"
|
||||
placeholder="Choose licenses..."
|
||||
:loading="licenses.length === 0"
|
||||
:options="licenses"
|
||||
track-by="name"
|
||||
label="name"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="true"
|
||||
@input="toggleLicense"
|
||||
/>
|
||||
</div>
|
||||
<Advertisement type="square" small-screen="destroy" />
|
||||
<m-footer class="footer" hide-small />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import SearchResult from '~/components/ui/ProjectCard'
|
||||
import Pagination from '~/components/ui/Pagination'
|
||||
import SearchFilter from '~/components/ui/search/SearchFilter'
|
||||
import LogoAnimated from '~/components/ui/search/LogoAnimated'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
|
||||
import MFooter from '~/components/layout/MFooter'
|
||||
import TechCategory from '~/assets/images/categories/tech.svg?inline'
|
||||
import AdventureCategory from '~/assets/images/categories/adventure.svg?inline'
|
||||
import CursedCategory from '~/assets/images/categories/cursed.svg?inline'
|
||||
import DecorationCategory from '~/assets/images/categories/decoration.svg?inline'
|
||||
import EquipmentCategory from '~/assets/images/categories/equipment.svg?inline'
|
||||
import FoodCategory from '~/assets/images/categories/food.svg?inline'
|
||||
import LibraryCategory from '~/assets/images/categories/library.svg?inline'
|
||||
import MagicCategory from '~/assets/images/categories/magic.svg?inline'
|
||||
import MiscCategory from '~/assets/images/categories/misc.svg?inline'
|
||||
import StorageCategory from '~/assets/images/categories/storage.svg?inline'
|
||||
import UtilityCategory from '~/assets/images/categories/utility.svg?inline'
|
||||
import WorldGenCategory from '~/assets/images/categories/worldgen.svg?inline'
|
||||
|
||||
import ForgeLoader from '~/assets/images/categories/forge.svg?inline'
|
||||
import FabricLoader from '~/assets/images/categories/fabric.svg?inline'
|
||||
|
||||
import ClientSide from '~/assets/images/categories/client.svg?inline'
|
||||
import ServerSide from '~/assets/images/categories/server.svg?inline'
|
||||
|
||||
import SearchIcon from '~/assets/images/utils/search.svg?inline'
|
||||
import ExitIcon from '~/assets/images/utils/exit.svg?inline'
|
||||
|
||||
import Advertisement from '~/components/ads/Advertisement'
|
||||
|
||||
export default {
|
||||
auth: false,
|
||||
components: {
|
||||
Advertisement,
|
||||
MFooter,
|
||||
SearchResult,
|
||||
Pagination,
|
||||
Multiselect,
|
||||
SearchFilter,
|
||||
Checkbox,
|
||||
TechCategory,
|
||||
AdventureCategory,
|
||||
CursedCategory,
|
||||
DecorationCategory,
|
||||
EquipmentCategory,
|
||||
FoodCategory,
|
||||
LibraryCategory,
|
||||
MagicCategory,
|
||||
MiscCategory,
|
||||
StorageCategory,
|
||||
UtilityCategory,
|
||||
WorldGenCategory,
|
||||
ForgeLoader,
|
||||
FabricLoader,
|
||||
ClientSide,
|
||||
ServerSide,
|
||||
SearchIcon,
|
||||
ExitIcon,
|
||||
LogoAnimated,
|
||||
},
|
||||
fetchOnServer: false,
|
||||
async fetch() {
|
||||
if (this.$route.query.q) this.query = this.$route.query.q
|
||||
if (this.$route.query.f) {
|
||||
const facets = this.$route.query.f.split(',')
|
||||
|
||||
for (const facet of facets) await this.toggleFacet(facet, false)
|
||||
}
|
||||
if (this.$route.query.v)
|
||||
this.selectedVersions = this.$route.query.v.split(',')
|
||||
if (this.$route.query.h) this.showSnapshots = this.$route.query.h === 'true'
|
||||
if (this.$route.query.e)
|
||||
this.selectedEnvironments = this.$route.query.e.split(',')
|
||||
if (this.$route.query.s) {
|
||||
this.sortType.name = this.$route.query.s
|
||||
|
||||
switch (this.sortType.name) {
|
||||
case 'relevance':
|
||||
this.sortType.display = 'Relevance'
|
||||
break
|
||||
case 'downloads':
|
||||
this.sortType.display = 'Downloads'
|
||||
break
|
||||
case 'newest':
|
||||
this.sortType.display = 'Recently created'
|
||||
break
|
||||
case 'updated':
|
||||
this.sortType.display = 'Recently updated'
|
||||
break
|
||||
case 'follows':
|
||||
this.sortType.display = 'Follow count'
|
||||
break
|
||||
}
|
||||
}
|
||||
if (this.$route.query.m) {
|
||||
this.maxResults = this.$route.query.m
|
||||
}
|
||||
if (this.$route.query.o)
|
||||
this.currentPage = Math.ceil(this.$route.query.o / this.maxResults) + 1
|
||||
|
||||
await Promise.all([
|
||||
this.fillVersions(),
|
||||
this.fillInitialLicenses(),
|
||||
this.onSearchChange(this.currentPage),
|
||||
])
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
|
||||
displayLicense: '',
|
||||
selectedLicense: '',
|
||||
licenses: [],
|
||||
|
||||
showSnapshots: false,
|
||||
selectedVersions: [],
|
||||
versions: [],
|
||||
|
||||
selectedEnvironments: [],
|
||||
|
||||
facets: [],
|
||||
results: null,
|
||||
pages: [],
|
||||
currentPage: 1,
|
||||
|
||||
sortTypes: [
|
||||
{ display: 'Relevance', name: 'relevance' },
|
||||
{ display: 'Download count', name: 'downloads' },
|
||||
{ display: 'Follow count', name: 'follows' },
|
||||
{ display: 'Recently created', name: 'newest' },
|
||||
{ display: 'Recently updated', name: 'updated' },
|
||||
],
|
||||
sortType: { display: 'Relevance', name: 'relevance' },
|
||||
|
||||
maxResults: 20,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async '$route.query'(to, from) {
|
||||
// Detects when the query is removed from the URL
|
||||
if (Object.keys(to).length === 0 && Object.keys(from).length !== 0) {
|
||||
this.query = ''
|
||||
this.displayLicense = ''
|
||||
this.selectedLicense = ''
|
||||
this.showSnapshots = false
|
||||
this.selectedVersions = []
|
||||
this.selectedEnvironments = []
|
||||
this.facets = []
|
||||
this.currentPage = 1
|
||||
this.sortType = { display: 'Relevance', name: 'relevance' }
|
||||
this.maxResults = 20
|
||||
|
||||
await this.onSearchChange(1)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async fillVersions() {
|
||||
try {
|
||||
const url = this.showSnapshots
|
||||
? 'tag/game_version'
|
||||
: 'tag/game_version?type=release'
|
||||
|
||||
const res = await this.$axios.get(url)
|
||||
|
||||
this.versions = res.data
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
async reloadVersions() {
|
||||
this.fillVersions()
|
||||
await this.onSearchChange(1)
|
||||
},
|
||||
async fillInitialLicenses() {
|
||||
const licences = (await this.$axios.get('tag/license')).data
|
||||
licences.sort((x, y) => {
|
||||
// Custom case for custom, so it goes to the bottom of the list.
|
||||
if (x.short === 'custom') return 1
|
||||
if (y.short === 'custom') return -1
|
||||
if (x.name < y.name) return -1
|
||||
if (x.name > y.name) return 1
|
||||
return 0
|
||||
})
|
||||
this.licenses = licences
|
||||
},
|
||||
async toggleLicense(license) {
|
||||
if (this.selectedLicense) {
|
||||
const index = this.facets.indexOf(this.selectedLicense)
|
||||
|
||||
this.facets.splice(index, 1)
|
||||
}
|
||||
|
||||
if (license) {
|
||||
this.selectedLicense = `license:${license.short}`
|
||||
this.facets.push(this.selectedLicense)
|
||||
}
|
||||
|
||||
await this.onSearchChange(1)
|
||||
},
|
||||
async clearFilters() {
|
||||
for (const facet of [...this.facets]) await this.toggleFacet(facet, true)
|
||||
|
||||
this.displayLicense = null
|
||||
this.selectedLicense = null
|
||||
this.selectedVersions = []
|
||||
this.selectedEnvironments = []
|
||||
await this.onSearchChange(1)
|
||||
},
|
||||
async toggleFacet(elementName, sendRequest) {
|
||||
const index = this.facets.indexOf(elementName)
|
||||
if (index !== -1) {
|
||||
this.facets.splice(index, 1)
|
||||
} else {
|
||||
this.facets.push(elementName)
|
||||
}
|
||||
|
||||
if (!sendRequest) await this.onSearchChange(1)
|
||||
},
|
||||
async toggleEnv(environment, sendRequest) {
|
||||
const index = this.selectedEnvironments.indexOf(environment)
|
||||
if (index !== -1) {
|
||||
this.selectedEnvironments.splice(index, 1)
|
||||
} else {
|
||||
this.selectedEnvironments.push(environment)
|
||||
}
|
||||
|
||||
if (!sendRequest) await this.onSearchChange(1)
|
||||
},
|
||||
async onSearchChangeToTop(newPageNumber) {
|
||||
if (process.client) window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
|
||||
await this.onSearchChange(newPageNumber)
|
||||
},
|
||||
async onSearchChange(newPageNumber) {
|
||||
if (this.query === null) return
|
||||
|
||||
try {
|
||||
const params = [
|
||||
`limit=${this.maxResults}`,
|
||||
`index=${this.sortType.name}`,
|
||||
]
|
||||
|
||||
if (this.query.length > 0) {
|
||||
params.push(`query=${this.query.replace(/ /g, '+')}`)
|
||||
}
|
||||
|
||||
if (
|
||||
this.facets.length > 0 ||
|
||||
this.selectedVersions.length > 0 ||
|
||||
this.selectedEnvironments.length > 0
|
||||
) {
|
||||
let formattedFacets = []
|
||||
for (const facet of this.facets) {
|
||||
formattedFacets.push([facet])
|
||||
}
|
||||
|
||||
if (this.selectedVersions.length > 0) {
|
||||
const versionFacets = []
|
||||
for (const facet of this.selectedVersions) {
|
||||
versionFacets.push('versions:' + facet)
|
||||
}
|
||||
formattedFacets.push(versionFacets)
|
||||
}
|
||||
|
||||
if (this.selectedEnvironments.length > 0) {
|
||||
let environmentFacets = []
|
||||
|
||||
const includesClient = this.selectedEnvironments.includes('client')
|
||||
const includesServer = this.selectedEnvironments.includes('server')
|
||||
if (includesClient && includesServer) {
|
||||
environmentFacets = [
|
||||
['client_side:required'],
|
||||
['server_side:required'],
|
||||
]
|
||||
} else {
|
||||
if (includesClient) {
|
||||
environmentFacets = [
|
||||
['client_side:optional', 'client_side:required'],
|
||||
['server_side:optional', 'server_side:unsupported'],
|
||||
]
|
||||
}
|
||||
if (includesServer) {
|
||||
environmentFacets = [
|
||||
['client_side:optional', 'client_side:unsupported'],
|
||||
['server_side:optional', 'server_side:required'],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
formattedFacets = [...formattedFacets, ...environmentFacets]
|
||||
}
|
||||
|
||||
params.push(`facets=${JSON.stringify(formattedFacets)}`)
|
||||
}
|
||||
|
||||
const offset = (newPageNumber - 1) * this.maxResults
|
||||
if (newPageNumber !== 1) {
|
||||
params.push(`offset=${offset}`)
|
||||
}
|
||||
|
||||
let url = 'mod'
|
||||
|
||||
if (params.length > 0) {
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
url += i === 0 ? `?${params[i]}` : `&${params[i]}`
|
||||
}
|
||||
}
|
||||
|
||||
const res = await this.$axios.get(url)
|
||||
this.results = res.data.hits
|
||||
|
||||
const pageAmount = Math.ceil(res.data.total_hits / res.data.limit)
|
||||
|
||||
this.currentPage = newPageNumber
|
||||
if (pageAmount > 7) {
|
||||
if (this.currentPage + 3 >= pageAmount) {
|
||||
this.pages = [
|
||||
1,
|
||||
'-',
|
||||
pageAmount - 4,
|
||||
pageAmount - 3,
|
||||
pageAmount - 2,
|
||||
pageAmount - 1,
|
||||
pageAmount,
|
||||
]
|
||||
} else if (this.currentPage > 4) {
|
||||
this.pages = [
|
||||
1,
|
||||
'-',
|
||||
this.currentPage - 1,
|
||||
this.currentPage,
|
||||
this.currentPage + 1,
|
||||
'-',
|
||||
pageAmount,
|
||||
]
|
||||
} else {
|
||||
this.pages = [1, 2, 3, 4, 5, '-', pageAmount]
|
||||
}
|
||||
} else {
|
||||
this.pages = Array.from({ length: pageAmount }, (_, i) => i + 1)
|
||||
}
|
||||
|
||||
if (process.client) {
|
||||
url = `mods?q=${encodeURIComponent(this.query)}`
|
||||
|
||||
if (offset > 0) url += `&o=${offset}`
|
||||
if (this.facets.length > 0)
|
||||
url += `&f=${encodeURIComponent(this.facets)}`
|
||||
if (this.selectedVersions.length > 0)
|
||||
url += `&v=${encodeURIComponent(this.selectedVersions)}`
|
||||
if (this.showSnapshots) url += `&h=true`
|
||||
if (this.selectedEnvironments.length > 0)
|
||||
url += `&e=${encodeURIComponent(this.selectedEnvironments)}`
|
||||
if (this.sortType.name !== 'relevance')
|
||||
url += `&s=${encodeURIComponent(this.sortType.name)}`
|
||||
if (this.maxResults > 20)
|
||||
url += `&m=${encodeURIComponent(this.maxResults)}`
|
||||
|
||||
// Check if URL needs to be changed, ignoring browser `,` to `%2C` changes
|
||||
if (
|
||||
url.replace(/%2C|,/g, '') !==
|
||||
this.$route.fullPath.substring(1).replace(/%2C|,/g, '')
|
||||
)
|
||||
this.$router.replace(url)
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
toggleFiltersMenu() {
|
||||
const currentlyActive = this.$refs.filters.className === 'filters active'
|
||||
this.$refs.filters.className = `filters${
|
||||
currentlyActive ? '' : ' active'
|
||||
}`
|
||||
document.body.style.overflow =
|
||||
document.body.style.overflow !== 'hidden' ? 'hidden' : 'auto'
|
||||
},
|
||||
},
|
||||
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 src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
<style lang="scss">
|
||||
.search-nav {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-flow: column;
|
||||
background: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
padding: 0.25rem 1rem 0.25rem 1rem;
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
min-width: 200px;
|
||||
}
|
||||
.iconified-input {
|
||||
width: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.sort-paginate {
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
display: flex;
|
||||
width: auto;
|
||||
@media screen and (max-width: 350px) {
|
||||
flex-direction: column;
|
||||
.mobile-filters-button {
|
||||
margin: 0.5rem 0 0 0;
|
||||
}
|
||||
}
|
||||
.per-page {
|
||||
margin-left: 0.5rem;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1050px) {
|
||||
flex-flow: row;
|
||||
.sort-paginate {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 450px) {
|
||||
.sort-paginate {
|
||||
.per-page {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-bottom {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
padding: 0 1rem;
|
||||
select {
|
||||
width: 100px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.per-page {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (min-width: 550px) {
|
||||
padding: 0.25rem 1rem 0.25rem 1rem;
|
||||
justify-content: flex-end;
|
||||
.per-page {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.labeled-control {
|
||||
h3 {
|
||||
@extend %small-label;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-filters-button {
|
||||
display: inline-block;
|
||||
margin-left: 0.5rem;
|
||||
button {
|
||||
margin-top: 0;
|
||||
height: 2.5rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
// Hide button on larger screens where it's not needed
|
||||
@media screen and (min-width: 1250px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
right: -100vw;
|
||||
max-height: 100vh;
|
||||
min-width: 15%;
|
||||
top: var(--size-navbar-height);
|
||||
height: calc(100vh - var(--size-navbar-height));
|
||||
transition: right 150ms;
|
||||
background-color: var(--color-raised-bg);
|
||||
flex-shrink: 0; // Stop shrinking when page contents change
|
||||
.filters-wrapper {
|
||||
padding: 0.25rem 0.75rem 0.75rem 0.75rem;
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
}
|
||||
h3 {
|
||||
@extend %small-label;
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
&.active {
|
||||
right: 0;
|
||||
}
|
||||
// Larger screens that don't need to collapse
|
||||
@media screen and (min-width: 1250px) {
|
||||
top: 0;
|
||||
right: auto;
|
||||
position: unset;
|
||||
height: unset;
|
||||
max-height: unset;
|
||||
transition: none;
|
||||
margin-left: var(--spacing-card-lg);
|
||||
overflow-y: unset;
|
||||
padding-right: 1rem;
|
||||
width: 18vw;
|
||||
background-color: transparent;
|
||||
.filters-wrapper {
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1250px) {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-clear-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// Large screens that don't collapse
|
||||
@media screen and (min-width: 1250px) {
|
||||
.filter-button-done {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sort-types {
|
||||
min-width: 200px;
|
||||
border: none;
|
||||
border-radius: var(--size-rounded-control);
|
||||
|
||||
.multiselect__tags {
|
||||
padding: 10px 50px 0 8px;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-text);
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
background: var(--color-raised-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
}
|
||||
|
||||
.max-results {
|
||||
max-width: 80px;
|
||||
}
|
||||
</style>
|
||||
205
pages/notifications.vue
Normal file
205
pages/notifications.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<div class="content">
|
||||
<h1>Notifications</h1>
|
||||
|
||||
<div class="divider card">
|
||||
<button class="iconified-button" @click="clearNotifications">
|
||||
<ClearIcon />
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
<div class="notifications">
|
||||
<div
|
||||
v-for="notification in $user.notifications"
|
||||
:key="notification.id"
|
||||
class="card notification"
|
||||
>
|
||||
<div class="icon">
|
||||
<UpdateIcon v-if="notification.type === 'project-update'" />
|
||||
<UsersIcon v-else-if="notification.type === 'team_invite'" />
|
||||
</div>
|
||||
<div class="text">
|
||||
<nuxt-link :to="notification.link" class="top">
|
||||
<h3>{{ notification.title }}</h3>
|
||||
<span>
|
||||
Notified {{ $dayjs(notification.created).fromNow() }}</span
|
||||
>
|
||||
</nuxt-link>
|
||||
<p>{{ notification.text }}</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button
|
||||
v-for="(action, actionIndex) in notification.actions"
|
||||
:key="actionIndex"
|
||||
class="iconified-button"
|
||||
@click="
|
||||
performAction(notification, notificationIndex, actionIndex)
|
||||
"
|
||||
>
|
||||
{{ action.title }}
|
||||
</button>
|
||||
<button
|
||||
v-if="$user.notifications.length === 0"
|
||||
class="iconified-button"
|
||||
@click="performAction(notification, notificationIndex, null)"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$user.notifications.length === 0" class="error">
|
||||
<UpToDate class="icon"></UpToDate>
|
||||
<br />
|
||||
<span class="text">You are up-to-date!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClearIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import UpdateIcon from '~/assets/images/utils/updated.svg?inline'
|
||||
import UsersIcon from '~/assets/images/utils/users.svg?inline'
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Notifications',
|
||||
components: {
|
||||
ClearIcon,
|
||||
UpdateIcon,
|
||||
UsersIcon,
|
||||
UpToDate,
|
||||
},
|
||||
async fetch() {
|
||||
await this.$store.dispatch('user/fetchNotifications')
|
||||
},
|
||||
head: {
|
||||
title: 'Notifications - Modrinth',
|
||||
},
|
||||
methods: {
|
||||
async clearNotifications() {
|
||||
try {
|
||||
const ids = this.$user.notifications.map((x) => x.id)
|
||||
|
||||
await this.$axios.delete(
|
||||
`notifications?ids=${JSON.stringify(ids)}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
ids.forEach((x) => this.$store.dispatch('user/deleteNotification', x))
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
async performAction(notification, notificationIndex, actionIndex) {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`notification/${notification.id}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
await this.$store.dispatch('user/deleteNotification', notification.id)
|
||||
|
||||
if (actionIndex !== null) {
|
||||
const config = {
|
||||
method:
|
||||
notification.actions[actionIndex].action_route[0].toLowerCase(),
|
||||
url: `${notification.actions[actionIndex].action_route[1]}`,
|
||||
headers: {
|
||||
Authorization: this.$auth.token,
|
||||
},
|
||||
}
|
||||
await this.$axios(config)
|
||||
}
|
||||
} 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>
|
||||
h1 {
|
||||
color: var(--color-text-dark);
|
||||
margin: 0 0 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
button {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications {
|
||||
.notification {
|
||||
display: flex;
|
||||
max-height: 4rem;
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-lg);
|
||||
|
||||
.icon svg {
|
||||
height: calc(4rem - var(--spacing-card-sm));
|
||||
width: auto;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
max-height: calc(4rem - var(--spacing-card-sm));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0 0.5rem 0 0;
|
||||
|
||||
strong {
|
||||
color: var(--color-brand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
margin-left: auto;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.page-contents {
|
||||
max-width: calc(1280px - 20rem) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
617
pages/search.vue
Normal file
617
pages/search.vue
Normal file
@@ -0,0 +1,617 @@
|
||||
<template>
|
||||
<div class="normal-page">
|
||||
<aside class="normal-page__sidebar">
|
||||
<section class="card">
|
||||
<button
|
||||
class="iconified-button sidebar-menu-close-button"
|
||||
@click="sidebarMenuOpen = !sidebarMenuOpen"
|
||||
>
|
||||
<EyeOffIcon v-if="sidebarMenuOpen" />
|
||||
<EyeIcon v-else />
|
||||
{{ sidebarMenuOpen ? 'Hide filters' : 'Show filters' }}
|
||||
</button>
|
||||
<div
|
||||
class="sidebar-menu"
|
||||
:class="{ 'sidebar-menu_open': sidebarMenuOpen }"
|
||||
>
|
||||
<button class="iconified-button" @click="clearFilters">
|
||||
<ExitIcon />
|
||||
Clear filters
|
||||
</button>
|
||||
<h3
|
||||
v-if="
|
||||
$tag.categories.filter((x) => x.project_type === projectType)
|
||||
.length > 0
|
||||
"
|
||||
class="sidebar-menu-heading"
|
||||
>
|
||||
Categories
|
||||
</h3>
|
||||
<SearchFilter
|
||||
v-for="category in $tag.categories.filter(
|
||||
(x) => x.project_type === projectType
|
||||
)"
|
||||
:key="category.name"
|
||||
:active-filters="facets"
|
||||
:display-name="category.name"
|
||||
:facet-name="`categories:${category.name}`"
|
||||
:icon="category.icon"
|
||||
@toggle="toggleFacet"
|
||||
/>
|
||||
<h3
|
||||
v-if="
|
||||
$tag.loaders.filter((x) =>
|
||||
x.supported_project_types.includes(projectType)
|
||||
).length > 0
|
||||
"
|
||||
class="sidebar-menu-heading"
|
||||
>
|
||||
Loaders
|
||||
</h3>
|
||||
<SearchFilter
|
||||
v-for="loader in $tag.loaders.filter((x) =>
|
||||
x.supported_project_types.includes(projectType)
|
||||
)"
|
||||
:key="loader.name"
|
||||
:active-filters="facets"
|
||||
:display-name="loader.name"
|
||||
:facet-name="`categories:${loader.name}`"
|
||||
:icon="loader.icon"
|
||||
@toggle="toggleFacet"
|
||||
/>
|
||||
<h3 class="sidebar-menu-heading">Environments</h3>
|
||||
<SearchFilter
|
||||
:active-filters="selectedEnvironments"
|
||||
display-name="Client"
|
||||
facet-name="client"
|
||||
@toggle="toggleEnv"
|
||||
>
|
||||
<ClientSide />
|
||||
</SearchFilter>
|
||||
<SearchFilter
|
||||
:active-filters="selectedEnvironments"
|
||||
display-name="Server"
|
||||
facet-name="server"
|
||||
@toggle="toggleEnv"
|
||||
>
|
||||
<ServerSide />
|
||||
</SearchFilter>
|
||||
<h3 class="sidebar-menu-heading">Minecraft versions</h3>
|
||||
<Checkbox
|
||||
v-model="showSnapshots"
|
||||
label="Include snapshots"
|
||||
style="margin-bottom: 0.5rem"
|
||||
:border="false"
|
||||
/>
|
||||
<multiselect
|
||||
v-model="selectedVersions"
|
||||
:options="
|
||||
showSnapshots
|
||||
? $tag.gameVersions.map((x) => x.version)
|
||||
: $tag.gameVersions
|
||||
.filter((it) => it.version_type === 'release')
|
||||
.map((x) => x.version)
|
||||
"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:show-no-results="false"
|
||||
:close-on-select="false"
|
||||
:clear-search-on-select="false"
|
||||
:show-labels="false"
|
||||
:selectable="() => selectedVersions.length <= 6"
|
||||
placeholder="Choose versions..."
|
||||
@input="onSearchChange(1)"
|
||||
></multiselect>
|
||||
<h3 class="sidebar-menu-heading">Licenses</h3>
|
||||
<Multiselect
|
||||
v-model="displayLicense"
|
||||
placeholder="Choose licenses..."
|
||||
:loading="$tag.licenses.length === 0"
|
||||
:options="$tag.licenses"
|
||||
track-by="name"
|
||||
label="name"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="true"
|
||||
@input="toggleLicense"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<Advertisement type="square" small-screen="destroy" />
|
||||
</aside>
|
||||
<section class="normal-page__content">
|
||||
<div class="card search-controls">
|
||||
<div class="iconified-input">
|
||||
<label class="hidden" for="search">Search Mods</label>
|
||||
<SearchIcon />
|
||||
<input
|
||||
id="search"
|
||||
v-model="query"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search..."
|
||||
autocomplete="off"
|
||||
@input="onSearchChange(1)"
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled-control">
|
||||
<span class="labeled-control__label">Sort by</span>
|
||||
<Multiselect
|
||||
v-model="sortType"
|
||||
placeholder="Select one"
|
||||
class="search-controls__sorting labeled-control__control"
|
||||
track-by="display"
|
||||
label="display"
|
||||
:options="sortTypes"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@input="onSearchChange(1)"
|
||||
>
|
||||
<template slot="singleLabel" slot-scope="{ option }">{{
|
||||
option.display
|
||||
}}</template>
|
||||
</Multiselect>
|
||||
</div>
|
||||
<div class="labeled-control">
|
||||
<span class="labeled-control__label">Show per page</span>
|
||||
<Multiselect
|
||||
v-model="maxResults"
|
||||
placeholder="Select one"
|
||||
class="labeled-control__control"
|
||||
:options="[5, 10, 15, 20, 50, 100]"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@input="onSearchChange(currentPage)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<pagination
|
||||
:current-page="currentPage"
|
||||
:pages="pages"
|
||||
@switch-page="onSearchChange"
|
||||
></pagination>
|
||||
<div>
|
||||
<Advertisement
|
||||
type="banner"
|
||||
small-screen="square"
|
||||
ethical-ads-small
|
||||
ethical-ads-big
|
||||
/>
|
||||
<div v-if="$fetchState.pending" class="no-results">
|
||||
<LogoAnimated />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<SearchResult
|
||||
v-for="result in results"
|
||||
:id="result.slug ? result.slug : result.project_id"
|
||||
:key="result.project_id"
|
||||
:type="result.project_type"
|
||||
:author="result.author"
|
||||
:name="result.title"
|
||||
:description="result.description"
|
||||
:created-at="result.date_created"
|
||||
:updated-at="result.date_modified"
|
||||
:downloads="result.downloads.toString()"
|
||||
:follows="result.follows.toString()"
|
||||
:icon-url="result.icon_url"
|
||||
:client-side="result.client_side"
|
||||
:server-side="result.server_side"
|
||||
:categories="result.categories"
|
||||
/>
|
||||
<div v-if="results && results.length === 0" class="no-results">
|
||||
<p>No results found for your query!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pagination
|
||||
:current-page="currentPage"
|
||||
:pages="pages"
|
||||
@switch-page="onSearchChangeToTop"
|
||||
></pagination>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import SearchResult from '~/components/ui/ProjectCard'
|
||||
import Pagination from '~/components/ui/Pagination'
|
||||
import SearchFilter from '~/components/ui/search/SearchFilter'
|
||||
import LogoAnimated from '~/components/ui/search/LogoAnimated'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
|
||||
import ClientSide from '~/assets/images/categories/client.svg?inline'
|
||||
import ServerSide from '~/assets/images/categories/server.svg?inline'
|
||||
|
||||
import SearchIcon from '~/assets/images/utils/search.svg?inline'
|
||||
import ExitIcon from '~/assets/images/utils/exit.svg?inline'
|
||||
import EyeIcon from '~/assets/images/utils/eye.svg?inline'
|
||||
import EyeOffIcon from '~/assets/images/utils/eye-off.svg?inline'
|
||||
|
||||
import Advertisement from '~/components/ads/Advertisement'
|
||||
|
||||
export default {
|
||||
auth: false,
|
||||
components: {
|
||||
Advertisement,
|
||||
SearchResult,
|
||||
Pagination,
|
||||
Multiselect,
|
||||
SearchFilter,
|
||||
Checkbox,
|
||||
ClientSide,
|
||||
ServerSide,
|
||||
SearchIcon,
|
||||
ExitIcon,
|
||||
EyeIcon,
|
||||
EyeOffIcon,
|
||||
LogoAnimated,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
|
||||
displayLicense: '',
|
||||
selectedLicense: '',
|
||||
|
||||
showSnapshots: false,
|
||||
selectedVersions: [],
|
||||
|
||||
selectedEnvironments: [],
|
||||
|
||||
facets: [],
|
||||
results: null,
|
||||
pages: [],
|
||||
currentPage: 1,
|
||||
|
||||
projectType: 'mod',
|
||||
|
||||
sortTypes: [
|
||||
{ display: 'Relevance', name: 'relevance' },
|
||||
{ display: 'Download count', name: 'downloads' },
|
||||
{ display: 'Follow count', name: 'follows' },
|
||||
{ display: 'Recently created', name: 'newest' },
|
||||
{ display: 'Recently updated', name: 'updated' },
|
||||
],
|
||||
sortType: { display: 'Relevance', name: 'relevance' },
|
||||
|
||||
maxResults: 20,
|
||||
|
||||
sidebarMenuOpen: false,
|
||||
}
|
||||
},
|
||||
async fetch() {
|
||||
if (this.$route.query.q) this.query = this.$route.query.q
|
||||
if (this.$route.query.f) {
|
||||
const facets = this.$route.query.f.split(',')
|
||||
|
||||
for (const facet of facets) await this.toggleFacet(facet, false)
|
||||
}
|
||||
if (this.$route.query.v)
|
||||
this.selectedVersions = this.$route.query.v.split(',')
|
||||
if (this.$route.query.h) this.showSnapshots = this.$route.query.h === 'true'
|
||||
if (this.$route.query.e)
|
||||
this.selectedEnvironments = this.$route.query.e.split(',')
|
||||
if (this.$route.query.s) {
|
||||
this.sortType.name = this.$route.query.s
|
||||
|
||||
switch (this.sortType.name) {
|
||||
case 'relevance':
|
||||
this.sortType.display = 'Relevance'
|
||||
break
|
||||
case 'downloads':
|
||||
this.sortType.display = 'Downloads'
|
||||
break
|
||||
case 'newest':
|
||||
this.sortType.display = 'Recently created'
|
||||
break
|
||||
case 'updated':
|
||||
this.sortType.display = 'Recently updated'
|
||||
break
|
||||
case 'follows':
|
||||
this.sortType.display = 'Follow count'
|
||||
break
|
||||
}
|
||||
}
|
||||
if (this.$route.query.m) {
|
||||
this.maxResults = this.$route.query.m
|
||||
}
|
||||
if (this.$route.query.o)
|
||||
this.currentPage = Math.ceil(this.$route.query.o / this.maxResults) + 1
|
||||
|
||||
this.projectType = this.$route.name.substring(
|
||||
0,
|
||||
this.$route.name.length - 1
|
||||
)
|
||||
|
||||
await this.onSearchChange(this.currentPage)
|
||||
},
|
||||
watch: {
|
||||
'$route.path': {
|
||||
async handler() {
|
||||
this.projectType = this.$route.name.substring(
|
||||
0,
|
||||
this.$route.name.length - 1
|
||||
)
|
||||
|
||||
this.results = null
|
||||
this.pages = []
|
||||
this.currentPage = 1
|
||||
this.query = ''
|
||||
this.maxResults = 20
|
||||
this.sortType = { display: 'Relevance', name: 'relevance' }
|
||||
|
||||
await this.clearFilters()
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async toggleLicense(license) {
|
||||
if (this.selectedLicense) {
|
||||
const index = this.facets.indexOf(this.selectedLicense)
|
||||
|
||||
this.facets.splice(index, 1)
|
||||
}
|
||||
|
||||
if (license) {
|
||||
this.selectedLicense = `license:${license.short}`
|
||||
this.facets.push(this.selectedLicense)
|
||||
}
|
||||
|
||||
await this.onSearchChange(1)
|
||||
},
|
||||
async clearFilters() {
|
||||
for (const facet of [...this.facets]) await this.toggleFacet(facet, true)
|
||||
|
||||
this.displayLicense = null
|
||||
this.selectedLicense = null
|
||||
this.selectedVersions = []
|
||||
this.selectedEnvironments = []
|
||||
await this.onSearchChange(1)
|
||||
},
|
||||
async toggleFacet(elementName, sendRequest) {
|
||||
const index = this.facets.indexOf(elementName)
|
||||
if (index !== -1) {
|
||||
this.facets.splice(index, 1)
|
||||
} else {
|
||||
this.facets.push(elementName)
|
||||
}
|
||||
|
||||
if (!sendRequest) await this.onSearchChange(1)
|
||||
},
|
||||
async toggleEnv(environment, sendRequest) {
|
||||
const index = this.selectedEnvironments.indexOf(environment)
|
||||
if (index !== -1) {
|
||||
this.selectedEnvironments.splice(index, 1)
|
||||
} else {
|
||||
this.selectedEnvironments.push(environment)
|
||||
}
|
||||
|
||||
if (!sendRequest) await this.onSearchChange(1)
|
||||
},
|
||||
async onSearchChangeToTop(newPageNumber) {
|
||||
if (process.client) window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
|
||||
await this.onSearchChange(newPageNumber)
|
||||
},
|
||||
async onSearchChange(newPageNumber) {
|
||||
if (this.query === null) return
|
||||
|
||||
try {
|
||||
const params = [
|
||||
`limit=${this.maxResults}`,
|
||||
`index=${this.sortType.name}`,
|
||||
]
|
||||
|
||||
if (this.query.length > 0) {
|
||||
params.push(`query=${this.query.replace(/ /g, '+')}`)
|
||||
}
|
||||
|
||||
if (
|
||||
this.facets.length > 0 ||
|
||||
this.selectedVersions.length > 0 ||
|
||||
this.selectedEnvironments.length > 0 ||
|
||||
this.projectType
|
||||
) {
|
||||
let formattedFacets = []
|
||||
for (const facet of this.facets) {
|
||||
formattedFacets.push([facet])
|
||||
}
|
||||
|
||||
if (this.selectedVersions.length > 0) {
|
||||
const versionFacets = []
|
||||
for (const facet of this.selectedVersions) {
|
||||
versionFacets.push('versions:' + facet)
|
||||
}
|
||||
formattedFacets.push(versionFacets)
|
||||
}
|
||||
|
||||
if (this.selectedEnvironments.length > 0) {
|
||||
let environmentFacets = []
|
||||
|
||||
const includesClient = this.selectedEnvironments.includes('client')
|
||||
const includesServer = this.selectedEnvironments.includes('server')
|
||||
if (includesClient && includesServer) {
|
||||
environmentFacets = [
|
||||
['client_side:required'],
|
||||
['server_side:required'],
|
||||
]
|
||||
} else {
|
||||
if (includesClient) {
|
||||
environmentFacets = [
|
||||
['client_side:optional', 'client_side:required'],
|
||||
['server_side:optional', 'server_side:unsupported'],
|
||||
]
|
||||
}
|
||||
if (includesServer) {
|
||||
environmentFacets = [
|
||||
['client_side:optional', 'client_side:unsupported'],
|
||||
['server_side:optional', 'server_side:required'],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
formattedFacets = [...formattedFacets, ...environmentFacets]
|
||||
}
|
||||
|
||||
if (this.projectType)
|
||||
formattedFacets.push([`project_type:${this.projectType}`])
|
||||
|
||||
params.push(`facets=${JSON.stringify(formattedFacets)}`)
|
||||
}
|
||||
|
||||
const offset = (newPageNumber - 1) * this.maxResults
|
||||
if (newPageNumber !== 1) {
|
||||
params.push(`offset=${offset}`)
|
||||
}
|
||||
|
||||
let url = 'search'
|
||||
|
||||
if (params.length > 0) {
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
url += i === 0 ? `?${params[i]}` : `&${params[i]}`
|
||||
}
|
||||
}
|
||||
|
||||
const res = await this.$axios.get(url)
|
||||
this.results = res.data.hits
|
||||
|
||||
const pageAmount = Math.ceil(res.data.total_hits / res.data.limit)
|
||||
|
||||
this.currentPage = newPageNumber
|
||||
if (pageAmount > 4) {
|
||||
if (this.currentPage + 3 >= pageAmount) {
|
||||
this.pages = [
|
||||
1,
|
||||
'-',
|
||||
pageAmount - 4,
|
||||
pageAmount - 3,
|
||||
pageAmount - 2,
|
||||
pageAmount - 1,
|
||||
pageAmount,
|
||||
]
|
||||
} else if (this.currentPage > 4) {
|
||||
this.pages = [
|
||||
1,
|
||||
'-',
|
||||
this.currentPage - 1,
|
||||
this.currentPage,
|
||||
this.currentPage + 1,
|
||||
'-',
|
||||
pageAmount,
|
||||
]
|
||||
} else {
|
||||
this.pages = [1, 2, 3, 4, 5, '-', pageAmount]
|
||||
}
|
||||
} else {
|
||||
this.pages = Array.from({ length: pageAmount }, (_, i) => i + 1)
|
||||
}
|
||||
|
||||
if (process.client) {
|
||||
const queryItems = []
|
||||
|
||||
if (this.query) queryItems.push(`q=${encodeURIComponent(this.query)}`)
|
||||
if (offset > 0) queryItems.push(`o=${offset}`)
|
||||
if (this.facets.length > 0)
|
||||
queryItems.push(`f=${encodeURIComponent(this.facets)}`)
|
||||
if (this.selectedVersions.length > 0)
|
||||
queryItems.push(`v=${encodeURIComponent(this.selectedVersions)}`)
|
||||
if (this.showSnapshots) url += `h=true`
|
||||
if (this.selectedEnvironments.length > 0)
|
||||
queryItems.push(
|
||||
`e=${encodeURIComponent(this.selectedEnvironments)}`
|
||||
)
|
||||
if (this.sortType.name !== 'relevance')
|
||||
queryItems.push(`s=${encodeURIComponent(this.sortType.name)}`)
|
||||
if (this.maxResults !== 20)
|
||||
queryItems.push(`m=${encodeURIComponent(this.maxResults)}`)
|
||||
|
||||
url = `${this.$route.path}`
|
||||
|
||||
if (queryItems.length > 0) {
|
||||
url += `?${queryItems[0]}`
|
||||
|
||||
for (let i = 1; i < queryItems.length; i++) {
|
||||
url += `&${queryItems[i]}`
|
||||
}
|
||||
}
|
||||
|
||||
await this.$router.push({ path: url })
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-menu {
|
||||
display: none;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-menu_open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar-menu-heading {
|
||||
margin: 1.5rem 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-controls__sorting {
|
||||
min-width: 15rem;
|
||||
}
|
||||
|
||||
.labeled-control__label,
|
||||
.labeled-control__control {
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.sidebar-menu {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sidebar-menu-close-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.labeled-control {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.labeled-control__label,
|
||||
.labeled-control__control {
|
||||
margin: 0 0 0 1rem;
|
||||
}
|
||||
|
||||
.labeled-control__label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
pages/search/modpacks.vue
Normal file
34
pages/search/modpacks.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Modpacks',
|
||||
asyncData(ctx) {
|
||||
ctx.params.projectType = 'modpack'
|
||||
},
|
||||
head: {
|
||||
title: 'Modpacks - Modrinth',
|
||||
meta: [
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: 'Modpacks',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: 'Modpacks',
|
||||
},
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/modpacks`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
34
pages/search/mods.vue
Normal file
34
pages/search/mods.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Mods',
|
||||
asyncData(ctx) {
|
||||
ctx.params.projectType = 'mod'
|
||||
},
|
||||
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>
|
||||
94
pages/settings.vue
Normal file
94
pages/settings.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<div class="content">
|
||||
<h1 v-if="$auth.user">Settings for {{ $auth.user.username }}</h1>
|
||||
<h1 v-else>Settings</h1>
|
||||
<div class="card styled-tabs">
|
||||
<nuxt-link v-if="$auth.user" class="tab" to="/settings" exact
|
||||
><span>Profile</span></nuxt-link
|
||||
>
|
||||
<nuxt-link v-if="$auth.user" class="tab" to="/settings/follows">
|
||||
<span>Followed projects</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="$auth.user" class="tab" to="/settings/security">
|
||||
<span>Security</span>
|
||||
</nuxt-link>
|
||||
<nuxt-link class="tab" to="/settings/privacy">
|
||||
<span>Privacy</span>
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="actionButton"
|
||||
class="iconified-button brand-button-colors right"
|
||||
@click="actionButtonCallback"
|
||||
>
|
||||
<CheckIcon />
|
||||
{{ actionButton }}
|
||||
</button>
|
||||
</div>
|
||||
<NuxtChild
|
||||
:action-button.sync="actionButton"
|
||||
:action-button-callback.sync="actionButtonCallback"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
components: {
|
||||
CheckIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actionButton: '',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.path': {
|
||||
handler() {
|
||||
this.actionButton = ''
|
||||
this.actionButtonCallback = () => {}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
actionButtonCallback() {},
|
||||
changeTheme() {
|
||||
const shift = event.shiftKey
|
||||
switch (this.$colorMode.preference) {
|
||||
case 'dark':
|
||||
this.$colorMode.preference = shift ? 'light' : 'oled'
|
||||
break
|
||||
case 'oled':
|
||||
this.$colorMode.preference = shift ? 'dark' : 'light'
|
||||
break
|
||||
default:
|
||||
this.$colorMode.preference = shift ? 'oled' : 'dark'
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-contents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h1 {
|
||||
color: var(--color-text-dark);
|
||||
margin: 0 0 0.5rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.page-contents {
|
||||
max-width: 60rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
pages/settings/follows.vue
Normal file
58
pages/settings/follows.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div v-if="$user.follows.length > 0">
|
||||
<ProjectCard
|
||||
v-for="project in $user.follows"
|
||||
:id="project.id"
|
||||
:key="project.id"
|
||||
:type="project.project_type"
|
||||
:categories="project.categories"
|
||||
:created-at="project.published"
|
||||
:updated-at="project.updated"
|
||||
:description="project.description"
|
||||
:downloads="project.downloads.toString()"
|
||||
:icon-url="project.icon_url"
|
||||
:name="project.title"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="$store.dispatch('user/unfollowProject', project)"
|
||||
>
|
||||
<HeartIcon />
|
||||
Unfollow
|
||||
</button>
|
||||
</ProjectCard>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<FollowIllustration class="icon" />
|
||||
<br />
|
||||
<span class="text"
|
||||
>You don't have any followed mods. <br />
|
||||
Why don't you <nuxt-link class="link" to="/mods">search</nuxt-link> for
|
||||
new ones?</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProjectCard from '~/components/ui/ProjectCard'
|
||||
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import FollowIllustration from '~/assets/images/illustrations/follow_illustration.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProjectCard,
|
||||
HeartIcon,
|
||||
FollowIllustration,
|
||||
},
|
||||
async fetch() {
|
||||
await this.$store.dispatch('user/fetchFollows')
|
||||
},
|
||||
head: {
|
||||
title: 'Followed Projects - Modrinth',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
323
pages/settings/index.vue
Normal file
323
pages/settings/index.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div class="edit-page">
|
||||
<div class="left-side">
|
||||
<div class="profile-picture card">
|
||||
<h3>Profile picture</h3>
|
||||
<div class="uploader">
|
||||
<img :src="previewImage ? previewImage : $auth.user.avatar_url" />
|
||||
<file-input
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image"
|
||||
prompt="Choose image or drag it here"
|
||||
@change="showPreviewImage"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
icon = null
|
||||
previewImage = null
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
Reset icon
|
||||
</button>
|
||||
</div>
|
||||
<div class="recap">
|
||||
<section>
|
||||
<h2>Quick recap of you</h2>
|
||||
<div>
|
||||
<Badge
|
||||
v-if="$auth.user.role === 'admin'"
|
||||
type="You are an admin"
|
||||
color="red"
|
||||
/>
|
||||
<Badge
|
||||
v-else-if="$auth.user.role === 'moderator'"
|
||||
type="You are a moderator"
|
||||
color="yellow"
|
||||
/>
|
||||
<Badge v-else type="You are a developer" color="green" />
|
||||
<div class="stat">
|
||||
<SunriseIcon />
|
||||
<span>You joined {{ $dayjs($auth.user.created).fromNow() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>You have</h2>
|
||||
<div class="stat">
|
||||
<DownloadIcon />
|
||||
<span>
|
||||
<strong>{{ sumDownloads() }}</strong> downloads
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<HeartIcon />
|
||||
<span>
|
||||
<strong>{{ sumFollows() }}</strong> followers of projects
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<section class="card">
|
||||
<h3>Username</h3>
|
||||
<label>
|
||||
<span>
|
||||
The username used on Modrinth to identify yourself. This must be
|
||||
unique.
|
||||
</span>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
placeholder="Enter your username"
|
||||
/>
|
||||
</label>
|
||||
<h3>Email</h3>
|
||||
<label>
|
||||
<span>
|
||||
The email for your account. This is private information which is not
|
||||
exposed in any API routes or on your profile. It is also optional.
|
||||
</span>
|
||||
<input v-model="email" type="email" placeholder="Enter your email" />
|
||||
</label>
|
||||
<h3>Bio</h3>
|
||||
<label>
|
||||
<span>
|
||||
A description of yourself which other users can see on your profile.
|
||||
</span>
|
||||
<input v-model="bio" type="text" placeholder="Enter your bio" />
|
||||
</label>
|
||||
<h3>Theme</h3>
|
||||
<label>
|
||||
<span
|
||||
>Change the global site theme of Modrinth. You can switch it here or
|
||||
anywhere by accessing the theme switcher in the navigation bar
|
||||
dropdown.</span
|
||||
>
|
||||
<Multiselect
|
||||
v-model="$colorMode.preference"
|
||||
:options="['light', 'dark', 'oled']"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import SunriseIcon from '~/assets/images/utils/sunrise.svg?inline'
|
||||
import DownloadIcon from '~/assets/images/utils/download-alt.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TrashIcon,
|
||||
SunriseIcon,
|
||||
DownloadIcon,
|
||||
HeartIcon,
|
||||
Badge,
|
||||
FileInput,
|
||||
Multiselect,
|
||||
},
|
||||
asyncData(ctx) {
|
||||
return {
|
||||
username: ctx.$auth.user.username,
|
||||
email: ctx.$auth.user.email,
|
||||
bio: ctx.$auth.user.bio,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
icon: null,
|
||||
previewImage: null,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.$emit('update:action-button', 'Save profile settings')
|
||||
this.$emit('update:action-button-callback', this.saveChanges)
|
||||
},
|
||||
head: {
|
||||
title: 'Settings - Modrinth',
|
||||
},
|
||||
created() {
|
||||
this.$emit('update:action-button', 'Save profile settings')
|
||||
this.$emit('update:action-button-callback', this.saveChanges)
|
||||
},
|
||||
methods: {
|
||||
changeTheme() {
|
||||
const shift = event.shiftKey
|
||||
switch (this.$colorMode.preference) {
|
||||
case 'dark':
|
||||
this.$colorMode.preference = shift ? 'light' : 'oled'
|
||||
break
|
||||
case 'oled':
|
||||
this.$colorMode.preference = shift ? 'dark' : 'light'
|
||||
break
|
||||
default:
|
||||
this.$colorMode.preference = shift ? 'oled' : 'dark'
|
||||
}
|
||||
},
|
||||
showPreviewImage(files) {
|
||||
const reader = new FileReader()
|
||||
this.icon = files[0]
|
||||
reader.readAsDataURL(this.icon)
|
||||
|
||||
reader.onload = (event) => {
|
||||
this.previewImage = event.target.result
|
||||
}
|
||||
},
|
||||
formatNumber(x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
},
|
||||
sumDownloads() {
|
||||
let sum = 0
|
||||
|
||||
for (const projects of this.$user.projects) {
|
||||
sum += projects.downloads
|
||||
}
|
||||
|
||||
return this.formatNumber(sum)
|
||||
},
|
||||
sumFollows() {
|
||||
let sum = 0
|
||||
|
||||
for (const projects of this.$user.projects) {
|
||||
sum += projects.followers
|
||||
}
|
||||
|
||||
return this.formatNumber(sum)
|
||||
},
|
||||
async saveChanges() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
if (this.icon) {
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}/icon?ext=${
|
||||
this.icon.type.split('/')[this.icon.type.split('/').length - 1]
|
||||
}`,
|
||||
this.icon,
|
||||
this.$auth.headers
|
||||
)
|
||||
}
|
||||
|
||||
const data = {
|
||||
email: this.email,
|
||||
bio: this.bio,
|
||||
}
|
||||
|
||||
if (this.username !== this.$auth.user.username) {
|
||||
data.username = this.username
|
||||
}
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}`,
|
||||
data,
|
||||
this.$auth.headers
|
||||
)
|
||||
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
})
|
||||
} 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>
|
||||
.edit-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.left-side {
|
||||
min-width: 20rem;
|
||||
|
||||
.profile-picture {
|
||||
margin-right: var(--spacing-card-bg);
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.uploader {
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
box-shadow: var(--shadow-card);
|
||||
|
||||
border-radius: var(--size-rounded-md);
|
||||
width: 8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recap {
|
||||
section {
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
text-transform: none;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
&::first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
width: auto;
|
||||
height: 1.25rem;
|
||||
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
span {
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,11 @@
|
||||
<!--suppress HtmlFormInputWithoutLabel -->
|
||||
<template>
|
||||
<div class="popup card">
|
||||
<div class="rows card">
|
||||
<div class="consent-container">
|
||||
<div class="h1">Privacy settings</div>
|
||||
<div>
|
||||
Modrinth relies on different providers and in-house tools to allow us to
|
||||
provide custom-tailored experiences and personalized advertising. You
|
||||
can change your privacy settings at any time by going to this settings
|
||||
page, via the dashboard or via the footer of any page.
|
||||
page or via the footer of any page.
|
||||
</div>
|
||||
<br class="divider" />
|
||||
<div class="toggles">
|
||||
@@ -33,58 +31,66 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn button" @click="toggleOff">Refuse All</button>
|
||||
<button class="btn button" @click="toggleOn">Accept All</button>
|
||||
<button class="btn brand-button" @click="confirm">
|
||||
Confirm my choices
|
||||
<button class="iconified-button" @click="toggleAll(false)">
|
||||
Refuse All
|
||||
</button>
|
||||
<button class="iconified-button" @click="toggleAll(true)">
|
||||
Accept All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-disable require-await */
|
||||
import scopes from '@/privacy-toggles'
|
||||
import toggles from '@/privacy-toggles'
|
||||
|
||||
export default {
|
||||
auth: false,
|
||||
name: 'Privacy',
|
||||
data: () => {
|
||||
const settings = toggles.settings
|
||||
return {
|
||||
scopes: settings,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.$emit('update:action-button', 'Confirm my choices')
|
||||
this.$emit('update:action-button-callback', this.confirm)
|
||||
|
||||
this.$store.dispatch('consent/loadFromCookies', this.$cookies)
|
||||
|
||||
if (this.$store.state.consent.is_consent_given) {
|
||||
Object.keys(scopes.settings).forEach((key) => {
|
||||
scopes.settings[key].value = false
|
||||
Object.keys(toggles.settings).forEach((key) => {
|
||||
toggles.settings[key].value = false
|
||||
})
|
||||
|
||||
// Load the allowed scopes from the store
|
||||
this.$store.state.consent.scopes_allowed.forEach((scope) => {
|
||||
if (this.scopes[scope] != null)
|
||||
this.$set(this.scopes[scope], 'value', true)
|
||||
})
|
||||
} else {
|
||||
Object.keys(scopes.settings).forEach((key) => {
|
||||
scopes.settings[key].value = scopes.settings[key].default
|
||||
Object.keys(toggles.settings).forEach((key) => {
|
||||
toggles.settings[key].value = toggles.settings[key].default
|
||||
})
|
||||
}
|
||||
},
|
||||
data: () => {
|
||||
const settings = scopes.settings
|
||||
return {
|
||||
scopes: settings,
|
||||
}
|
||||
head: {
|
||||
title: 'Privacy Settings - Modrinth',
|
||||
},
|
||||
created() {
|
||||
this.$emit('update:action-button', 'Confirm my choices')
|
||||
this.$emit('update:action-button-callback', this.confirm)
|
||||
},
|
||||
options: {
|
||||
auth: false,
|
||||
},
|
||||
methods: {
|
||||
toggleOff() {
|
||||
toggleAll(value) {
|
||||
for (const elem in this.scopes) {
|
||||
this.$set(this.scopes[elem], 'value', false)
|
||||
}
|
||||
},
|
||||
toggleOn() {
|
||||
for (const elem in this.scopes) {
|
||||
this.$set(this.scopes[elem], 'value', true)
|
||||
this.scopes[elem].value = value
|
||||
this.$set(this.scopes[elem], 'value', value)
|
||||
}
|
||||
|
||||
this.$forceUpdate()
|
||||
},
|
||||
confirm() {
|
||||
this.$store.commit('consent/set_consent', true)
|
||||
@@ -104,24 +110,13 @@ export default {
|
||||
})
|
||||
},
|
||||
},
|
||||
head: {
|
||||
title: 'Privacy Settings - Modrinth',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-lg);
|
||||
}
|
||||
.popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
<style lang="scss" scoped>
|
||||
.spacer {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 1rem;
|
||||
margin-right: -0.5rem;
|
||||
@@ -129,41 +124,45 @@ export default {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
.btn {
|
||||
|
||||
button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.consent-container {
|
||||
.h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.divider {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.toggles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.toggle-text {
|
||||
.title {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.contents {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.toggle-action {
|
||||
margin-left: 1rem;
|
||||
display: flex;
|
||||
@@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="section-header columns">
|
||||
<h3 class="column-grow-1">Revoke your Modrinth token</h3>
|
||||
</div>
|
||||
<section class="essentials pad-maker">
|
||||
<section class="card essentials pad-maker">
|
||||
<h3>Revoke your Modrinth token</h3>
|
||||
<p>
|
||||
Revoking your Modrinth token can have unintended consequences. Please be
|
||||
aware that the following could break:
|
||||
@@ -44,71 +42,45 @@
|
||||
token will be regenerated.
|
||||
</strong>
|
||||
</p>
|
||||
<button @click="logout">Continue</button>
|
||||
<button class="iconified-button brand-button-colors" @click="logout">
|
||||
<CheckIcon />
|
||||
Continue
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CheckIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
components: {
|
||||
CheckIcon,
|
||||
},
|
||||
head: {
|
||||
title: 'Revoke Token - Modrinth',
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
async logout() {
|
||||
this.$cookies.set('auth-token-reset', true)
|
||||
window.location.href = '/'
|
||||
await this.$router.replace(
|
||||
`auth/init?url=${process.env.domain}${this.$route.fullPath}`
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pad-rem {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pad-maker {
|
||||
margin-top: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.save-btn-div {
|
||||
overflow: hidden;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
float: right;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
section {
|
||||
@extend %card;
|
||||
padding: var(--spacing-card-md) var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
flex: 2;
|
||||
padding-right: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 3;
|
||||
height: fit-content;
|
||||
a {
|
||||
color: var(--color-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button {
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
height: fit-content;
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
98
pages/settings/security.vue
Normal file
98
pages/settings/security.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div>
|
||||
<ConfirmPopup
|
||||
ref="delete_popup"
|
||||
title="Are you sure you want to delete your account?"
|
||||
description="If you proceed, your user and all attached data will be removed from our
|
||||
servers. This cannot be reversed, so be careful!"
|
||||
proceed-label="Delete account"
|
||||
:confirmation-text="$auth.user.username"
|
||||
:has-to-type="true"
|
||||
@proceed="deleteAccount"
|
||||
/>
|
||||
|
||||
<section class="card">
|
||||
<h3>Authorization token</h3>
|
||||
<label>
|
||||
<span>
|
||||
Your authorization token can be used with the Modrinth API, the
|
||||
Minotaur Gradle plugin, and other applications that interact with
|
||||
Modrinth's API. Be sure to keep this secret!
|
||||
</span>
|
||||
<input
|
||||
type="button"
|
||||
class="iconified-button"
|
||||
value="Copy to clipboard"
|
||||
@click="copyToken"
|
||||
/>
|
||||
</label>
|
||||
<h3>Revoke your token</h3>
|
||||
<label>
|
||||
<span
|
||||
>This will log you out of Modrinth, and you will have to log in again
|
||||
to access Modrinth with a new token.</span
|
||||
>
|
||||
<input
|
||||
type="button"
|
||||
class="iconified-button"
|
||||
value="Revoke token"
|
||||
@click="$router.replace('/settings/revoke-token')"
|
||||
/>
|
||||
</label>
|
||||
<h3>Delete your account</h3>
|
||||
<label>
|
||||
<span
|
||||
>Clicking on this WILL delete your account. Do not click on this
|
||||
unless you want your account deleted. If you delete your account, all
|
||||
attached data, including projects, will be removed from our servers.
|
||||
This cannot be reversed, so be careful!</span
|
||||
>
|
||||
<input
|
||||
value="Delete Account"
|
||||
type="button"
|
||||
class="iconified-button"
|
||||
@click="$refs.delete_popup.show()"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmPopup from '~/components/ui/ConfirmPopup'
|
||||
|
||||
export default {
|
||||
components: { ConfirmPopup },
|
||||
head: {
|
||||
title: 'Security - Modrinth',
|
||||
},
|
||||
methods: {
|
||||
async copyToken() {
|
||||
await navigator.clipboard.writeText(this.$auth.token)
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Copied to clipboard.',
|
||||
text: 'Copied your Modrinth token to the clipboard.',
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
async deleteAccount() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`user/${this.$auth.user.id}`,
|
||||
this.$auth.headers
|
||||
)
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,147 +1,173 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-contents">
|
||||
<div class="sidebar-l">
|
||||
<div class="card">
|
||||
<div class="user-info">
|
||||
<img :src="user.avatar_url" :alt="user.username" />
|
||||
<div class="text">
|
||||
<h2>{{ user.username }}</h2>
|
||||
<p v-if="user.role === 'admin'" class="badge red">Admin</p>
|
||||
<p v-if="user.role === 'moderator'" class="badge yellow">
|
||||
Moderator
|
||||
</p>
|
||||
<p v-if="user.role === 'developer'" class="badge green">
|
||||
Developer
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="user.bio" class="bio">{{ user.bio }}</p>
|
||||
<div class="buttons">
|
||||
<nuxt-link
|
||||
v-if="this.$auth.user && this.$auth.user.id != user.id"
|
||||
:to="`/report/create?id=${user.id}&t=user`"
|
||||
class="iconified-button"
|
||||
>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card stats">
|
||||
<div class="stat">
|
||||
<DownloadIcon />
|
||||
<div class="info">
|
||||
<h4>Downloads</h4>
|
||||
<p class="value">
|
||||
{{ sumDownloads() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<HeartIcon />
|
||||
<div class="info">
|
||||
<h4>Followers</h4>
|
||||
<p class="value">
|
||||
{{ sumFollowers() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<CalendarIcon />
|
||||
<div class="info">
|
||||
<h4>Joined</h4>
|
||||
<p
|
||||
v-tooltip="
|
||||
$dayjs(user.created).format(
|
||||
'[Joined] YYYY-MM-DD [at] HH:mm A'
|
||||
)
|
||||
"
|
||||
class="value"
|
||||
>
|
||||
{{ $dayjs(user.created).fromNow() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<UserIcon />
|
||||
<div class="info">
|
||||
<h4>User ID</h4>
|
||||
<p class="value">{{ user.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Advertisement
|
||||
type="square"
|
||||
small-screen="square"
|
||||
ethical-ads-big
|
||||
ethical-ads-small
|
||||
ethical-ad-type="image"
|
||||
<div class="normal-page">
|
||||
<div>
|
||||
<aside class="card sidebar normal-page__sidebar">
|
||||
<img
|
||||
class="sidebar__item profile-picture"
|
||||
:src="user.avatar_url"
|
||||
:alt="user.username"
|
||||
/>
|
||||
<m-footer class="footer" hide-small />
|
||||
</div>
|
||||
<div class="content">
|
||||
<Advertisement type="banner" small-screen="destroy" />
|
||||
<div class="mods">
|
||||
<SearchResult
|
||||
v-for="result in mods"
|
||||
:id="result.slug || result.id"
|
||||
:key="result.id"
|
||||
:name="result.title"
|
||||
:description="result.description"
|
||||
:created-at="result.published"
|
||||
:updated-at="result.updated"
|
||||
:downloads="result.downloads.toString()"
|
||||
:icon-url="result.icon_url"
|
||||
:author-url="result.author_url"
|
||||
:categories="result.categories"
|
||||
:is-modrinth="true"
|
||||
<h1 class="sidebar__item username">{{ user.username }}</h1>
|
||||
<nuxt-link
|
||||
v-if="$auth.user && $auth.user.id !== user.id"
|
||||
:to="`/create/report?id=${user.id}&t=user`"
|
||||
class="sidebar__item report-button iconified-button"
|
||||
>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</nuxt-link>
|
||||
<div class="sidebar__item">
|
||||
<Badge v-if="user.role === 'admin'" type="admin" color="red" />
|
||||
<Badge
|
||||
v-else-if="user.role === 'moderator'"
|
||||
type="moderator"
|
||||
color="yellow"
|
||||
/>
|
||||
<Badge v-else type="developer" color="green" />
|
||||
</div>
|
||||
<m-footer class="footer" hide-big centered />
|
||||
<h3 class="sidebar__item">About me</h3>
|
||||
<span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span>
|
||||
<div class="sidebar__item stats-block">
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<SunriseIcon class="secondary-stat__icon" />
|
||||
<span class="secondary-stat__text">
|
||||
Joined {{ $dayjs(user.created).fromNow() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<UserIcon class="secondary-stat__icon" />
|
||||
<span class="secondary-stat__text">User ID: {{ user.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar__item stats-block">
|
||||
<div class="stats-block__item primary-stat">
|
||||
<DownloadIcon class="primary-stat__icon" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">{{ sumDownloads() }}</span>
|
||||
<span class="primary-stat__label">downloads</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-block__item primary-stat">
|
||||
<HeartIcon class="primary-stat__icon" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">{{ sumFollows() }}</span>
|
||||
<span class="primary-stat__label">followers of projects</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<nav class="card user-navigation">
|
||||
<ThisOrThat v-model="selectedProjectType" :items="projectTypes" />
|
||||
<nuxt-link
|
||||
v-if="$auth.user && $auth.user.id === user.id"
|
||||
to="/create/project"
|
||||
class="iconified-button brand-button-colors"
|
||||
>
|
||||
<PlusIcon />
|
||||
Create a project
|
||||
</nuxt-link>
|
||||
</nav>
|
||||
<Advertisement
|
||||
type="banner"
|
||||
small-screen="square"
|
||||
ethical-ads-small
|
||||
ethical-ads-big
|
||||
/>
|
||||
<div v-if="projects.length > 0">
|
||||
<ProjectCard
|
||||
v-for="project in selectedProjectType !== 'all'
|
||||
? projects.filter((x) => x.project_type === selectedProjectType)
|
||||
: projects"
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
:name="project.title"
|
||||
:description="project.description"
|
||||
:created-at="project.published"
|
||||
:updated-at="project.updated"
|
||||
:downloads="project.downloads.toString()"
|
||||
:follows="project.followers.toString()"
|
||||
:icon-url="project.icon_url"
|
||||
:categories="project.categories"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:status="project.status"
|
||||
:type="project.project_type"
|
||||
>
|
||||
<nuxt-link
|
||||
v-if="$auth.user && $auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
:to="`/${project.project_type}/${
|
||||
project.slug ? project.slug : project.id
|
||||
}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</nuxt-link>
|
||||
</ProjectCard>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<UpToDate class="icon" /><br />
|
||||
<span v-if="$auth.user && $auth.user.id === user.id" class="text"
|
||||
>You don't have any projects.<br />
|
||||
Would you like to
|
||||
<nuxt-link class="link" to="/create/project">create one</nuxt-link
|
||||
>?</span
|
||||
>
|
||||
<span v-else class="text">This user has no projects!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchResult from '~/components/ui/ProjectCard'
|
||||
import MFooter from '~/components/layout/MFooter'
|
||||
import ProjectCard from '~/components/ui/ProjectCard'
|
||||
import ThisOrThat from '~/components/ui/ThisOrThat'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
import Advertisement from '~/components/ads/Advertisement'
|
||||
|
||||
import ReportIcon from '~/assets/images/utils/report.svg?inline'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import SunriseIcon from '~/assets/images/utils/sunrise.svg?inline'
|
||||
import DownloadIcon from '~/assets/images/utils/download-alt.svg?inline'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
import Advertisement from '~/components/ads/Advertisement'
|
||||
|
||||
export default {
|
||||
auth: false,
|
||||
components: {
|
||||
Advertisement,
|
||||
SearchResult,
|
||||
CalendarIcon,
|
||||
ProjectCard,
|
||||
SunriseIcon,
|
||||
DownloadIcon,
|
||||
MFooter,
|
||||
ReportIcon,
|
||||
HeartIcon,
|
||||
Badge,
|
||||
SettingsIcon,
|
||||
PlusIcon,
|
||||
ThisOrThat,
|
||||
UpToDate,
|
||||
UserIcon,
|
||||
Advertisement,
|
||||
},
|
||||
async asyncData(data) {
|
||||
try {
|
||||
let res = await data.$axios.get(`user/${data.params.id}`)
|
||||
const user = res.data
|
||||
|
||||
let mods = []
|
||||
res = await data.$axios.get(`user/${user.id}/mods`)
|
||||
if (res.data) {
|
||||
res = await data.$axios.get(`mods?ids=${JSON.stringify(res.data)}`)
|
||||
mods = res.data
|
||||
}
|
||||
const [user, projects] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`user/${data.params.id}`, data.$auth.headers),
|
||||
data.$axios.get(
|
||||
`user/${data.params.id}/projects`,
|
||||
data.$auth.headers
|
||||
),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
return {
|
||||
mods,
|
||||
selectedProjectType: 'all',
|
||||
user,
|
||||
projects,
|
||||
}
|
||||
} catch {
|
||||
data.error({
|
||||
@@ -150,29 +176,6 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatNumber(x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
},
|
||||
sumDownloads() {
|
||||
let sum = 0
|
||||
|
||||
for (const mod of this.mods) {
|
||||
sum += mod.downloads
|
||||
}
|
||||
|
||||
return this.formatNumber(sum)
|
||||
},
|
||||
sumFollowers() {
|
||||
let sum = 0
|
||||
|
||||
for (const mod of this.mods) {
|
||||
sum += mod.followers
|
||||
}
|
||||
|
||||
return this.formatNumber(sum)
|
||||
},
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.user.username + ' - Modrinth',
|
||||
@@ -218,57 +221,106 @@ export default {
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
projectTypes() {
|
||||
const obj = { all: true }
|
||||
|
||||
for (const project of this.projects) {
|
||||
obj[project.project_type] = true
|
||||
}
|
||||
|
||||
return Object.keys(obj)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatNumber(x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
},
|
||||
sumDownloads() {
|
||||
let sum = 0
|
||||
|
||||
for (const projects of this.projects) {
|
||||
sum += projects.downloads
|
||||
}
|
||||
|
||||
return this.formatNumber(sum)
|
||||
},
|
||||
sumFollows() {
|
||||
let sum = 0
|
||||
|
||||
for (const projects of this.projects) {
|
||||
sum += projects.followers
|
||||
}
|
||||
|
||||
return this.formatNumber(sum)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-l {
|
||||
@media screen and (min-width: 1024px) {
|
||||
min-width: 21rem;
|
||||
}
|
||||
<style scoped>
|
||||
.user-navigation {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.sidebar__item:not(:last-child) {
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
@extend %row;
|
||||
img {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin-right: var(--spacing-card-md);
|
||||
border-radius: var(--size-rounded-icon);
|
||||
}
|
||||
.text {
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttons {
|
||||
@extend %column;
|
||||
.profile-picture {
|
||||
border-radius: var(--size-rounded-lg);
|
||||
height: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
margin-top: 16px;
|
||||
.username {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.iconified-button {
|
||||
max-width: 4.5rem;
|
||||
}
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.stat {
|
||||
width: 8.5rem;
|
||||
margin: 0.5rem;
|
||||
@extend %stat;
|
||||
.report-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
svg {
|
||||
padding: 0.25rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.bio {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats-block__item {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.secondary-stat {
|
||||
align-items: center;
|
||||
color: var(--color-text-secondary);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.secondary-stat__icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.secondary-stat__text {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.primary-stat {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.primary-stat__icon {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.primary-stat__text {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.primary-stat__counter {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user