Rewrite Parity (#647)

* Rewrite Parity

* Update SEO, fix modals, add dashes to changelog

* Edit create version title

* Cache tags, SEO for search/partial noscript support, notifications fix

* Deploy?

* Fix vercel config

* Fix it again

* Finish user editing

* Remove broken docker build

* Switch reports to modals

* Update project card

* Navbar line animation in most places

* Add chips

* Move to navlink query params

* remove autogen file

* Add copy code

* Fix webkit text box outlines, port report modal

* Update error page

* Switch to avatar component

* Make keyboard nav work

* Fix team member spacing

* improve project ID display (#676)

* Bug fixes

* Update OG site title

* More fixes

* Design tweaks

* Fix card wrapping on mobile

* Darken light theme color a little

* Sidebar navigation for settings, notifications, and moderation

* Change follow icon from a heart to a bell

* Revert "Change follow icon from a heart to a bell"

This reverts commit e30b46ec5d93c57df847be88eba123c7419dd03b.

* Change follows icon in settings

* AaaaUUUUUUUGghghhhhhhhh

* Project sidebar transparent button animations

* Update file input button styling and change icon remove button text

* Fix environments filter condition being inverted

* Remove -> revert

* Improve readability of warning banners on light mode

* Fix mobile menu button colors

* Clean up notifications page more

* Creator dashboard and monetization work

* Add processing fees declarations and acknowledgement box

* Beta badges

* Downgrade Nuxt Vercel Builder

* Update the style of button groups to be more consistent

* More button consistency

* Remove desktop navbar on mobile

* Update home page progress indicators

* Fix page jumping (Thanks @stairman06)

* Make checkbox checked style consistent with other selection indicators

* More home page updates

* Properly reset NavRows

* Move filters menu on mobile

* Stylized checkbox updated to match active styling

* Filters icon

* Respect prefers-reduced-motion

* Add most backend payouts changes (untested)

* Finish tested payouts code

* Allow monetization unenrolling

* No longer use brand color for active highlights on standard nav elements

* More consistent button group on project page

* Rounded tables

* Fix some things (#716)

* Team member fixes + re-add changelog/versions stuff

* Remove dummy data

* The great CSS refactor

* Remove commented out css

* Give modals the legacy label styles and update profile edit labels

* Fix active chip size

* Remove shadow from selected chip

* Require email set for CMP

* Update styles of notifications to universal-card

* Equivalent exchange, trading some jank for some less bad jank

* Fix all gallery buttons being missing when there is only 1 image

* Update project creation modal

* Make beta badge less bright

* Beta badge heading styling

* Update withdraw processing fees info

* Remove redundant label

* be

* Fix inverted logic

* 2% is 0.02

* Add toggle to turn off alpha modpacks banner

* Why warning button?

* Add more footer links (#719)

* Add more footer links

* Move twitter

* Make items on user pages less comically large and move ad above navigation

* Bump text down a little on home page

* Update favicon colors

* Remove task list package and change default description to use bullet points

* I don't remember why I made this important but let's not

* Ah, yes

* this doesn't actually need to be important

* Align items in input groups

* Adjust some spacings and clear creation modal on opening

* Versions now clickable

* Add link to edit page to default description

* Improve monetization information text

* Make wrapped text inputs not shrink

* Make chips work better

* smol margin on clear mod message button

* Allow non-authenticated users to access settings

* Remove settings anchors

* Fix versions page button style on firefox

* Add advanced rendering toggle

* Update slug input and icon card in project edit page

* Legal sidebar

* h1 at beginning of description no longer has top margin

* Use universal card for legal pages

* Update email addresses on legal pages

* Update various page titles and descriptions for consistency

* Various fixes and consolidation to API URL retrieval

Prevents a bug where it's possible to generate the tags under one API, switch the API, and still have tags leftover from the old API

Also finally fixes staging URL being jank

* Make the theme button show regardless of login state

Also remove the change theme from the user dropdown because it's very redundant with the several other ways of changing theme

* Make mobile profile dropdown ordering consistent with desktop

* Change the base url back

* Revert "Change the base url back"

This reverts commit c1da89fddb83776b39f626eab33c8dc67f8a75e4.

* constantize

* Tiny fixes (#722)

* Box-shadow chip outlines

* Show settings when signed out

* mods -> projects

* space

* Beta badge border

* Slug input overflow fix, scrollable

* 🙈 it will all be okay 🙊 this is just temporary 🙉 😭😭 forgive me

* Fix minor bugs

* fix moderation  page

* More fixes

* Temp fix for download button

* BEGONE TABLES

* Fix download button

Co-authored-by: Ryan Cao <70191398+ryanccn@users.noreply.github.com>
Co-authored-by: Prospector <prospectordev@gmail.com>
Co-authored-by: stairman06 <36215135+stairman06@users.noreply.github.com>
Co-authored-by: triphora <emmaffle@modrinth.com>
This commit is contained in:
Geometrically
2022-11-12 17:57:40 -07:00
committed by GitHub
parent 66d0ee8156
commit 20785926e2
100 changed files with 7572 additions and 7284 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,56 +6,63 @@
@updateVersions="updateVersions"
/>
<div class="card">
<div v-for="version in filteredVersions" :key="version.id">
<div class="version-header">
<span :class="'circle ' + version.version_type" />
<div class="version-header-text">
<h2 class="name title-link">
<nuxt-link
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`"
>{{ version.name }}</nuxt-link
>
</h2>
<span v-if="members.find((x) => x.user.id === version.author_id)">
by
<nuxt-link
class="text-link"
:to="
'/user/' +
members.find((x) => x.user.id === version.author_id).user
.username
"
>{{
members.find((x) => x.user.id === version.author_id).user
.username
}}</nuxt-link
>
</span>
<span>
on
{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span
>
</div>
<a
:href="$parent.findPrimary(version).url"
class="iconified-button download"
:title="`Download ${version.name}`"
>
<DownloadIcon aria-hidden="true" />
Download
</a>
</div>
<div
v-for="version in filteredVersions"
:key="version.id"
class="changelog-item"
>
<div
v-highlightjs
:class="'markdown-body ' + version.version_type"
v-html="
version.changelog
? $xss($md.render(version.changelog))
: 'No changelog specified.'
"
/>
:class="`changelog-bar ${version.version_type} ${
version.duplicate ? 'duplicate' : ''
}`"
></div>
<div class="version-wrapper">
<div class="version-header">
<div class="version-header-text">
<h2 class="name">
<nuxt-link
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`"
>{{ version.name }}</nuxt-link
>
</h2>
<span v-if="members.find((x) => x.user.id === version.author_id)">
by
<nuxt-link
class="text-link"
:to="
'/user/' +
members.find((x) => x.user.id === version.author_id).user
.username
"
>{{
members.find((x) => x.user.id === version.author_id).user
.username
}}</nuxt-link
>
</span>
<span>
on
{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span
>
</div>
<a
:href="$parent.findPrimary(version).url"
class="iconified-button download"
:title="`Download ${version.name}`"
>
<DownloadIcon aria-hidden="true" />
Download
</a>
</div>
<div
v-if="version.changelog && !version.duplicate"
v-highlightjs
class="markdown-body"
v-html="$xss($md.render(version.changelog))"
/>
</div>
</div>
</div>
</div>
@@ -66,8 +73,8 @@ import VersionFilterControl from '~/components/ui/VersionFilterControl'
export default {
components: {
DownloadIcon,
VersionFilterControl,
DownloadIcon,
},
props: {
project: {
@@ -92,21 +99,145 @@ export default {
data() {
return {
filteredVersions: this.versions,
currentPage: 1,
}
},
fetch() {
if (this.$route.query.page)
this.currentPage = parseInt(this.$route.query.page)
this.filteredVersions = this.versions.map((version, index) => {
const nextVersion = this.versions[index + 1]
if (
nextVersion &&
version.changelog &&
nextVersion.changelog === version.changelog
) {
return { duplicate: true, ...version }
} else {
return { duplicate: false, ...version }
}
})
},
head() {
const title = `${this.project.title} - Changelog`
const description = `Explore the changelog of ${this.project.title}'s ${this.versions.length} versions.`
return {
title,
meta: [
{
hid: 'og:title',
name: 'og:title',
content: title,
},
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: title,
},
{
hid: 'og:description',
name: 'og:description',
content: description,
},
{
hid: 'description',
name: 'description',
content: description,
},
],
}
},
auth: false,
methods: {
switchPage(page, toTop) {
this.currentPage = page
this.$router.replace(this.getPageLink(page))
if (toTop) {
setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 50)
setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 50)
}
},
getPageLink(page) {
if (page === 1) {
return this.$route.path
} else {
return `${this.$route.path}?page=${this.currentPage}`
}
},
updateVersions(updatedVersions) {
this.filteredVersions = updatedVersions
},
},
auth: false,
}
</script>
<style lang="scss" scoped>
.changelog-item {
display: flex;
margin-bottom: 1rem;
position: relative;
padding-left: 1.8rem;
.changelog-bar {
--color: var(--color-badge-green-bg);
&.alpha {
--color: var(--color-badge-red-bg);
}
&.release {
--color: var(--color-badge-green-bg);
}
&.beta {
--color: var(--color-badge-yellow-bg);
}
left: 0;
top: 0.5rem;
width: 0.2rem;
min-width: 0.2rem;
position: absolute;
margin: 0 0.4rem;
border-radius: var(--size-rounded-max);
min-height: 100%;
background-color: var(--color);
&:before {
content: '';
width: 1rem;
height: 1rem;
position: absolute;
top: 0;
left: -0.4rem;
border-radius: var(--size-rounded-max);
background-color: var(--color);
}
&.duplicate {
background: linear-gradient(
to bottom,
transparent,
transparent 30%,
var(--color) 30%,
var(--color)
);
background-size: 100% 10px;
}
&.duplicate {
height: calc(100% + 1.5rem);
}
}
}
.version-header {
display: flex;
align-items: center;
margin-top: 0.2rem;
.circle {
min-width: 0.75rem;
@@ -114,24 +245,11 @@ export default {
border-radius: 50%;
display: inline-block;
margin-right: 0.25rem;
&.alpha {
background-color: var(--color-badge-red-bg);
}
&.release {
background-color: var(--color-badge-green-bg);
}
&.beta {
background-color: var(--color-badge-yellow-bg);
}
}
.version-header-text {
display: flex;
align-items: baseline;
margin: 0 0.75rem;
flex-wrap: wrap;
h2 {
@@ -155,20 +273,6 @@ export default {
}
.markdown-body {
margin: 0.5rem 0.5rem 1rem calc(0.375rem - 1px);
padding-left: 1.275rem;
border-left: 2px solid var(--color-text);
&.alpha {
border-left-color: var(--color-badge-red-bg);
}
&.release {
border-left-color: var(--color-badge-green-bg);
}
&.beta {
border-left-color: var(--color-badge-yellow-bg);
}
margin: 0.5rem 0.5rem 0 0;
}
</style>

View File

@@ -1,40 +1,42 @@
<template>
<div class="page-contents">
<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 class="page-contents legacy-label-styles">
<header class="header-card">
<div class="header__row">
<h2 class="header__title">Edit project</h2>
<div class="input-group">
<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 column"
:disabled="!$nuxt.$loading"
@click="saveProjectNotForReview"
>
<SaveIcon />
Save changes
</button>
</div>
</div>
<div v-if="showKnownErrors" class="known-errors">
<ul>
@@ -43,7 +45,7 @@
Your project must have a summary.
</li>
<li v-if="newProject.slug === ''">
Your project must have a vanity URL.
Your project cannot have an empty URL suffix.
</li>
<li v-if="!savingAsDraft && newProject.body === ''">
Your project must have a body to submit for review.
@@ -74,6 +76,7 @@
:class="{ 'known-error': newProject.title === '' && showKnownErrors }"
type="text"
placeholder="Enter the name"
maxlength="64"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
@@ -94,6 +97,7 @@
}"
type="text"
placeholder="Enter the summary"
maxlength="256"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
@@ -164,45 +168,49 @@
@input="setCategories"
/>
</label>
<label class="vertical-input">
<span>
<h3>Vanity URL (slug)<span class="required">*</span></h3>
<span class="slug-description"
>https://modrinth.com/{{ project.project_type.toLowerCase() }}/{{
newProject.slug ? newProject.slug : 'your-slug'
}}
</span>
</span>
<input
id="name"
v-model="newProject.slug"
<div class="universal-labels">
<label for="slug">
<span class="label__title">URL<span class="required">*</span></span>
</label>
<div
class="text-input-wrapper"
:class="{ 'known-error': newProject.slug === '' && showKnownErrors }"
type="text"
placeholder="Enter the vanity URL"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
/>
</label>
>
<div class="text-input-wrapper__before">
https://modrinth.com/{{ project.project_type.toLowerCase() }}/
</div>
<!-- this is a textarea so it is horizontally scrollable on mobile -->
<textarea
id="slug"
v-model="newProject.slug"
type="text"
maxlength="64"
autocorrect="off"
autocomplete="off"
autocapitalize="none"
rows="1"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
@input="manualSlug = true"
/>
</div>
</div>
</section>
<section class="card project-icon">
<h3>Icon</h3>
<img
:src="
previewImage
? previewImage
: newProject.icon_url && !iconChanged
? newProject.icon_url
: 'https://cdn.modrinth.com/placeholder.svg'
"
<Avatar
size="lg"
class="avatar"
:src="previewImage ? previewImage : newProject.icon_url"
alt="preview-image"
/>
<SmartFileInput
<FileInput
:max-size="262144"
:show-icon="false"
:show-icon="true"
accept="image/png,image/jpeg,image/gif,image/webp"
class="choose-image"
prompt="Choose image or drag it here"
prompt="Choose image"
:disabled="(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS"
@change="showPreviewImage"
/>
@@ -212,11 +220,11 @@
@click="
icon = null
previewImage = null
iconChanged = true
iconChanged = false
"
>
<TrashIcon />
Reset
<RevertIcon />
Revert
</button>
</section>
<section
@@ -289,7 +297,7 @@
>. HTML can also be used inside your description, not including styles,
scripts, and iframes (though YouTube iframes are allowed).
</span>
<ThisOrThat
<Chips
v-model="bodyViewMode"
class="separator"
:items="['source', 'preview']"
@@ -329,6 +337,7 @@
v-model="newProject.issues_url"
type="url"
placeholder="Enter a valid URL"
maxlength="2048"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
@@ -341,6 +350,7 @@
<input
v-model="newProject.source_url"
type="url"
maxlength="2048"
placeholder="Enter a valid URL"
/>
</label>
@@ -351,17 +361,22 @@
<input
v-model="newProject.wiki_url"
type="url"
maxlength="2048"
placeholder="Enter a valid URL"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
/>
</label>
<label title="An invitation link to your Discord server.">
<label
class="no-margin"
title="An invitation link to your Discord server."
>
<span>Discord invite</span>
<input
v-model="newProject.discord_url"
type="url"
maxlength="2048"
placeholder="Enter a valid URL"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
@@ -389,7 +404,7 @@
>
for more information.
</span>
<div class="input-group">
<div class="legacy-input-group">
<Multiselect
v-model="license"
placeholder="Choose license..."
@@ -407,7 +422,11 @@
<input
v-model="license_url"
type="url"
maxlength="2048"
placeholder="License URL"
:class="{
'known-error': newProject.license_url === '' && showKnownErrors,
}"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
@@ -415,21 +434,23 @@
</div>
</label>
</section>
<section class="card donations">
<div class="title">
<h3>Donation links</h3>
<button
title="Add a link"
class="iconified-button"
:disabled="false"
@click="
donationPlatforms.push({})
donationLinks.push('')
"
>
<PlusIcon />
Add a link
</button>
<section class="header-card donations">
<div class="header__row">
<h3 class="header__title">Donation links</h3>
<div class="input-group">
<button
title="Add a link"
class="iconified-button"
:disabled="false"
@click="
donationPlatforms.push({})
donationLinks.push('')
"
>
<PlusIcon />
Add a link
</button>
</div>
</div>
<div v-for="(item, index) in donationPlatforms" :key="index">
<label title="The donation link.">
@@ -438,6 +459,7 @@
v-model="donationLinks[index]"
type="url"
placeholder="Enter a valid URL"
class="donation-link-input"
/>
</label>
<label title="The donation platform of the link.">
@@ -482,29 +504,24 @@ 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 RevertIcon from '~/assets/images/utils/undo.svg?inline'
import ThisOrThat from '~/components/ui/ThisOrThat'
import SmartFileInput from '~/components/ui/SmartFileInput'
import Chips from '~/components/ui/Chips'
import FileInput from '~/components/ui/FileInput'
import Avatar from '~/components/ui/Avatar'
export default {
components: {
SmartFileInput,
ThisOrThat,
Avatar,
FileInput,
Chips,
Multiselect,
CrossIcon,
CheckIcon,
PlusIcon,
SaveIcon,
TrashIcon,
},
beforeRouteLeave(to, from, next) {
if (
this.isEditing &&
!window.confirm('Are you sure that you want to leave without saving?')
) {
return
}
next()
RevertIcon,
},
props: {
project: {
@@ -550,6 +567,7 @@ export default {
showKnownErrors: false,
savingAsDraft: false,
manualSlug: false,
}
},
fetch() {
@@ -601,16 +619,6 @@ export default {
}
},
},
mounted() {
function preventLeave(e) {
e.preventDefault()
e.returnValue = ''
}
window.addEventListener('beforeunload', preventLeave)
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('beforeunload', preventLeave)
})
},
created() {
this.UPLOAD_VERSION = 1 << 0
this.DELETE_VERSION = 1 << 1
@@ -643,7 +651,7 @@ export default {
const reviewConditions =
this.newProject.body !== '' && this.newProject.versions.length > 0
if (
this.newProject.name !== '' &&
this.newProject.title !== '' &&
this.newProject.description !== '' &&
this.newProject.slug !== '' &&
this.license.short !== null &&
@@ -789,13 +797,13 @@ label {
input,
.multiselect,
.input-group {
.legacy-input-group {
flex: 3;
height: fit-content;
}
}
.input-group {
.legacy-input-group {
display: flex;
flex-direction: column;
@@ -849,24 +857,10 @@ label {
header {
grid-area: header;
padding: var(--spacing-card-md) var(--spacing-card-lg);
h3 {
margin: auto 0;
color: var(--color-text-dark);
font-weight: var(--font-weight-extrabold);
}
button {
margin-left: 0.5rem;
}
}
section.essentials {
grid-area: essentials;
label {
margin-bottom: 0.5rem;
}
@media screen and (min-width: 1024px) {
input {
@@ -877,15 +871,17 @@ section.essentials {
section.project-icon {
grid-area: project-icon;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-card-sm);
img {
max-width: 100%;
margin-bottom: 0.25rem;
border-radius: var(--size-rounded-lg);
.avatar {
margin-bottom: var(--spacing-card-sm);
}
.iconified-button {
margin-top: 0.5rem;
margin-top: var(--spacing-card-sm);
}
}
@@ -901,10 +897,6 @@ section.game-sides {
.labeled-control {
margin-left: var(--spacing-card-lg);
h3 {
margin-bottom: var(--spacing-card-sm);
}
}
}
}
@@ -959,10 +951,6 @@ section.donations {
flex: 1;
}
}
button {
margin: 0.5rem 0;
}
}
.footer {
@@ -973,7 +961,9 @@ section.donations {
cursor: pointer;
}
.card {
.card,
.universal-card,
.header-card {
margin-bottom: 0;
}
@@ -991,4 +981,35 @@ section.donations {
margin-left: 0 !important;
}
}
.legacy-input-group {
display: flex;
flex-direction: column;
* {
margin-bottom: var(--spacing-card-sm);
}
}
.text-input-wrapper {
width: 100%;
display: flex;
align-items: center;
textarea {
width: 100%;
height: 100%;
margin-left: 0 !important;
white-space: nowrap;
overflow-x: auto;
overflow-y: none;
resize: none;
min-height: 0;
}
margin-bottom: var(--spacing-card-md);
}
.donation-link-input {
width: 100%;
}
</style>

View File

@@ -32,7 +32,7 @@
</p>
</div>
<div class="controls">
<div v-if="gallery.length > 1" class="buttons">
<div class="buttons">
<button
class="close circle-button"
@click="expandedGalleryItem = null"
@@ -54,10 +54,18 @@
<ExpandIcon v-if="!zoomedIn" aria-hidden="true" />
<ContractIcon v-else aria-hidden="true" />
</button>
<button class="previous circle-button" @click="previousImage()">
<button
v-if="gallery.length > 1"
class="previous circle-button"
@click="previousImage()"
>
<LeftArrowIcon aria-hidden="true" />
</button>
<button class="next circle-button" @click="nextImage()">
<button
v-if="gallery.length > 1"
class="next circle-button"
@click="nextImage()"
>
<RightArrowIcon aria-hidden="true" />
</button>
</div>
@@ -66,6 +74,32 @@
</div>
</div>
<div v-if="currentMember" class="card buttons header-buttons">
<button
class="iconified-button"
:class="{
'brand-button':
newGalleryItems.length === 0 &&
editGalleryIndexes.length === 0 &&
deleteGalleryUrls.length === 0,
}"
@click="
newGalleryItems.push({
title: '',
description: '',
featured: false,
url: '',
})
"
>
<PlusIcon />
{{
newGalleryItems.length === 0 &&
editGalleryIndexes.length === 0 &&
deleteGalleryUrls.length === 0
? 'Add an image'
: 'Add another image'
}}
</button>
<button
v-if="
newGalleryItems.length > 0 ||
@@ -84,26 +118,12 @@
editGalleryIndexes.length > 0 ||
deleteGalleryUrls.length > 0
"
class="action brand-button-colors iconified-button"
class="action brand-button iconified-button"
@click="saveGallery"
>
<CheckIcon />
Save changes
</button>
<button
class="iconified-button"
@click="
newGalleryItems.push({
title: '',
description: '',
featured: false,
url: '',
})
"
>
<PlusIcon />
Add an image
</button>
</div>
<div class="items">
<div
@@ -147,7 +167,7 @@
<CalendarIcon />
{{ $dayjs(item.created).format('MMMM D, YYYY') }}
</div>
<div v-if="currentMember" class="gallery-buttons">
<div v-if="currentMember" class="gallery-buttons input-group">
<button
v-if="editGalleryIndexes.includes(index)"
class="iconified-button"
@@ -213,7 +233,7 @@
</div>
</div>
<div class="gallery-bottom">
<SmartFileInput
<FileInput
:max-size="5242880"
accept="image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp"
prompt="Choose image or drag it here"
@@ -249,7 +269,7 @@ import ExternalIcon from '~/assets/images/utils/external.svg?inline'
import ExpandIcon from '~/assets/images/utils/expand.svg?inline'
import ContractIcon from '~/assets/images/utils/contract.svg?inline'
import SmartFileInput from '~/components/ui/SmartFileInput'
import FileInput from '~/components/ui/FileInput'
import Checkbox from '~/components/ui/Checkbox'
export default {
@@ -260,7 +280,7 @@ export default {
EditIcon,
TrashIcon,
CheckIcon,
SmartFileInput,
FileInput,
CrossIcon,
RightArrowIcon,
LeftArrowIcon,
@@ -302,6 +322,36 @@ export default {
fetch() {
this.gallery = JSON.parse(JSON.stringify(this.project.gallery))
},
head() {
const title = `${this.project.title} - Gallery`
const description = `View ${this.project.gallery.length} images of ${this.project.title} on Modrinth.`
return {
title,
meta: [
{
hid: 'og:title',
name: 'og:title',
content: title,
},
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: title,
},
{
hid: 'og:description',
name: 'og:description',
content: description,
},
{
hid: 'description',
name: 'description',
content: description,
},
],
}
},
mounted() {
this._keyListener = function (e) {
if (this.expandedGalleryItem) {
@@ -456,6 +506,7 @@ export default {
line-height: 1;
display: flex;
max-width: 2rem;
color: var(--color-button-text);
background-color: var(--color-button-bg);
border-radius: var(--size-rounded-max);
margin: 0;
@@ -563,18 +614,6 @@ export default {
button {
margin-right: 0.5rem;
&.brand-button-colors {
background-color: var(--color-brand);
&:hover {
background-color: var(--color-brand-hover);
}
&:active {
background-color: var(--color-brand-active);
}
}
}
}

View File

@@ -22,6 +22,8 @@ export default {
<style lang="scss" scoped>
.markdown-body {
max-width: calc(100% - (2 * var(--spacing-card-lg)));
max-width: calc(
60rem - 2 * var(--spacing-card-lg) - 9px
); // $2.50 to anyone who can figure out why the 9px is needed
}
</style>

View File

@@ -1,103 +1,94 @@
<template>
<div>
<ConfirmPopup
ref="delete_popup"
<ModalConfirm
ref="modal_confirm"
title="Are you sure you want to delete this project?"
description="If you proceed, all versions and any attached data will be removed from our servers. This may break other projects, so be careful."
:has-to-type="true"
:confirmation-text="project.title"
proceed-label="Delete project"
proceed-label="Delete"
@proceed="deleteProject"
/>
<div class="card">
<h3>General</h3>
</div>
<section class="card main-settings">
<label>
<span>
<h3>Edit project</h3>
<span>
This leads you to a page where you can edit your project.
<div class="universal-card">
<h2>General settings</h2>
<div class="adjacent-input">
<label>
<span class="label__title">Edit project information</span>
<span class="label__description">
Edit your project's name, description, categories, and more.
</span>
</span>
<div>
<nuxt-link class="iconified-button" to="edit"
><EditIcon />Edit</nuxt-link
>
</div>
</label>
<label>
<span>
<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>
</label>
<nuxt-link class="iconified-button" to="edit"
><EditIcon />Edit</nuxt-link
>
</div>
<div class="adjacent-input">
<span class="label">
<span class="label__title">Delete project</span>
<span class="label__description">
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"
<button
class="iconified-button danger-button"
:disabled="
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
"
@click="showPopup"
@click="$refs.modal_confirm.show()"
>
<TrashIcon />Delete project
</div>
</label>
</section>
<div class="card columns team-invite">
<h3>Team members</h3>
<div
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
class="column"
>
<input
id="username"
v-model="currentUsername"
type="text"
placeholder="Username"
/>
<label for="username" class="hidden">Username</label>
<button
class="iconified-button brand-button-colors column"
@click="inviteTeamMember"
>
<PlusIcon />
Invite
</button>
</div>
</div>
<div class="universal-card">
<h2>Manage members</h2>
<div class="adjacent-input">
<span class="label">
<span class="label__title">Invite a member</span>
<span class="label__description">
Enter the Modrinth username of the person you'd like to invite to be
a member of this project.
</span>
</span>
<div
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
class="input-group"
>
<input
id="username"
v-model="currentUsername"
type="text"
placeholder="Username"
/>
<label for="username" class="hidden">Username</label>
<button
class="iconified-button brand-button"
@click="inviteTeamMember"
>
<PlusIcon />
Invite
</button>
</div>
</div>
</div>
<div
v-for="(member, index) in allTeamMembers"
:key="member.user.id"
class="card member"
class="universal-card member"
:class="{ open: openTeamMembers.includes(member.user.id) }"
>
<div class="member-header">
<div class="info">
<img :src="member.avatar_url" :alt="member.name" />
<Avatar
:src="member.avatar_url"
:alt="member.username"
size="sm"
circle
/>
<div class="text">
<nuxt-link :to="'/user/' + member.user.username" class="name">
<p class="title-link">{{ member.name }}</p>
<p>{{ member.name }}</p>
</nuxt-link>
<p>{{ member.role }}</p>
</div>
@@ -106,7 +97,6 @@
<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
@@ -121,98 +111,148 @@
</div>
</div>
<div class="content">
<div class="main-info">
<label>
Role:
<input
v-model="allTeamMembers[index].role"
type="text"
:class="{ 'known-error': member.role === 'Owner' }"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
"
/>
<div v-if="member.oldRole !== 'Owner'" class="adjacent-input">
<label :for="`member-${allTeamMembers[index].user.username}-role`">
<span class="label__title">Role</span>
<span class="label__description">
The title of the role that this member plays for this project.
</span>
</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"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
"
label="Upload version"
@input="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
/>
<Checkbox
:value="(member.permissions & DELETE_VERSION) === DELETE_VERSION"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & DELETE_VERSION) !== DELETE_VERSION
"
label="Delete version"
@input="allTeamMembers[index].permissions ^= DELETE_VERSION"
/>
<Checkbox
:value="(member.permissions & EDIT_DETAILS) === EDIT_DETAILS"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
label="Edit details"
@input="allTeamMembers[index].permissions ^= EDIT_DETAILS"
/>
<Checkbox
:value="(member.permissions & EDIT_BODY) === EDIT_BODY"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & EDIT_BODY) !== EDIT_BODY
"
label="Edit body"
@input="allTeamMembers[index].permissions ^= EDIT_BODY"
/>
<Checkbox
:value="(member.permissions & MANAGE_INVITES) === MANAGE_INVITES"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES
"
label="Manage invites"
@input="allTeamMembers[index].permissions ^= MANAGE_INVITES"
/>
<Checkbox
:value="(member.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
"
label="Remove member"
@input="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
/>
<Checkbox
:value="(member.permissions & EDIT_MEMBER) === EDIT_MEMBER"
<input
:id="`member-${allTeamMembers[index].user.username}-role`"
v-model="allTeamMembers[index].role"
type="text"
:class="{ 'known-error': member.role === 'Owner' }"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
"
label="Edit member"
@input="allTeamMembers[index].permissions ^= EDIT_MEMBER"
/>
<Checkbox
:value="(member.permissions & DELETE_PROJECT) === DELETE_PROJECT"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
"
label="Delete project"
@input="allTeamMembers[index].permissions ^= DELETE_PROJECT"
/>
</div>
<div class="actions">
<div class="adjacent-input">
<label
:for="`member-${allTeamMembers[index].user.username}-monetization-weight`"
>
<span class="label__title">Monetization weight</span>
<span class="label__description">
Relative to all other members' monetization weights, this
determines what portion of this project's revenue goes to this
member.
</span>
</label>
<input
:id="`member-${allTeamMembers[index].user.username}-monetization-weight`"
v-model="allTeamMembers[index].payouts_split"
type="number"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
"
/>
</div>
<ul
v-if="member.role === 'Owner' && member.oldRole !== 'Owner'"
class="known-errors"
>
<li>A project can only have one 'Owner'.</li>
</ul>
<template v-if="member.oldRole !== 'Owner'">
<span class="label">
<span class="label__title">Permissions</span>
</span>
<div class="permissions">
<Checkbox
:value="(member.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
"
label="Upload version"
@input="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
/>
<Checkbox
:value="(member.permissions & DELETE_VERSION) === DELETE_VERSION"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & DELETE_VERSION) !== DELETE_VERSION
"
label="Delete version"
@input="allTeamMembers[index].permissions ^= DELETE_VERSION"
/>
<Checkbox
:value="(member.permissions & EDIT_DETAILS) === EDIT_DETAILS"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
label="Edit details"
@input="allTeamMembers[index].permissions ^= EDIT_DETAILS"
/>
<Checkbox
:value="(member.permissions & EDIT_BODY) === EDIT_BODY"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & EDIT_BODY) !== EDIT_BODY
"
label="Edit body"
@input="allTeamMembers[index].permissions ^= EDIT_BODY"
/>
<Checkbox
:value="(member.permissions & MANAGE_INVITES) === MANAGE_INVITES"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES
"
label="Manage invites"
@input="allTeamMembers[index].permissions ^= MANAGE_INVITES"
/>
<Checkbox
:value="(member.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
"
label="Remove member"
@input="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
/>
<Checkbox
:value="(member.permissions & EDIT_MEMBER) === EDIT_MEMBER"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
"
label="Edit member"
@input="allTeamMembers[index].permissions ^= EDIT_MEMBER"
/>
<Checkbox
:value="(member.permissions & DELETE_PROJECT) === DELETE_PROJECT"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & DELETE_PROJECT) !== DELETE_PROJECT
"
label="Delete project"
@input="allTeamMembers[index].permissions ^= DELETE_PROJECT"
/>
<Checkbox
:value="(member.permissions & VIEW_ANALYTICS) === VIEW_ANALYTICS"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & VIEW_ANALYTICS) !== VIEW_ANALYTICS
"
label="View analytics"
@input="allTeamMembers[index].permissions ^= VIEW_ANALYTICS"
/>
<Checkbox
:value="(member.permissions & VIEW_PAYOUTS) === VIEW_PAYOUTS"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
(currentMember.permissions & VIEW_PAYOUTS) !== VIEW_PAYOUTS
"
label="View revenue"
@input="allTeamMembers[index].permissions ^= VIEW_PAYOUTS"
/>
</div>
</template>
<div class="button-group push-right">
<button
v-if="member.oldRole !== 'Owner'"
class="iconified-button"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
@@ -224,7 +264,7 @@
</button>
<button
v-if="
member.role !== 'Owner' &&
member.oldRole !== 'Owner' &&
currentMember.role === 'Owner' &&
member.accepted
"
@@ -235,10 +275,9 @@
Transfer ownership
</button>
<button
class="iconified-button brand-button-colors"
class="iconified-button brand-button"
:disabled="
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
member.role === 'Owner'
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
"
@click="updateTeamMember(index)"
>
@@ -252,7 +291,7 @@
</template>
<script>
import ConfirmPopup from '~/components/ui/ConfirmPopup'
import ModalConfirm from '~/components/ui/ModalConfirm'
import Checkbox from '~/components/ui/Checkbox'
import Badge from '~/components/ui/Badge'
@@ -262,11 +301,13 @@ 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'
import Avatar from '~/components/ui/Avatar'
export default {
components: {
Avatar,
DropdownIcon,
ConfirmPopup,
ModalConfirm,
Checkbox,
Badge,
PlusIcon,
@@ -304,6 +345,8 @@ export default {
},
fetch() {
this.allTeamMembers = this.allMembers
this.allTeamMembers.forEach((x) => (x.oldRole = x.role))
},
created() {
this.UPLOAD_VERSION = 1 << 0
@@ -314,6 +357,8 @@ export default {
this.REMOVE_MEMBER = 1 << 5
this.EDIT_MEMBER = 1 << 6
this.DELETE_PROJECT = 1 << 7
this.VIEW_ANALYTICS = 1 << 8
this.VIEW_PAYOUTS = 1 << 9
},
methods: {
async inviteTeamMember() {
@@ -368,10 +413,16 @@ export default {
this.$nuxt.$loading.start()
try {
const data = {
permissions: this.allTeamMembers[index].permissions,
role: this.allTeamMembers[index].role,
}
const data =
this.allTeamMembers[index].oldRole !== 'Owner'
? {
permissions: this.allTeamMembers[index].permissions,
role: this.allTeamMembers[index].role,
payouts_split: this.allTeamMembers[index].payouts_split,
}
: {
payouts_split: this.allTeamMembers[index].payouts_split,
}
await this.$axios.patch(
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
@@ -413,14 +464,6 @@ export default {
this.$nuxt.$loading.finish()
},
showPopup() {
if (
(this.currentMember.permissions & this.DELETE_PROJECT) ===
this.DELETE_PROJECT
) {
this.$refs.delete_popup.show()
}
},
async deleteProject() {
await this.$axios.delete(
`project/${this.project.id}`,
@@ -444,6 +487,7 @@ export default {
).data.map((it) => ({
avatar_url: it.user.avatar_url,
name: it.user.username,
oldRole: it.role,
...it,
}))
},
@@ -452,26 +496,12 @@ export default {
</script>
<style lang="scss" scoped>
.card {
h3 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
}
.member {
margin-bottom: var(--spacing-card-md);
.member-header {
display: flex;
justify-content: space-between;
.info {
display: flex;
img {
border-radius: var(--size-rounded-icon);
height: 50px;
width: 50px;
}
.text {
margin: auto 0 auto 0.5rem;
font-size: var(--font-size-sm);
@@ -499,33 +529,18 @@ export default {
.content {
display: none;
flex-direction: column;
padding-top: var(--spacing-card-md);
.main-info {
margin-bottom: var(--spacing-card-lg);
@media screen and (min-width: 1024px) {
label {
align-items: center;
input {
margin-left: 1rem;
}
}
}
}
.permissions {
margin: 1rem 0;
margin-bottom: var(--spacing-card-md);
max-width: 45rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-gap: 0.5rem;
label {
flex-direction: row;
input {
flex: none;
margin-right: 0.5rem;
}
}
}
}
@@ -536,90 +551,8 @@ export default {
}
}
.content {
display: unset;
margin: var(--spacing-card-lg);
}
}
}
input,
button {
&:disabled {
cursor: not-allowed !important;
}
}
section {
margin-bottom: var(--spacing-card-md);
label {
display: flex;
span {
flex: 2;
padding-right: var(--spacing-card-lg);
}
div {
flex: none;
}
input {
flex: 3;
height: fit-content;
}
}
}
.team-invite {
gap: 0.5rem;
@media screen and (max-width: 1024px) {
flex-direction: column;
h3 {
margin-bottom: 0.5rem;
}
}
h3 {
margin: auto auto auto 0;
}
> div {
display: flex;
align-items: center;
input {
margin-right: 1rem;
}
@media screen and (max-width: 500px) {
display: flex;
flex-direction: column;
input {
margin: 0;
}
button {
margin-top: 0.5rem;
}
}
}
}
.actions {
display: flex;
button {
margin-right: 0.5rem;
&:first-child {
margin-left: auto;
}
}
}
.main-settings span {
margin-bottom: 1rem;
}
</style>

View File

@@ -22,26 +22,31 @@
</ul>
</div>
<div class="content card">
<ConfirmPopup
ref="delete_version_popup"
<ModalConfirm
ref="modal_confirm"
title="Are you sure you want to delete this version?"
description="This will remove this version forever (like really forever)."
:has-to-type="false"
proceed-label="Delete version"
proceed-label="Delete"
@proceed="deleteVersion()"
/>
<ModalReport
ref="modal_version_report"
:item-id="version.id"
item-type="version"
/>
<div class="columns">
<nuxt-link
v-if="mode === 'version'"
class="iconified-button back-button"
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/${
$nuxt.context.from
? $nuxt.context.from.name === 'type-id-changelog'
? 'changelog'
: 'versions'
: 'versions'
:to="`${
$nuxt.context.from &&
($nuxt.context.from.name === 'type-id-changelog' ||
$nuxt.context.from.name === 'type-id-versions')
? $nuxt.context.from.fullPath
: `/${project.project_type}/${
project.slug ? project.slug : project.id
}/versions`
}`"
>
<BackIcon aria-hidden="true" />
@@ -73,11 +78,12 @@
class="full-width-input"
type="text"
placeholder="Enter an optional version name..."
maxlength="64"
/>
<Checkbox v-model="version.featured" label="Featured" />
<hr class="card-divider" />
</div>
<div v-if="mode === 'edit'" class="header-buttons buttons columns">
<div v-if="mode === 'edit'" class="header-buttons button-group columns">
<h3 class="column-grow-1">Edit version</h3>
<nuxt-link
v-if="$auth.user"
@@ -90,7 +96,7 @@
Cancel
</nuxt-link>
<button
class="iconified-button brand-button-colors"
class="iconified-button brand-button"
@click="saveEditedVersion"
>
<SaveIcon aria-hidden="true" />
@@ -99,7 +105,7 @@
</div>
<div
v-else-if="mode === 'create'"
class="header-buttons buttons columns"
class="header-buttons button-group columns"
>
<h3 class="column-grow-1">Create version</h3>
<nuxt-link
@@ -112,42 +118,36 @@
<CrossIcon aria-hidden="true" />
Cancel
</nuxt-link>
<button
class="iconified-button brand-button-colors"
@click="createVersion"
>
<button class="iconified-button brand-button" @click="createVersion">
<CheckIcon aria-hidden="true" />
Create
</button>
</div>
<div v-else class="buttons">
<div v-else class="button-group">
<a
v-if="primaryFile"
v-tooltip="
primaryFile.filename + ' (' + $formatBytes(primaryFile.size) + ')'
"
:href="primaryFile.url"
class="bold-button iconified-button brand-button-colors"
class="bold-button iconified-button brand-button"
:title="`Download ${primaryFile.filename}`"
>
<DownloadIcon aria-hidden="true" />
Download
</a>
<nuxt-link
:to="`/create/report?id=${version.id}&t=version`"
<button
v-if="$auth.user"
class="action iconified-button"
@click="$refs.modal_version_report.show()"
>
<ReportIcon aria-hidden="true" />
Report
</nuxt-link>
<button
v-if="currentMember"
class="action iconified-button"
@click="$refs.delete_version_popup.show()"
>
<TrashIcon aria-hidden="true" />
Delete
</button>
<a v-else class="action iconified-button" :href="authUrl">
<ReportIcon aria-hidden="true" />
Report
</a>
<nuxt-link
v-if="currentMember"
class="action iconified-button"
@@ -159,11 +159,20 @@
<EditIcon aria-hidden="true" />
Edit
</nuxt-link>
<button
v-if="currentMember"
class="action iconified-button danger-button"
@click="$refs.modal_confirm.show()"
>
<TrashIcon aria-hidden="true" />
Delete
</button>
</div>
<section v-if="mode === 'edit' || mode === 'create'">
<h3>Changelog</h3>
<ThisOrThat
<Chips
v-model="changelogViewMode"
class="separator"
:items="['source', 'preview']"
/>
<div v-if="changelogViewMode === 'source'" class="textarea-wrapper">
@@ -289,6 +298,7 @@
v-model="version.version_number"
type="text"
placeholder="Enter the version number..."
maxlength="32"
/>
<p v-else class="value">{{ version.version_number }}</p>
</div>
@@ -358,7 +368,7 @@
</div>
<div v-if="mode === 'version'" class="data">
<p class="title">Version ID</p>
<p class="value">{{ version.id }}</p>
<p class="value"><CopyCode :text="version.id" /></p>
</div>
</div>
<hr class="card-divider" />
@@ -380,16 +390,10 @@
:key="index"
class="dependency"
>
<img
class="icon"
:src="
dependency.project
? dependency.project.icon_url
? dependency.project.icon_url
: 'https://cdn.modrinth.com/placeholder.svg?inline'
: 'https://cdn.modrinth.com/placeholder.svg?inline'
"
<Avatar
:src="dependency.project ? dependency.project.icon_url : null"
alt="dependency-icon"
size="sm"
/>
<div class="info">
<nuxt-link
@@ -443,8 +447,9 @@
class="edit-dependency"
>
<h4>Add dependency</h4>
<ThisOrThat
<Chips
v-model="dependencyAddMode"
class="separator"
:items="['project', 'version']"
/>
<div class="edit-info">
@@ -582,9 +587,10 @@
</button>
</div>
</div>
<StatelessFileInput
<FileInput
v-if="mode === 'edit' || mode === 'create'"
multiple
should-always-reset
class="choose-files"
:accept="
project.actualProjectType.toLowerCase() === 'modpack'
@@ -629,8 +635,7 @@
</template>
<script>
import Multiselect from 'vue-multiselect'
import ConfirmPopup from '~/components/ui/ConfirmPopup'
import StatelessFileInput from '~/components/ui/StatelessFileInput'
import FileInput from '~/components/ui/FileInput'
import InfoIcon from '~/assets/images/utils/info.svg?inline'
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
@@ -645,11 +650,20 @@ import StarIcon from '~/assets/images/utils/star.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg?inline'
import VersionBadge from '~/components/ui/Badge'
import Checkbox from '~/components/ui/Checkbox'
import ThisOrThat from '~/components/ui/ThisOrThat'
import Chips from '~/components/ui/Chips'
import ModalConfirm from '~/components/ui/ModalConfirm'
import ModalReport from '~/components/ui/ModalReport'
import CopyCode from '~/components/ui/CopyCode'
import Avatar from '~/components/ui/Avatar'
export default {
components: {
ThisOrThat,
Avatar,
CopyCode,
ModalConfirm,
ModalReport,
FileInput,
Chips,
Checkbox,
VersionBadge,
DownloadIcon,
@@ -657,29 +671,14 @@ export default {
EditIcon,
ReportIcon,
BackIcon,
ConfirmPopup,
StarIcon,
CheckIcon,
Multiselect,
SaveIcon,
PlusIcon,
CrossIcon,
StatelessFileInput,
InfoIcon,
},
beforeRouteLeave(to, from, next) {
if (this.mode === 'create') {
if (
!window.confirm('Are you sure that you want to leave without saving?')
) {
return
}
}
this.setVersion()
next()
},
props: {
project: {
type: Object,
@@ -738,8 +737,57 @@ export default {
}
},
async fetch() {
console.log(this.$nuxt.context.from)
await this.setVersion()
},
head() {
if (!this.version.game_versions) {
return {}
}
const title = `${
this.mode === 'create' ? 'Create Version' : this.version.name
} - ${this.project.title}`
const description = `Download ${this.project.title} ${
this.version.version_number
} on Modrinth. Supports ${this.$formatVersion(
this.version.game_versions
)} ${this.version.loaders
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
.join(' & ')}. Published on ${this.$dayjs(
this.version.date_published
).format('MMM D, YYYY')}. ${this.version.downloads} downloads.`
return {
title,
meta: [
{
hid: 'og:title',
name: 'og:title',
content: title,
},
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: title,
},
{
hid: 'og:description',
name: 'og:description',
content: description,
},
{
hid: 'description',
name: 'description',
content: description,
},
],
}
},
computed: {
authUrl() {
return `${process.env.authURLBase}auth/init?url=${process.env.domain}${this.$route.path}`
},
},
watch: {
'$route.path': {
async handler() {
@@ -747,31 +795,15 @@ export default {
},
},
},
mounted() {
if (this.mode === 'create') {
function preventLeave(e) {
e.preventDefault()
e.returnValue = ''
}
window.addEventListener('beforeunload', preventLeave)
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('beforeunload', preventLeave)
})
}
},
methods: {
checkFields() {
if (
return !(
this.version.version_number === '' ||
this.version.game_versions.length === 0 ||
(this.version.loaders.length === 0 &&
this.project.project_type !== 'resourcepack') ||
(this.newFiles.length === 0 && this.version.files.length === 0)
) {
return false
}
return true
)
},
reset() {
this.changelogViewMode = 'source'
@@ -1112,8 +1144,12 @@ export default {
section {
margin: 1rem 0;
h3 {
margin-bottom: 0.5rem;
.separator {
margin: var(--spacing-card-sm) 0;
}
.choose-files {
margin-bottom: var(--spacing-card-sm);
}
}
@@ -1126,10 +1162,6 @@ section {
flex-wrap: wrap;
row-gap: 0.5rem;
.bold-button {
font-weight: bold;
}
@media screen and (min-width: 1024px) {
margin-left: auto;
}
@@ -1186,19 +1218,12 @@ section {
.dependency {
align-items: center;
display: flex;
gap: 0.25rem;
@media screen and (min-width: 800px) {
flex-basis: 30%;
}
.icon {
width: 3rem;
height: 3rem;
margin-right: 0.5rem;
border-radius: var(--size-rounded-xs);
object-fit: contain;
}
.info {
display: flex;
flex-direction: column;
@@ -1279,10 +1304,6 @@ section {
margin-bottom: var(--spacing-card-sm);
}
.styled-tabs {
margin-bottom: var(--spacing-card-sm);
}
.textarea-wrapper {
display: inline-block;
width: 100%;

View File

@@ -1,10 +1,7 @@
<template>
<div class="content">
<div v-if="currentMember" class="card header-buttons">
<nuxt-link
to="version/create"
class="brand-button-colors iconified-button"
>
<nuxt-link to="version/create" class="brand-button iconified-button">
<PlusIcon />
Create a version
</nuxt-link>
@@ -14,111 +11,79 @@
:versions="versions"
@updateVersions="updateVersions"
/>
<div v-if="versions.length > 0" class="card">
<table>
<thead>
<tr>
<th role="presentation"></th>
<th>Version</th>
<th>Supports</th>
<th>Stats</th>
</tr>
</thead>
<tbody>
<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"
:title="`Download ${version.name}`"
>
<DownloadIcon aria-hidden="true" />
</a>
</td>
<td>
<div class="info">
<div class="top title-link">
<nuxt-link
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`"
>
{{ version.name }}
</nuxt-link>
</div>
<div class="bottom">
<VersionBadge
v-if="version.version_type === 'release'"
type="release"
color="green"
/>
<VersionBadge
v-else-if="version.version_type === 'beta'"
type="beta"
color="yellow"
/>
<VersionBadge
v-else-if="version.version_type === 'alpha'"
type="alpha"
color="red"
/>
<span class="divider" />
<span class="version_number">{{
version.version_number
}}</span>
</div>
<div class="mobile-info">
<p>
{{
version.loaders
.map((x) => $formatCategory(x))
.join(', ') +
' ' +
$formatVersion(version.game_versions)
}}
</p>
<p></p>
<p>
<strong>{{ $formatNumber(version.downloads) }}</strong>
downloads
</p>
<p>
Published on
<strong>{{
$dayjs(version.date_published).format('MMM D, YYYY')
}}</strong>
</p>
</div>
</div>
</td>
<td>
<p>
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
</p>
<p>{{ $formatVersion(version.game_versions) }}</p>
</td>
<td>
<p>
<span>{{ $formatNumber(version.downloads) }}</span>
downloads
</p>
<p>
Published on
<span>{{
$dayjs(version.date_published).format('MMM D, YYYY')
}}</span>
</p>
</td>
</tr>
</tbody>
</table>
<div v-if="versions.length > 0" class="universal-card all-versions">
<div class="header">
<div></div>
<div>Version</div>
<div>Supports</div>
<div>Stats</div>
</div>
<div
v-for="version in filteredVersions"
:key="version.id + '-new'"
class="version-button button-transparent"
@click="
$router.push(
`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`
)
"
>
<a
v-tooltip="
$parent.findPrimary(version).filename +
' (' +
$formatBytes($parent.findPrimary(version).size) +
')'
"
:href="$parent.findPrimary(version).url"
class="download-button"
:class="version.version_type"
:title="`Download ${version.name}`"
@click.stop="(event) => event.stopPropagation()"
>
<DownloadIcon aria-hidden="true" />
</a>
<span class="version__title">{{ version.name }}</span>
<div class="version__metadata">
<VersionBadge
v-if="version.version_type === 'release'"
type="release"
color="green"
/>
<VersionBadge
v-else-if="version.version_type === 'beta'"
type="beta"
color="yellow"
/>
<VersionBadge
v-else-if="version.version_type === 'alpha'"
type="alpha"
color="red"
/>
<span class="divider" />
<span class="version_number">{{ version.version_number }}</span>
</div>
<div class="version__supports">
<span>
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
</span>
<span>{{ $formatVersion(version.game_versions) }}</span>
</div>
<div class="version__stats">
<span>
<strong>{{ $formatNumber(version.downloads) }}</strong>
downloads
</span>
<span>
Published on
<strong>{{
$dayjs(version.date_published).format('MMM D, YYYY')
}}</strong>
</span>
</div>
</div>
</div>
</div>
</template>
@@ -161,6 +126,46 @@ export default {
filteredVersions: this.versions,
}
},
fetch() {
if (this.$route.query.page)
this.currentPage = parseInt(this.$route.query.page)
},
head() {
const title = `${this.project.title} - Versions`
const description = `Download and browse ${this.versions.length} ${
this.project.title
} versions. ${this.$formatNumber(
this.project.downloads
)} total downloads. Last updated ${this.$dayjs(
this.versions[0] ? this.versions[0].date_published : null
).format('MMM D, YYYY')}.`
return {
title,
meta: [
{
hid: 'og:title',
name: 'og:title',
content: title,
},
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: title,
},
{
hid: 'og:description',
name: 'og:description',
content: description,
},
{
hid: 'description',
name: 'description',
content: description,
},
],
}
},
methods: {
updateVersions(updatedVersions) {
this.filteredVersions = updatedVersions
@@ -170,89 +175,116 @@ export default {
</script>
<style lang="scss" scoped>
table {
border-collapse: separate;
border-spacing: 0 0.75rem;
th {
text-align: left;
font-size: var(--font-size-md);
&:nth-child(3),
&:nth-child(4) {
display: none;
}
}
tr {
td:nth-child(2) {
padding-right: 2rem;
min-width: 16rem;
.top {
font-weight: bold;
}
.bottom {
display: flex;
flex-direction: row;
align-items: center;
text-overflow: ellipsis;
margin-top: 0.25rem;
.divider {
width: 0.25rem;
height: 0.25rem;
border-radius: 50%;
display: inline-block;
margin: 0 0.25rem;
background-color: var(--color-text);
}
}
.mobile-info {
p {
margin: 0.25rem 0 0;
}
}
}
td:nth-child(3) {
display: none;
width: 100%;
p {
margin: 0.25rem 0;
}
}
td:nth-child(4) {
display: none;
min-width: 15rem;
p {
margin: 0.25rem 0;
span {
font-weight: bold;
}
}
}
}
}
@media screen and (min-width: 1024px) {
table {
tr {
th:nth-child(3),
td:nth-child(3),
th:nth-child(4),
td:nth-child(4) {
display: table-cell;
}
}
}
.mobile-info {
display: none;
}
}
.header-buttons {
display: flex;
justify-content: right;
}
.all-versions {
display: flex;
flex-direction: column;
.header {
display: grid;
grid-template: 'download title supports stats';
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr 1fr 1fr;
color: var(--color-text-dark);
font-size: var(--font-size-md);
font-weight: bold;
justify-content: left;
margin-inline: var(--spacing-card-md);
margin-bottom: var(--spacing-card-sm);
column-gap: var(--spacing-card-sm);
div:first-child {
grid-area: download;
}
div:nth-child(2) {
grid-area: title;
}
div:nth-child(3) {
grid-area: supports;
}
div:nth-child(4) {
grid-area: stats;
}
}
.version-button {
display: grid;
grid-template: 'download title supports stats' 'download metadata supports stats';
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr 1fr 1fr;
column-gap: var(--spacing-card-sm);
justify-content: left;
padding: var(--spacing-card-md);
.download-button {
grid-area: download;
}
.version__title {
grid-area: title;
font-weight: bold;
}
.version__metadata {
grid-area: metadata;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--spacing-card-xs);
margin-top: var(--spacing-card-xs);
}
.version__supports {
grid-area: supports;
display: flex;
flex-direction: column;
gap: var(--spacing-card-xs);
}
.version__stats {
grid-area: stats;
display: flex;
flex-direction: column;
gap: var(--spacing-card-xs);
}
&:active:not(&:disabled) {
transform: scale(0.99) !important;
}
}
}
@media screen and (max-width: 1024px) {
.all-versions {
.header {
grid-template: 'download title';
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr;
div:nth-child(3) {
display: none;
}
div:nth-child(4) {
display: none;
}
}
.version-button {
grid-template: 'download title' 'download metadata' 'download supports' 'download stats';
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr;
row-gap: var(--spacing-card-xs);
.version__supports {
display: flex;
flex-direction: row;
flex-wrap: wrap;
column-gap: var(--spacing-card-xs);
}
.version__metadata {
margin: 0;
}
}
}
}
</style>