Files
AstralRinth/pages/mod/_id/edit.vue
Geometrically 5017c5a5f1 Use base URL for axios (#241)
* Switch site to use axios base url

* Fix team invites

* Fix find/replace setting the wrong thing

* Fix analytics being blocking, small issues
2021-05-28 10:19:13 -07:00

673 lines
16 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'"
title="Submit for Review"
class="button column"
:disabled="!this.$nuxt.$loading"
@click="saveModReview"
>
Submit for Review
</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 description to your mod. It will appear in the search
</span>
<input
v-model="mod.description"
type="text"
placeholder="Enter the summary"
/>
</label>
<h3>Categories</h3>
<label>
<span>
Select up to 3 categories. They will help to 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 URLs to your mod are 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 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 universal. For
example, IC2 will be required + required, while OptiFine will be
required + no functionality
</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 the of the long form of your description here."
>
Description
</label>
</h3>
<span>
You can type the of the long form of your description here. This editor
supports markdown. You can find the syntax
<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">
<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 inivitation 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 really important to choose a proper license for your mod. You
may choose one from our list or provide a URL to your own license. URL
field will be filled automatically for provided licenses
</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)
}
}
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'],
}
},
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`
}
},
},
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
)
}
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;
}
</style>