Files
Rocketmc/pages/mod/_id/edit.vue
Emma ec92a4cd34 Fix a bug introduced in #278 ("submit for approval" always being present) (#283)
This fixes a bug where the "Submit for approval" button was always present no matter the project status. These things are finnicky.
2021-07-14 12:10:01 -07:00

716 lines
17 KiB
Vue

<template>
<div class="page-contents">
<header class="columns">
<h3 class="column-grow-1">Edit Mod</h3>
<nuxt-link
:to="'/mod/' + (mod.slug ? mod.slug : mod.id)"
class="button column"
>
Back
</nuxt-link>
<button
v-if="
mod.status === 'rejected' ||
mod.status === 'draft' ||
mod.status === 'unlisted'
"
title="Submit for approval"
class="button column"
:disabled="!this.$nuxt.$loading"
@click="saveModReview"
>
Submit for approval
</button>
<button
title="Save"
class="brand-button column"
:disabled="!this.$nuxt.$loading"
@click="saveMod"
>
Save
</button>
</header>
<section class="essentials">
<h3>Name</h3>
<label>
<span>
Be creative. TechCraft v7 won't be searchable and won't be clicked on.
</span>
<input v-model="mod.title" type="text" placeholder="Enter the name" />
</label>
<h3>Summary</h3>
<label>
<span>
Give a quick summary of your mod. This will appear in search.
</span>
<input
v-model="mod.description"
type="text"
placeholder="Enter the summary"
/>
</label>
<h3>Categories</h3>
<label>
<span>
Select up to 3 categories. These will help others find your mod.
</span>
<multiselect
id="categories"
v-model="mod.categories"
:options="availableCategories"
:loading="availableCategories.length === 0"
:multiple="true"
:searchable="false"
:show-no-results="false"
:close-on-select="false"
:clear-on-select="false"
:show-labels="false"
:max="3"
:limit="6"
:hide-selected="true"
placeholder="Choose categories"
/>
</label>
<h3>Vanity URL (slug)</h3>
<label>
<span>
Set this to something pretty, so your mod's URL can be more readable.
</span>
<input
id="name"
v-model="mod.slug"
type="text"
placeholder="Enter the vanity URL's last bit"
/>
</label>
</section>
<section class="mod-icon rows">
<h3>Icon</h3>
<div class="columns row-grow-1">
<div class="column-grow-1 rows">
<file-input
accept="image/png,image/jpeg,image/gif,image/webp"
class="choose-image"
prompt="Choose an image or drag it here"
@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>
<button
class="transparent-button"
@click="
icon = null
previewImage = null
iconChanged = true
"
>
Reset icon
</button>
</div>
<img
:src="
previewImage
? previewImage
: mod.icon_url && !iconChanged
? mod.icon_url
: 'https://cdn.modrinth.com/placeholder.svg'
"
alt="preview-image"
/>
</div>
</section>
<section class="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>
<div class="labeled-control">
<h3>Client</h3>
<Multiselect
v-model="clientSideType"
placeholder="Select one"
:options="sideTypes"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
/>
</div>
<div class="labeled-control">
<h3>Server</h3>
<Multiselect
v-model="serverSideType"
placeholder="Select one"
:options="sideTypes"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
/>
</div>
</div>
</section>
<section class="description">
<h3>
<label
for="body"
title="You can type an extended description of your mod here."
>
Description
</label>
</h3>
<span>
You can type an extended description of your mod here. This editor
supports Markdown. Its syntax can be found
<a
href="https://guides.github.com/features/mastering-markdown/"
target="_blank"
rel="noopener noreferrer"
>here</a
>.
</span>
<div class="columns">
<div class="textarea-wrapper">
<textarea id="body" v-model="mod.body"></textarea>
</div>
<div v-compiled-markdown="mod.body" class="markdown-body"></div>
</div>
</section>
<section class="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."
>
<span>Issue tracker</span>
<input
v-model="mod.issues_url"
type="url"
placeholder="Enter a valid URL"
/>
</label>
<label title="A page/repository containing the source code for your mod.">
<span>Source code</span>
<input
v-model="mod.source_url"
type="url"
placeholder="Enter a valid URL"
/>
</label>
<label
title="A page containing information, documentation, and help for the mod."
>
<span>Wiki page</span>
<input
v-model="mod.wiki_url"
type="url"
placeholder="Enter a valid URL"
/>
</label>
<label title="An invitation link to your Discord server.">
<span>Discord invite</span>
<input
v-model="mod.discord_url"
type="url"
placeholder="Enter a valid URL"
/>
</label>
</section>
<section class="license">
<div class="title">
<h3>License</h3>
</div>
<label>
<span>
It is very important to choose a proper license for your mod. You may
choose one from our list or provide a URL to a custom license.
<br />
Confused? See our
<a
href="https://blog.modrinth.com/licensing-guide/"
target="_blank"
rel="noopener noreferrer"
>
licensing guide</a
>
for more information.
</span>
<div class="input-group">
<Multiselect
v-model="license"
placeholder="Select one"
track-by="short"
label="name"
:options="availableLicenses"
:searchable="true"
:close-on-select="true"
:show-labels="false"
/>
<input v-model="license_url" type="url" placeholder="License URL" />
</div>
</label>
</section>
<!--
<section class="donations">
<div class="title">
<h3>Donation links</h3>
<button
title="Add a link"
class="button"
:disabled="false"
@click="
donationPlatforms.push({})
donationLinks.push('')
"
>
Add a link
</button>
</div>
<div v-for="(item, index) in donationPlatforms" :key="index">
<label title="The donation link.">
<span>Donation Link</span>
<input
v-model="donationLinks[index]"
type="url"
placeholder="Enter a valid URL"
/>
</label>
<label title="The donation platform of the link.">
<span>Donation Platform</span>
<Multiselect
v-model="donationPlatforms[index]"
placeholder="Select one"
track-by="short"
label="name"
:options="availableDonationPlatforms"
:searchable="false"
:close-on-select="true"
:show-labels="false"
/>
</label>
<button
class="button"
@click="
donationPlatforms.splice(index, 1)
donationLinks.splice(index, 1)
"
>
Remove Link
</button>
<hr />
</div>
</section>
-->
</div>
</template>
<script>
import Multiselect from 'vue-multiselect'
import FileInput from '~/components/ui/FileInput'
export default {
components: {
FileInput,
Multiselect,
},
async asyncData(data) {
try {
const [
mod,
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)
mod.license = {
short: mod.license.id,
name: mod.license.name,
url: mod.license.url,
}
if (mod.body_url && !mod.body) {
mod.body = (await data.$axios.get(mod.body_url)).data
}
const donationPlatforms = []
const donationLinks = []
if (mod.donation_urls) {
for (const platform of mod.donation_urls) {
donationPlatforms.push({
short: platform.id,
name: platform.platform,
})
donationLinks.push(platform.url)
}
}
availableLicenses.sort((a, b) => a.name.localeCompare(b.name))
return {
mod,
clientSideType: mod.client_side.charAt(0) + mod.client_side.slice(1),
serverSideType: mod.server_side.charAt(0) + mod.server_side.slice(1),
availableCategories,
availableLoaders,
availableGameVersions,
availableLicenses,
license: {
short: mod.license.id,
name: mod.license.name,
},
license_url: mod.license.url,
availableDonationPlatforms,
donationPlatforms,
donationLinks,
}
} catch {
data.error({
statusCode: 404,
message: 'Mod not found',
})
}
},
data() {
return {
isProcessing: false,
previewImage: null,
compiledBody: '',
icon: null,
iconChanged: false,
sideTypes: ['Required', 'Optional', 'Unsupported'],
isEditing: true,
}
},
watch: {
license(newValue, oldValue) {
if (newValue == null) {
this.license_url = ''
return
}
switch (newValue.short) {
case 'custom':
this.license_url = ''
break
default:
this.license_url = `https://cdn.modrinth.com/licenses/${newValue.short}.txt`
}
},
},
mounted() {
function preventLeave(e) {
e.preventDefault()
e.returnValue = ''
}
window.addEventListener('beforeunload', preventLeave)
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('beforeunload', preventLeave)
})
},
beforeRouteLeave(to, from, next) {
if (
this.isEditing &&
!window.confirm('Are you sure that you want to leave without saving?')
) {
return
}
next()
},
created() {
this.$emit('update:link-bar', [['Edit', 'edit']])
},
methods: {
async saveModReview() {
this.isProcessing = true
await this.saveMod()
},
async saveMod() {
this.$nuxt.$loading.start()
try {
const data = {
title: this.mod.title,
description: this.mod.description,
body: this.mod.body,
categories: this.mod.categories,
issues_url: this.mod.issues_url,
source_url: this.mod.source_url,
wiki_url: this.mod.wiki_url,
license_url: this.license_url,
discord_url: this.mod.discord_url,
license_id: this.license.short,
client_side: this.clientSideType.toLowerCase(),
server_side: this.serverSideType.toLowerCase(),
slug: this.mod.slug,
license: this.license.short,
donation_urls: this.donationPlatforms.map((it, index) => {
return {
id: it.short,
platform: it.name,
url: this.donationLinks[index],
}
}),
}
if (this.isProcessing) {
data.status = 'processing'
}
await this.$axios.patch(`mod/${this.mod.id}`, data, this.$auth.headers)
if (this.iconChanged) {
await this.$axios.patch(
`mod/${this.mod.id}/icon?ext=${
this.icon.type.split('/')[this.icon.type.split('/').length - 1]
}`,
this.icon,
this.$auth.headers
)
}
this.isEditing = false
await this.$router.replace(
`/mod/${this.mod.slug ? this.mod.slug : this.mod.id}`
)
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response.data.description,
type: 'error',
})
window.scrollTo({ top: 0, behavior: 'smooth' })
}
this.$nuxt.$loading.finish()
},
showPreviewImage(files) {
const reader = new FileReader()
this.iconChanged = true
this.icon = files[0]
reader.readAsDataURL(this.icon)
reader.onload = (event) => {
this.previewImage = event.target.result
}
},
},
}
</script>
<style lang="scss" scoped>
.title {
* {
display: inline;
}
.button {
margin-left: 1rem;
}
}
label {
display: flex;
span {
flex: 2;
padding-right: var(--spacing-card-lg);
}
input,
.multiselect,
.input-group {
flex: 3;
height: fit-content;
}
}
.input-group {
display: flex;
flex-direction: column;
* {
margin-bottom: var(--spacing-card-sm);
}
}
.textarea-wrapper {
display: flex;
flex-direction: column;
align-items: stretch;
textarea {
flex: 1;
overflow-y: auto;
resize: none;
max-width: 100%;
}
}
.page-contents {
display: grid;
grid-template:
'header header header' auto
'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;
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);
h3 {
margin: auto 0;
color: var(--color-text-dark);
font-weight: var(--font-weight-extrabold);
}
button {
margin-left: 0.5rem;
}
}
.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;
img {
align-self: flex-start;
max-width: 50%;
margin-left: var(--spacing-card-lg);
}
}
section.game-sides {
grid-area: game-sides;
.columns {
flex-wrap: wrap;
span {
flex: 2;
}
.labeled-control {
flex: 2;
margin-left: var(--spacing-card-lg);
}
}
}
section.description {
grid-area: description;
& > .columns {
align-items: stretch;
min-height: 10rem;
max-height: 40rem;
& > * {
flex: 1;
max-width: 50%;
}
}
.markdown-body {
overflow-y: auto;
padding: 0 var(--spacing-card-sm);
}
}
section.extra-links {
grid-area: extra-links;
label {
align-items: center;
margin-top: var(--spacing-card-sm);
span {
flex: 1;
}
}
}
section.license {
grid-area: license;
label {
margin-top: var(--spacing-card-sm);
}
}
section.donations {
grid-area: donations;
label {
align-items: center;
margin-top: var(--spacing-card-sm);
span {
flex: 1;
}
}
}
.footer {
grid-area: footer;
}
.choose-image {
cursor: pointer;
}
a {
text-decoration: underline;
color: var(--color-link);
}
</style>