You've already forked AstralRinth
forked from didirus/AstralRinth
* 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>
1466 lines
38 KiB
Vue
1466 lines
38 KiB
Vue
<template>
|
|
<div class="page-container">
|
|
<div class="page-contents">
|
|
<header class="card columns">
|
|
<h3 class="column-grow-1">Create a project</h3>
|
|
<button
|
|
title="Save draft"
|
|
class="iconified-button column"
|
|
:disabled="!$nuxt.$loading"
|
|
@click="createDraft"
|
|
>
|
|
Save draft
|
|
</button>
|
|
<button
|
|
title="Submit for review"
|
|
class="iconified-button brand-button-colors column"
|
|
:disabled="!$nuxt.$loading"
|
|
@click="createProject"
|
|
>
|
|
<CheckIcon />
|
|
Submit for review
|
|
</button>
|
|
</header>
|
|
<section class="card essentials">
|
|
<h3>Name</h3>
|
|
<label>
|
|
<span>
|
|
Be creative! Generic project names will be harder to search for.
|
|
</span>
|
|
<input v-model="name" type="text" placeholder="Enter the name" />
|
|
</label>
|
|
<h3>Summary</h3>
|
|
<label>
|
|
<span>
|
|
Give a short description of your project that will appear on search
|
|
pages.
|
|
</span>
|
|
<input
|
|
v-model="description"
|
|
type="text"
|
|
placeholder="Enter the summary"
|
|
/>
|
|
</label>
|
|
<h3>Categories</h3>
|
|
<label>
|
|
<span>
|
|
Select up to 3 categories that will help others find your project.
|
|
</span>
|
|
<multiselect
|
|
id="categories"
|
|
v-model="categories"
|
|
:options="
|
|
$tag.categories
|
|
.filter((x) => x.project_type === projectType.toLowerCase())
|
|
.map((it) => it.name)
|
|
"
|
|
:loading="$tag.categories.length === 0"
|
|
:multiple="true"
|
|
:searchable="false"
|
|
:show-no-results="false"
|
|
:close-on-select="false"
|
|
:clear-on-select="false"
|
|
:show-labels="false"
|
|
:max="3"
|
|
:limit="6"
|
|
:hide-selected="true"
|
|
placeholder="Choose categories"
|
|
/>
|
|
</label>
|
|
<h3>Vanity URL (slug)</h3>
|
|
<label>
|
|
<span>
|
|
Set this to something that will looks nice in your project's URL.
|
|
</span>
|
|
<input
|
|
id="name"
|
|
v-model="slug"
|
|
type="text"
|
|
placeholder="Enter the vanity URL slug"
|
|
/>
|
|
</label>
|
|
<h3>Project type</h3>
|
|
<label>
|
|
<span>The project type of your project.</span>
|
|
<Multiselect
|
|
v-model="projectType"
|
|
placeholder="Select one"
|
|
:options="projectTypes"
|
|
:searchable="false"
|
|
:close-on-select="true"
|
|
:show-labels="false"
|
|
:allow-empty="false"
|
|
/>
|
|
</label>
|
|
</section>
|
|
<section class="card project-icon">
|
|
<h3>Icon</h3>
|
|
<img
|
|
:src="
|
|
previewImage
|
|
? previewImage
|
|
: '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"
|
|
/>
|
|
<button
|
|
class="iconified-button"
|
|
@click="
|
|
icon = null
|
|
previewImage = null
|
|
"
|
|
>
|
|
<TrashIcon />
|
|
Reset icon
|
|
</button>
|
|
</section>
|
|
<section class="card game-sides">
|
|
<h3>Supported environments</h3>
|
|
<div class="columns">
|
|
<span>
|
|
Let others know what environments your project supports.
|
|
</span>
|
|
<div class="labeled-control">
|
|
<h3>Client</h3>
|
|
<Multiselect
|
|
v-model="clientSideType"
|
|
placeholder="Select one"
|
|
:options="sideTypes"
|
|
:searchable="false"
|
|
:close-on-select="true"
|
|
:show-labels="false"
|
|
:allow-empty="false"
|
|
/>
|
|
</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="card description">
|
|
<h3>
|
|
<label
|
|
for="body"
|
|
title="You can type an extended description of your project here."
|
|
>
|
|
Body
|
|
</label>
|
|
</h3>
|
|
<span>
|
|
You can type an extended description of your mod here. This editor
|
|
supports Markdown. Its syntax can be found
|
|
<a
|
|
class="text-link"
|
|
href="https://guides.github.com/features/mastering-markdown/"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>here</a
|
|
>. HTML can also be used inside your description, excluding scripts
|
|
and iframes.
|
|
</span>
|
|
<ThisOrThat
|
|
v-model="bodyViewMode"
|
|
class="separator"
|
|
:items="['source', 'preview']"
|
|
/>
|
|
<div class="edit-wrapper">
|
|
<div v-if="bodyViewMode === 'source'" class="textarea-wrapper">
|
|
<textarea id="body" v-model="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>
|
|
<section class="card versions">
|
|
<div class="title">
|
|
<h3>Create versions</h3>
|
|
<button
|
|
title="Add a version"
|
|
class="iconified-button"
|
|
:disabled="currentVersionIndex !== -1"
|
|
@click="createVersion"
|
|
>
|
|
<PlusIcon />
|
|
Add a version
|
|
</button>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Version</th>
|
|
<th>Mod loader</th>
|
|
<th>Minecraft version</th>
|
|
<th>Channel</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="(version, index) in versions.filter((it) =>
|
|
currentVersionIndex !== -1
|
|
? it !== versions[currentVersionIndex]
|
|
: true
|
|
)"
|
|
:key="version.id"
|
|
>
|
|
<td>{{ version.version_title }}</td>
|
|
<td>{{ version.version_number }}</td>
|
|
<td>{{ version.loaders.join(', ') }}</td>
|
|
<td>{{ version.game_versions.join(', ') }}</td>
|
|
<td>
|
|
<VersionBadge
|
|
v-if="version.release_channel === 'release'"
|
|
type="release"
|
|
color="green"
|
|
/>
|
|
<VersionBadge
|
|
v-else-if="version.release_channel === 'beta'"
|
|
type="beta"
|
|
color="yellow"
|
|
/>
|
|
<VersionBadge
|
|
v-else-if="version.release_channel === 'alpha'"
|
|
type="alpha"
|
|
color="red"
|
|
/>
|
|
</td>
|
|
<td>
|
|
<button
|
|
class="iconified-button"
|
|
title="Remove version"
|
|
@click="versions.splice(index, 1)"
|
|
>
|
|
Remove
|
|
</button>
|
|
<button
|
|
class="iconified-button"
|
|
title="Edit version"
|
|
@click="currentVersionIndex = index"
|
|
>
|
|
Edit
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<hr v-if="currentVersionIndex !== -1" />
|
|
<div v-if="currentVersionIndex !== -1" class="new-version">
|
|
<div class="controls">
|
|
<button
|
|
class="brand-button-colors iconified-button"
|
|
title="Save version"
|
|
@click="currentVersionIndex = -1"
|
|
>
|
|
Save version
|
|
</button>
|
|
<button
|
|
class="iconified-button"
|
|
title="Discard version"
|
|
@click="deleteVersion"
|
|
>
|
|
Discard
|
|
</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="versions[currentVersionIndex].version_title"
|
|
type="text"
|
|
placeholder="Enter the name"
|
|
/>
|
|
</label>
|
|
<h3>Number</h3>
|
|
<label>
|
|
<span>
|
|
This is how your version will appear in project lists and URLs.
|
|
</span>
|
|
<input
|
|
v-model="versions[currentVersionIndex].version_number"
|
|
type="text"
|
|
placeholder="Enter the number"
|
|
/>
|
|
</label>
|
|
<h3>Channel</h3>
|
|
<label>
|
|
<span>
|
|
It is important to notify everyone whether the version is stable
|
|
or if it's still in development.
|
|
</span>
|
|
<multiselect
|
|
v-model="versions[currentVersionIndex].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="versions[currentVersionIndex].loaders"
|
|
:options="
|
|
$tag.loaders
|
|
.filter((x) =>
|
|
x.supported_project_types.includes(
|
|
projectType.toLowerCase()
|
|
)
|
|
)
|
|
.map((it) => it.name)
|
|
"
|
|
:loading="$tag.loaders.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 mod loaders..."
|
|
/>
|
|
</label>
|
|
<h3>Minecraft versions</h3>
|
|
<label>
|
|
<span>
|
|
Mark all Minecraft version this project's version supports.
|
|
</span>
|
|
<multiselect
|
|
v-model="versions[currentVersionIndex].game_versions"
|
|
:options="$tag.gameVersions.map((it) => it.version)"
|
|
:loading="$tag.gameVersions.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 must upload at least one file, however, you are allowed to
|
|
upload multiple files.
|
|
</span>
|
|
<FileInput
|
|
accept=".jar,application/java-archive,.zip,application/zip"
|
|
multiple
|
|
prompt="Choose files or drag them here"
|
|
@change="updateVersionFiles"
|
|
/>
|
|
</label>
|
|
</div>
|
|
<div class="dependencies">
|
|
<h3>Dependencies</h3>
|
|
<div class="dependency-selector">
|
|
<ThisOrThat
|
|
v-model="dependencyAddMode"
|
|
class="separator"
|
|
:items="['project', 'version']"
|
|
/>
|
|
<div class="edit-info">
|
|
<input
|
|
v-model="newDependencyId"
|
|
type="text"
|
|
:placeholder="`Enter the ${dependencyAddMode} id...`"
|
|
/>
|
|
<Multiselect
|
|
v-model="newDependencyType"
|
|
class="input"
|
|
placeholder="Select one"
|
|
:options="['required', 'optional', 'incompatible']"
|
|
:searchable="false"
|
|
:close-on-select="true"
|
|
:show-labels="false"
|
|
:allow-empty="false"
|
|
/>
|
|
<button class="iconified-button" @click="addDependency">
|
|
<PlusIcon />
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="new-dependencies">
|
|
<div
|
|
v-for="(dependency, index) in versions[currentVersionIndex]
|
|
.dependencies"
|
|
:key="index"
|
|
class="dependency"
|
|
>
|
|
<img
|
|
class="icon"
|
|
:src="
|
|
dependency.project
|
|
? dependency.project.icon_url
|
|
? dependency.project.icon_url
|
|
: 'https://cdn.modrinth.com/placeholder.svg?inline'
|
|
: 'https://cdn.modrinth.com/placeholder.svg?inline'
|
|
"
|
|
alt="dependency-icon"
|
|
/>
|
|
<div class="info">
|
|
<h4 class="title">
|
|
{{
|
|
dependency.project
|
|
? dependency.project.title
|
|
: 'Unknown Project'
|
|
}}
|
|
</h4>
|
|
<p v-if="dependency.version" class="version-number">
|
|
Version {{ dependency.version.version_number }} is
|
|
{{ dependency.dependency_type }}
|
|
</p>
|
|
<div class="bottom">
|
|
<button
|
|
class="iconified-button"
|
|
@click="
|
|
versions[currentVersionIndex].dependencies.splice(
|
|
index,
|
|
1
|
|
)
|
|
"
|
|
>
|
|
<TrashIcon /> Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="changelog">
|
|
<h3>Changes</h3>
|
|
<span>
|
|
Tell everyone what's new. It supports the same Markdown formatting
|
|
as the description.
|
|
</span>
|
|
<ThisOrThat
|
|
v-model="changelogViewMode"
|
|
class="separator"
|
|
:items="['source', 'preview']"
|
|
/>
|
|
<div class="edit-wrapper">
|
|
<div
|
|
v-if="changelogViewMode === 'source'"
|
|
class="textarea-wrapper"
|
|
>
|
|
<textarea
|
|
id="body"
|
|
v-model="versions[currentVersionIndex].changelog"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-if="changelogViewMode === 'preview'"
|
|
v-highlightjs
|
|
class="markdown-body"
|
|
v-html="
|
|
versions[currentVersionIndex].changelog
|
|
? $xss($md.render(versions[currentVersionIndex].changelog))
|
|
: 'No changelog specified.'
|
|
"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="card gallery">
|
|
<div class="title">
|
|
<div class="text">
|
|
<h3>Gallery</h3>
|
|
<i>— this section is optional</i>
|
|
</div>
|
|
<button
|
|
title="Add an image"
|
|
class="iconified-button"
|
|
:disabled="false"
|
|
@click="gallery.push({})"
|
|
>
|
|
<PlusIcon />
|
|
Add a image
|
|
</button>
|
|
</div>
|
|
<div v-for="(item, index) in gallery" :key="index" class="gallery-item">
|
|
<div class="info">
|
|
<label title="The title of the gallery item">
|
|
<span>Title</span>
|
|
<input
|
|
v-model="item.title"
|
|
type="text"
|
|
placeholder="Enter a title"
|
|
/>
|
|
</label>
|
|
<label
|
|
title="The description of what is being featured in the link."
|
|
>
|
|
<span>Description (optional)</span>
|
|
<input
|
|
v-model="item.description"
|
|
type="text"
|
|
placeholder="Enter a description"
|
|
/>
|
|
</label>
|
|
</div>
|
|
<div class="image">
|
|
<img
|
|
:src="
|
|
item.preview
|
|
? item.preview
|
|
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
|
"
|
|
alt="preview-image"
|
|
/>
|
|
<div class="bottom">
|
|
<SmartFileInput
|
|
accept="image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp"
|
|
prompt="Upload"
|
|
@change="(files) => showGalleryPreviewImage(files, index)"
|
|
/>
|
|
<button
|
|
class="iconified-button"
|
|
@click="
|
|
item.icon = null
|
|
item.preview = null
|
|
$forceUpdate()
|
|
"
|
|
>
|
|
<TrashIcon />
|
|
Reset image
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="buttons">
|
|
<button class="iconified-button" @click="gallery.splice(index, 1)">
|
|
<TrashIcon />
|
|
Remove Gallery Image
|
|
</button>
|
|
|
|
<hr v-if="gallery.length > 0 && index !== gallery.length - 1" />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="card extra-links">
|
|
<div class="title">
|
|
<div class="text">
|
|
<h3>External links</h3>
|
|
<i>— this section is optional</i>
|
|
</div>
|
|
</div>
|
|
<label
|
|
title="A place for users to report bugs, issues, and concerns about your project."
|
|
>
|
|
<span>Issue tracker</span>
|
|
<input
|
|
v-model="issues_url"
|
|
type="url"
|
|
placeholder="Enter a valid URL"
|
|
/>
|
|
</label>
|
|
<label
|
|
title="A page/repository containing the source code for your project"
|
|
>
|
|
<span>Source code</span>
|
|
<input
|
|
v-model="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="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="discord_url"
|
|
type="url"
|
|
placeholder="Enter a valid URL"
|
|
/>
|
|
</label>
|
|
</section>
|
|
<section class="card license">
|
|
<div class="title">
|
|
<div class="text">
|
|
<h3>License</h3>
|
|
<i>— this section is optional</i>
|
|
</div>
|
|
</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
|
|
class="text-link"
|
|
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"
|
|
:searchable="true"
|
|
:options="$tag.licenses"
|
|
:close-on-select="true"
|
|
:show-labels="false"
|
|
/>
|
|
<input v-model="license_url" type="url" placeholder="License URL" />
|
|
</div>
|
|
</label>
|
|
</section>
|
|
<section class="card donations">
|
|
<div class="title">
|
|
<div class="text">
|
|
<h3>Donation links</h3>
|
|
<i>— this section is optional</i>
|
|
</div>
|
|
<button
|
|
title="Add a link"
|
|
class="iconified-button"
|
|
:disabled="false"
|
|
@click="
|
|
donationPlatforms.push({})
|
|
donationLinks.push('')
|
|
"
|
|
>
|
|
<PlusIcon />
|
|
Add a link
|
|
</button>
|
|
</div>
|
|
<div v-for="(item, index) in donationPlatforms" :key="index">
|
|
<label title="The donation link.">
|
|
<span>Donation Link</span>
|
|
<input
|
|
v-model="donationLinks[index]"
|
|
type="url"
|
|
placeholder="Enter a valid URL"
|
|
/>
|
|
</label>
|
|
<label title="The donation platform of the link.">
|
|
<span>Donation Platform</span>
|
|
<Multiselect
|
|
v-model="donationPlatforms[index]"
|
|
placeholder="Select one"
|
|
track-by="short"
|
|
label="name"
|
|
:options="$tag.donationPlatforms"
|
|
:searchable="false"
|
|
:close-on-select="true"
|
|
:show-labels="false"
|
|
/>
|
|
</label>
|
|
<button
|
|
class="iconified-button"
|
|
@click="
|
|
donationPlatforms.splice(index, 1)
|
|
donationLinks.splice(index, 1)
|
|
"
|
|
>
|
|
<TrashIcon />
|
|
Remove Link
|
|
</button>
|
|
<hr
|
|
v-if="
|
|
donationPlatforms.length > 0 &&
|
|
index !== donationPlatforms.length - 1
|
|
"
|
|
/>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import Multiselect from 'vue-multiselect'
|
|
import FileInput from '~/components/ui/FileInput'
|
|
import SmartFileInput from '~/components/ui/SmartFileInput'
|
|
import ThisOrThat from '~/components/ui/ThisOrThat'
|
|
import VersionBadge from '~/components/ui/Badge'
|
|
|
|
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
|
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
|
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
|
|
|
export default {
|
|
components: {
|
|
FileInput,
|
|
SmartFileInput,
|
|
Multiselect,
|
|
CheckIcon,
|
|
PlusIcon,
|
|
TrashIcon,
|
|
ThisOrThat,
|
|
VersionBadge,
|
|
},
|
|
beforeRouteLeave(to, from, next) {
|
|
if (
|
|
this.isEditing &&
|
|
!window.confirm('Are you sure that you want to leave without saving?')
|
|
) {
|
|
return
|
|
}
|
|
next()
|
|
},
|
|
data() {
|
|
return {
|
|
previewImage: null,
|
|
compiledBody: '',
|
|
releaseChannels: ['beta', 'alpha', 'release'],
|
|
currentVersionIndex: -1,
|
|
name: '',
|
|
slug: '',
|
|
draft: false,
|
|
description: '',
|
|
body: '',
|
|
versions: [],
|
|
categories: [],
|
|
issues_url: null,
|
|
source_url: null,
|
|
wiki_url: null,
|
|
discord_url: null,
|
|
icon: null,
|
|
license: null,
|
|
license_url: null,
|
|
|
|
projectTypes: ['Mod', 'Modpack'],
|
|
projectType: 'Mod',
|
|
|
|
sideTypes: ['Required', 'Optional', 'Unsupported'],
|
|
clientSideType: 'Required',
|
|
serverSideType: 'Required',
|
|
|
|
donationLinks: [],
|
|
donationPlatforms: [],
|
|
|
|
gallery: [],
|
|
|
|
isEditing: true,
|
|
bodyViewMode: 'source',
|
|
changelogViewMode: 'source',
|
|
|
|
newDependencyType: 'required',
|
|
dependencyAddMode: 'project',
|
|
newDependencyId: '',
|
|
}
|
|
},
|
|
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)
|
|
})
|
|
},
|
|
methods: {
|
|
async createDraft() {
|
|
this.draft = true
|
|
await this.createProject()
|
|
},
|
|
async createProject() {
|
|
this.$nuxt.$loading.start()
|
|
|
|
for (const version of this.versions) {
|
|
if (!version.version_title) {
|
|
version.version_title = version.version_number
|
|
}
|
|
}
|
|
|
|
const formData = new FormData()
|
|
|
|
formData.append(
|
|
'data',
|
|
JSON.stringify({
|
|
title: this.name,
|
|
project_type: this.projectType.toLowerCase(),
|
|
slug: this.slug,
|
|
description: this.description,
|
|
body: this.body,
|
|
initial_versions: this.versions,
|
|
team_members: [
|
|
{
|
|
user_id: this.$auth.user.id,
|
|
name: this.$auth.user.username,
|
|
role: 'Owner',
|
|
},
|
|
],
|
|
categories: this.categories,
|
|
issues_url: this.issues_url,
|
|
source_url: this.source_url,
|
|
wiki_url: this.wiki_url,
|
|
discord_url: this.discord_url,
|
|
client_side: this.clientSideType.toLowerCase(),
|
|
server_side: this.serverSideType.toLowerCase(),
|
|
license_id: this.license ? this.license.short : 'arr',
|
|
license_url: this.license_url,
|
|
is_draft: this.draft,
|
|
donation_urls: this.donationPlatforms.map((it, index) => {
|
|
return {
|
|
id: it.short,
|
|
platform: it.name,
|
|
url: this.donationLinks[index],
|
|
}
|
|
}),
|
|
gallery_items: this.gallery.map((x, index) => {
|
|
return {
|
|
item: `gallery-${index}`,
|
|
featured: false,
|
|
title: x.title,
|
|
description: x.description,
|
|
}
|
|
}),
|
|
})
|
|
)
|
|
|
|
if (this.icon) {
|
|
formData.append('icon', new Blob([this.icon]), this.icon.name)
|
|
}
|
|
|
|
for (let i = 0; i < this.gallery.length; i++) {
|
|
formData.append(
|
|
`gallery-${i}`,
|
|
new Blob([this.gallery[i].icon]),
|
|
this.gallery[i].icon.name
|
|
)
|
|
}
|
|
|
|
for (let i = 0; i < this.versions.length; i++) {
|
|
const version = this.versions[i]
|
|
for (let j = 0; j < version.raw_files.length; j++) {
|
|
formData.append(
|
|
`version-${i}-${j}`,
|
|
new Blob([version.raw_files[j]]),
|
|
version.raw_files[j].name
|
|
)
|
|
}
|
|
}
|
|
|
|
try {
|
|
await this.$axios({
|
|
url: 'project',
|
|
method: 'POST',
|
|
data: formData,
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
Authorization: this.$auth.token,
|
|
},
|
|
})
|
|
|
|
this.isEditing = false
|
|
await this.$store.dispatch('user/fetchProjects')
|
|
|
|
await this.$router.replace(`/user/${this.$auth.user.username}`)
|
|
} catch (err) {
|
|
let description = err.response.data.description
|
|
|
|
if (description.includes('JSON')) {
|
|
description = 'Please fill in missing required fields.'
|
|
}
|
|
|
|
this.$notify({
|
|
group: 'main',
|
|
title: 'An error occurred',
|
|
text: description,
|
|
type: 'error',
|
|
})
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}
|
|
|
|
this.$nuxt.$loading.finish()
|
|
},
|
|
|
|
showPreviewImage(files) {
|
|
const reader = new FileReader()
|
|
this.icon = files[0]
|
|
|
|
if (this.icon instanceof Blob) {
|
|
reader.readAsDataURL(this.icon)
|
|
|
|
reader.onload = (event) => {
|
|
this.previewImage = event.target.result
|
|
}
|
|
}
|
|
},
|
|
|
|
showGalleryPreviewImage(files, index) {
|
|
const reader = new FileReader()
|
|
this.gallery[index].icon = files[0]
|
|
|
|
if (this.gallery[index].icon instanceof Blob) {
|
|
reader.readAsDataURL(this.gallery[index].icon)
|
|
|
|
reader.onload = (event) => {
|
|
this.gallery[index].preview = event.target.result
|
|
|
|
// TODO: Find an alternative for this!
|
|
this.$forceUpdate()
|
|
}
|
|
}
|
|
},
|
|
|
|
updateVersionFiles(files) {
|
|
this.versions[this.currentVersionIndex].raw_files = files
|
|
|
|
const newFileParts = []
|
|
for (let i = 0; i < files.length; i++) {
|
|
newFileParts.push(`version-${this.currentVersionIndex}-${i}`)
|
|
}
|
|
|
|
this.versions[this.currentVersionIndex].file_parts = newFileParts
|
|
},
|
|
|
|
createVersion() {
|
|
this.versions.push({
|
|
raw_files: [],
|
|
file_parts: [],
|
|
version_number: '',
|
|
version_title: '',
|
|
version_body: '',
|
|
dependencies: [],
|
|
game_versions: [],
|
|
release_channel: 'release',
|
|
loaders: [],
|
|
featured: false,
|
|
})
|
|
|
|
this.currentVersionIndex = this.versions.length - 1
|
|
},
|
|
|
|
deleteVersion() {
|
|
this.versions.splice(this.currentVersionIndex, 1)
|
|
this.currentVersionIndex = -1
|
|
},
|
|
|
|
async addDependency() {
|
|
try {
|
|
if (this.dependencyAddMode === 'project') {
|
|
const project = (
|
|
await this.$axios.get(`project/${this.newDependencyId}`)
|
|
).data
|
|
|
|
this.versions[this.currentVersionIndex].dependencies.push({
|
|
project,
|
|
project_id: project.id,
|
|
dependency_type: this.newDependencyType,
|
|
})
|
|
} else if (this.dependencyAddMode === 'version') {
|
|
const version = (
|
|
await this.$axios.get(`version/${this.newDependencyId}`)
|
|
).data
|
|
const project = (
|
|
await this.$axios.get(`project/${version.project_id}`)
|
|
).data
|
|
|
|
this.versions[this.currentVersionIndex].dependencies.push({
|
|
version,
|
|
project,
|
|
version_id: version.id,
|
|
project_id: project.id,
|
|
dependency_type: this.newDependencyType,
|
|
})
|
|
}
|
|
} catch {
|
|
this.$notify({
|
|
group: 'main',
|
|
title: 'Invalid Dependency',
|
|
text: 'The specified dependency does not exist',
|
|
type: 'error',
|
|
})
|
|
}
|
|
|
|
this.newDependencyId = ''
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.title {
|
|
.text * {
|
|
display: inline;
|
|
}
|
|
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.page-contents {
|
|
display: grid;
|
|
grid-template:
|
|
'header header header' auto
|
|
'essentials essentials essentials' auto
|
|
'project-icon project-icon project-icon' auto
|
|
'game-sides game-sides game-sides' auto
|
|
'description description description' auto
|
|
'versions versions versions' auto
|
|
'gallery gallery gallery' auto
|
|
'extra-links extra-links extra-links' auto
|
|
'license license license' auto
|
|
'donations donations donations' auto
|
|
'footer footer footer' auto
|
|
/ 4fr 1fr 4fr;
|
|
|
|
@media screen and (min-width: 1024px) {
|
|
grid-template:
|
|
'header header header' auto
|
|
'essentials essentials project-icon' auto
|
|
'game-sides game-sides game-sides' auto
|
|
'description description description' auto
|
|
'versions versions versions' auto
|
|
'gallery gallery gallery' auto
|
|
'extra-links license license' auto
|
|
'donations donations donations' auto
|
|
'footer footer footer' auto
|
|
/ 4fr 2fr 1.5fr;
|
|
}
|
|
|
|
column-gap: var(--spacing-card-md);
|
|
row-gap: var(--spacing-card-md);
|
|
}
|
|
|
|
header {
|
|
grid-area: header;
|
|
|
|
h3 {
|
|
margin: auto 0;
|
|
color: var(--color-text-dark);
|
|
font-weight: var(--font-weight-extrabold);
|
|
}
|
|
|
|
button {
|
|
margin-left: 0.5rem;
|
|
}
|
|
}
|
|
|
|
section.essentials {
|
|
grid-area: essentials;
|
|
}
|
|
|
|
section.project-icon {
|
|
grid-area: project-icon;
|
|
|
|
img {
|
|
max-width: 100%;
|
|
margin-bottom: 1rem;
|
|
border-radius: var(--size-rounded-lg);
|
|
}
|
|
|
|
.iconified-button {
|
|
width: 9rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
}
|
|
|
|
section.game-sides {
|
|
grid-area: game-sides;
|
|
|
|
.columns {
|
|
flex-wrap: wrap;
|
|
|
|
span {
|
|
flex: 2;
|
|
}
|
|
|
|
.labeled-control {
|
|
flex: 2;
|
|
margin-left: var(--spacing-card-lg);
|
|
}
|
|
}
|
|
}
|
|
|
|
section.description {
|
|
grid-area: description;
|
|
|
|
.separator {
|
|
margin: var(--spacing-card-sm) 0;
|
|
}
|
|
|
|
.edit-wrapper * {
|
|
min-height: 10rem;
|
|
max-height: 40rem;
|
|
}
|
|
|
|
.markdown-body {
|
|
overflow-y: auto;
|
|
padding: 0 var(--spacing-card-sm);
|
|
}
|
|
}
|
|
|
|
section.versions {
|
|
grid-area: versions;
|
|
|
|
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%;
|
|
|
|
svg {
|
|
color: var(--color-text);
|
|
|
|
&:hover,
|
|
&:focus {
|
|
color: var(--color-text-hover);
|
|
}
|
|
}
|
|
}
|
|
|
|
&:nth-child(2),
|
|
&:nth-child(5) {
|
|
padding-left: 0;
|
|
width: 12%;
|
|
}
|
|
|
|
&:last-child {
|
|
display: flex;
|
|
}
|
|
}
|
|
|
|
th {
|
|
color: var(--color-heading);
|
|
font-size: 0.8rem;
|
|
letter-spacing: 0.02rem;
|
|
padding: 0.75rem 1rem;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
td {
|
|
overflow: hidden;
|
|
padding: 0.75rem 1rem;
|
|
|
|
img {
|
|
height: 3rem;
|
|
width: 3rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
hr {
|
|
background-color: var(--color-divider);
|
|
border: none;
|
|
color: var(--color-divider);
|
|
height: 2px;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.new-version {
|
|
display: grid;
|
|
grid-template:
|
|
'controls controls' auto
|
|
'main main' auto
|
|
'dependencies dependencies' auto
|
|
'changelog changelog'
|
|
/ 5fr 4fr;
|
|
|
|
@media screen and (min-width: 1024px) {
|
|
grid-template:
|
|
'controls controls' auto
|
|
'main dependencies' auto
|
|
'changelog changelog'
|
|
/ 5fr 4fr;
|
|
}
|
|
|
|
column-gap: var(--spacing-card-sm);
|
|
|
|
.controls {
|
|
grid-area: controls;
|
|
display: flex;
|
|
flex-direction: row-reverse;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.main {
|
|
grid-area: main;
|
|
}
|
|
|
|
.dependencies {
|
|
grid-area: dependencies;
|
|
|
|
.dependency-selector {
|
|
.separator {
|
|
margin: var(--spacing-card-sm) 0;
|
|
}
|
|
|
|
h4 {
|
|
margin: 0 0 0.25rem 0;
|
|
}
|
|
|
|
.edit-info {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
row-gap: 0.25rem;
|
|
|
|
.multiselect {
|
|
max-width: 10rem;
|
|
}
|
|
|
|
.iconified-button {
|
|
min-height: 2.5rem;
|
|
}
|
|
|
|
input,
|
|
.multiselect,
|
|
.iconified-button {
|
|
margin: 0 0.5rem 0 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.new-dependencies {
|
|
margin-top: var(--spacing-card-sm);
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
|
|
.dependency {
|
|
align-items: center;
|
|
display: flex;
|
|
flex-basis: 50%;
|
|
margin-bottom: 0.5rem;
|
|
|
|
.icon {
|
|
width: 3rem;
|
|
height: 3rem;
|
|
margin-right: 0.5rem;
|
|
border-radius: var(--size-rounded-xs);
|
|
object-fit: contain;
|
|
}
|
|
|
|
.info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
padding: 0.25rem;
|
|
|
|
p {
|
|
margin: 0;
|
|
}
|
|
|
|
.title {
|
|
margin: 0 0.25rem 0 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.changelog {
|
|
grid-area: changelog;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.separator {
|
|
margin: var(--spacing-card-sm) 0;
|
|
}
|
|
|
|
.edit-wrapper {
|
|
flex: 1;
|
|
|
|
.textarea-wrapper {
|
|
min-height: 10rem;
|
|
height: 100%;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
section.gallery {
|
|
grid-area: gallery;
|
|
|
|
.gallery-item {
|
|
margin-top: 1rem;
|
|
display: grid;
|
|
grid-template:
|
|
'info' auto
|
|
'image' auto
|
|
'buttons' auto
|
|
/ 1fr;
|
|
|
|
@media screen and (min-width: 1024px) {
|
|
grid-template:
|
|
'info image' auto
|
|
'buttons buttons'
|
|
/ 2fr 1fr;
|
|
}
|
|
|
|
column-gap: var(--spacing-card-md);
|
|
row-gap: var(--spacing-card-sm);
|
|
|
|
.info {
|
|
grid-area: info;
|
|
|
|
label {
|
|
align-items: center;
|
|
margin-top: var(--spacing-card-sm);
|
|
|
|
span {
|
|
flex: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
.image {
|
|
grid-area: image;
|
|
|
|
img {
|
|
width: 100%;
|
|
margin-top: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
|
|
height: 14rem;
|
|
object-fit: cover;
|
|
|
|
border-radius: var(--size-rounded-card);
|
|
}
|
|
|
|
.bottom {
|
|
display: flex;
|
|
|
|
input,
|
|
button {
|
|
margin-right: 0.5rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
.buttons {
|
|
grid-area: buttons;
|
|
|
|
hr {
|
|
margin-top: 0.5rem;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
section.extra-links {
|
|
grid-area: extra-links;
|
|
|
|
label {
|
|
align-items: center;
|
|
margin-top: var(--spacing-card-sm);
|
|
|
|
span {
|
|
flex: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
section.license {
|
|
grid-area: license;
|
|
|
|
label {
|
|
margin-top: var(--spacing-card-sm);
|
|
}
|
|
}
|
|
|
|
section.donations {
|
|
grid-area: donations;
|
|
|
|
label {
|
|
align-items: center;
|
|
margin-top: var(--spacing-card-sm);
|
|
|
|
span {
|
|
flex: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
.footer {
|
|
grid-area: footer;
|
|
}
|
|
|
|
.choose-image {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.card {
|
|
margin-bottom: 0;
|
|
}
|
|
</style>
|