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" <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">
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"> <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" />
<circle cx="12" cy="12" r="10"></circle> </svg>
<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>

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; 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 { .multiselect {
color: var(--color-text) !important; color: var(--color-text) !important;
@@ -554,6 +563,7 @@ label {
@media screen and (min-width: 1024px) { @media screen and (min-width: 1024px) {
flex-direction: row; flex-direction: row;
align-items: center;
} }
span { 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 { .vue-notification-group {
right: 25px !important; right: 25px !important;
@@ -839,3 +869,17 @@ label {
height: 1px; height: 1px;
margin: var(--spacing-card-bg) 0; 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 { h3 {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0; margin-bottom: 0.25rem;
color: var(--color-text-dark); color: var(--color-text-dark);
} }
@@ -258,6 +258,7 @@ button {
input { input {
border-radius: 2rem; border-radius: 2rem;
box-sizing: border-box;
} }
pre { 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"> <div class="columns">
<label class="button" @drop.prevent="addFile" @dragover.prevent> <label class="button" @drop.prevent="addFile" @dragover.prevent>
<span> <span>
<UploadIcon /> <UploadIcon v-if="showIcon" />
{{ prompt }} {{ prompt }}
</span> </span>
<input <input
@@ -36,6 +36,14 @@ export default {
type: String, type: String,
default: null, default: null,
}, },
maxSize: {
type: Number,
default: null,
},
showIcon: {
type: Boolean,
default: true,
},
}, },
data() { data() {
return { return {
@@ -46,7 +54,26 @@ export default {
onChange(files, shouldNotReset) { onChange(files, shouldNotReset) {
if (!shouldNotReset) this.files = files.target.files 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) { addFile(e) {
const droppedFiles = e.dataTransfer.files const droppedFiles = e.dataTransfer.files
@@ -74,7 +101,6 @@ label {
justify-content: center; justify-content: center;
text-align: center; text-align: center;
padding: var(--spacing-card-sm) var(--spacing-card-md); padding: var(--spacing-card-sm) var(--spacing-card-md);
margin-bottom: var(--spacing-card-sm);
} }
span { span {
@@ -95,4 +121,17 @@ span {
input { input {
display: none; 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> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -17,61 +17,16 @@
<nuxt-link class="tab" to="/settings/privacy"> <nuxt-link class="tab" to="/settings/privacy">
<span>Privacy</span> <span>Privacy</span>
</nuxt-link> </nuxt-link>
<button
v-if="actionButton"
class="iconified-button brand-button-colors right"
@click="actionButtonCallback"
>
<CheckIcon />
{{ actionButton }}
</button>
</div> </div>
<NuxtChild <NuxtChild />
:action-button.sync="actionButton"
:action-button-callback.sync="actionButtonCallback"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import CheckIcon from '~/assets/images/utils/check.svg?inline'
export default { export default {
name: 'Settings', 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> </script>

View File

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

View File

@@ -1,5 +1,26 @@
<template> <template>
<div class="rows card"> <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 class="privacy-settings-container">
<div> <div>
Modrinth relies on different providers and in-house tools to allow us to Modrinth relies on different providers and in-house tools to allow us to
@@ -30,23 +51,23 @@
</div> </div>
</div> </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> </div>
</template> </template>
<script> <script>
import toggles from '@/privacy-toggles' 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 { export default {
auth: false, auth: false,
name: 'Privacy', name: 'Privacy',
components: {
DenyIcon,
AllowIcon,
SaveIcon,
},
data: () => { data: () => {
const settings = toggles.settings const settings = toggles.settings
return { return {
@@ -54,9 +75,6 @@ export default {
} }
}, },
fetch() { fetch() {
this.$emit('update:action-button', 'Confirm')
this.$emit('update:action-button-callback', this.confirm)
this.$store.dispatch('consent/loadFromCookies', this.$cookies) this.$store.dispatch('consent/loadFromCookies', this.$cookies)
if (this.$store.state.consent.is_consent_given) { if (this.$store.state.consent.is_consent_given) {
Object.keys(toggles.settings).forEach((key) => { Object.keys(toggles.settings).forEach((key) => {
@@ -76,10 +94,6 @@ export default {
head: { head: {
title: 'Privacy Settings - Modrinth', title: 'Privacy Settings - Modrinth',
}, },
created() {
this.$emit('update:action-button', 'Confirm')
this.$emit('update:action-button-callback', this.confirm)
},
options: { options: {
auth: false, auth: false,
}, },
@@ -91,6 +105,7 @@ export default {
} }
this.$forceUpdate() this.$forceUpdate()
this.confirm()
}, },
confirm() { confirm() {
this.$store.commit('consent/set_consent', true) 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> </style>

View File

@@ -12,12 +12,17 @@
/> />
<section class="card"> <section class="card">
<h3>Authorization token</h3> <div class="header">
<h2 class="title">Security settings</h2>
</div>
<label> <label>
<span> <span>
Your authorization token can be used with the Modrinth API, the <h3>Authorization token</h3>
Minotaur Gradle plugin, and other applications that interact with <span>
Modrinth's API. Be sure to keep this secret! 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> </span>
<input <input
type="button" type="button"
@@ -26,12 +31,14 @@
@click="copyToken" @click="copyToken"
/> />
</label> </label>
<h3>Revoke your token</h3>
<label> <label>
<span <span>
>This will log you out of Modrinth, and you will have to log in again <h3>Revoke your token</h3>
to access Modrinth with a new token.</span <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 <input
type="button" type="button"
class="iconified-button" class="iconified-button"
@@ -39,14 +46,16 @@
@click="$router.replace('/settings/revoke-token')" @click="$router.replace('/settings/revoke-token')"
/> />
</label> </label>
<h3>Delete your account</h3>
<label> <label>
<span <span>
>Clicking on this WILL delete your account. Do not click on this <h3>Delete your account</h3>
unless you want your account deleted. If you delete your account, all <span
attached data, including projects, will be removed from our servers. >Clicking on this WILL delete your account. Do not click on this
This cannot be reversed, so be careful!</span 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 <input
value="Delete account" value="Delete account"
type="button" type="button"
@@ -96,3 +105,26 @@ export default {
}, },
} }
</script> </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" /> <hr class="card-divider" />
<h3 class="sidebar__item">About me</h3> <h3 class="sidebar__item">About me</h3>
<span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span> <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="sidebar__item stats-block">
<div class="stats-block__item secondary-stat"> <div class="stats-block__item secondary-stat">
<SunriseIcon class="secondary-stat__icon" aria-hidden="true" /> <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 Badge from '~/components/ui/Badge'
import Advertisement from '~/components/ads/Advertisement' 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 ReportIcon from '~/assets/images/utils/report.svg?inline'
import SunriseIcon from '~/assets/images/utils/sunrise.svg?inline' import SunriseIcon from '~/assets/images/utils/sunrise.svg?inline'
import DownloadIcon from '~/assets/images/utils/download.svg?inline' import DownloadIcon from '~/assets/images/utils/download.svg?inline'
@@ -139,6 +148,7 @@ export default {
ProjectCard, ProjectCard,
SunriseIcon, SunriseIcon,
DownloadIcon, DownloadIcon,
GitHubIcon,
ReportIcon, ReportIcon,
Badge, Badge,
SettingsIcon, SettingsIcon,
@@ -160,10 +170,17 @@ export default {
]) ])
).map((it) => it.data) ).map((it) => it.data)
const githubUrl = (
await (
await fetch(`https://api.github.com/user/` + user.github_id)
).json()
).html_url
return { return {
selectedProjectType: 'all', selectedProjectType: 'all',
user, user,
projects, projects,
githubUrl,
} }
} catch { } catch {
data.error({ data.error({

View File

@@ -98,4 +98,15 @@ export default ({ store }, inject) => {
return output.join(', ') 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]
})
} }