Nuxt Season Finale (#531)

Co-authored-by: Emma Cypress Pointer-Null <emmaffle@modrinth.com>
This commit is contained in:
Prospector
2022-06-18 18:39:53 -07:00
committed by GitHub
parent 2bda7566b4
commit 405a3eda60
26 changed files with 1690 additions and 952 deletions

View File

@@ -1,10 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10"></circle>
<line x1="14.31" y1="8" x2="20.05" y2="17.94"></line>
<line x1="9.69" y1="8" x2="21.17" y2="8"></line>
<line x1="7.38" y1="12" x2="13.12" y2="2.06"></line>
<line x1="9.69" y1="16" x2="3.95" y2="6.06"></line>
<line x1="14.31" y1="16" x2="2.83" y2="16"></line>
<line x1="16.62" y1="12" x2="10.88" y2="21.94"></line>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3" />
</svg>

Before

Width:  |  Height:  |  Size: 552 B

After

Width:  |  Height:  |  Size: 342 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path>
<path d="m9 12 2 2 4-4"></path>
</svg>

After

Width:  |  Height:  |  Size: 315 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -454,6 +454,15 @@
border-top-right-radius: var(--size-rounded-card) !important;
}
.known-error .multiselect__tags {
border-color: var(--color-badge-red-bg) !important;
background-color: var(--color-warning-bg) !important;
&::placeholder {
color: var(--color-warning-text);
}
}
.multiselect {
color: var(--color-text) !important;
@@ -554,6 +563,7 @@ label {
@media screen and (min-width: 1024px) {
flex-direction: row;
align-items: center;
}
span {
@@ -814,6 +824,26 @@ label {
}
}
.vue-notification {
background: #44A4FC;
border-left: 5px solid #44A4FC;
&.success {
background: #68CD86;
border-left-color: #68CD86;
}
&.warn {
background: #ffb648;
border-left-color: #ffb648;
}
&.error {
background: #E54D42;
border-left-color: #E54D42;
}
}
.vue-notification-group {
right: 25px !important;
@@ -839,3 +869,17 @@ label {
height: 1px;
margin: var(--spacing-card-bg) 0;
}
input.known-error,
textarea.known-error {
border-color: var(--color-badge-red-bg) !important;
background-color: var(--color-warning-bg) !important;
&::placeholder {
color: var(--color-warning-text);
}
}
.known-errors {
color: var(--color-badge-red-bg);
}

View File

@@ -247,7 +247,7 @@ h2 {
h3 {
margin-top: 0.5rem;
margin-bottom: 0;
margin-bottom: 0.25rem;
color: var(--color-text-dark);
}
@@ -258,6 +258,7 @@ button {
input {
border-radius: 2rem;
box-sizing: border-box;
}
pre {

View File

@@ -1,88 +0,0 @@
<template>
<label class="button" @drop.prevent="addFile" @dragover.prevent>
<span>
{{ text }}
</span>
<input
type="file"
:multiple="multiple"
:accept="accept"
@change="onChange"
/>
</label>
</template>
<script>
export default {
name: 'FileInput',
props: {
prompt: {
type: String,
default: 'Select file',
},
multiple: {
type: Boolean,
default: false,
},
accept: {
type: String,
default: null,
},
},
data() {
return {
text: this.prompt,
files: [],
}
},
methods: {
onChange(files, shouldNotReset) {
if (!shouldNotReset) this.files = files.target.files
const length = this.files.length
if (length === 0) {
this.text = this.prompt
} else if (length === 1) {
this.text = '1 file selected'
} else if (length > 1) {
this.text = length + ' files selected'
}
this.$emit('change', this.files)
},
addFile(e) {
const droppedFiles = e.dataTransfer.files
if (!this.multiple) this.files = []
if (!droppedFiles) return
;[...droppedFiles].forEach((f) => {
this.files.push(f)
})
if (!this.multiple && this.files.length > 0) this.files = [this.files[0]]
if (this.files.length > 0) this.onChange(null, true)
},
},
}
</script>
<style lang="scss" scoped>
label {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: var(--spacing-card-sm) var(--spacing-card-md);
}
span {
border: 2px dashed var(--color-divider-dark);
border-radius: var(--size-rounded-control);
padding: var(--spacing-card-md) var(--spacing-card-lg);
}
input {
display: none;
}
</style>

View File

@@ -2,7 +2,7 @@
<div class="columns">
<label class="button" @drop.prevent="addFile" @dragover.prevent>
<span>
<UploadIcon />
<UploadIcon v-if="showIcon" />
{{ prompt }}
</span>
<input
@@ -36,6 +36,14 @@ export default {
type: String,
default: null,
},
maxSize: {
type: Number,
default: null,
},
showIcon: {
type: Boolean,
default: true,
},
},
data() {
return {
@@ -46,7 +54,26 @@ export default {
onChange(files, shouldNotReset) {
if (!shouldNotReset) this.files = files.target.files
this.$emit('change', this.files)
this.files = [...this.files].filter((file) => {
if (this.maxSize === null) {
return true
} else if (file.size > this.maxSize) {
console.log('File size: ' + file.size + ', max size: ' + this.maxSize)
alert(
'File ' +
file.name +
' is too big! Must be less than ' +
this.$formatBytes(this.maxSize)
)
return false
} else {
return true
}
})
if (this.files.length > 0) {
this.$emit('change', this.files)
}
},
addFile(e) {
const droppedFiles = e.dataTransfer.files
@@ -74,7 +101,6 @@ label {
justify-content: center;
text-align: center;
padding: var(--spacing-card-sm) var(--spacing-card-md);
margin-bottom: var(--spacing-card-sm);
}
span {
@@ -95,4 +121,17 @@ span {
input {
display: none;
}
.known-error label {
border-color: var(--color-badge-red-bg) !important;
background-color: var(--color-warning-bg) !important;
span {
border-color: var(--color-badge-red-bg);
}
&::placeholder {
color: var(--color-warning-text);
}
}
</style>

View File

@@ -6,9 +6,8 @@
<Multiselect
v-if="getValidLoaders().length > 1"
v-model="selectedLoader"
:options="
getValidLoaders().map((x) => x.charAt(0).toUpperCase() + x.slice(1))
"
:options="getValidLoaders()"
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
:multiple="false"
:searchable="false"
:show-no-results="false"
@@ -60,7 +59,7 @@
updateVersionFilters()
"
>
<CrossIcon />
<ClearIcon />
Clear filters
</button>
</div>
@@ -69,14 +68,14 @@
<script>
import Multiselect from 'vue-multiselect'
import Checkbox from '~/components/ui/Checkbox'
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import ClearIcon from '~/assets/images/utils/clear.svg?inline'
export default {
name: 'VersionFilterControl',
components: {
Multiselect,
Checkbox,
CrossIcon,
ClearIcon,
},
props: {
versions: {
@@ -123,9 +122,10 @@ export default {
(projectVersion) =>
(this.selectedGameVersions.length === 0 ||
this.selectedGameVersions.some((gameVersion) =>
projectVersion.game_versions.includes(gameVersion.toLowerCase())
projectVersion.game_versions.includes(gameVersion)
)) &&
projectVersion.loaders.includes(this.selectedLoader.toLowerCase())
(this.selectedLoader === null ||
projectVersion.loaders.includes(this.selectedLoader))
)
this.$emit('updateVersions', temp)
},

View File

@@ -56,6 +56,21 @@
{{ $user.notifications.length }}
</div>
</nuxt-link>
<nuxt-link
v-if="
$auth.user &&
($auth.user.role === 'moderator' ||
$auth.user.role === 'admin')
"
to="/moderation"
class="control-button"
title="Moderation"
>
<ModerationIcon aria-hidden="true" />
<div v-if="moderationNotifications > 0" class="bubble">
{{ moderationNotifications }}
</div>
</nuxt-link>
<div v-if="$auth.user" ref="mobileMenu" class="dropdown">
<button class="control" value="Profile Dropdown">
<img
@@ -230,6 +245,7 @@
position="bottom right"
:max="5"
:ignore-duplicates="true"
:duration="10000"
/>
<Nuxt id="main" />
</main>
@@ -237,16 +253,23 @@
<div class="logo-info" role="region" aria-label="Modrinth information">
<ModrinthLogo aria-hidden="true" class="text-logo" />
<p>
Modrinth is open source software. You may view the source code at
Modrinth is
<a
target="_blank"
href="https://github.com/modrinth/knossos"
href="https://github.com/modrinth"
class="text-link"
>
our GitHub page</a
open source</a
>.
</p>
<p>
{{ owner }}/{{ slug }} {{ branch }}@<a
target="_blank"
:href="'https://github.com/' + owner + '/' + slug + '/tree/' + hash"
class="text-link"
>{{ hash.substring(0, 7) }}</a
>
</p>
<p>{{ owner }}/{{ slug }} {{ branch }}@{{ hash.substring(0, 7) }}</p>
<p>© Rinth, Inc.</p>
</div>
<div class="links links-1" role="region" aria-label="Legal">
@@ -343,6 +366,7 @@ export default {
hash: process.env.hash || 'unknown',
isMobileMenuOpen: false,
registeredSkipLink: null,
moderationNotifications: 0,
}
},
async fetch() {
@@ -351,6 +375,19 @@ export default {
this.$store.dispatch('tag/fetchAllTags'),
this.$store.dispatch('cosmetics/fetchCosmetics', this.$cookies),
])
if (
(this.$auth.user && this.$auth.user.role === 'moderator') ||
this.$auth.user.role === 'admin'
) {
const [projects, reports] = (
await Promise.all([
this.$axios.get(`moderation/projects`, this.$auth.headers),
this.$axios.get(`report`, this.$auth.headers),
])
).map((it) => it.data)
this.moderationNotifications = projects.length + reports.length
}
},
computed: {
authUrl() {
@@ -416,6 +453,16 @@ export default {
removeFocus() {
document.activeElement.blur() // This doesn't work, sadly. Help
},
async getModerationCount() {
const [projects, reports] = (
await Promise.all([
this.$axios.get(`moderation/projects`, this.$auth.headers),
this.$axios.get(`report`, this.$auth.headers),
])
).map((it) => it.data)
return projects.length + reports.length
},
},
}
</script>

View File

@@ -271,7 +271,7 @@ export default {
breaks: false,
},
loading: {
color: 'green',
color: '#1bd96a',
height: '2px',
},
env: {

View File

@@ -139,8 +139,11 @@
</div>
<div
v-if="
currentMember &&
(project.status === 'processing' ||
(currentMember ||
($auth.user &&
($auth.user.role === 'moderator' ||
$auth.user.role === 'admin'))) &&
(project.status !== 'approved' ||
(project.moderator_message &&
(project.moderator_message.message ||
project.moderator_message.body)))
@@ -193,22 +196,44 @@
</div>
<div class="buttons">
<button
v-if="
project.status !== 'processing' && project.status !== 'approved'
"
class="iconified-button"
v-if="project.status === 'rejected'"
class="iconified-button brand-button-colors"
@click="submitForReview"
>
Resubmit for approval
<CheckIcon />
Resubmit for review
</button>
<button
v-if="project.status === 'draft'"
class="iconified-button brand-button-colors"
@click="submitForReview"
>
<CheckIcon />
Submit for review
</button>
<button
v-if="project.status === 'approved'"
class="iconified-button"
@click="clearMessage"
>
<ClearIcon />
Clear message
</button>
</div>
<div v-if="showKnownErrors" class="known-errors">
<ul>
<li v-if="project.body === ''">
Your project must have a body to submit for review.
</li>
<li v-if="project.versions.length < 1">
Your project must have at least one version to submit for review.
</li>
</ul>
</div>
<p v-if="project.status === 'rejected'">
Do not resubmit for review until you've addressed the moderator
message!
</p>
</div>
<div class="extra-info card">
<template
@@ -344,6 +369,12 @@
class="featured-version"
>
<a
v-tooltip="
findPrimary(version).filename +
' (' +
$formatBytes(findPrimary(version).size) +
')'
"
:href="findPrimary(version).url"
class="download"
:title="`Download ${version.name}`"
@@ -365,7 +396,11 @@
>
{{
version.loaders
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
.map((x) =>
x.toLowerCase() === 'modloader'
? 'ModLoader'
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ')
}}
{{ $formatVersion(version.game_versions) }}
@@ -485,7 +520,7 @@
<span>Changelog</span>
</nuxt-link>
<nuxt-link
v-if="project.versions.length > 0"
v-if="project.versions.length > 0 || currentMember"
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/versions`"
@@ -531,6 +566,8 @@
<script>
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg?inline'
import ClearIcon from '~/assets/images/utils/clear.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'
@@ -556,6 +593,8 @@ export default {
IssuesIcon,
DownloadIcon,
CalendarIcon,
CheckIcon,
ClearIcon,
UpdateIcon,
CodeIcon,
ReportIcon,
@@ -645,6 +684,11 @@ export default {
})
}
},
data() {
return {
showKnownErrors: false,
}
},
head() {
return {
title: `${this.project.title} - ${
@@ -737,28 +781,32 @@ export default {
this.$nuxt.$loading.finish()
},
async submitForReview() {
this.$nuxt.$loading.start()
if (this.project.body === '' || this.project.versions.length < 1) {
this.showKnownErrors = true
} else {
this.$nuxt.$loading.start()
try {
await this.$axios.patch(
`project/${this.project.id}`,
{
status: 'processing',
},
this.$auth.headers
)
try {
await this.$axios.patch(
`project/${this.project.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.project.status = 'processing'
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response.data.description,
type: 'error',
})
}
this.$nuxt.$loading.finish()
}
this.$nuxt.$loading.finish()
},
},
}

View File

@@ -1,46 +1,77 @@
<template>
<div class="page-contents">
<header class="card columns">
<h3 class="column-grow-1">Edit project</h3>
<nuxt-link
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/settings`"
class="iconified-button column"
>
Back
</nuxt-link>
<button
v-if="
project.status === 'rejected' ||
project.status === 'draft' ||
project.status === 'unlisted'
"
title="Submit for approval"
class="iconified-button column"
:disabled="!$nuxt.$loading"
@click="saveProjectReview"
>
Submit for approval
</button>
<button
title="Save"
class="iconified-button brand-button-colors column"
:disabled="!$nuxt.$loading"
@click="saveProject"
>
<CheckIcon />
Save
</button>
<header class="card">
<div class="columns">
<h3 class="column-grow-1">Edit project</h3>
<nuxt-link
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/settings`"
class="iconified-button column"
>
<CrossIcon />
Cancel
</nuxt-link>
<button
v-if="
project.status === 'rejected' ||
project.status === 'draft' ||
project.status === 'unlisted'
"
title="Submit for review"
class="iconified-button column"
:disabled="!$nuxt.$loading"
@click="saveProjectReview"
>
<CheckIcon />
Submit for review
</button>
<button
title="Save"
class="iconified-button brand-button-colors column"
:disabled="!$nuxt.$loading"
@click="saveProjectNotForReview"
>
<SaveIcon />
Save changes
</button>
</div>
<div v-if="showKnownErrors" class="known-errors">
<ul>
<li v-if="newProject.title === ''">Your project must have a name.</li>
<li v-if="newProject.description === ''">
Your project must have a summary.
</li>
<li v-if="newProject.slug === ''">
Your project must have a vanity URL.
</li>
<li v-if="!savingAsDraft && newProject.body === ''">
Your project must have a body to submit for review.
</li>
<li v-if="!savingAsDraft && project.versions.length < 1">
Your project must have at least one version to submit for review.
</li>
<li
v-if="
license === null || license_url === null || license_url === ''
"
>
Your project must have a license.
</li>
</ul>
</div>
</header>
<section class="card essentials">
<h3>Name<span class="required">*</span></h3>
<label>
<span>
Be creative! Generic project names will be harder to search for.
<h3>Name<span class="required">*</span></h3>
<span>
Be creative! Generic project names will be harder to search for.
</span>
</span>
<input
v-model="newProject.title"
:class="{ 'known-error': newProject.title === '' && showKnownErrors }"
type="text"
placeholder="Enter the name"
:disabled="
@@ -48,14 +79,19 @@
"
/>
</label>
<h3>Summary<span class="required">*</span></h3>
<label>
<span>
Give a short description of your project that will appear on search
pages.
<h3>Summary<span class="required">*</span></h3>
<span>
Give a short description of your project that will appear on search
pages.
</span>
</span>
<input
v-model="newProject.description"
:class="{
'known-error': newProject.description === '' && showKnownErrors,
}"
type="text"
placeholder="Enter the summary"
:disabled="
@@ -63,11 +99,13 @@
"
/>
</label>
<h3>Categories</h3>
<label>
<span class="no-padding">
Select up to 3 categories that will help others <br />
find your project.
<span>
<h3>Categories</h3>
<span class="no-padding">
Select up to 3 categories that will help others <br />
find your project.
</span>
</span>
<Multiselect
id="categories"
@@ -77,6 +115,9 @@
.filter((x) => x.project_type === project.project_type)
.map((it) => it.name)
"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:loading="$tag.categories.length === 0"
:multiple="true"
:searchable="false"
@@ -93,23 +134,28 @@
"
/>
</label>
<h3>Vanity URL (slug)<span class="required">*</span></h3>
<label>
<label class="vertical-input">
<span>
Set this to something that will looks nice in your project's URL.
<h3>Vanity URL (slug)<span class="required">*</span></h3>
<span class="slug-description"
>https://modrinth.com/{{ newProject.project_type.toLowerCase() }}/{{
newProject.slug ? newProject.slug : 'your-slug'
}}
</span>
</span>
<input
id="name"
v-model="newProject.slug"
:class="{ 'known-error': newProject.slug === '' && showKnownErrors }"
type="text"
placeholder="Enter the vanity URL slug"
placeholder="Enter the vanity URL"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
/>
</label>
</section>
<section class="card project-icon rows">
<section class="card project-icon">
<h3>Icon</h3>
<img
:src="
@@ -121,7 +167,9 @@
"
alt="preview-image"
/>
<file-input
<SmartFileInput
:max-size="262144"
:show-icon="false"
accept="image/png,image/jpeg,image/gif,image/webp"
class="choose-image"
prompt="Choose image or drag it here"
@@ -142,15 +190,22 @@
</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>
<h3>Supported environments</h3>
<span>
Let others know what environments your project supports.
</span>
</div>
<div class="labeled-control">
<h3>Client<span class="required">*</span></h3>
<Multiselect
v-model="clientSideType"
placeholder="Select one"
:options="sideTypes"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:searchable="false"
:close-on-select="true"
:show-labels="false"
@@ -166,6 +221,9 @@
v-model="serverSideType"
placeholder="Select one"
:options="sideTypes"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:searchable="false"
:close-on-select="true"
:show-labels="false"
@@ -183,19 +241,20 @@
for="body"
title="You can type an extended description of your project here."
>
Description
Body<span class="required">*</span>
</label>
</h3>
<span>
You can type an extended description of your mod here. This editor
supports Markdown. Its syntax can be found
supports
<a
class="text-link"
href="https://guides.github.com/features/mastering-markdown/"
target="_blank"
rel="noopener noreferrer"
class="text-link"
>here</a
>.
>Markdown</a
>. HTML can also be used inside your description, not including styles,
scripts, and iframes (though YouTube iframes are allowed).
</span>
<ThisOrThat
v-model="bodyViewMode"
@@ -207,6 +266,9 @@
<textarea
id="body"
v-model="newProject.body"
:class="{
'known-error': newProject.body === '' && showKnownErrors,
}"
:disabled="(currentMember.permissions & EDIT_BODY) !== EDIT_BODY"
/>
</div>
@@ -297,10 +359,11 @@
<div class="input-group">
<Multiselect
v-model="license"
placeholder="Select one"
placeholder="Choose license..."
track-by="short"
label="short"
:options="$tag.licenses"
:custom-label="(value) => value.short.toUpperCase()"
:searchable="true"
:close-on-select="true"
:show-labels="false"
@@ -365,7 +428,7 @@
"
>
<TrashIcon />
Remove Link
Remove link
</button>
<hr
v-if="
@@ -381,21 +444,25 @@
<script>
import Multiselect from 'vue-multiselect'
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg?inline'
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
import SaveIcon from '~/assets/images/utils/save.svg?inline'
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
import FileInput from '~/components/ui/FileInput'
import ThisOrThat from '~/components/ui/ThisOrThat'
import SmartFileInput from '~/components/ui/SmartFileInput'
export default {
components: {
FileInput,
SmartFileInput,
ThisOrThat,
Multiselect,
TrashIcon,
CrossIcon,
CheckIcon,
PlusIcon,
SaveIcon,
TrashIcon,
},
beforeRouteLeave(to, from, next) {
if (
@@ -444,6 +511,9 @@ export default {
isEditing: true,
bodyViewMode: 'source',
showKnownErrors: false,
savingAsDraft: false,
}
},
fetch() {
@@ -514,9 +584,38 @@ export default {
this.DELETE_PROJECT = 1 << 7
},
methods: {
checkFields() {
const reviewConditions =
this.newProject.body !== '' && this.newProject.versions.length > 0
if (
this.newProject.name !== '' &&
this.newProject.description !== '' &&
this.newProject.slug !== '' &&
this.license.short !== null &&
this.license_url !== null &&
this.license_url !== ''
) {
if (this.savingAsDraft) {
return true
} else if (reviewConditions) {
return true
}
}
this.showKnownErrors = true
return false
},
async saveProjectReview() {
this.isProcessing = true
await this.saveProject()
this.savingAsDraft = false
if (this.checkFields()) {
this.isProcessing = true
await this.saveProject()
}
},
async saveProjectNotForReview() {
this.savingAsDraft = true
if (this.checkFields()) {
await this.saveProject()
}
},
async saveProject() {
this.$nuxt.$loading.start()
@@ -709,6 +808,15 @@ header {
section.essentials {
grid-area: essentials;
label {
margin-bottom: 0.5rem;
}
@media screen and (min-width: 1024px) {
input {
margin-left: 1.5rem;
}
}
}
section.project-icon {
@@ -716,12 +824,11 @@ section.project-icon {
img {
max-width: 100%;
margin-bottom: 1rem;
margin-bottom: 0.25rem;
border-radius: var(--size-rounded-lg);
}
.iconified-button {
width: 9rem;
margin-top: 0.5rem;
}
}
@@ -732,12 +839,11 @@ section.game-sides {
.columns {
flex-wrap: wrap;
span {
div {
flex: 2;
}
.labeled-control {
flex: 2;
margin-left: var(--spacing-card-lg);
h3 {
@@ -818,4 +924,15 @@ section.donations {
.required {
color: var(--color-badge-red-bg);
}
.vertical-input {
flex-direction: column;
justify-content: left;
align-items: unset;
gap: 0.5rem;
input {
margin-left: 0 !important;
}
}
</style>

View File

@@ -65,20 +65,18 @@
</div>
</div>
</div>
<div v-if="currentMember" class="card buttons">
<div v-if="currentMember" class="card buttons header-buttons">
<button
class="iconified-button"
@click="
newGalleryItems.push({
title: '',
description: '',
featured: false,
url: '',
})
v-if="
newGalleryItems.length > 0 ||
editGalleryIndexes.length > 0 ||
deleteGalleryUrls.length > 0
"
class="action iconified-button"
@click="resetGallery"
>
<UploadIcon />
Upload
<CrossIcon />
Cancel
</button>
<button
v-if="
@@ -90,19 +88,21 @@
@click="saveGallery"
>
<CheckIcon />
Save
Save changes
</button>
<button
v-if="
newGalleryItems.length > 0 ||
editGalleryIndexes.length > 0 ||
deleteGalleryUrls.length > 0
class="iconified-button"
@click="
newGalleryItems.push({
title: '',
description: '',
featured: false,
url: '',
})
"
class="action iconified-button"
@click="resetGallery"
>
<TrashIcon />
Discard Changes
<PlusIcon />
Add an image
</button>
</div>
<div class="items">
@@ -177,7 +177,7 @@
"
>
<TrashIcon />
Delete
Remove
</button>
</div>
</div>
@@ -214,6 +214,7 @@
</div>
<div class="gallery-bottom">
<SmartFileInput
:max-size="5242880"
accept="image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp"
prompt="Choose image or drag it here"
@change="(files) => showPreviewImage(files, index)"
@@ -225,7 +226,7 @@
@click="newGalleryItems.splice(index, 1)"
>
<TrashIcon />
Delete
Remove
</button>
</div>
</div>
@@ -236,7 +237,7 @@
</template>
<script>
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
import PlusIcon from '~/assets/images/utils/plus.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'
@@ -254,7 +255,7 @@ import Checkbox from '~/components/ui/Checkbox'
export default {
components: {
CalendarIcon,
UploadIcon,
PlusIcon,
Checkbox,
EditIcon,
TrashIcon,
@@ -615,7 +616,7 @@ export default {
}
input {
width: calc(100% - 2rem - 4px);
width: 100%;
margin: 0 0 0.25rem;
}
@@ -663,6 +664,15 @@ export default {
.gallery-buttons {
display: flex;
}
.columns {
margin-bottom: 0.5rem;
}
}
}
.header-buttons {
display: flex;
justify-content: right;
}
</style>

View File

@@ -12,31 +12,46 @@
<div class="card">
<h3>General</h3>
</div>
<section class="card">
<h3>Edit project</h3>
<label>
<span> This leads you to a page where you can edit your project. </span>
<nuxt-link class="iconified-button" to="edit">Edit</nuxt-link>
</label>
<h3>Create version</h3>
<section class="card main-settings">
<label>
<span>
This leads to a page where you can create a version for your project.
<h3>Edit project</h3>
<span>
This leads you to a page where you can edit your project.
</span>
</span>
<nuxt-link
class="iconified-button"
to="version/create"
:disabled="
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
"
>Create version</nuxt-link
>
<div>
<nuxt-link class="iconified-button" to="edit"
><EditIcon />Edit</nuxt-link
>
</div>
</label>
<h3>Delete project</h3>
<label>
<span>
Removes your project from Modrinth's servers and search. Clicking on
this will delete your project, so be extra careful!
<h3>Create a version</h3>
<span>
This leads to a page where you can create a version for your
project.
</span>
</span>
<div>
<nuxt-link
class="iconified-button"
to="version/create"
:disabled="
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
"
><PlusIcon />Create a version</nuxt-link
>
</div>
</label>
<label>
<span>
<h3>Delete project</h3>
<span>
Removes your project from Modrinth's servers and search. Clicking on
this will delete your project, so be extra careful!
</span>
</span>
<div
class="iconified-button"
@@ -45,7 +60,7 @@
"
@click="showPopup"
>
Delete project
<TrashIcon />Delete project
</div>
</label>
</section>
@@ -81,14 +96,17 @@
<div class="info">
<img :src="member.avatar_url" :alt="member.name" />
<div class="text">
<h4>{{ member.name }}</h4>
<h3>{{ member.role }}</h3>
<nuxt-link :to="'/user/' + member.user.username" class="name">
<p class="title-link">{{ member.name }}</p>
</nuxt-link>
<p>{{ member.role }}</p>
</div>
</div>
<div class="side-buttons">
<Badge v-if="member.accepted" type="accepted" color="green" />
<Badge v-else type="pending" color="yellow" />
<button
v-if="member.role !== 'Owner'"
class="dropdown-icon"
@click="
openTeamMembers.indexOf(member.user.id) === -1
@@ -109,22 +127,21 @@
<input
v-model="allTeamMembers[index].role"
type="text"
:class="{ 'known-error': member.role === 'Owner' }"
:disabled="
member.role === 'Owner' ||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
"
/>
</label>
<ul v-if="member.role === 'Owner'" class="known-errors">
<li>A project can only have one 'Owner'.</li>
</ul>
</div>
<h3>Permissions</h3>
<div class="permissions">
<Checkbox
:value="
(member.permissions & UPLOAD_VERSION) === UPLOAD_VERSION ||
member.role === 'Owner'
"
:value="(member.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
:disabled="
member.role === 'Owner' ||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
"
@@ -132,12 +149,8 @@
@input="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
/>
<Checkbox
:value="
(member.permissions & DELETE_VERSION) === DELETE_VERSION ||
member.role === 'Owner'
"
:value="(member.permissions & DELETE_VERSION) === DELETE_VERSION"
:disabled="
member.role === 'Owner' ||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & DELETE_VERSION) !== DELETE_VERSION
"
@@ -145,12 +158,8 @@
@input="allTeamMembers[index].permissions ^= DELETE_VERSION"
/>
<Checkbox
:value="
(member.permissions & EDIT_DETAILS) === EDIT_DETAILS ||
member.role === 'Owner'
"
:value="(member.permissions & EDIT_DETAILS) === EDIT_DETAILS"
:disabled="
member.role === 'Owner' ||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
@@ -158,12 +167,8 @@
@input="allTeamMembers[index].permissions ^= EDIT_DETAILS"
/>
<Checkbox
:value="
(member.permissions & EDIT_BODY) === EDIT_BODY ||
member.role === 'Owner'
"
:value="(member.permissions & EDIT_BODY) === EDIT_BODY"
:disabled="
member.role === 'Owner' ||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & EDIT_BODY) !== EDIT_BODY
"
@@ -171,12 +176,8 @@
@input="allTeamMembers[index].permissions ^= EDIT_BODY"
/>
<Checkbox
:value="
(member.permissions & MANAGE_INVITES) === MANAGE_INVITES ||
member.role === 'Owner'
"
:value="(member.permissions & MANAGE_INVITES) === MANAGE_INVITES"
:disabled="
member.role === 'Owner' ||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES
"
@@ -184,12 +185,8 @@
@input="allTeamMembers[index].permissions ^= MANAGE_INVITES"
/>
<Checkbox
:value="
(member.permissions & REMOVE_MEMBER) === REMOVE_MEMBER ||
member.role === 'Owner'
"
:value="(member.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
:disabled="
member.role === 'Owner' ||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
"
@@ -197,24 +194,16 @@
@input="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
/>
<Checkbox
:value="
(member.permissions & EDIT_MEMBER) === EDIT_MEMBER ||
member.role === 'Owner'
"
:value="(member.permissions & EDIT_MEMBER) === EDIT_MEMBER"
:disabled="
member.role === 'Owner' ||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
"
label="Edit member"
@input="allTeamMembers[index].permissions ^= EDIT_MEMBER"
/>
<Checkbox
:value="
(member.permissions & DELETE_PROJECT) === DELETE_PROJECT ||
member.role === 'Owner'
"
:value="(member.permissions & DELETE_PROJECT) === DELETE_PROJECT"
:disabled="
member.role === 'Owner' ||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
"
@@ -226,7 +215,6 @@
<button
class="iconified-button"
:disabled="
member.role === 'Owner' ||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
"
@click="removeTeamMember(index)"
@@ -249,7 +237,8 @@
<button
class="iconified-button brand-button-colors"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
member.role === 'Owner'
"
@click="updateTeamMember(index)"
>
@@ -270,6 +259,7 @@ 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 EditIcon from '~/assets/images/utils/edit.svg?inline'
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
import UserIcon from '~/assets/images/utils/user.svg?inline'
@@ -281,6 +271,7 @@ export default {
Badge,
PlusIcon,
CheckIcon,
EditIcon,
TrashIcon,
UserIcon,
},
@@ -480,17 +471,12 @@ export default {
}
.text {
margin: auto 0 auto 0.5rem;
h4 {
font-weight: normal;
margin: 0;
font-size: var(--font-size-sm);
.name {
font-weight: bold;
}
h3 {
text-transform: uppercase;
margin-top: 0.1rem;
margin-bottom: 0;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-extrabold);
letter-spacing: 0.02rem;
p {
margin: 0.2rem 0;
}
}
}
@@ -571,28 +557,19 @@ section {
padding-right: var(--spacing-card-lg);
}
div {
flex: none;
}
input {
flex: 3;
height: fit-content;
}
div,
a {
display: block;
text-align: center;
height: fit-content;
flex: 1;
@media screen and (max-width: 1024px) {
margin: 0.5rem 0 1rem 0;
}
}
div:hover {
cursor: pointer;
}
}
}
.team-invite {
gap: 0.5rem;
@media screen and (max-width: 1024px) {
flex-direction: column;
h3 {
@@ -638,4 +615,8 @@ section {
}
}
}
.main-settings span {
margin-bottom: 1rem;
}
</style>

View File

@@ -42,49 +42,52 @@
Auto-featured
</div>
</div>
<div v-if="mode === 'edit'" class="buttons">
<button
class="action iconified-button brand-button-colors"
@click="saveEditedVersion"
>
<CheckIcon aria-hidden="true" />
Save
</button>
<div v-if="mode === 'edit'" class="header-buttons buttons">
<nuxt-link
v-if="$auth.user"
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}`"
class="action iconified-button"
class="iconified-button"
>
<TrashIcon aria-hidden="true" />
Discard changes
<CrossIcon aria-hidden="true" />
Cancel
</nuxt-link>
</div>
<div v-else-if="mode === 'create'" class="buttons">
<button
class="action iconified-button brand-button-colors"
@click="createVersion"
class="iconified-button brand-button-colors"
@click="saveEditedVersion"
>
<CheckIcon aria-hidden="true" />
<SaveIcon aria-hidden="true" />
Save
</button>
</div>
<div v-else-if="mode === 'create'" class="header-buttons buttons">
<nuxt-link
v-if="$auth.user"
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/versions`"
class="action iconified-button"
class="iconified-button"
>
<TrashIcon aria-hidden="true" />
Discard version
<CrossIcon aria-hidden="true" />
Cancel
</nuxt-link>
<button
class="iconified-button brand-button-colors"
@click="createVersion"
>
<CheckIcon aria-hidden="true" />
Create
</button>
</div>
<div v-else class="buttons">
<a
v-if="primaryFile"
v-tooltip="
primaryFile.filename + ' (' + $formatBytes(primaryFile.size) + ')'
"
:href="primaryFile.url"
class="action iconified-button brand-button-colors"
class="bold-button iconified-button brand-button-colors"
:title="`Download ${primaryFile.filename}`"
>
<DownloadIcon aria-hidden="true" />
@@ -124,6 +127,7 @@
>
<input
v-model="version.name"
class="full-width-input"
type="text"
placeholder="Enter the version name..."
/>
@@ -179,6 +183,9 @@
class="input"
placeholder="Select one"
:options="['release', 'beta', 'alpha']"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:searchable="false"
:close-on-select="true"
:show-labels="false"
@@ -217,6 +224,12 @@
)
.map((it) => it.name)
"
:custom-label="
(value) =>
value === 'modloader'
? 'Risugami\'s ModLoader'
: value.charAt(0).toUpperCase() + value.slice(1)
"
:loading="$tag.loaders.length === 0"
:multiple="true"
:searchable="false"
@@ -231,7 +244,11 @@
<p v-else class="value">
{{
version.loaders
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
.map((x) =>
x.toLowerCase() === 'modloader'
? "Risugami's ModLoader"
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ')
}}
</p>
@@ -381,7 +398,7 @@
class="iconified-button"
@click="version.dependencies.splice(index, 1)"
>
<TrashIcon /> Delete
<TrashIcon /> Remove
</button>
</div>
</div>
@@ -401,13 +418,19 @@
v-model="newDependencyId"
type="text"
oninput="this.value = this.value.replace(' ', '')"
:placeholder="`Enter the ${dependencyAddMode} ID...`"
:placeholder="`Enter the ${dependencyAddMode} ID${
dependencyAddMode === 'project' ? '/slug' : ''
}`"
@keyup.enter="addDependency"
/>
<Multiselect
v-model="newDependencyType"
class="input"
placeholder="Select one"
:options="['required', 'optional', 'incompatible']"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:searchable="false"
:close-on-select="true"
:show-labels="false"
@@ -415,7 +438,7 @@
/>
<button class="iconified-button" @click="addDependency">
<PlusIcon />
Add dependency
Add
</button>
</div>
</div>
@@ -456,7 +479,9 @@
:key="file.hashes.sha1"
class="file"
>
<p class="filename">{{ file.filename }}</p>
<p class="filename">
{{ file.filename }}
</p>
<div
v-if="primaryFile.hashes.sha1 === file.hashes.sha1"
class="featured"
@@ -473,6 +498,7 @@
<DownloadIcon aria-hidden="true" />
Download
</a>
<p v-if="mode === 'version'">({{ $formatBytes(file.size) }})</p>
<button
v-if="mode === 'edit'"
class="action iconified-button"
@@ -482,7 +508,7 @@
"
>
<TrashIcon aria-hidden="true" />
Delete
Remove
</button>
<button
v-if="
@@ -507,7 +533,7 @@
@click="newFiles.splice(index, 1)"
>
<TrashIcon aria-hidden="true" />
Delete
Remove
</button>
</div>
</div>
@@ -517,6 +543,7 @@
class="choose-files"
accept=".jar,application/java-archive,.zip,application/zip,.mrpack"
prompt="Choose files or drag them here"
:max-size="524288000"
@change="(x) => x.forEach((y) => newFiles.push(y))"
/>
</section>
@@ -531,7 +558,9 @@ import StatelessFileInput from '~/components/ui/StatelessFileInput'
import InfoIcon from '~/assets/images/utils/info.svg?inline'
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
import SaveIcon from '~/assets/images/utils/save.svg?inline'
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import EditIcon from '~/assets/images/utils/edit.svg?inline'
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
import ReportIcon from '~/assets/images/utils/report.svg?inline'
@@ -556,7 +585,9 @@ export default {
StarIcon,
CheckIcon,
Multiselect,
SaveIcon,
PlusIcon,
CrossIcon,
StatelessFileInput,
InfoIcon,
},
@@ -951,22 +982,22 @@ section {
}
}
.header-buttons {
justify-content: right;
}
.buttons {
display: flex;
flex-wrap: wrap;
row-gap: 0.5rem;
.brand-button-colors {
.bold-button {
font-weight: bold;
}
@media screen and (min-width: 1024px) {
margin-left: auto;
}
.action:not(:first-child) {
margin: 0 0 0 0.5rem;
}
}
.version-data-inputs {
@@ -1005,8 +1036,7 @@ section {
input {
margin: 0;
height: 1.25rem;
width: calc(100% - 2.25rem);
width: 100%;
}
}
}
@@ -1129,4 +1159,9 @@ section {
min-height: 10rem;
display: block;
}
.full-width-input {
width: 100%;
margin-bottom: 0.5rem;
}
</style>

View File

@@ -1,9 +1,12 @@
<template>
<div class="content">
<div class="card" v-if="currentMember">
<nuxt-link to="version/create" class="iconified-button new-version">
<UploadIcon />
Upload
<div v-if="currentMember" class="card header-buttons">
<nuxt-link
to="version/create"
class="brand-button-colors iconified-button"
>
<PlusIcon />
Create a version
</nuxt-link>
</div>
<VersionFilterControl
@@ -11,7 +14,7 @@
:versions="versions"
@updateVersions="updateVersions"
/>
<div class="card">
<div v-if="versions.length > 0" class="card">
<table>
<thead>
<tr>
@@ -25,6 +28,12 @@
<tr v-for="version in filteredVersions" :key="version.id">
<td>
<a
v-tooltip="
$parent.findPrimary(version).filename +
' (' +
$formatBytes($parent.findPrimary(version).size) +
')'
"
:href="$parent.findPrimary(version).url"
class="download-button"
:class="version.version_type"
@@ -69,7 +78,11 @@
<p>
{{
version.loaders
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
.map((x) =>
x.toLowerCase() === 'modloader'
? 'ModLoader'
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ') +
' ' +
$formatVersion(version.game_versions)
@@ -93,7 +106,11 @@
<p>
{{
version.loaders
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
.map((x) =>
x.toLowerCase() === 'modloader'
? 'ModLoader'
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ')
}}
</p>
@@ -118,14 +135,14 @@
</div>
</template>
<script>
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
import VersionBadge from '~/components/ui/Badge'
import VersionFilterControl from '~/components/ui/VersionFilterControl'
export default {
components: {
UploadIcon,
PlusIcon,
DownloadIcon,
VersionBadge,
VersionFilterControl,
@@ -165,10 +182,6 @@ export default {
</script>
<style lang="scss" scoped>
.new-version {
max-width: 5.25rem;
}
table {
border-collapse: separate;
border-spacing: 0 0.75rem;
@@ -249,4 +262,9 @@ table {
display: none;
}
}
.header-buttons {
display: flex;
justify-content: right;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -9,29 +9,36 @@
:disabled="!$nuxt.$loading"
@click="createReport"
>
<PlusIcon />
Create
<CheckIcon />
Submit
</button>
</header>
<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
project would be its project ID, found on the right side of that
project's page under "Project ID".
<h3>Item ID</h3>
<span>
The ID of the item you are reporting. For example, the item ID of
a project would be its project ID, found on the right side of that
project's page under "Project ID".
</span>
</span>
<input v-model="itemId" type="text" placeholder="Enter the item ID" />
</label>
<h3>Item type</h3>
<label>
<span class="no-padding"
>The type of the item that is being reported.</span
>
<span>
<h3>Item type</h3>
<span class="no-padding"
>The type of the item that is being reported.</span
>
</span>
<multiselect
id="item-type"
v-model="itemType"
:options="['project', 'version', 'user']"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:multiple="false"
:searchable="false"
:show-no-results="false"
@@ -39,16 +46,21 @@
placeholder="Choose item type"
/>
</label>
<h3>Report type</h3>
<label>
<span class="no-padding">
The type of report. This is the category that this report falls
under.
<span>
<h3>Report type</h3>
<span class="no-padding">
The type of report. This is the category that this report falls
under.
</span>
</span>
<multiselect
id="report-type"
v-model="reportType"
:options="reportTypes"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:multiple="false"
:searchable="false"
:show-no-results="false"
@@ -68,13 +80,13 @@
</h3>
<span>
You can type the of the long form of your description here. This
editor supports markdown. You can find the syntax
editor supports
<a
href="https://guides.github.com/features/mastering-markdown/"
target="_blank"
rel="noopener noreferrer"
class="text-link"
>here</a
>Markdown</a
>.
</span>
<ThisOrThat
@@ -102,13 +114,13 @@
import Multiselect from 'vue-multiselect'
import ThisOrThat from '~/components/ui/ThisOrThat'
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg?inline'
export default {
components: {
Multiselect,
ThisOrThat,
PlusIcon,
CheckIcon,
},
async asyncData(data) {
const reportTypes = (await data.$axios.get(`tag/report_type`)).data
@@ -261,4 +273,12 @@ section.description {
.card {
margin-bottom: 0;
}
.card span {
margin-bottom: 1rem;
}
label {
align-items: center;
}
</style>

View File

@@ -69,7 +69,7 @@
</template>
<script>
import ClearIcon from '~/assets/images/utils/trash.svg?inline'
import ClearIcon from '~/assets/images/utils/clear.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'

View File

@@ -6,7 +6,7 @@
}"
>
<aside class="normal-page__sidebar" aria-label="Filters">
<section class="card" role="presentation">
<section class="card filters-card" role="presentation">
<button
class="iconified-button sidebar-menu-close-button"
@click="sidebarMenuOpen = !sidebarMenuOpen"
@@ -30,7 +30,7 @@
class="iconified-button"
@click="clearFilters"
>
<ExitIcon aria-hidden="true" />
<ClearIcon aria-hidden="true" />
Clear filters
</button>
<section aria-label="Category filters">
@@ -291,7 +291,7 @@ 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 ClearIcon from '~/assets/images/utils/clear.svg?inline'
import EyeIcon from '~/assets/images/utils/eye.svg?inline'
import EyeOffIcon from '~/assets/images/utils/eye-off.svg?inline'
@@ -309,7 +309,7 @@ export default {
ClientSide,
ServerSide,
SearchIcon,
ExitIcon,
ClearIcon,
EyeIcon,
EyeOffIcon,
LogoAnimated,
@@ -649,6 +649,10 @@ export default {
</script>
<style lang="scss" scoped>
.filters-card {
padding: var(--spacing-card-lg);
}
.sidebar-menu {
display: none;
margin-top: 1rem;

View File

@@ -17,61 +17,16 @@
<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"
/>
<NuxtChild />
</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>

View File

@@ -1,180 +1,144 @@
<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"
@click="developerMode++"
/>
<file-input
accept="image/png,image/jpeg,image/gif,image/webp"
class="choose-image"
prompt="Choose image or drag it here"
@change="showPreviewImage"
/>
<section class="card account-settings">
<div class="header">
<h2 class="title">Account settings</h2>
<div class="controls">
<button
class="brand-button-colors iconified-button"
title="Save account settings changes"
@click="saveChanges()"
>
<SaveIcon />
Save changes
</button>
</div>
<button
class="iconified-button"
@click="
icon = null
previewImage = null
"
>
<TrashIcon />
Reset
</button>
</div>
<div class="recap card">
<section>
<h2>Profile Recap</h2>
<div>
<Badge
v-if="$auth.user.role === 'admin'"
type="Admin"
color="red"
<div class="left-side">
<h3>Profile picture</h3>
<div class="profile-picture">
<img :src="previewImage ? previewImage : $auth.user.avatar_url" />
<div class="uploader">
<SmartFileInput
:show-icon="false"
:max-size="2097152"
accept="image/png,image/jpeg,image/gif,image/webp"
class="choose-image"
prompt="Choose image or drag it here"
@change="showPreviewImage"
/>
<Badge
v-else-if="$auth.user.role === 'moderator'"
type="Moderator"
color="yellow"
/>
<Badge v-else type="Developer" color="green" />
<div class="stat">
<SunriseIcon />
<span>Joined {{ $dayjs($auth.user.created).fromNow() }}</span>
</div>
<button
class="iconified-button"
@click="
icon = null
previewImage = null
"
>
<TrashIcon />
Reset
</button>
</div>
</section>
<section>
<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>
<div class="right-side">
<section class="card">
<h3>Username</h3>
<div class="right-side">
<label>
<span>
The username used on Modrinth to identify yourself. This must be
unique.
<h3>Username</h3>
<span>This must be unique.</span>
</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.
<h3>Email (optional)</h3>
<span>This is kept private.</span>
</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.
<h3>Bio</h3>
<span>Describe yourself to other users!</span>
</span>
<input v-model="bio" type="text" placeholder="Enter your bio" />
</label>
<h3>Theme</h3>
<label>
</div>
</section>
<section class="card">
<div class="header">
<h2 class="title">Display settings</h2>
</div>
<label>
<span>
<h3>Theme</h3>
<span>Change the global site theme.</span>
</span>
<Multiselect
v-model="$colorMode.preference"
:options="['system', 'light', 'dark', 'oled']"
:custom-label="
(value) =>
value === 'oled'
? 'OLED'
: value.charAt(0).toUpperCase() + value.slice(1)
"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
/>
</label>
<label>
<span>
<h3>Search sidebar on the right</h3>
<span>
Change the global site theme. It can also be changed between light
and dark in the navigation bar.
Enabling this will put the search page's filters sidebar on the
right side.
</span>
<Multiselect
v-model="$colorMode.preference"
:options="['system', 'light', 'dark', 'oled']"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
/>
</label>
<h3>Search sidebar on right side</h3>
<label>
</span>
<input
v-model="searchLayout"
class="switch stylized-toggle"
type="checkbox"
@change="changeLayout"
/>
</label>
<label>
<span>
<h3>Project sidebar on the right</h3>
<span>
Sets the sidebar direction for search pages. Enabling this will put
the search bar on the right side.
Enabling this will put the project pages' info sidebars on the right
side.
</span>
<input
v-model="searchLayout"
class="switch stylized-toggle"
type="checkbox"
@change="changeLayout"
/>
</label>
<h3>Project sidebar on right side</h3>
<label>
<span>
Sets the sidebar direction for project pages. Enabling this will
make projects look closer to the legacy layout, with project
information on the right side.
</span>
<input
v-model="projectLayout"
class="switch stylized-toggle"
type="checkbox"
@change="changeLayout"
/>
</label>
<section v-if="developerMode > 6">
<h3>Developer options</h3>
<label>
<span>
Set the API endpoint. This value is not stored, and is intended
for temporary usage.</span
>
<Multiselect
v-model="apiEndpoint"
:options="['production', 'staging']"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@input="changeApiEndpoint()"
/>
</label>
</section>
</section>
</div>
</span>
<input
v-model="projectLayout"
class="switch stylized-toggle"
type="checkbox"
@change="changeLayout"
/>
</label>
</section>
</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 SmartFileInput from '~/components/ui/SmartFileInput'
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.svg?inline'
import SaveIcon from '~/assets/images/utils/save.svg?inline'
export default {
components: {
TrashIcon,
SunriseIcon,
DownloadIcon,
HeartIcon,
Badge,
FileInput,
SaveIcon,
SmartFileInput,
Multiselect,
},
asyncData(ctx) {
@@ -190,24 +154,15 @@ export default {
previewImage: null,
searchLayout: false,
projectLayout: false,
apiEndpoint: this.getApiEndpoint(),
developerMode: 0,
}
},
fetch() {
this.searchLayout = this.$store.state.cosmetics.searchLayout
this.projectLayout = this.$store.state.cosmetics.projectLayout
this.$emit('update:action-button', 'Save')
this.$emit('update:action-button-callback', this.saveChanges)
},
head: {
title: 'Settings - Modrinth',
},
created() {
this.$emit('update:action-button', 'Save')
this.$emit('update:action-button-callback', this.saveChanges)
},
methods: {
changeTheme() {
const shift = event.shiftKey
@@ -222,17 +177,6 @@ export default {
this.$colorMode.preference = shift ? 'oled' : 'dark'
}
},
changeApiEndpoint() {
const subdomain =
this.apiEndpoint === 'production' ? 'api' : 'staging-api'
this.$axios.defaults.baseURL =
'https://' + subdomain + '.modrinth.com/v2/'
},
getApiEndpoint() {
return this.$axios.defaults.baseURL === 'https://api.modrinth.com/v2/'
? 'production'
: 'staging'
},
showPreviewImage(files) {
const reader = new FileReader()
this.icon = files[0]
@@ -311,76 +255,72 @@ export default {
}
</script>
<style lang="scss" scoped>
.edit-page {
display: flex;
flex-direction: column;
.account-settings {
display: grid;
grid-template: 'header header' auto 'left-side left-side' auto 'right-side right-side' auto;
@media screen and (min-width: 1024px) {
flex-direction: row;
.left-side {
margin-right: var(--spacing-card-bg);
}
grid-template:
'header header' auto
'left-side right-side' auto;
}
}
.left-side {
min-width: 20rem;
.left-side {
grid-area: left-side;
min-width: 20rem;
.profile-picture {
h3 {
font-size: var(--font-size-lg);
}
.uploader {
margin: 1rem 0;
text-align: center;
.profile-picture {
display: flex;
flex-direction: row;
gap: 0.5rem;
align-items: center;
img {
box-shadow: var(--shadow-card);
border-radius: var(--size-rounded-md);
width: 8rem;
width: 10rem;
height: 10rem;
object-fit: contain;
margin-bottom: 0.5rem;
}
}
}
.recap {
section {
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;
.uploader {
text-align: center;
.iconified-button {
margin-top: 0.5rem;
}
}
}
}
.right-side {
grid-area: right-side;
margin-left: var(--spacing-card-lg);
}
}
.stat {
.card span {
margin-bottom: 1rem;
}
label {
align-items: center;
}
.header {
display: flex;
align-items: center;
margin: 0.5rem 0;
padding-bottom: 1rem;
grid-area: header;
svg {
width: auto;
height: 1.25rem;
margin-right: 0.25rem;
.title {
flex-grow: 1;
margin: 0;
}
span {
strong {
font-weight: bolder;
}
.controls {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
}
</style>

View File

@@ -1,5 +1,26 @@
<template>
<div class="rows card">
<div class="header">
<h2 class="title">Privacy settings</h2>
<div class="controls">
<button class="iconified-button" @click="toggleAll(false)">
<DenyIcon />
Deny all
</button>
<button class="iconified-button" @click="toggleAll(true)">
<AllowIcon />
Allow all
</button>
<button
class="brand-button-colors iconified-button"
title="Confirm privacy settings"
@click="confirm()"
>
<SaveIcon />
Save changes
</button>
</div>
</div>
<div class="privacy-settings-container">
<div>
Modrinth relies on different providers and in-house tools to allow us to
@@ -30,23 +51,23 @@
</div>
</div>
</div>
<div class="actions">
<button class="iconified-button" @click="toggleAll(false)">
Select none
</button>
<button class="iconified-button" @click="toggleAll(true)">
Select all
</button>
</div>
</div>
</template>
<script>
import toggles from '@/privacy-toggles'
import DenyIcon from '~/assets/images/utils/clear.svg?inline'
import AllowIcon from '~/assets/images/utils/check-circle.svg?inline'
import SaveIcon from '~/assets/images/utils/save.svg?inline'
export default {
auth: false,
name: 'Privacy',
components: {
DenyIcon,
AllowIcon,
SaveIcon,
},
data: () => {
const settings = toggles.settings
return {
@@ -54,9 +75,6 @@ export default {
}
},
fetch() {
this.$emit('update:action-button', 'Confirm')
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(toggles.settings).forEach((key) => {
@@ -76,10 +94,6 @@ export default {
head: {
title: 'Privacy Settings - Modrinth',
},
created() {
this.$emit('update:action-button', 'Confirm')
this.$emit('update:action-button-callback', this.confirm)
},
options: {
auth: false,
},
@@ -91,6 +105,7 @@ export default {
}
this.$forceUpdate()
this.confirm()
},
confirm() {
this.$store.commit('consent/set_consent', true)
@@ -173,4 +188,22 @@ export default {
}
}
}
.header {
display: flex;
align-items: center;
padding-bottom: 1rem;
grid-area: header;
.title {
flex-grow: 1;
margin: 0;
}
.controls {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
}
</style>

View File

@@ -12,12 +12,17 @@
/>
<section class="card">
<h3>Authorization token</h3>
<div class="header">
<h2 class="title">Security settings</h2>
</div>
<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!
<h3>Authorization token</h3>
<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>
</span>
<input
type="button"
@@ -26,12 +31,14 @@
@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
>
<span>
<h3>Revoke your token</h3>
<span
>This will log you out of Modrinth, and you will have to log in
again to access Modrinth with a new token.</span
>
</span>
<input
type="button"
class="iconified-button"
@@ -39,14 +46,16 @@
@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
>
<span>
<h3>Delete your account</h3>
<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
>
</span>
<input
value="Delete account"
type="button"
@@ -96,3 +105,26 @@ export default {
},
}
</script>
<style lang="scss" scoped>
.card span {
margin-bottom: 1rem;
}
.header {
display: flex;
align-items: center;
padding-bottom: 1rem;
grid-area: header;
.title {
flex-grow: 1;
margin: 0;
}
.controls {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
}
</style>

View File

@@ -20,6 +20,14 @@
<hr class="card-divider" />
<h3 class="sidebar__item">About me</h3>
<span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span>
<a
:href="githubUrl"
target="_blank"
class="sidebar__item report-button iconified-button"
>
<GitHubIcon aria-hidden="true" />
View GitHub profile
</a>
<div class="sidebar__item stats-block">
<div class="stats-block__item secondary-stat">
<SunriseIcon class="secondary-stat__icon" aria-hidden="true" />
@@ -125,6 +133,7 @@ import ThisOrThat from '~/components/ui/ThisOrThat'
import Badge from '~/components/ui/Badge'
import Advertisement from '~/components/ads/Advertisement'
import GitHubIcon from '~/assets/images/utils/github.svg?inline'
import ReportIcon from '~/assets/images/utils/report.svg?inline'
import SunriseIcon from '~/assets/images/utils/sunrise.svg?inline'
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
@@ -139,6 +148,7 @@ export default {
ProjectCard,
SunriseIcon,
DownloadIcon,
GitHubIcon,
ReportIcon,
Badge,
SettingsIcon,
@@ -160,10 +170,17 @@ export default {
])
).map((it) => it.data)
const githubUrl = (
await (
await fetch(`https://api.github.com/user/` + user.github_id)
).json()
).html_url
return {
selectedProjectType: 'all',
user,
projects,
githubUrl,
}
} catch {
data.error({

View File

@@ -98,4 +98,15 @@ export default ({ store }, inject) => {
return output.join(', ')
})
inject('formatBytes', (bytes, decimals = 2) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
})
}