You've already forked AstralRinth
forked from didirus/AstralRinth
Migrate to Nuxt 3 (#933)
* Migrate to Nuxt 3 * Update vercel config * remove tsconfig comment * Changelog experiment + working proj pages * Fix package json * Prevent vercel complaining * fix deploy (hopefully) * Tag generator * Switch to yarn * Vercel pls 🙏 * Fix tag generation bug * Make (most) non-logged in pages work * fix base build * Linting + state * Eradicate axios, make most user pages work * Fix checkbox state being set incorrectly * Make most things work * Final stretch * Finish (most) things * Move to update model value * Fix modal text getting blurred from transforms (#964) * Adjust nav-link border radius when focused (#961) * Transition between animation states on TextLogo (#955) * Transition between animation states on TextLogo * Remove unused refs * Fixes from review * Disable tabbing to pagination arrows when disabled (#972) * Make position of the "no results" text on grid/gallery views consistent (fixes #963) (#965) * Fix position of the "no results" text on grid view * fix padding * Remove extra margin on main page, fixes #957 (#959) * Fix layout shift and placeholder line height (#973) * Fix a lot of issues * Fix more nuxt 3 issues * fix not all versions showing up (temp) * inline inter css file * More nuxt 3 fixes * [skip ci] broken- backup changes * Change modpack warnings to blue instead of red (#991) * Fix some hydration issues * Update nuxt * Fix some images not showing * Add pagination to versions page + fix lag * Make changelog page consistent with versions page * sync before merge * Delete old file * Fix actions failing * update branch * Fixes navbar transition animation. (#1012) * Fixes navbar transition animation. * Fixes Y-axis animation. Fixes mobile menu. Removes highlightjs prop. * Changes xss call to renderString. * Fixes renderString call. * Removes unnecessary styling. * Reverts mobile nav change. * Nuxt 3 Lazy Loading Search (#1022) * Uses lazyFetch for results. onSearchChange refreshes. Adds loading circle. * Removes console.log * Preserves old page when paging. * Diagnosing filtering bugs. * Fix single facet filtering * Implements useAuth in settings/account. * tiny ssr fix * Updating nuxt.config checklist. * Implements useAuth in revenue, moneitzation, and dashboard index pages. * Fixes setups. * Eliminates results when path changes. Adds animated logo. * Ensures loading animation renders on search page. --------- Co-authored-by: Jai A <jaiagr+gpg@pm.me> * Fix navigation issues * Square button fix (#1023) * Removes checklist from nuxt.config. * Modifies Nuxt CI to build after linting. * Fixes prettierignore file. * bug fixes * Update whitelist domains * Page improvements, fix CLS * Fix a lot of things * Fix project type redirect * Fix 404 errors * Fix user settings + hydration error * Final fixes * fix(creator-section): border radius on icons not aligning with bg (#1027) Co-authored-by: MagnusHJensen <magnus.holm.jensen@lego.dk> * Improvements to the mobile navbar (#984) * Transition between animation states on TextLogo * Remove unused refs * Fixes from review * Improvements to the mobile nav menu * fix avatar alt text * Nevermind, got confused for a moment * Tab bar, menu layout improvements * Highlight search icon when menu is open * Update layouts/default.vue Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com> * Fix some issues * Use caret instead * Run prettier * Add create a project --------- Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com> Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> * Fix mobile menu issues * More issues * Fix lint --------- Co-authored-by: Kaeden Murphy <kmurphy@kaedenmurphy.dev> Co-authored-by: triphora <emmaffle@modrinth.com> Co-authored-by: Zach Baird <30800863+ZachBaird@users.noreply.github.com> Co-authored-by: stairman06 <36215135+stairman06@users.noreply.github.com> Co-authored-by: Zachary Baird <zdb1994@yahoo.com> Co-authored-by: Magnus Jensen <magnushjensen.mail@gmail.com> Co-authored-by: MagnusHJensen <magnus.holm.jensen@lego.dk>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
235
pages/[type]/[id]/changelog.vue
Normal file
235
pages/[type]/[id]/changelog.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<Head>
|
||||
<Title> {{ project.title }} - Changelog </Title>
|
||||
<Meta name="og:title" :content="`${props.project.title} - Changelog`" />
|
||||
<Meta name="description" :content="metaDescription" />
|
||||
<Meta name="apple-mobile-web-app-title" :content="`${props.project.title} - Changelog`" />
|
||||
<Meta name="og:description" :content="metaDescription" />
|
||||
</Head>
|
||||
<VersionFilterControl
|
||||
:versions="props.versions"
|
||||
@update-versions="
|
||||
(v) => {
|
||||
filteredVersions = v
|
||||
switchPage(1)
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
class="pagination-before"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
<div class="card">
|
||||
<div
|
||||
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
||||
:key="version.id"
|
||||
class="changelog-item"
|
||||
>
|
||||
<div
|
||||
:class="`changelog-bar ${version.version_type} ${version.duplicate ? 'duplicate' : ''}`"
|
||||
/>
|
||||
<div class="version-wrapper">
|
||||
<div class="version-header">
|
||||
<div class="version-header-text">
|
||||
<h2 class="name">
|
||||
<nuxt-link
|
||||
:to="`/${props.project.project_type}/${
|
||||
props.project.slug ? props.project.slug : props.project.id
|
||||
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||
>
|
||||
{{ version.name }}
|
||||
</nuxt-link>
|
||||
</h2>
|
||||
<span v-if="version.author">
|
||||
by
|
||||
<nuxt-link class="text-link" :to="'/user/' + version.author.user.username">{{
|
||||
version.author.user.username
|
||||
}}</nuxt-link>
|
||||
</span>
|
||||
<span>
|
||||
on
|
||||
{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span
|
||||
>
|
||||
</div>
|
||||
<a
|
||||
:href="version.primaryFile.url"
|
||||
class="iconified-button download"
|
||||
:title="`Download ${version.name}`"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="version.changelog && !version.duplicate"
|
||||
class="markdown-body"
|
||||
v-html="renderHighlightedString(version.changelog)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
class="pagination-before"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg'
|
||||
import { renderHighlightedString } from '~/helpers/highlight'
|
||||
import VersionFilterControl from '~/components/ui/VersionFilterControl'
|
||||
import Pagination from '~/components/ui/Pagination'
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const metaDescription = computed(
|
||||
() => `View the changelog of ${props.project.title}'s ${props.versions.length} versions.`
|
||||
)
|
||||
|
||||
const route = useRoute()
|
||||
const currentPage = ref(Number(route.query.p ?? 1))
|
||||
const filteredVersions = shallowRef(props.versions)
|
||||
|
||||
async function switchPage(page) {
|
||||
currentPage.value = page
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
await router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
p: currentPage.value !== 1 ? currentPage.value : undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.changelog-item {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
padding-left: 1.8rem;
|
||||
|
||||
.changelog-bar {
|
||||
--color: var(--color-special-green);
|
||||
|
||||
&.alpha {
|
||||
--color: var(--color-special-red);
|
||||
}
|
||||
|
||||
&.release {
|
||||
--color: var(--color-special-green);
|
||||
}
|
||||
|
||||
&.beta {
|
||||
--color: var(--color-special-orange);
|
||||
}
|
||||
|
||||
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;
|
||||
min-height: 0.75rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.version-header-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
h2,
|
||||
span {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.download {
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
margin: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<Head>
|
||||
<Title> {{ project.title }} - Gallery </Title>
|
||||
<Meta name="og:title" :content="`${project.title} - Gallery`" />
|
||||
<Meta name="description" :content="metaDescription" />
|
||||
<Meta name="apple-mobile-web-app-title" :content="`${project.title} - Gallery`" />
|
||||
<Meta name="og:description" :contcent="metaDescription" />
|
||||
</Head>
|
||||
<Modal
|
||||
v-if="$auth.user && currentMember"
|
||||
ref="modal_edit_item"
|
||||
@@ -71,8 +78,8 @@
|
||||
<label for="gallery-image-featured">
|
||||
<span class="label__title">Featured</span>
|
||||
<span class="label__description">
|
||||
A featured gallery image shows up in search and your project card.
|
||||
Only one gallery image can be featured.
|
||||
A featured gallery image shows up in search and your project card. Only one gallery
|
||||
image can be featured.
|
||||
</span>
|
||||
</label>
|
||||
<button
|
||||
@@ -94,10 +101,7 @@
|
||||
Unfeature image
|
||||
</button>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_edit_item.hide()"
|
||||
>
|
||||
<button class="iconified-button" @click="$refs.modal_edit_item.hide()">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
@@ -145,11 +149,7 @@
|
||||
? expandedGalleryItem.url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
:alt="
|
||||
expandedGalleryItem.title
|
||||
? expandedGalleryItem.title
|
||||
: 'gallery-image'
|
||||
"
|
||||
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
|
||||
@click.stop=""
|
||||
/>
|
||||
|
||||
@@ -164,10 +164,7 @@
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="close circle-button"
|
||||
@click="expandedGalleryItem = null"
|
||||
>
|
||||
<button class="close circle-button" @click="expandedGalleryItem = null">
|
||||
<CrossIcon aria-hidden="true" />
|
||||
</button>
|
||||
<a
|
||||
@@ -220,25 +217,21 @@
|
||||
<DropArea :accept="acceptFileTypes" @change="handleFiles" />
|
||||
</div>
|
||||
<div class="items">
|
||||
<div
|
||||
v-for="(item, index) in project.gallery"
|
||||
:key="index"
|
||||
class="card gallery-item"
|
||||
>
|
||||
<div v-for="(item, index) in project.gallery" :key="index" class="card gallery-item">
|
||||
<a class="gallery-thumbnail" @click="expandImage(item, index)">
|
||||
<img
|
||||
:src="
|
||||
item.url
|
||||
? item.url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
:src="item.url ? item.url : 'https://cdn.modrinth.com/placeholder-banner.svg'"
|
||||
:alt="item.title ? item.title : 'gallery-image'"
|
||||
/>
|
||||
</a>
|
||||
<div class="gallery-body">
|
||||
<div class="gallery-info">
|
||||
<h2 v-if="item.title">{{ item.title }}</h2>
|
||||
<p v-if="item.description">{{ item.description }}</p>
|
||||
<h2 v-if="item.title">
|
||||
{{ item.title }}
|
||||
</h2>
|
||||
<p v-if="item.description">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-bottom">
|
||||
@@ -250,13 +243,15 @@
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
resetEdit()
|
||||
editIndex = index
|
||||
editTitle = item.title
|
||||
editDescription = item.description
|
||||
editFeatured = item.featured
|
||||
editOrder = item.ordering
|
||||
$refs.modal_edit_item.show()
|
||||
() => {
|
||||
resetEdit()
|
||||
editIndex = index
|
||||
editTitle = item.title
|
||||
editDescription = item.description
|
||||
editFeatured = item.featured
|
||||
editOrder = item.ordering
|
||||
$refs.modal_edit_item.show()
|
||||
}
|
||||
"
|
||||
>
|
||||
<EditIcon />
|
||||
@@ -265,8 +260,10 @@
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
deleteIndex = index
|
||||
$refs.modal_confirm.show()
|
||||
() => {
|
||||
deleteIndex = index
|
||||
$refs.modal_confirm.show()
|
||||
}
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
@@ -280,29 +277,29 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg?inline'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
||||
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
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 StarIcon from '~/assets/images/utils/star.svg?inline'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
import InfoIcon from '~/assets/images/utils/info.svg?inline'
|
||||
import ImageIcon from '~/assets/images/utils/image.svg?inline'
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg'
|
||||
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import ExternalIcon from '~/assets/images/utils/external.svg'
|
||||
import ExpandIcon from '~/assets/images/utils/expand.svg'
|
||||
import ContractIcon from '~/assets/images/utils/contract.svg'
|
||||
import StarIcon from '~/assets/images/utils/star.svg'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg'
|
||||
import InfoIcon from '~/assets/images/utils/info.svg'
|
||||
import ImageIcon from '~/assets/images/utils/image.svg'
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
import DropArea from '~/components/ui/DropArea'
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
CalendarIcon,
|
||||
PlusIcon,
|
||||
@@ -325,7 +322,6 @@ export default {
|
||||
FileInput,
|
||||
DropArea,
|
||||
},
|
||||
auth: false,
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
@@ -356,36 +352,8 @@ export default {
|
||||
editFile: null,
|
||||
previewImage: null,
|
||||
shouldPreventActions: false,
|
||||
}
|
||||
},
|
||||
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,
|
||||
},
|
||||
],
|
||||
metaDescription: `View ${this.project.gallery.length} images of ${this.project.title} on Modrinth.`,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -456,24 +424,30 @@ export default {
|
||||
},
|
||||
async createGalleryItem() {
|
||||
this.shouldPreventActions = true
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
let url = `project/${this.project.id}/gallery?ext=${
|
||||
this.editFile
|
||||
? this.editFile.type.split('/')[
|
||||
this.editFile.type.split('/').length - 1
|
||||
]
|
||||
? this.editFile.type.split('/')[this.editFile.type.split('/').length - 1]
|
||||
: null
|
||||
}&featured=${this.editFeatured}`
|
||||
|
||||
if (this.editTitle)
|
||||
if (this.editTitle) {
|
||||
url += `&title=${encodeURIComponent(this.editTitle)}`
|
||||
if (this.editDescription)
|
||||
}
|
||||
if (this.editDescription) {
|
||||
url += `&description=${encodeURIComponent(this.editDescription)}`
|
||||
if (this.editOrder) url += `&ordering=${this.editOrder}`
|
||||
}
|
||||
if (this.editOrder) {
|
||||
url += `&ordering=${this.editOrder}`
|
||||
}
|
||||
|
||||
await this.$axios.post(url, this.editFile, this.$defaultHeaders())
|
||||
await useBaseFetch(url, {
|
||||
method: 'POST',
|
||||
body: this.editFile,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await this.updateProject()
|
||||
|
||||
this.$refs.modal_edit_item.hide()
|
||||
@@ -481,30 +455,37 @@ export default {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response ? err.response.data.description : err,
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
this.shouldPreventActions = false
|
||||
},
|
||||
async editGalleryItem() {
|
||||
this.shouldPreventActions = true
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
let url = `project/${this.project.id}/gallery?url=${encodeURIComponent(
|
||||
this.project.gallery[this.editIndex].url
|
||||
)}&featured=${this.editFeatured}`
|
||||
|
||||
if (this.editTitle)
|
||||
if (this.editTitle) {
|
||||
url += `&title=${encodeURIComponent(this.editTitle)}`
|
||||
if (this.editDescription)
|
||||
}
|
||||
if (this.editDescription) {
|
||||
url += `&description=${encodeURIComponent(this.editDescription)}`
|
||||
if (this.editOrder) url += `&ordering=${this.editOrder}`
|
||||
}
|
||||
if (this.editOrder) {
|
||||
url += `&ordering=${this.editOrder}`
|
||||
}
|
||||
|
||||
await this.$axios.patch(url, {}, this.$defaultHeaders())
|
||||
await useBaseFetch(url, {
|
||||
method: 'PATCH',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
|
||||
await this.updateProject()
|
||||
this.$refs.modal_edit_item.hide()
|
||||
@@ -512,23 +493,26 @@ export default {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response ? err.response.data.description : err,
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
this.shouldPreventActions = false
|
||||
},
|
||||
async deleteGalleryImage() {
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
await useBaseFetch(
|
||||
`project/${this.project.id}/gallery?url=${encodeURIComponent(
|
||||
this.project.gallery[this.deleteIndex].url
|
||||
)}`,
|
||||
this.$defaultHeaders()
|
||||
{
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
|
||||
await this.updateProject()
|
||||
@@ -536,19 +520,25 @@ export default {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response ? err.response.data.description : err,
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
},
|
||||
async updateProject() {
|
||||
await this.$parent.resetProject()
|
||||
const project = await useBaseFetch(`project/${this.project.id}`, this.$defaultHeaders())
|
||||
|
||||
project.actualProjectType = JSON.parse(JSON.stringify(project.project_type))
|
||||
|
||||
project.project_type = this.$getProjectTypeForUrl(project.project_type, project.loaders)
|
||||
|
||||
this.$emit('update:project', project)
|
||||
this.resetEdit()
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -756,8 +746,7 @@ export default {
|
||||
|
||||
.gallery-bottom {
|
||||
width: calc(100% - 2 * var(--spacing-card-md));
|
||||
padding: 0 var(--spacing-card-md) var(--spacing-card-sm)
|
||||
var(--spacing-card-md);
|
||||
padding: 0 var(--spacing-card-md) var(--spacing-card-sm) var(--spacing-card-md);
|
||||
|
||||
.gallery-created {
|
||||
display: flex;
|
||||
21
pages/[type]/[id]/index.vue
Normal file
21
pages/[type]/[id]/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="markdown-body card" v-html="renderHighlightedString(project.body)" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { renderHighlightedString } from '~/helpers/highlight'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: { renderHighlightedString },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -4,22 +4,19 @@
|
||||
<label for="project-description">
|
||||
<span class="label__title size-card-header">Description</span>
|
||||
<span class="label__description">
|
||||
You can type an extended description of your mod here. This editor
|
||||
supports
|
||||
You can type an extended description of your mod here. This editor supports
|
||||
<a
|
||||
class="text-link"
|
||||
href="https://guides.github.com/features/mastering-markdown/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
>Markdown</a
|
||||
>. HTML can also be used inside your description, not including
|
||||
styles, scripts, and iframes (though YouTube iframes are allowed).
|
||||
>. HTML can also be used inside your description, not including styles, scripts, and
|
||||
iframes (though YouTube iframes are allowed).
|
||||
<span class="label__subdescription">
|
||||
The description must clearly and honestly describe the purpose and
|
||||
function of the project. See section 2.1 of the
|
||||
<nuxt-link to="/legal/rules" class="text-link" target="_blank"
|
||||
>Content Rules</nuxt-link
|
||||
>
|
||||
The description must clearly and honestly describe the purpose and function of the
|
||||
project. See section 2.1 of the
|
||||
<nuxt-link to="/legal/rules" class="text-link" target="_blank">Content Rules</nuxt-link>
|
||||
for the full requirements.
|
||||
</span>
|
||||
</span>
|
||||
@@ -34,12 +31,9 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="bodyViewMode === 'preview'"
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="
|
||||
description ? $xss($md.render(description)) : 'No body specified.'
|
||||
"
|
||||
></div>
|
||||
v-html="description ? renderHighlightedString(description) : 'No body specified.'"
|
||||
/>
|
||||
<div class="input-group">
|
||||
<button
|
||||
type="button"
|
||||
@@ -57,10 +51,10 @@
|
||||
|
||||
<script>
|
||||
import Chips from '~/components/ui/Chips'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import { renderHighlightedString } from '~/helpers/highlight'
|
||||
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Chips,
|
||||
SaveIcon,
|
||||
@@ -100,13 +94,10 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
description: '',
|
||||
description: this.project.body,
|
||||
bodyViewMode: 'source',
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.description = this.project.body
|
||||
},
|
||||
computed: {
|
||||
patchData() {
|
||||
const data = {}
|
||||
@@ -125,13 +116,14 @@ export default {
|
||||
this.EDIT_BODY = 1 << 3
|
||||
},
|
||||
methods: {
|
||||
renderHighlightedString,
|
||||
saveChanges() {
|
||||
if (this.hasChanges) {
|
||||
this.patchProject(this.patchData)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.resizable-textarea-wrapper textarea {
|
||||
@@ -20,9 +20,7 @@
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<Avatar
|
||||
:src="
|
||||
deletedIcon ? null : previewImage ? previewImage : project.icon_url
|
||||
"
|
||||
:src="deletedIcon ? null : previewImage ? previewImage : project.icon_url"
|
||||
:alt="project.title"
|
||||
size="md"
|
||||
class="project__icon"
|
||||
@@ -86,7 +84,7 @@
|
||||
v-model="summary"
|
||||
maxlength="256"
|
||||
:disabled="!hasPermission"
|
||||
></textarea>
|
||||
/>
|
||||
</div>
|
||||
<template
|
||||
v-if="
|
||||
@@ -101,9 +99,9 @@
|
||||
<span class="label__title">Client-side</span>
|
||||
<span class="label__description">
|
||||
Select based on if the
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} has
|
||||
functionality on the client side. Just because a mod works in
|
||||
Singleplayer doesn't mean it has actual client-side functionality.
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
||||
client side. Just because a mod works in Singleplayer doesn't mean it has actual
|
||||
client-side functionality.
|
||||
</span>
|
||||
</label>
|
||||
<Multiselect
|
||||
@@ -111,9 +109,7 @@
|
||||
v-model="clientSide"
|
||||
placeholder="Select one"
|
||||
:options="sideTypes"
|
||||
:custom-label="
|
||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||
"
|
||||
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
@@ -126,9 +122,9 @@
|
||||
<span class="label__title">Server-side</span>
|
||||
<span class="label__description">
|
||||
Select based on if the
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} has
|
||||
functionality on the <strong>logical</strong> server. Remember
|
||||
that Singleplayer contains an integrated server.
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
||||
<strong>logical</strong> server. Remember that Singleplayer contains an integrated
|
||||
server.
|
||||
</span>
|
||||
</label>
|
||||
<Multiselect
|
||||
@@ -136,9 +132,7 @@
|
||||
v-model="serverSide"
|
||||
placeholder="Select one"
|
||||
:options="sideTypes"
|
||||
:custom-label="
|
||||
(value) => value.charAt(0).toUpperCase() + value.slice(1)
|
||||
"
|
||||
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
@@ -151,10 +145,9 @@
|
||||
<label for="project-visibility">
|
||||
<span class="label__title">Visibility</span>
|
||||
<span class="label__description">
|
||||
Set the visibility of your project. Listed and archived projects are
|
||||
visible in search. Unlisted projects are published, but not visible
|
||||
in search or on user profiles. Private projects are only accessible
|
||||
by members of the project.
|
||||
Set the visibility of your project. Listed and archived projects are visible in search.
|
||||
Unlisted projects are published, but not visible in search or on user profiles. Private
|
||||
projects are only accessible by members of the project.
|
||||
</span>
|
||||
</label>
|
||||
<Multiselect
|
||||
@@ -190,8 +183,8 @@
|
||||
</h3>
|
||||
</div>
|
||||
<p>
|
||||
Removes your project from Modrinth's servers and search. Clicking on
|
||||
this will delete your project, so be extra careful!
|
||||
Removes your project from Modrinth's servers and search. Clicking on this will delete your
|
||||
project, so be extra careful!
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
@@ -212,11 +205,11 @@ import Avatar from '~/components/ui/Avatar'
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Avatar,
|
||||
ModalConfirm,
|
||||
@@ -281,27 +274,19 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
slug: '',
|
||||
summary: '',
|
||||
name: this.project.title,
|
||||
slug: this.project.slug,
|
||||
summary: this.project.description,
|
||||
icon: null,
|
||||
previewImage: null,
|
||||
clientSide: '',
|
||||
serverSide: '',
|
||||
clientSide: this.project.client_side,
|
||||
serverSide: this.project.server_side,
|
||||
deletedIcon: false,
|
||||
visibility: '',
|
||||
visibility: this.$tag.approvedStatuses.includes(this.project.status)
|
||||
? this.project.status
|
||||
: this.project.requested_status,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.name = this.project.title
|
||||
this.slug = this.project.slug
|
||||
this.summary = this.project.description
|
||||
this.clientSide = this.project.client_side
|
||||
this.serverSide = this.project.server_side
|
||||
this.visibility = this.$tag.approvedStatuses.includes(this.project.status)
|
||||
? this.project.status
|
||||
: this.project.requested_status
|
||||
},
|
||||
computed: {
|
||||
hasPermission() {
|
||||
const EDIT_DETAILS = 1 << 2
|
||||
@@ -309,9 +294,7 @@ export default {
|
||||
},
|
||||
hasDeletePermission() {
|
||||
const DELETE_PROJECT = 1 << 7
|
||||
return (
|
||||
(this.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT
|
||||
)
|
||||
return (this.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT
|
||||
},
|
||||
sideTypes() {
|
||||
return ['required', 'optional', 'unsupported']
|
||||
@@ -345,9 +328,7 @@ export default {
|
||||
return data
|
||||
},
|
||||
hasChanges() {
|
||||
return (
|
||||
Object.keys(this.patchData).length > 0 || this.deletedIcon || this.icon
|
||||
)
|
||||
return Object.keys(this.patchData).length > 0 || this.deletedIcon || this.icon
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -374,12 +355,12 @@ export default {
|
||||
}
|
||||
},
|
||||
async deleteProject() {
|
||||
await this.$axios.delete(
|
||||
`project/${this.project.id}`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('user/fetchProjects')
|
||||
await this.$router.push(`/dashboard/projects`)
|
||||
await useBaseFetch(`project/${this.project.id}`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await initUserProjects()
|
||||
await this.$router.push('/dashboard/projects')
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Project deleted',
|
||||
@@ -393,10 +374,10 @@ export default {
|
||||
this.previewImage = null
|
||||
},
|
||||
async deleteIcon() {
|
||||
await this.$axios.delete(
|
||||
`project/${this.project.id}/icon`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await useBaseFetch(`project/${this.project.id}/icon`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await this.updateIcon()
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
@@ -406,7 +387,7 @@ export default {
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.summary-input {
|
||||
@@ -6,34 +6,24 @@
|
||||
<span class="label__title size-card-header">License</span>
|
||||
<span class="label__description">
|
||||
It is very important to choose a proper license for your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. You
|
||||
may choose one from our list or provide a custom license. You may
|
||||
also provide a custom URL to your chosen license; otherwise, the
|
||||
license text will be displayed.
|
||||
<span
|
||||
v-if="license && license.friendly === 'Custom'"
|
||||
class="label__subdescription"
|
||||
>
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. You may choose one from
|
||||
our list or provide a custom license. You may also provide a custom URL to your chosen
|
||||
license; otherwise, the license text will be displayed.
|
||||
<span v-if="license && license.friendly === 'Custom'" class="label__subdescription">
|
||||
Enter a valid
|
||||
<a
|
||||
href="https://spdx.org/licenses/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-link"
|
||||
>
|
||||
<a href="https://spdx.org/licenses/" target="_blank" rel="noopener" class="text-link">
|
||||
SPDX license identifier</a
|
||||
>
|
||||
in the marked area. If your license does not have a SPDX
|
||||
identifier (for example, if you created the license yourself or if
|
||||
the license is Minecraft-specific), simply check the box and enter
|
||||
the name of the license instead.
|
||||
in the marked area. If your license does not have a SPDX identifier (for example, if
|
||||
you created the license yourself or if the license is Minecraft-specific), simply
|
||||
check the box and enter the name of the license instead.
|
||||
</span>
|
||||
<span class="label__subdescription">
|
||||
Confused? See our
|
||||
<a
|
||||
href="https://blog.modrinth.com/licensing-guide/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
class="text-link"
|
||||
>
|
||||
licensing guide</a
|
||||
@@ -62,6 +52,7 @@
|
||||
v-if="license.requiresOnlyOrLater"
|
||||
v-model="allowOrLater"
|
||||
:disabled="!hasPermission"
|
||||
description="Allow later editions of this license"
|
||||
>
|
||||
Allow later editions of this license
|
||||
</Checkbox>
|
||||
@@ -69,6 +60,7 @@
|
||||
v-if="license.friendly === 'Custom'"
|
||||
v-model="nonSpdxLicense"
|
||||
:disabled="!hasPermission"
|
||||
description="License does not have a SPDX identifier"
|
||||
>
|
||||
License does not have a SPDX identifier
|
||||
</Checkbox>
|
||||
@@ -110,9 +102,9 @@
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Multiselect,
|
||||
Checkbox,
|
||||
@@ -154,28 +146,110 @@ export default {
|
||||
showKnownErrors: false,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.licenseUrl = this.project.license.url
|
||||
async setup(props) {
|
||||
const defaultLicenses = shallowRef([
|
||||
{ friendly: 'Custom', short: '' },
|
||||
{
|
||||
friendly: 'All Rights Reserved/No License',
|
||||
short: 'All-Rights-Reserved',
|
||||
},
|
||||
{ friendly: 'Apache License 2.0', short: 'Apache-2.0' },
|
||||
{
|
||||
friendly: 'BSD 2-Clause "Simplified" License',
|
||||
short: 'BSD-2-Clause',
|
||||
},
|
||||
{
|
||||
friendly: 'BSD 3-Clause "New" or "Revised" License',
|
||||
short: 'BSD-3-Clause',
|
||||
},
|
||||
{
|
||||
friendly: 'CC Zero (Public Domain equivalent)',
|
||||
short: 'CC0-1.0',
|
||||
},
|
||||
{ friendly: 'CC-BY 4.0', short: 'CC-BY-4.0' },
|
||||
{
|
||||
friendly: 'CC-BY-SA 4.0',
|
||||
short: 'CC-BY-SA-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC 4.0',
|
||||
short: 'CC-BY-NC-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC-SA 4.0',
|
||||
short: 'CC-BY-NC-SA-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-ND 4.0',
|
||||
short: 'CC-BY-ND-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC-ND 4.0',
|
||||
short: 'CC-BY-NC-ND-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Affero General Public License v3',
|
||||
short: 'AGPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Lesser General Public License v2.1',
|
||||
short: 'LGPL-2.1',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Lesser General Public License v3',
|
||||
short: 'LGPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU General Public License v2',
|
||||
short: 'GPL-2.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU General Public License v3',
|
||||
short: 'GPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{ friendly: 'ISC License', short: 'ISC' },
|
||||
{ friendly: 'MIT License', short: 'MIT' },
|
||||
{ friendly: 'Mozilla Public License 2.0', short: 'MPL-2.0' },
|
||||
{ friendly: 'zlib License', short: 'Zlib' },
|
||||
])
|
||||
|
||||
const licenseId = this.project.license.id
|
||||
const licenseUrl = ref(props.project.license.url)
|
||||
|
||||
const licenseId = props.project.license.id
|
||||
const trimmedLicenseId = licenseId
|
||||
.replaceAll('-only', '')
|
||||
.replaceAll('-or-later', '')
|
||||
.replaceAll('LicenseRef-', '')
|
||||
this.license = this.defaultLicenses.find(
|
||||
(x) => x.short === trimmedLicenseId
|
||||
) ?? {
|
||||
friendly: 'Custom',
|
||||
short: licenseId.replaceAll('LicenseRef-', ''),
|
||||
}
|
||||
|
||||
const license = ref(
|
||||
defaultLicenses.value.find((x) => x.short === trimmedLicenseId) ?? {
|
||||
friendly: 'Custom',
|
||||
short: licenseId.replaceAll('LicenseRef-', ''),
|
||||
}
|
||||
)
|
||||
|
||||
if (licenseId === 'LicenseRef-Unknown') {
|
||||
this.license = {
|
||||
license.value = {
|
||||
friendly: 'Unknown',
|
||||
short: licenseId.replaceAll('LicenseRef-', ''),
|
||||
}
|
||||
}
|
||||
this.allowOrLater = licenseId.includes('-or-later')
|
||||
this.nonSpdxLicense = licenseId.includes('LicenseRef-')
|
||||
|
||||
const allowOrLater = computed(() => props.project.license.id.includes('-or-later'))
|
||||
const nonSpdxLicense = computed(() => props.project.license.id.includes('LicenseRef-'))
|
||||
|
||||
return {
|
||||
defaultLicenses,
|
||||
licenseUrl,
|
||||
license,
|
||||
allowOrLater,
|
||||
nonSpdxLicense,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPermission() {
|
||||
@@ -188,87 +262,18 @@ export default {
|
||||
(this.nonSpdxLicense && this.license.friendly === 'Custom') ||
|
||||
this.license.short === 'All-Rights-Reserved' ||
|
||||
this.license.short === 'Unknown'
|
||||
)
|
||||
) {
|
||||
id += 'LicenseRef-'
|
||||
}
|
||||
id += this.license.short
|
||||
if (this.license.requiresOnlyOrLater)
|
||||
if (this.license.requiresOnlyOrLater) {
|
||||
id += this.allowOrLater ? '-or-later' : '-only'
|
||||
if (this.nonSpdxLicense && this.license.friendly === 'Custom')
|
||||
}
|
||||
if (this.nonSpdxLicense && this.license.friendly === 'Custom') {
|
||||
id = id.replaceAll(' ', '-')
|
||||
}
|
||||
return id
|
||||
},
|
||||
defaultLicenses() {
|
||||
return [
|
||||
{ friendly: 'Custom', short: '' },
|
||||
{
|
||||
friendly: 'All Rights Reserved/No License',
|
||||
short: 'All-Rights-Reserved',
|
||||
},
|
||||
{ friendly: 'Apache License 2.0', short: 'Apache-2.0' },
|
||||
{
|
||||
friendly: 'BSD 2-Clause "Simplified" License',
|
||||
short: 'BSD-2-Clause',
|
||||
},
|
||||
{
|
||||
friendly: 'BSD 3-Clause "New" or "Revised" License',
|
||||
short: 'BSD-3-Clause',
|
||||
},
|
||||
{
|
||||
friendly: 'CC Zero (Public Domain equivalent)',
|
||||
short: 'CC0-1.0',
|
||||
},
|
||||
{ friendly: 'CC-BY 4.0', short: 'CC-BY-4.0' },
|
||||
{
|
||||
friendly: 'CC-BY-SA 4.0',
|
||||
short: 'CC-BY-SA-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC 4.0',
|
||||
short: 'CC-BY-NC-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC-SA 4.0',
|
||||
short: 'CC-BY-NC-SA-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-ND 4.0',
|
||||
short: 'CC-BY-ND-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC-ND 4.0',
|
||||
short: 'CC-BY-NC-ND-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Affero General Public License v3',
|
||||
short: 'AGPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Lesser General Public License v2.1',
|
||||
short: 'LGPL-2.1',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Lesser General Public License v3',
|
||||
short: 'LGPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU General Public License v2',
|
||||
short: 'GPL-2.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU General Public License v3',
|
||||
short: 'GPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{ friendly: 'ISC License', short: 'ISC' },
|
||||
{ friendly: 'MIT License', short: 'MIT' },
|
||||
{ friendly: 'Mozilla Public License 2.0', short: 'MPL-2.0' },
|
||||
{ friendly: 'zlib License', short: 'Zlib' },
|
||||
]
|
||||
},
|
||||
patchData() {
|
||||
const data = {}
|
||||
|
||||
@@ -292,6 +297,6 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -9,8 +9,7 @@
|
||||
>
|
||||
<span class="label__title">Issue tracker</span>
|
||||
<span class="label__description">
|
||||
A place for users to report bugs, issues, and concerns about your
|
||||
project.
|
||||
A place for users to report bugs, issues, and concerns about your project.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -48,8 +47,7 @@
|
||||
>
|
||||
<span class="label__title">Wiki page</span>
|
||||
<span class="label__description">
|
||||
A page containing information, documentation, and help for the
|
||||
project.
|
||||
A page containing information, documentation, and help for the project.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
@@ -62,14 +60,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label
|
||||
id="project-discord-invite"
|
||||
title="An invitation link to your Discord server."
|
||||
>
|
||||
<label id="project-discord-invite" title="An invitation link to your Discord server.">
|
||||
<span class="label__title">Discord invite</span>
|
||||
<span class="label__description">
|
||||
An invitation link to your Discord server.
|
||||
</span>
|
||||
<span class="label__description"> An invitation link to your Discord server. </span>
|
||||
</label>
|
||||
<input
|
||||
id="project-discord-invite"
|
||||
@@ -100,7 +93,7 @@
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:disabled="!hasPermission"
|
||||
@input="updateDonationLinks"
|
||||
@update:model-value="updateDonationLinks"
|
||||
/>
|
||||
<input
|
||||
v-model="donationLink.url"
|
||||
@@ -128,9 +121,9 @@
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Multiselect,
|
||||
SaveIcon,
|
||||
@@ -163,23 +156,22 @@ export default {
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const donationLinks = JSON.parse(JSON.stringify(this.project.donation_urls))
|
||||
donationLinks.push({
|
||||
id: null,
|
||||
platform: null,
|
||||
url: null,
|
||||
})
|
||||
|
||||
return {
|
||||
issuesUrl: '',
|
||||
sourceUrl: '',
|
||||
wikiUrl: '',
|
||||
discordUrl: '',
|
||||
issuesUrl: this.project.issues_url,
|
||||
sourceUrl: this.project.source_url,
|
||||
wikiUrl: this.project.wiki_url,
|
||||
discordUrl: this.project.discord_url,
|
||||
|
||||
donationLinks: [],
|
||||
donationLinks,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.issuesUrl = this.project.issues_url
|
||||
this.sourceUrl = this.project.source_url
|
||||
this.wikiUrl = this.project.wiki_url
|
||||
this.discordUrl = this.project.discord_url
|
||||
|
||||
this.resetDonationLinks()
|
||||
},
|
||||
computed: {
|
||||
hasPermission() {
|
||||
const EDIT_DETAILS = 1 << 2
|
||||
@@ -198,13 +190,10 @@ export default {
|
||||
data.wiki_url = this.wikiUrl === '' ? null : this.wikiUrl.trim()
|
||||
}
|
||||
if (this.checkDifference(this.discordUrl, this.project.discord_url)) {
|
||||
data.discord_url =
|
||||
this.discordUrl === '' ? null : this.discordUrl.trim()
|
||||
data.discord_url = this.discordUrl === '' ? null : this.discordUrl.trim()
|
||||
}
|
||||
|
||||
const donationLinks = this.donationLinks.filter(
|
||||
(link) => link.url && link.platform
|
||||
)
|
||||
const donationLinks = this.donationLinks.filter((link) => link.url && link.platform)
|
||||
donationLinks.forEach((link) => {
|
||||
link.id = this.$tag.donationPlatforms.find(
|
||||
(platform) => platform.name === link.platform
|
||||
@@ -230,7 +219,12 @@ export default {
|
||||
methods: {
|
||||
async saveChanges() {
|
||||
if (this.patchData && (await this.patchProject(this.patchData))) {
|
||||
this.resetDonationLinks()
|
||||
this.donationLinks = JSON.parse(JSON.stringify(this.project.donation_urls))
|
||||
this.donationLinks.push({
|
||||
id: null,
|
||||
platform: null,
|
||||
url: null,
|
||||
})
|
||||
}
|
||||
},
|
||||
updateDonationLinks() {
|
||||
@@ -258,16 +252,6 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
resetDonationLinks() {
|
||||
this.donationLinks = JSON.parse(
|
||||
JSON.stringify(this.project.donation_urls)
|
||||
)
|
||||
this.donationLinks.push({
|
||||
id: null,
|
||||
platform: null,
|
||||
url: null,
|
||||
})
|
||||
},
|
||||
checkDifference(newLink, existingLink) {
|
||||
if (newLink === '' && existingLink !== null) {
|
||||
return true
|
||||
@@ -278,7 +262,7 @@ export default {
|
||||
return newLink !== existingLink
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.donation-link-group {
|
||||
@@ -9,20 +9,15 @@
|
||||
<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.
|
||||
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"
|
||||
/>
|
||||
<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">
|
||||
<UserPlusIcon />
|
||||
@@ -38,12 +33,7 @@
|
||||
>
|
||||
<div class="member-header">
|
||||
<div class="info">
|
||||
<Avatar
|
||||
:src="member.avatar_url"
|
||||
:alt="member.username"
|
||||
size="sm"
|
||||
circle
|
||||
/>
|
||||
<Avatar :src="member.avatar_url" :alt="member.username" size="sm" circle />
|
||||
<div class="text">
|
||||
<nuxt-link :to="'/user/' + member.user.username" class="name">
|
||||
<p>{{ member.name }}</p>
|
||||
@@ -59,9 +49,7 @@
|
||||
@click="
|
||||
openTeamMembers.indexOf(member.user.id) === -1
|
||||
? openTeamMembers.push(member.user.id)
|
||||
: (openTeamMembers = openTeamMembers.filter(
|
||||
(it) => it !== member.user.id
|
||||
))
|
||||
: (openTeamMembers = openTeamMembers.filter((it) => it !== member.user.id))
|
||||
"
|
||||
>
|
||||
<DropdownIcon />
|
||||
@@ -81,37 +69,27 @@
|
||||
v-model="allTeamMembers[index].role"
|
||||
type="text"
|
||||
:class="{ 'known-error': member.role === 'Owner' }"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
:disabled="(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label
|
||||
:for="`member-${allTeamMembers[index].user.username}-monetization-weight`"
|
||||
>
|
||||
<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.
|
||||
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
|
||||
"
|
||||
:disabled="(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="member.role === 'Owner' && member.oldRole !== 'Owner'"
|
||||
class="known-errors"
|
||||
>
|
||||
A project can only have one 'Owner'. Use the 'Transfer ownership'
|
||||
button below if you no longer wish to be owner.
|
||||
<p v-if="member.role === 'Owner' && member.oldRole !== 'Owner'" class="known-errors">
|
||||
A project can only have one 'Owner'. Use the 'Transfer ownership' button below if you no
|
||||
longer wish to be owner.
|
||||
</p>
|
||||
<template v-if="member.oldRole !== 'Owner'">
|
||||
<span class="label">
|
||||
@@ -119,102 +97,98 @@
|
||||
</span>
|
||||
<div class="permissions">
|
||||
<Checkbox
|
||||
:value="(member.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
|
||||
:model-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"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & DELETE_VERSION) === DELETE_VERSION"
|
||||
:model-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"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= DELETE_VERSION"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||
:model-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"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= EDIT_DETAILS"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & EDIT_BODY) === EDIT_BODY"
|
||||
:model-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"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= EDIT_BODY"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||
:model-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"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= MANAGE_INVITES"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
|
||||
:model-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"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & EDIT_MEMBER) === EDIT_MEMBER"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
:model-value="(member.permissions & EDIT_MEMBER) === EDIT_MEMBER"
|
||||
:disabled="(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||
label="Edit member"
|
||||
@input="allTeamMembers[index].permissions ^= EDIT_MEMBER"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= EDIT_MEMBER"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & DELETE_PROJECT) === DELETE_PROJECT"
|
||||
:model-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"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= DELETE_PROJECT"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & VIEW_ANALYTICS) === VIEW_ANALYTICS"
|
||||
:model-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"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= VIEW_ANALYTICS"
|
||||
/>
|
||||
<Checkbox
|
||||
:value="(member.permissions & VIEW_PAYOUTS) === VIEW_PAYOUTS"
|
||||
:model-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"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= VIEW_PAYOUTS"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
:disabled="(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||
@click="updateTeamMember(index)"
|
||||
>
|
||||
<SaveIcon />
|
||||
@@ -223,20 +197,14 @@
|
||||
<button
|
||||
v-if="member.oldRole !== 'Owner'"
|
||||
class="iconified-button danger-button"
|
||||
:disabled="
|
||||
(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER
|
||||
"
|
||||
:disabled="(currentMember.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||
@click="removeTeamMember(index)"
|
||||
>
|
||||
<UserRemoveIcon />
|
||||
Remove member
|
||||
</button>
|
||||
<button
|
||||
v-if="
|
||||
member.oldRole !== 'Owner' &&
|
||||
currentMember.role === 'Owner' &&
|
||||
member.accepted
|
||||
"
|
||||
v-if="member.oldRole !== 'Owner' && currentMember.role === 'Owner' && member.accepted"
|
||||
class="iconified-button"
|
||||
@click="transferOwnership(index)"
|
||||
>
|
||||
@@ -253,14 +221,14 @@
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
|
||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
||||
import UserPlusIcon from '~/assets/images/utils/user-plus.svg?inline'
|
||||
import UserRemoveIcon from '~/assets/images/utils/user-x.svg?inline'
|
||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||
import UserPlusIcon from '~/assets/images/utils/user-plus.svg'
|
||||
import UserRemoveIcon from '~/assets/images/utils/user-x.svg'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Avatar,
|
||||
DropdownIcon,
|
||||
@@ -295,14 +263,12 @@ export default {
|
||||
return {
|
||||
currentUsername: '',
|
||||
openTeamMembers: [],
|
||||
allTeamMembers: [],
|
||||
allTeamMembers: this.allMembers.map((x) => {
|
||||
x.oldRole = x.role
|
||||
return x
|
||||
}),
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.allTeamMembers = this.allMembers
|
||||
|
||||
this.allTeamMembers.forEach((x) => (x.oldRole = x.role))
|
||||
},
|
||||
created() {
|
||||
this.UPLOAD_VERSION = 1 << 0
|
||||
this.DELETE_VERSION = 1 << 1
|
||||
@@ -317,55 +283,57 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async inviteTeamMember() {
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
const user = (await this.$axios.get(`user/${this.currentUsername}`))
|
||||
.data
|
||||
const user = await useBaseFetch(`user/${this.currentUsername}`)
|
||||
|
||||
const data = {
|
||||
user_id: user.id.trim(),
|
||||
}
|
||||
|
||||
await this.$axios.post(
|
||||
`team/${this.project.team}/members`,
|
||||
data,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await useBaseFetch(`team/${this.project.team}/members`, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
},
|
||||
async removeTeamMember(index) {
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
await useBaseFetch(
|
||||
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
||||
this.$defaultHeaders()
|
||||
{
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
},
|
||||
async updateTeamMember(index) {
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
const data =
|
||||
@@ -379,59 +347,59 @@ export default {
|
||||
payouts_split: this.allTeamMembers[index].payouts_split,
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
await useBaseFetch(
|
||||
`team/${this.project.team}/members/${this.allTeamMembers[index].user.id}`,
|
||||
data,
|
||||
this.$defaultHeaders()
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
await this.updateMembers()
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Member(s) updated',
|
||||
text: `Your project's member(s) has been updated.`,
|
||||
text: "Your project's member(s) has been updated.",
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
},
|
||||
async transferOwnership(index) {
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
await this.$axios.patch(
|
||||
`team/${this.project.team}/owner`,
|
||||
{
|
||||
await useBaseFetch(`team/${this.project.team}/owner`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
user_id: this.allTeamMembers[index].user.id,
|
||||
},
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
},
|
||||
async updateMembers() {
|
||||
this.allTeamMembers = (
|
||||
await this.$axios.get(
|
||||
`team/${this.project.team}/members`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
).data.map((it) => ({
|
||||
await useBaseFetch(`team/${this.project.team}/members`, this.$defaultHeaders())
|
||||
).map((it) => ({
|
||||
avatar_url: it.user.avatar_url,
|
||||
name: it.user.username,
|
||||
oldRole: it.role,
|
||||
@@ -439,7 +407,7 @@ export default {
|
||||
}))
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -8,15 +8,13 @@
|
||||
</div>
|
||||
<p>
|
||||
Accurate tagging is important to help people find your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. Make sure
|
||||
to select all tags that apply.
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
|
||||
that apply.
|
||||
</p>
|
||||
<template v-for="header in Object.keys(categoryLists)">
|
||||
<div :key="`categories-${header}`" class="label">
|
||||
<template v-for="header in Object.keys(categoryLists)" :key="`categories-${header}`">
|
||||
<div class="label">
|
||||
<h4>
|
||||
<span class="label__title">{{
|
||||
$formatCategoryHeader(header)
|
||||
}}</span>
|
||||
<span class="label__title">{{ $formatCategoryHeader(header) }}</span>
|
||||
</h4>
|
||||
<span class="label__description">
|
||||
<template v-if="header === 'categories'">
|
||||
@@ -25,8 +23,7 @@
|
||||
</template>
|
||||
<template v-else-if="header === 'features'">
|
||||
Select all of the features that your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} makes
|
||||
use of.
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} makes use of.
|
||||
</template>
|
||||
<template v-else-if="header === 'resolutions'">
|
||||
Select the resolution(s) of textures in your
|
||||
@@ -34,21 +31,20 @@
|
||||
</template>
|
||||
<template v-else-if="header === 'performance impact'">
|
||||
Select the realistic performance impact of your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}.
|
||||
Select multiple if the
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} is
|
||||
configurable to different levels of performance impact.
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. Select multiple if the
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }} is configurable to
|
||||
different levels of performance impact.
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div :key="`categories-${header}-list`" class="category-list input-div">
|
||||
<div class="category-list input-div">
|
||||
<Checkbox
|
||||
v-for="category in categoryLists[header]"
|
||||
:key="`category-${header}-${category.name}`"
|
||||
:value="selectedTags.includes(category)"
|
||||
:model-value="selectedTags.includes(category)"
|
||||
:description="$formatCategory(category.name)"
|
||||
class="category-selector"
|
||||
@input="toggleCategory(category)"
|
||||
@update:model-value="toggleCategory(category)"
|
||||
>
|
||||
<div class="category-selector__label">
|
||||
<div
|
||||
@@ -56,10 +52,8 @@
|
||||
aria-hidden="true"
|
||||
class="icon"
|
||||
v-html="category.icon"
|
||||
></div>
|
||||
<span aria-hidden="true">
|
||||
{{ $formatCategory(category.name) }}</span
|
||||
>
|
||||
/>
|
||||
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
@@ -69,8 +63,8 @@
|
||||
<span class="label__title"><StarIcon /> Featured tags</span>
|
||||
</h4>
|
||||
<span class="label__description">
|
||||
You can feature up to 3 of your most relevant tags. Other tags may be
|
||||
promoted to featured if you do not select all 3.
|
||||
You can feature up to 3 of your most relevant tags. Other tags may be promoted to featured
|
||||
if you do not select all 3.
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="selectedTags.length < 1">
|
||||
@@ -81,12 +75,10 @@
|
||||
v-for="category in selectedTags"
|
||||
:key="`featured-category-${category.name}`"
|
||||
class="category-selector"
|
||||
:value="featuredTags.includes(category)"
|
||||
:model-value="featuredTags.includes(category)"
|
||||
:description="$formatCategory(category.name)"
|
||||
:disabled="
|
||||
featuredTags.length >= 3 && !featuredTags.includes(category)
|
||||
"
|
||||
@input="toggleFeaturedCategory(category)"
|
||||
:disabled="featuredTags.length >= 3 && !featuredTags.includes(category)"
|
||||
@update:model-value="toggleFeaturedCategory(category)"
|
||||
>
|
||||
<div class="category-selector__label">
|
||||
<div
|
||||
@@ -94,10 +86,8 @@
|
||||
aria-hidden="true"
|
||||
class="icon"
|
||||
v-html="category.icon"
|
||||
></div>
|
||||
<span aria-hidden="true">
|
||||
{{ $formatCategory(category.name) }}</span
|
||||
>
|
||||
/>
|
||||
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
@@ -118,10 +108,10 @@
|
||||
|
||||
<script>
|
||||
import Checkbox from '~/components/ui/Checkbox'
|
||||
import StarIcon from '~/assets/images/utils/star.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import StarIcon from '~/assets/images/utils/star.svg'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Checkbox,
|
||||
SaveIcon,
|
||||
@@ -162,23 +152,19 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTags: [],
|
||||
featuredTags: [],
|
||||
selectedTags: this.$sortedCategories.filter(
|
||||
(x) =>
|
||||
x.project_type === this.project.actualProjectType &&
|
||||
(this.project.categories.includes(x.name) ||
|
||||
this.project.additional_categories.includes(x.name))
|
||||
),
|
||||
featuredTags: this.$sortedCategories.filter(
|
||||
(x) =>
|
||||
x.project_type === this.project.actualProjectType &&
|
||||
this.project.categories.includes(x.name)
|
||||
),
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.selectedTags = this.$sortedCategories.filter(
|
||||
(x) =>
|
||||
x.project_type === this.project.actualProjectType &&
|
||||
(this.project.categories.includes(x.name) ||
|
||||
this.project.additional_categories.includes(x.name))
|
||||
)
|
||||
this.featuredTags = this.$sortedCategories.filter(
|
||||
(x) =>
|
||||
x.project_type === this.project.actualProjectType &&
|
||||
this.project.categories.includes(x.name)
|
||||
)
|
||||
},
|
||||
computed: {
|
||||
categoryLists() {
|
||||
const lists = {}
|
||||
@@ -197,19 +183,11 @@ export default {
|
||||
const data = {}
|
||||
// Promote selected categories to featured if there are less than 3 featured
|
||||
const newFeaturedTags = this.featuredTags.slice()
|
||||
if (
|
||||
newFeaturedTags.length < 1 &&
|
||||
this.selectedTags.length > newFeaturedTags.length
|
||||
) {
|
||||
const nonFeaturedCategories = this.selectedTags.filter(
|
||||
(x) => !newFeaturedTags.includes(x)
|
||||
)
|
||||
if (newFeaturedTags.length < 1 && this.selectedTags.length > newFeaturedTags.length) {
|
||||
const nonFeaturedCategories = this.selectedTags.filter((x) => !newFeaturedTags.includes(x))
|
||||
|
||||
nonFeaturedCategories
|
||||
.slice(
|
||||
0,
|
||||
Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length)
|
||||
)
|
||||
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
|
||||
.forEach((x) => newFeaturedTags.push(x))
|
||||
}
|
||||
// Convert selected and featured categories to backend-usable arrays
|
||||
@@ -226,11 +204,8 @@ export default {
|
||||
}
|
||||
|
||||
if (
|
||||
additionalCategories.length !==
|
||||
this.project.additional_categories.length ||
|
||||
additionalCategories.some(
|
||||
(value) => !this.project.additional_categories.includes(value)
|
||||
)
|
||||
additionalCategories.length !== this.project.additional_categories.length ||
|
||||
additionalCategories.some((value) => !this.project.additional_categories.includes(value))
|
||||
) {
|
||||
data.additional_categories = additionalCategories
|
||||
}
|
||||
@@ -265,7 +240,7 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.label__title {
|
||||
@@ -281,7 +256,7 @@ export default {
|
||||
column-gap: var(--spacing-card-lg);
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
|
||||
.category-selector ::v-deep {
|
||||
:deep(.category-selector) {
|
||||
margin-bottom: 0.5rem;
|
||||
.category-selector__label {
|
||||
display: flex;
|
||||
File diff suppressed because it is too large
Load Diff
8
pages/[type]/[id]/version/[version]/edit.vue
Normal file
8
pages/[type]/[id]/version/[version]/edit.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<Head>
|
||||
<Title> {{ project.title }} - Versions </Title>
|
||||
<Meta name="og:title" :content="`${project.title} - Versions`" />
|
||||
<Meta name="description" :content="metaDescription" />
|
||||
<Meta name="apple-mobile-web-app-title" :content="`${project.title} - Versions`" />
|
||||
<Meta name="og:description" :content="metaDescription" />
|
||||
</Head>
|
||||
<div v-if="currentMember" class="card header-buttons">
|
||||
<FileInput
|
||||
:max-size="524288000"
|
||||
@@ -13,25 +20,33 @@
|
||||
<span class="indicator">
|
||||
<InfoIcon /> Click to choose a file or drag one onto this page
|
||||
</span>
|
||||
<DropArea
|
||||
:accept="acceptFileFromProjectType(project.project_type)"
|
||||
@change="handleFiles"
|
||||
/>
|
||||
<DropArea :accept="acceptFileFromProjectType(project.project_type)" @change="handleFiles" />
|
||||
</div>
|
||||
<VersionFilterControl
|
||||
class="card"
|
||||
:versions="versions"
|
||||
@updateVersions="updateVersions"
|
||||
:versions="props.versions"
|
||||
@update-versions="
|
||||
(v) => {
|
||||
filteredVersions = v
|
||||
switchPage(1)
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div v-if="versions.length > 0" class="universal-card all-versions">
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
class="pagination-before"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
<div v-if="filteredVersions.length > 0" class="universal-card all-versions">
|
||||
<div class="header">
|
||||
<div></div>
|
||||
<div />
|
||||
<div>Version</div>
|
||||
<div>Supports</div>
|
||||
<div>Stats</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="version in filteredVersions"
|
||||
v-for="version in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
|
||||
:key="version.id"
|
||||
class="version-button button-transparent"
|
||||
@click="
|
||||
@@ -44,15 +59,12 @@
|
||||
>
|
||||
<a
|
||||
v-tooltip="
|
||||
$parent.findPrimary(version).filename +
|
||||
' (' +
|
||||
$formatBytes($parent.findPrimary(version).size) +
|
||||
')'
|
||||
version.primaryFile.filename + ' (' + $formatBytes(version.primaryFile.size) + ')'
|
||||
"
|
||||
:href="$parent.findPrimary(version).url"
|
||||
:href="version.primaryFile.url"
|
||||
class="download-button square-button brand-button"
|
||||
:class="version.version_type"
|
||||
:title="`Download ${version.name}`"
|
||||
:aria-label="`Download ${version.name}`"
|
||||
@click.stop="(event) => event.stopPropagation()"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
@@ -64,24 +76,11 @@
|
||||
class="version__title"
|
||||
>
|
||||
{{ version.name }}
|
||||
<FeaturedIcon v-if="featuredVersionIds.includes(version.id)" />
|
||||
</nuxt-link>
|
||||
<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="orange"
|
||||
/>
|
||||
<VersionBadge
|
||||
v-else-if="version.version_type === 'alpha'"
|
||||
type="alpha"
|
||||
color="red"
|
||||
/>
|
||||
<VersionBadge v-if="version.version_type === 'release'" type="release" color="green" />
|
||||
<VersionBadge v-else-if="version.version_type === 'beta'" type="beta" color="orange" />
|
||||
<VersionBadge v-else-if="version.version_type === 'alpha'" type="alpha" color="red" />
|
||||
<span class="divider" />
|
||||
<span class="version_number">{{ version.version_number }}</span>
|
||||
</div>
|
||||
@@ -98,130 +97,99 @@
|
||||
</span>
|
||||
<span>
|
||||
Published on
|
||||
<strong>{{
|
||||
$dayjs(version.date_published).format('MMM D, YYYY')
|
||||
}}</strong>
|
||||
<strong>{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:count="Math.ceil(filteredVersions.length / 20)"
|
||||
class="pagination-before"
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { acceptFileFromProjectType } from '~/plugins/fileUtils'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
import InfoIcon from '~/assets/images/utils/info.svg?inline'
|
||||
import FeaturedIcon from '~/assets/images/utils/star.svg?inline'
|
||||
<script setup>
|
||||
import { acceptFileFromProjectType } from '~/helpers/fileUtils'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg'
|
||||
import InfoIcon from '~/assets/images/utils/info.svg'
|
||||
import VersionBadge from '~/components/ui/Badge'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
import DropArea from '~/components/ui/DropArea'
|
||||
import Pagination from '~/components/ui/Pagination'
|
||||
import VersionFilterControl from '~/components/ui/VersionFilterControl'
|
||||
import DropArea from '~/components/ui/DropArea.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DropArea,
|
||||
DownloadIcon,
|
||||
UploadIcon,
|
||||
InfoIcon,
|
||||
FeaturedIcon,
|
||||
VersionBadge,
|
||||
VersionFilterControl,
|
||||
FileInput,
|
||||
},
|
||||
auth: false,
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
featuredVersions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filteredVersions: this.versions,
|
||||
}
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
fetch() {
|
||||
if (this.$route.query.page)
|
||||
this.currentPage = parseInt(this.$route.query.page)
|
||||
members: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
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')}.`
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
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: {
|
||||
featuredVersionIds() {
|
||||
return this.featuredVersions.map((x) => x.id)
|
||||
const data = useNuxtApp()
|
||||
const metaDescription = computed(
|
||||
() =>
|
||||
`Download and browse ${props.versions.length} ${
|
||||
props.project.title
|
||||
} versions. ${data.$formatNumber(props.project.downloads)} total downloads. Last updated ${data
|
||||
.$dayjs(props.project.updated)
|
||||
.format('MMM D, YYYY')}.`
|
||||
)
|
||||
|
||||
const route = useRoute()
|
||||
const currentPage = ref(Number(route.query.p ?? 1))
|
||||
const filteredVersions = shallowRef(props.versions)
|
||||
|
||||
async function switchPage(page) {
|
||||
currentPage.value = page
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
await router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
p: currentPage.value !== 1 ? currentPage.value : undefined,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
acceptFileFromProjectType,
|
||||
updateVersions(updatedVersions) {
|
||||
this.filteredVersions = updatedVersions
|
||||
})
|
||||
}
|
||||
|
||||
async function handleFiles(files) {
|
||||
const router = useRouter()
|
||||
await router.push({
|
||||
name: 'type-id-version-version',
|
||||
params: {
|
||||
type: props.project.project_type,
|
||||
id: props.project.slug ? props.project.slug : props.project.id,
|
||||
version: 'create',
|
||||
},
|
||||
async handleFiles(files) {
|
||||
await this.$router.push({
|
||||
name: 'type-id-version-create',
|
||||
params: {
|
||||
type: this.project.project_type,
|
||||
id: this.project.slug ? this.project.slug : this.project.id,
|
||||
newPrimaryFile: files[0],
|
||||
},
|
||||
})
|
||||
state: {
|
||||
newPrimaryFile: files[0],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -246,7 +214,7 @@ export default {
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template: 'download title supports stats';
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr 1fr 1fr;
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1.25fr 1fr 1fr;
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: bold;
|
||||
@@ -278,7 +246,7 @@ export default {
|
||||
'download title supports stats'
|
||||
'download metadata supports stats'
|
||||
'download dummy supports stats';
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1fr 1fr 1fr;
|
||||
grid-template-columns: calc(2.25rem + var(--spacing-card-sm)) 1.25fr 1fr 1fr;
|
||||
column-gap: var(--spacing-card-sm);
|
||||
justify-content: left;
|
||||
padding: var(--spacing-card-md);
|
||||
@@ -354,13 +322,17 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.modal-create {
|
||||
padding: var(--spacing-card-bg);
|
||||
|
||||
.input-group {
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
margin-top: 1.5rem;
|
||||
.search-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-card-md);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
.multiselect {
|
||||
flex: 1;
|
||||
}
|
||||
.checkbox-outer {
|
||||
min-width: fit-content;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,266 +0,0 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<VersionFilterControl
|
||||
class="card"
|
||||
:versions="versions"
|
||||
@updateVersions="updateVersions"
|
||||
/>
|
||||
<div class="card">
|
||||
<div
|
||||
v-for="version in filteredVersions"
|
||||
:key="version.id"
|
||||
class="changelog-item"
|
||||
>
|
||||
<div
|
||||
: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>
|
||||
</template>
|
||||
<script>
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import VersionFilterControl from '~/components/ui/VersionFilterControl'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VersionFilterControl,
|
||||
DownloadIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filteredVersions: this.$calculateDuplicates(this.versions),
|
||||
currentPage: 1,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
if (this.$route.query.page) {
|
||||
this.currentPage = parseInt(this.$route.query.page)
|
||||
}
|
||||
},
|
||||
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,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async switchPage(page, toTop) {
|
||||
this.currentPage = page
|
||||
await 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 = this.$calculateDuplicates(updatedVersions)
|
||||
},
|
||||
},
|
||||
auth: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.changelog-item {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
padding-left: 1.8rem;
|
||||
|
||||
.changelog-bar {
|
||||
--color: var(--color-special-green);
|
||||
|
||||
&.alpha {
|
||||
--color: var(--color-special-red);
|
||||
}
|
||||
|
||||
&.release {
|
||||
--color: var(--color-special-green);
|
||||
}
|
||||
|
||||
&.beta {
|
||||
--color: var(--color-special-orange);
|
||||
}
|
||||
|
||||
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;
|
||||
min-height: 0.75rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.version-header-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
h2,
|
||||
span {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.download {
|
||||
display: none;
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
margin: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-highlightjs
|
||||
class="markdown-body card"
|
||||
v-html="$xss($md.render(project.body))"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,10 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,10 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -13,49 +13,27 @@
|
||||
<!-- <NavStackItem link="/dashboard/analytics" label="Analytics">-->
|
||||
<!-- <ChartIcon />-->
|
||||
<!-- </NavStackItem>-->
|
||||
<NavStackItem
|
||||
v-if="hasMonetization()"
|
||||
link="/dashboard/revenue"
|
||||
label="Revenue"
|
||||
>
|
||||
<NavStackItem link="/dashboard/revenue" label="Revenue">
|
||||
<CurrencyIcon />
|
||||
</NavStackItem>
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<NuxtChild />
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
|
||||
import DashboardIcon from '~/assets/images/utils/dashboard.svg?inline'
|
||||
// import ChartIcon from '~/assets/images/utils/chart.svg?inline'
|
||||
import CurrencyIcon from '~/assets/images/utils/currency.svg?inline'
|
||||
import ListIcon from '~/assets/images/utils/list.svg?inline'
|
||||
import DashboardIcon from '~/assets/images/utils/dashboard.svg'
|
||||
import CurrencyIcon from '~/assets/images/utils/currency.svg'
|
||||
import ListIcon from '~/assets/images/utils/list.svg'
|
||||
|
||||
const monetization = true
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
NavStack,
|
||||
NavStackItem,
|
||||
DashboardIcon,
|
||||
// ChartIcon,
|
||||
CurrencyIcon,
|
||||
ListIcon,
|
||||
},
|
||||
methods: {
|
||||
hasMonetization() {
|
||||
return monetization
|
||||
},
|
||||
},
|
||||
}
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -3,24 +3,16 @@
|
||||
<section class="universal-card">
|
||||
<h2>Analytics</h2>
|
||||
<p>You found a secret!</p>
|
||||
<nuxt-link to="/frog" class="goto-link"
|
||||
>Click here for fancy graphs!</nuxt-link
|
||||
>
|
||||
<nuxt-link to="/frog" class="goto-link"> Click here for fancy graphs! </nuxt-link>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
fetch() {},
|
||||
export default defineNuxtComponent({
|
||||
head: {
|
||||
title: 'Analytics - Modrinth',
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -6,11 +6,7 @@
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Total downloads</div>
|
||||
<div class="value">
|
||||
{{
|
||||
$formatNumber(
|
||||
$user.projects.reduce((agg, x) => agg + x.downloads, 0)
|
||||
)
|
||||
}}
|
||||
{{ $formatNumber(user.projects.reduce((agg, x) => agg + x.downloads, 0)) }}
|
||||
</div>
|
||||
<span
|
||||
>from
|
||||
@@ -27,11 +23,7 @@
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Total followers</div>
|
||||
<div class="value">
|
||||
{{
|
||||
$formatNumber(
|
||||
$user.projects.reduce((agg, x) => agg + x.followers, 0)
|
||||
)
|
||||
}}
|
||||
{{ $formatNumber(user.projects.reduce((agg, x) => agg + x.followers, 0)) }}
|
||||
</div>
|
||||
<span>
|
||||
<span
|
||||
@@ -49,7 +41,9 @@
|
||||
</div>
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Total revenue</div>
|
||||
<div class="value">{{ $formatMoney(payouts.all_time) }}</div>
|
||||
<div class="value">
|
||||
{{ $formatMoney(payouts.all_time) }}
|
||||
</div>
|
||||
<span>{{ $formatMoney(payouts.last_month) }} this month</span>
|
||||
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
|
||||
<!-- >View breakdown-->
|
||||
@@ -61,17 +55,16 @@
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Current balance</div>
|
||||
<div class="value">
|
||||
{{ $formatMoney($auth.user.payout_data.balance) }}
|
||||
{{ $formatMoney(auth.user.payout_data.balance) }}
|
||||
</div>
|
||||
<NuxtLink
|
||||
v-if="$auth.user.payout_data.balance >= minWithdraw"
|
||||
v-if="auth.user.payout_data.balance >= minWithdraw"
|
||||
class="goto-link"
|
||||
to="/dashboard/revenue"
|
||||
>Withdraw earnings
|
||||
<ChevronRightIcon
|
||||
class="featured-header-chevron"
|
||||
aria-hidden="true"
|
||||
/></NuxtLink>
|
||||
>
|
||||
Withdraw earnings
|
||||
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
|
||||
</NuxtLink>
|
||||
<span v-else>${{ minWithdraw }} is the withdraw minimum</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,55 +72,37 @@
|
||||
<section class="universal-card more-soon">
|
||||
<h2>More coming soon!</h2>
|
||||
<p>
|
||||
Stay tuned for more metrics and analytics (pretty graphs, anyone? 👀)
|
||||
coming to the creators dashboard soon!
|
||||
Stay tuned for more metrics and analytics (pretty graphs, anyone? 👀) coming to the creators
|
||||
dashboard soon!
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
||||
|
||||
<script>
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg?inline'
|
||||
useHead({
|
||||
title: 'Creator dashboard - Modrinth',
|
||||
})
|
||||
|
||||
export default {
|
||||
components: { ChevronRightIcon },
|
||||
async asyncData(data) {
|
||||
const [payouts] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(
|
||||
`user/${data.$auth.user.id}/payouts`,
|
||||
data.$defaultHeaders()
|
||||
),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
const auth = await useAuth()
|
||||
const app = useNuxtApp()
|
||||
|
||||
payouts.all_time = Math.floor(payouts.all_time * 100) / 100
|
||||
payouts.last_month = Math.floor(payouts.last_month * 100) / 100
|
||||
const [raw] = await Promise.all([
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`, app.$defaultHeaders()),
|
||||
])
|
||||
const user = await useUser()
|
||||
|
||||
return {
|
||||
payouts,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
minWithdraw: 0.26,
|
||||
}
|
||||
},
|
||||
fetch() {},
|
||||
head: {
|
||||
title: 'Creator dashboard - Modrinth',
|
||||
},
|
||||
computed: {
|
||||
downloadsProjectCount() {
|
||||
return this.$user.projects.filter((project) => project.downloads > 0)
|
||||
.length
|
||||
},
|
||||
followersProjectCount() {
|
||||
return this.$user.projects.filter((project) => project.followers > 0)
|
||||
.length
|
||||
},
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
raw.all_time = Math.floor(raw.all_time * 100) / 100
|
||||
raw.last_month = Math.floor(raw.last_month * 100) / 100
|
||||
|
||||
const payouts = ref(raw)
|
||||
const minWithdraw = ref(0.26)
|
||||
|
||||
const downloadsProjectCount = computed(
|
||||
() => user.value.projects.filter((project) => project.downloads > 0).length
|
||||
)
|
||||
const followersProjectCount = computed(
|
||||
() => user.value.projects.filter((project) => project.followers > 0).length
|
||||
)
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<Modal ref="editLinksModal" header="Edit links">
|
||||
<div class="universal-modal links-modal">
|
||||
<p>
|
||||
Any links you specify below will be overwritten on each of the
|
||||
selected projects. Any you leave blank will be ignored. You can clear
|
||||
a link from all selected projects using the trash can button.
|
||||
Any links you specify below will be overwritten on each of the selected projects. Any you
|
||||
leave blank will be ignored. You can clear a link from all selected projects using the
|
||||
trash can button.
|
||||
</p>
|
||||
<section class="links">
|
||||
<label
|
||||
@@ -21,14 +21,13 @@
|
||||
:disabled="editLinks.issues.clear"
|
||||
type="url"
|
||||
:placeholder="
|
||||
editLinks.issues.clear
|
||||
? 'Existing link will be cleared'
|
||||
: 'Enter a valid URL'
|
||||
editLinks.issues.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
|
||||
"
|
||||
maxlength="2048"
|
||||
/>
|
||||
<button
|
||||
v-tooltip="'Clear link'"
|
||||
aria-label="Clear link"
|
||||
class="square-button label-button"
|
||||
:data-active="editLinks.issues.clear"
|
||||
@click="editLinks.issues.clear = !editLinks.issues.clear"
|
||||
@@ -50,13 +49,12 @@
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
:placeholder="
|
||||
editLinks.source.clear
|
||||
? 'Existing link will be cleared'
|
||||
: 'Enter a valid URL'
|
||||
editLinks.source.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
|
||||
"
|
||||
/>
|
||||
<button
|
||||
v-tooltip="'Clear link'"
|
||||
aria-label="Clear link"
|
||||
class="square-button label-button"
|
||||
:data-active="editLinks.source.clear"
|
||||
@click="editLinks.source.clear = !editLinks.source.clear"
|
||||
@@ -78,13 +76,12 @@
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
:placeholder="
|
||||
editLinks.wiki.clear
|
||||
? 'Existing link will be cleared'
|
||||
: 'Enter a valid URL'
|
||||
editLinks.wiki.clear ? 'Existing link will be cleared' : 'Enter a valid URL'
|
||||
"
|
||||
/>
|
||||
<button
|
||||
v-tooltip="'Clear link'"
|
||||
aria-label="Clear link"
|
||||
class="square-button label-button"
|
||||
:data-active="editLinks.wiki.clear"
|
||||
@click="editLinks.wiki.clear = !editLinks.wiki.clear"
|
||||
@@ -92,10 +89,7 @@
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
<label
|
||||
for="discord-invite-input"
|
||||
title="An invitation link to your Discord server."
|
||||
>
|
||||
<label for="discord-invite-input" title="An invitation link to your Discord server.">
|
||||
<span class="label__title">Discord invite</span>
|
||||
</label>
|
||||
<div class="input-group shrink-first">
|
||||
@@ -113,6 +107,7 @@
|
||||
/>
|
||||
<button
|
||||
v-tooltip="'Clear link'"
|
||||
aria-label="Clear link"
|
||||
class="square-button label-button"
|
||||
:data-active="editLinks.discord.clear"
|
||||
@click="editLinks.discord.clear = !editLinks.discord.clear"
|
||||
@@ -154,10 +149,7 @@
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="bulkEditLinks()"
|
||||
>
|
||||
<button class="iconified-button brand-button" @click="bulkEditLinks()">
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</button>
|
||||
@@ -169,10 +161,7 @@
|
||||
<div class="header__row">
|
||||
<h2 class="header__title">Projects</h2>
|
||||
<div class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="$refs.modal_creation.show()"
|
||||
>
|
||||
<button class="iconified-button brand-button" @click="$refs.modal_creation.show()">
|
||||
<PlusIcon />
|
||||
Create a project
|
||||
</button>
|
||||
@@ -203,8 +192,8 @@
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@input="updateSort()"
|
||||
></Multiselect>
|
||||
@update:model-value="projects = updateSort(projects, sortBy)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,8 +201,8 @@
|
||||
<div class="grid-table__row grid-table__header">
|
||||
<div>
|
||||
<Checkbox
|
||||
:value="selectedProjects === projects"
|
||||
@input="
|
||||
:model-value="selectedProjects === projects"
|
||||
@update:model-value="
|
||||
selectedProjects === projects
|
||||
? (selectedProjects = [])
|
||||
: (selectedProjects = projects)
|
||||
@@ -225,33 +214,22 @@
|
||||
<div>ID</div>
|
||||
<div>Type</div>
|
||||
<div>Status</div>
|
||||
<div></div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
v-for="project in projects"
|
||||
:key="`project-${project.id}`"
|
||||
class="grid-table__row"
|
||||
>
|
||||
<div v-for="project in projects" :key="`project-${project.id}`" class="grid-table__row">
|
||||
<div>
|
||||
<Checkbox
|
||||
:disabled="
|
||||
(project.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
||||
"
|
||||
:value="selectedProjects.includes(project)"
|
||||
@input="
|
||||
:disabled="(project.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||
:model-value="selectedProjects.includes(project)"
|
||||
@update:model-value="
|
||||
selectedProjects.includes(project)
|
||||
? (selectedProjects = selectedProjects.filter(
|
||||
(it) => it !== project
|
||||
))
|
||||
? (selectedProjects = selectedProjects.filter((it) => it !== project))
|
||||
: selectedProjects.push(project)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<nuxt-link
|
||||
tabindex="-1"
|
||||
:to="`/${project.project_type}/${project.slug}`"
|
||||
>
|
||||
<nuxt-link tabindex="-1" :to="`/${project.project_type}/${project.slug}`">
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
aria-hidden="true"
|
||||
@@ -265,9 +243,6 @@
|
||||
<span class="project-title">
|
||||
<IssuesIcon
|
||||
v-if="project.moderator_message"
|
||||
v-tooltip="
|
||||
'Project has a message from the moderators. View the project to see more.'
|
||||
"
|
||||
aria-label="Project has a message from the moderators. View the project to see more."
|
||||
/>
|
||||
|
||||
@@ -285,15 +260,11 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ $formatProjectType(project.project_type) }}
|
||||
{{ $formatProjectType($getProjectTypeForUrl(project.project_type, project.loaders)) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Badge
|
||||
v-if="project.status"
|
||||
:type="project.status"
|
||||
class="status"
|
||||
/>
|
||||
<Badge v-if="project.status" :type="project.status" class="status" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -317,20 +288,19 @@ import Multiselect from 'vue-multiselect'
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
// import ModalConfirm from '~/components/ui/ModalConfirm.vue'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
import ModalCreation from '~/components/ui/ModalCreation.vue'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import IssuesIcon from '~/assets/images/utils/issues.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 SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||
import IssuesIcon from '~/assets/images/utils/issues.svg'
|
||||
import PlusIcon from '~/assets/images/utils/plus.svg'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Avatar,
|
||||
Badge,
|
||||
@@ -343,14 +313,21 @@ export default {
|
||||
EditIcon,
|
||||
SaveIcon,
|
||||
Modal,
|
||||
// ModalConfirm,
|
||||
ModalCreation,
|
||||
Multiselect,
|
||||
CopyCode,
|
||||
},
|
||||
async setup() {
|
||||
const user = await useUser()
|
||||
if (process.client) {
|
||||
await initUserProjects()
|
||||
}
|
||||
|
||||
return { user: ref(user) }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projects: [],
|
||||
projects: this.updateSort(this.user.projects, 'Name'),
|
||||
versions: [],
|
||||
selectedProjects: [],
|
||||
sortBy: 'Name',
|
||||
@@ -375,10 +352,6 @@ export default {
|
||||
},
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.projects = this.$user.projects
|
||||
this.updateSort()
|
||||
},
|
||||
head: {
|
||||
title: 'Projects - Modrinth',
|
||||
},
|
||||
@@ -392,12 +365,11 @@ export default {
|
||||
this.EDIT_MEMBER = 1 << 6
|
||||
this.DELETE_PROJECT = 1 << 7
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
updateSort() {
|
||||
switch (this.sortBy) {
|
||||
updateSort(projects, sort) {
|
||||
switch (sort) {
|
||||
case 'Name':
|
||||
this.projects = this.projects.slice().sort((a, b) => {
|
||||
return projects.slice().sort((a, b) => {
|
||||
if (a.title < b.title) {
|
||||
return -1
|
||||
}
|
||||
@@ -406,9 +378,8 @@ export default {
|
||||
}
|
||||
return 0
|
||||
})
|
||||
break
|
||||
case 'Status':
|
||||
this.projects = this.projects.slice().sort((a, b) => {
|
||||
return projects.slice().sort((a, b) => {
|
||||
if (a.status < b.status) {
|
||||
return -1
|
||||
}
|
||||
@@ -417,9 +388,8 @@ export default {
|
||||
}
|
||||
return 0
|
||||
})
|
||||
break
|
||||
case 'Type':
|
||||
this.projects = this.projects.slice().sort((a, b) => {
|
||||
return projects.slice().sort((a, b) => {
|
||||
if (a.project_type < b.project_type) {
|
||||
return -1
|
||||
}
|
||||
@@ -428,7 +398,6 @@ export default {
|
||||
}
|
||||
return 0
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -437,13 +406,11 @@ export default {
|
||||
try {
|
||||
const baseData = {
|
||||
issues_url:
|
||||
!this.editLinks.issues.clear &&
|
||||
this.editLinks.issues.val.trim() !== ''
|
||||
!this.editLinks.issues.clear && this.editLinks.issues.val.trim() !== ''
|
||||
? this.editLinks.issues.val
|
||||
: null,
|
||||
source_url:
|
||||
!this.editLinks.source.clear &&
|
||||
this.editLinks.source.val.trim() !== ''
|
||||
!this.editLinks.source.clear && this.editLinks.source.val.trim() !== ''
|
||||
? this.editLinks.source.val
|
||||
: null,
|
||||
wiki_url:
|
||||
@@ -451,18 +418,18 @@ export default {
|
||||
? this.editLinks.wiki.val
|
||||
: null,
|
||||
discord_url:
|
||||
!this.editLinks.discord.clear &&
|
||||
this.editLinks.discord.val.trim() !== ''
|
||||
!this.editLinks.discord.clear && this.editLinks.discord.val.trim() !== ''
|
||||
? this.editLinks.discord.val
|
||||
: null,
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`projects?ids=${JSON.stringify(
|
||||
this.selectedProjects.map((x) => x.id)
|
||||
)}`,
|
||||
baseData,
|
||||
this.$defaultHeaders()
|
||||
await useBaseFetch(
|
||||
`projects?ids=${JSON.stringify(this.selectedProjects.map((x) => x.id))}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: baseData,
|
||||
...this.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
|
||||
this.$refs.editLinksModal.hide()
|
||||
@@ -483,7 +450,7 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.grid-table {
|
||||
|
||||
@@ -3,47 +3,42 @@
|
||||
<ModalTransfer
|
||||
v-if="enrolled"
|
||||
ref="modal_transfer"
|
||||
:wallet="$auth.user.payout_data.payout_wallet"
|
||||
:account-type="$auth.user.payout_data.payout_wallet_type"
|
||||
:account="$auth.user.payout_data.payout_address"
|
||||
:balance="$auth.user.payout_data.balance"
|
||||
:wallet="auth.user.payout_data.payout_wallet"
|
||||
:account-type="auth.user.payout_data.payout_wallet_type"
|
||||
:account="auth.user.payout_data.payout_address"
|
||||
:balance="auth.user.payout_data.balance"
|
||||
:min-withdraw="minWithdraw"
|
||||
/>
|
||||
<section class="universal-card">
|
||||
<h2>Withdraw</h2>
|
||||
<div v-if="$auth.user.payout_data.balance >= minWithdraw">
|
||||
<div v-if="auth.user.payout_data.balance >= minWithdraw">
|
||||
<p>
|
||||
You have
|
||||
<strong>{{ $formatMoney($auth.user.payout_data.balance) }}</strong>
|
||||
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong>
|
||||
available to withdraw.
|
||||
<span v-if="!enrolled"
|
||||
>Enroll in the Creator Monetization Program to withdraw your
|
||||
revenue.</span
|
||||
>Enroll in the Creator Monetization Program to withdraw your revenue.</span
|
||||
>
|
||||
</p>
|
||||
|
||||
<div v-if="enrolled" class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="$refs.modal_transfer.show()"
|
||||
>
|
||||
<button class="iconified-button brand-button" @click="$refs.modal_transfer.show()">
|
||||
<TransferIcon /> Transfer to
|
||||
{{ $formatWallet($auth.user.payout_data.payout_wallet) }}
|
||||
{{ $formatWallet(auth.user.payout_data.payout_wallet) }}
|
||||
</button>
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Monetization settings
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else-if="$auth.user.payout_data.balance > 0">
|
||||
<p v-else-if="auth.user.payout_data.balance > 0">
|
||||
You have made
|
||||
<strong>{{ $formatMoney($auth.user.payout_data.balance) }}</strong
|
||||
>, however you have not yet met the minimum of ${{ minWithdraw }} to
|
||||
withdraw.
|
||||
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong
|
||||
>, however you have not yet met the minimum of ${{ minWithdraw }} to withdraw.
|
||||
</p>
|
||||
<p v-else>
|
||||
You have made
|
||||
<strong>{{ $formatMoney($auth.user.payout_data.balance) }}</strong
|
||||
<strong>{{ $formatMoney(auth.user.payout_data.balance) }}</strong
|
||||
>, which is under the minimum of ${{ minWithdraw }} to withdraw.
|
||||
</p>
|
||||
<div v-if="!enrolled">
|
||||
@@ -55,9 +50,9 @@
|
||||
<section class="universal-card">
|
||||
<h2>Processing fees</h2>
|
||||
<p>
|
||||
To avoid paying unnecessary fee deductions, you may want to wait to
|
||||
transfer your money out after it accumulates for a bit rather than
|
||||
transferring as soon as you reach the minimum of ${{ minWithdraw }}.
|
||||
To avoid paying unnecessary fee deductions, you may want to wait to transfer your money out
|
||||
after it accumulates for a bit rather than transferring as soon as you reach the minimum of
|
||||
${{ minWithdraw }}.
|
||||
</p>
|
||||
<h3>PayPal</h3>
|
||||
<ul>
|
||||
@@ -67,55 +62,57 @@
|
||||
fee per transaction.
|
||||
</li>
|
||||
<li>
|
||||
In the rest of the world, PayPal charges a <strong>2%</strong> (up to
|
||||
$20) fee per transaction.
|
||||
In the rest of the world, PayPal charges a <strong>2%</strong> (up to $20) fee per
|
||||
transaction.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Modrinth will deduct <strong>2%</strong> for the fee (minimum of $0.25
|
||||
and maximum of $20) from <strong>all transfers</strong> and if the fee
|
||||
PayPal charges is less than the amount we deducted, the difference will
|
||||
be added back to your Modrinth balance. This happens as Modrinth cannot
|
||||
determine if a transaction will be in the United States or international
|
||||
or not until after the transaction has been made.
|
||||
Modrinth will deduct <strong>2%</strong> for the fee (minimum of $0.25 and maximum of $20)
|
||||
from <strong>all transfers</strong> and if the fee PayPal charges is less than the amount we
|
||||
deducted, the difference will be added back to your Modrinth balance. This happens as
|
||||
Modrinth cannot determine if a transaction will be in the United States or international or
|
||||
not until after the transaction has been made.
|
||||
</p>
|
||||
<h3>Venmo (United States only)</h3>
|
||||
<p>
|
||||
Venmo will charge a $0.25 processing fee per transaction, which will be
|
||||
deducted from the amount you choose to transfer.
|
||||
Venmo will charge a $0.25 processing fee per transaction, which will be deducted from the
|
||||
amount you choose to transfer.
|
||||
</p>
|
||||
<h2>Currency conversions</h2>
|
||||
<p>
|
||||
All revenue generated by Modrinth is in United States dollars. Any
|
||||
conversions to your local currency will happen at withdrawal and is not
|
||||
handled by Modrinth. Modrinth cannot guarantee any exchange rate, so
|
||||
only USD is displayed in the creator dashboard.
|
||||
All revenue generated by Modrinth is in United States dollars. Any conversions to your local
|
||||
currency will happen at withdrawal and is not handled by Modrinth. Modrinth cannot guarantee
|
||||
any exchange rate, so only USD is displayed in the creator dashboard.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||
import ModalTransfer from '~/components/ui/ModalTransfer'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: { TransferIcon, SettingsIcon, ModalTransfer },
|
||||
async setup() {
|
||||
const auth = await useAuth()
|
||||
|
||||
return { auth }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
minWithdraw: 0.26,
|
||||
enrolled:
|
||||
this.$auth.user.payout_data.payout_wallet &&
|
||||
this.$auth.user.payout_data.payout_wallet_type &&
|
||||
this.$auth.user.payout_data.payout_address,
|
||||
this.auth.user.payout_data.payout_wallet &&
|
||||
this.auth.user.payout_data.payout_wallet_type &&
|
||||
this.auth.user.payout_data.payout_address,
|
||||
}
|
||||
},
|
||||
head: {
|
||||
title: 'Revenue - Modrinth',
|
||||
},
|
||||
methods: {},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
strong {
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
width: calc(100% - 2 * var(--spacing-card-md));
|
||||
|
||||
278
pages/index.vue
278
pages/index.vue
File diff suppressed because one or more lines are too long
@@ -20,7 +20,7 @@
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<NuxtChild class="universal-card" />
|
||||
<NuxtPage class="universal-card" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,13 +29,12 @@
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
|
||||
import TermsIcon from '~/assets/images/utils/heart-handshake.svg?inline'
|
||||
import PrivacyIcon from '~/assets/images/utils/lock.svg?inline'
|
||||
import RulesIcon from '~/assets/images/sidebar/admin.svg?inline'
|
||||
import ShieldIcon from '~/assets/images/utils/shield.svg?inline'
|
||||
import TermsIcon from '~/assets/images/utils/heart-handshake.svg'
|
||||
import PrivacyIcon from '~/assets/images/utils/lock.svg'
|
||||
import RulesIcon from '~/assets/images/sidebar/admin.svg'
|
||||
import ShieldIcon from '~/assets/images/utils/shield.svg'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
NavStack,
|
||||
NavStackItem,
|
||||
@@ -44,11 +43,11 @@ export default {
|
||||
RulesIcon,
|
||||
ShieldIcon,
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.normal-page__content ::v-deep a {
|
||||
.normal-page__content :deep(a) {
|
||||
color: var(--color-link);
|
||||
text-decoration: underline;
|
||||
|
||||
|
||||
@@ -7,31 +7,27 @@
|
||||
<h2>Foreword</h2>
|
||||
|
||||
<p>
|
||||
The following document was created as required by several laws, including
|
||||
but not limited to:
|
||||
The following document was created as required by several laws, including but not limited to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
the California Consumer Privacy Act (CA CCPA), more information about
|
||||
which can be found on
|
||||
the California Consumer Privacy Act (CA CCPA), more information about which can be found on
|
||||
<a href="https://oag.ca.gov/privacy/ccpa">oag.ca.gov</a>
|
||||
</li>
|
||||
<li>
|
||||
the European Union General Data Protection Regulation (EU GDPR), more
|
||||
information about which can be found on
|
||||
the European Union General Data Protection Regulation (EU GDPR), more information about
|
||||
which can be found on
|
||||
<a href="https://gdpr.eu/">gdpr.eu</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<a href="https://modrinth.com">Modrinth</a> is part of Rinth, Inc. ("us",
|
||||
"we", "our"). This privacy policy explains how we collect data, process
|
||||
it, and your rights relative to your data.
|
||||
<a href="https://modrinth.com">Modrinth</a> is part of Rinth, Inc. ("us", "we", "our"). This
|
||||
privacy policy explains how we collect data, process it, and your rights relative to your
|
||||
data.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Rinth, Inc. is the data controller for data collected through Modrinth.
|
||||
</p>
|
||||
<p>Rinth, Inc. is the data controller for data collected through Modrinth.</p>
|
||||
|
||||
<h2>What data do we collect?</h2>
|
||||
|
||||
@@ -45,14 +41,12 @@
|
||||
<li>Your GitHub ID</li>
|
||||
</ul>
|
||||
<p>
|
||||
This data is used to identify you and display your profile. It will be
|
||||
linked to your projects.
|
||||
This data is used to identify you and display your profile. It will be linked to your
|
||||
projects.
|
||||
</p>
|
||||
|
||||
<h3>View data and download data</h3>
|
||||
<p>
|
||||
When you view a project page or download a file from Modrinth, we collect:
|
||||
</p>
|
||||
<p>When you view a project page or download a file from Modrinth, we collect:</p>
|
||||
<ul>
|
||||
<li>Your IP address</li>
|
||||
<li>Your user ID (if applicable)</li>
|
||||
@@ -60,10 +54,7 @@
|
||||
<li>Your country</li>
|
||||
<li>Some additional metadata about your connection (HTTP headers)</li>
|
||||
</ul>
|
||||
<p>
|
||||
This data is used to monitor automated access to our service and deliver
|
||||
statistics.
|
||||
</p>
|
||||
<p>This data is used to monitor automated access to our service and deliver statistics.</p>
|
||||
|
||||
<h3>Creator Monetization Program data</h3>
|
||||
<p>
|
||||
@@ -77,44 +68,37 @@
|
||||
<li>Your PayPal email address (if applicable)</li>
|
||||
<li>Your Venmo username (if applicable)</li>
|
||||
</ul>
|
||||
<p>
|
||||
This data is used to carry out the CMP. It will be linked to your
|
||||
transactions.
|
||||
</p>
|
||||
<p>This data is used to carry out the CMP. It will be linked to your transactions.</p>
|
||||
|
||||
<h2>Data retention</h2>
|
||||
<p>
|
||||
View data and download data are anonymized 24 months after being recorded.
|
||||
All personal information will be removed from those records during
|
||||
anonymization.<br />
|
||||
Data is retained indefinitely. We do not delete any data unless you
|
||||
request it.
|
||||
View data and download data are anonymized 24 months after being recorded. All personal
|
||||
information will be removed from those records during anonymization.<br />
|
||||
Data is retained indefinitely. We do not delete any data unless you request it.
|
||||
</p>
|
||||
|
||||
<h2>Third-party services</h2>
|
||||
<p>
|
||||
We use some third-party services to make Modrinth run. Please refer to
|
||||
each of their privacy policies for more information:
|
||||
We use some third-party services to make Modrinth run. Please refer to each of their privacy
|
||||
policies for more information:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://www.cloudflare.com/en-gb/gdpr/introduction/">
|
||||
Cloudflare
|
||||
</a>
|
||||
<a href="https://www.cloudflare.com/en-gb/gdpr/introduction/"> Cloudflare </a>
|
||||
</li>
|
||||
<li><a href="https://sentry.io/trust/privacy/">Sentry</a></li>
|
||||
</ul>
|
||||
<p>
|
||||
Data that we specifically collect isn't shared with any other third party.
|
||||
We do not sell any data.
|
||||
Data that we specifically collect isn't shared with any other third party. We do not sell any
|
||||
data.
|
||||
</p>
|
||||
|
||||
<h2>Data Governance</h2>
|
||||
<p>
|
||||
Database access is limited to the minimum amount of Rinth, Inc. employees
|
||||
required to run the service.<br />
|
||||
Data is stored in a jurisdiction that is part of the European Economic
|
||||
Area (EEA), encrypted both in storage and in transit.
|
||||
Database access is limited to the minimum amount of Rinth, Inc. employees required to run the
|
||||
service.<br />
|
||||
Data is stored in a jurisdiction that is part of the European Economic Area (EEA), encrypted
|
||||
both in storage and in transit.
|
||||
</p>
|
||||
|
||||
<h2>Marketing and advertising</h2>
|
||||
@@ -124,107 +108,94 @@
|
||||
</p>
|
||||
|
||||
<h2>Cookies</h2>
|
||||
<p>We use cookies to log you into your account and save your cosmetic preferences.</p>
|
||||
<p>
|
||||
We use cookies to log you into your account and save your cosmetic
|
||||
preferences.
|
||||
</p>
|
||||
<p>
|
||||
Cookies are text files placed on your computer to collect standard
|
||||
Internet information. For more information, please visit
|
||||
Cookies are text files placed on your computer to collect standard Internet information. For
|
||||
more information, please visit
|
||||
<a href="https://allaboutcookies.org/">allaboutcookies.org</a>.
|
||||
</p>
|
||||
<p>
|
||||
You can set your browser not to accept cookies, and the above website
|
||||
tells you how to remove cookies from your browser. However, in a few
|
||||
cases, some of our website features may not function as a result.
|
||||
You can set your browser not to accept cookies, and the above website tells you how to remove
|
||||
cookies from your browser. However, in a few cases, some of our website features may not
|
||||
function as a result.
|
||||
</p>
|
||||
|
||||
<h2>
|
||||
Access, rectification, erasure, restriction, portability, and objection
|
||||
</h2>
|
||||
<h2>Access, rectification, erasure, restriction, portability, and objection</h2>
|
||||
<p>Every user is entitled to the following:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>The right to access</strong> – You have the right to request
|
||||
copies of your personal data. We may charge you a small fee for this
|
||||
service.
|
||||
<strong>The right to access</strong> – You have the right to request copies of your personal
|
||||
data. We may charge you a small fee for this service.
|
||||
</li>
|
||||
<li>
|
||||
<strong>The right to rectification</strong> – You have the right to
|
||||
request that we correct any information you believe is inaccurate. You
|
||||
also have the right to request us to complete the information you
|
||||
believe is incomplete.
|
||||
<strong>The right to rectification</strong> – You have the right to request that we correct
|
||||
any information you believe is inaccurate. You also have the right to request us to complete
|
||||
the information you believe is incomplete.
|
||||
</li>
|
||||
<li>
|
||||
<strong>The right to erasure</strong> – You have the right to request
|
||||
that we erase your personal data, under certain conditions.
|
||||
<strong>The right to erasure</strong> – You have the right to request that we erase your
|
||||
personal data, under certain conditions.
|
||||
</li>
|
||||
<li>
|
||||
<strong>The right to restrict processing</strong> – You have the right
|
||||
to request that we restrict the processing of your personal data, under
|
||||
<strong>The right to restrict processing</strong> – You have the right to request that we
|
||||
restrict the processing of your personal data, under certain conditions.
|
||||
</li>
|
||||
<li>
|
||||
<strong>The right to data portability</strong> – You have the right to request that we
|
||||
transfer the data that we have collected to another organization, or directly to you, under
|
||||
certain conditions.
|
||||
</li>
|
||||
<li>
|
||||
<strong>The right to data portability</strong> – You have the right to
|
||||
request that we transfer the data that we have collected to another
|
||||
organization, or directly to you, under certain conditions.
|
||||
</li>
|
||||
<li>
|
||||
<strong>The right to object to processing</strong> – You have the right
|
||||
to object to our processing of your personal data, under certain
|
||||
conditions.
|
||||
<strong>The right to object to processing</strong> – You have the right to object to our
|
||||
processing of your personal data, under certain conditions.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you would like to exercise those rights, contact us at
|
||||
<a href="mailto:gdpr@modrinth.com">gdpr@modrinth.com</a>. We may ask you
|
||||
to verify your identity before proceeding and will respond to your request
|
||||
within 30 days as required by law, or notify you of an extended reply
|
||||
time.
|
||||
<a href="mailto:gdpr@modrinth.com">gdpr@modrinth.com</a>. We may ask you to verify your
|
||||
identity before proceeding and will respond to your request within 30 days as required by law,
|
||||
or notify you of an extended reply time.
|
||||
</p>
|
||||
|
||||
<h2>Children's Information</h2>
|
||||
<p>
|
||||
Another part of our priority is adding protection for children while using
|
||||
the Internet. We encourage parents and guardians to observe, participate
|
||||
in, and/or monitor and guide their online activity.
|
||||
Another part of our priority is adding protection for children while using the Internet. We
|
||||
encourage parents and guardians to observe, participate in, and/or monitor and guide their
|
||||
online activity.
|
||||
</p>
|
||||
<p>
|
||||
Modrinth does not knowingly collect any Personal Identifiable Information
|
||||
from children under the age of 13. If you think that your child provided
|
||||
this kind of information on our website, we strongly encourage you to
|
||||
contact us immediately and we will do our best efforts to promptly remove
|
||||
such information from our records.
|
||||
Modrinth does not knowingly collect any Personal Identifiable Information from children under
|
||||
the age of 13. If you think that your child provided this kind of information on our website,
|
||||
we strongly encourage you to contact us immediately and we will do our best efforts to
|
||||
promptly remove such information from our records.
|
||||
</p>
|
||||
|
||||
<h2>Online Privacy Policy Only</h2>
|
||||
<p>
|
||||
This Privacy Policy applies only to our online activities and is valid for
|
||||
visitors to our website with regards to the information that they shared
|
||||
and/or collect in Modrinth. This policy is not applicable to any
|
||||
information collected offline or via channels other than this website.
|
||||
This Privacy Policy applies only to our online activities and is valid for visitors to our
|
||||
website with regards to the information that they shared and/or collect in Modrinth. This
|
||||
policy is not applicable to any information collected offline or via channels other than this
|
||||
website.
|
||||
</p>
|
||||
|
||||
<h2>Consent</h2>
|
||||
<p>
|
||||
By using our website, you hereby consent to our Privacy Policy and agree
|
||||
to its Terms and Conditions.
|
||||
By using our website, you hereby consent to our Privacy Policy and agree to its Terms and
|
||||
Conditions.
|
||||
</p>
|
||||
|
||||
<h2>Changes to the Privacy Policy</h2>
|
||||
<p>
|
||||
We keep this privacy policy under regular review and place any updates on
|
||||
this web page. If we do this, we will post the changes on this page and
|
||||
update the "Last edited" date at the top of this page, after which such
|
||||
changes will become effective immediately. We will make an effort to keep
|
||||
users updated on any such changes, but because most changes do not affect
|
||||
how we process existing data, a notice will not be sent for all changes.
|
||||
We keep this privacy policy under regular review and place any updates on this web page. If we
|
||||
do this, we will post the changes on this page and update the "Last edited" date at the top of
|
||||
this page, after which such changes will become effective immediately. We will make an effort
|
||||
to keep users updated on any such changes, but because most changes do not affect how we
|
||||
process existing data, a notice will not be sent for all changes.
|
||||
</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>
|
||||
If you have any questions about this privacy policy or how we process your
|
||||
data, contact us at
|
||||
If you have any questions about this privacy policy or how we process your data, contact us at
|
||||
<a href="mailto:gdpr@modrinth.com">gdpr@modrinth.com</a> or write us at:
|
||||
</p>
|
||||
<p>
|
||||
@@ -236,8 +207,8 @@
|
||||
|
||||
<h3>How to contact the appropriate authority</h3>
|
||||
<p>
|
||||
Should you wish to fill a complaint or if you feel like we haven't
|
||||
addressed your concerns or request, you may contact the
|
||||
Should you wish to fill a complaint or if you feel like we haven't addressed your concerns or
|
||||
request, you may contact the
|
||||
<a href="https://ico.org.uk/">Information Commissioner's Office</a>
|
||||
using their online form or by writing at:
|
||||
</p>
|
||||
@@ -251,15 +222,14 @@
|
||||
United Kingdom
|
||||
</p>
|
||||
<p>
|
||||
You do not need to be a citizen of the United Kingdom to use this method
|
||||
of lodging complaints.
|
||||
You do not need to be a citizen of the United Kingdom to use this method of lodging
|
||||
complaints.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
export default defineNuxtComponent({
|
||||
head: {
|
||||
title: 'Privacy - Modrinth',
|
||||
meta: [
|
||||
@@ -282,11 +252,11 @@ export default {
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/legal/privacy`,
|
||||
content: 'https://modrinth.com/legal/privacy',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -4,165 +4,138 @@
|
||||
|
||||
<p>
|
||||
In order to facilitate Modrinth's
|
||||
<nuxt-link to="/legal/terms">Terms and Conditions</nuxt-link>, all Content
|
||||
must obey the following Rules. For more information on what exactly
|
||||
Content is, please refer to the Content section of the Terms.
|
||||
<nuxt-link to="/legal/terms"> Terms and Conditions </nuxt-link>, all Content must obey the
|
||||
following Rules. For more information on what exactly Content is, please refer to the Content
|
||||
section of the Terms.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Please note that these are general rules and will not be enforced "to the
|
||||
letter". We reserve the right to modify and/or remove any file, project,
|
||||
or other Content uploaded to our platform for any reason. We reserve the
|
||||
right to introduce new rules at any time, which may or may not
|
||||
retroactively apply to already uploaded Content at the discretion of our
|
||||
moderators.
|
||||
Please note that these are general rules and will not be enforced "to the letter". We reserve
|
||||
the right to modify and/or remove any file, project, or other Content uploaded to our platform
|
||||
for any reason. We reserve the right to introduce new rules at any time, which may or may not
|
||||
retroactively apply to already uploaded Content at the discretion of our moderators.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you find any violations of these Rules on our website, it is your
|
||||
responsibility to report it. You may use the Report button on any project,
|
||||
version, or user page, or you may email us at
|
||||
If you find any violations of these Rules on our website, it is your responsibility to report
|
||||
it. You may use the Report button on any project, version, or user page, or you may email us
|
||||
at
|
||||
<a href="mailto:support@modrinth.com">support@modrinth.com</a>.
|
||||
</p>
|
||||
|
||||
<h2 id="malicious-content">1. Malicious Content</h2>
|
||||
|
||||
<p>
|
||||
Content cannot contain or download malware, which we define as anything
|
||||
that is designed:
|
||||
</p>
|
||||
<p>Content cannot contain or download malware, which we define as anything that is designed:</p>
|
||||
<ul>
|
||||
<li>
|
||||
to upload any data to a remote server (i.e. one that the user does not
|
||||
directly choose to connect to in-game) without clear disclosure
|
||||
to upload any data to a remote server (i.e. one that the user does not directly choose to
|
||||
connect to in-game) without clear disclosure
|
||||
</li>
|
||||
<li>
|
||||
to disrupt, damage, or otherwise cause harm or damage to an individual,
|
||||
computer, or network
|
||||
to disrupt, damage, or otherwise cause harm or damage to an individual, computer, or network
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="clear-and-honest-function">2. Clear and honest function</h2>
|
||||
|
||||
<p>
|
||||
Content, especially projects, must make a clear and honest attempt to
|
||||
describe their purpose on the page(s) where it may be found.
|
||||
Content, especially projects, must make a clear and honest attempt to describe their purpose
|
||||
on the page(s) where it may be found.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Content must not make or share intentionally wrong or misleading claims.
|
||||
This includes but is not limited to claims regarding the Content itself,
|
||||
claims regarding other Content, and claims not relating to Content on
|
||||
Modrinth.
|
||||
Content must not make or share intentionally wrong or misleading claims. This includes but is
|
||||
not limited to claims regarding the Content itself, claims regarding other Content, and claims
|
||||
not relating to Content on Modrinth.
|
||||
</p>
|
||||
|
||||
<h3 id="general-expectations">2.1. General expectations</h3>
|
||||
|
||||
<p>
|
||||
Projects in particular must attempt to describe the following three things
|
||||
within their description:
|
||||
Projects in particular must attempt to describe the following three things within their
|
||||
description:
|
||||
</p>
|
||||
<ul>
|
||||
<li>what a project specifically does or adds</li>
|
||||
<li>why someone should want to download the project</li>
|
||||
<li>
|
||||
any other critical information the user must know before downloading
|
||||
</li>
|
||||
<li>any other critical information the user must know before downloading</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Project descriptions must also be accessible. For the most part, this
|
||||
means that descriptions cannot mostly consist of text within images, and
|
||||
necessary information cannot be obscured.
|
||||
Project descriptions must also be accessible. For the most part, this means that descriptions
|
||||
cannot mostly consist of text within images, and necessary information cannot be obscured.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Projects which don't meet of these expectations may be removed from search
|
||||
rather than removed from the platform altogether, at the moderators'
|
||||
discretion.
|
||||
Projects which don't meet of these expectations may be removed from search rather than removed
|
||||
from the platform altogether, at the moderators' discretion.
|
||||
</p>
|
||||
|
||||
<h2 id="cheats-and-hacks">3. Cheats and Hacks</h2>
|
||||
|
||||
<p>
|
||||
Projects cannot contain or download "cheats", which we define as a
|
||||
client-side modification that:
|
||||
Projects cannot contain or download "cheats", which we define as a client-side modification
|
||||
that:
|
||||
</p>
|
||||
<ul>
|
||||
<li>is advertised as a "cheat", "hack", or "hacked client"</li>
|
||||
<li>
|
||||
gives an unfair advantage in a multiplayer setting over other players
|
||||
that do not have a comparable modification and does not provide a
|
||||
server-side opt-out
|
||||
gives an unfair advantage in a multiplayer setting over other players that do not have a
|
||||
comparable modification and does not provide a server-side opt-out
|
||||
</li>
|
||||
<li>
|
||||
contains any of the following functions without requiring a server-side
|
||||
opt-in:
|
||||
contains any of the following functions without requiring a server-side opt-in:
|
||||
<ul>
|
||||
<li>X-ray or the ability to see through opaque blocks</li>
|
||||
<li>aim bot or aim assist</li>
|
||||
<li>flight, speed, or other movement modifications</li>
|
||||
<li>automatic PvP</li>
|
||||
<li>
|
||||
active client-side hiding of third party modifications that have
|
||||
server-side opt-outs
|
||||
active client-side hiding of third party modifications that have server-side opt-outs
|
||||
</li>
|
||||
<li>item duplication</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="copyright-and-legality-of-content">
|
||||
4. Copyright and legality of Content
|
||||
</h2>
|
||||
<h2 id="copyright-and-legality-of-content">4. Copyright and legality of Content</h2>
|
||||
|
||||
<p>
|
||||
You must own or have the necessary licenses, rights, consents, and
|
||||
permissions to store, share, or distribute the Content that is uploaded
|
||||
under your Modrinth account.
|
||||
You must own or have the necessary licenses, rights, consents, and permissions to store,
|
||||
share, or distribute the Content that is uploaded under your Modrinth account.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Content may not be directly "reuploaded" from another platform without the
|
||||
permission of the author or copyright holder, even with the appropriate
|
||||
licensing or other rights. This restriction does not apply to content
|
||||
within modpacks or to so called "forks" - that is, modified copies of a
|
||||
project which have diverged substantially enough from the original
|
||||
Content may not be directly "reuploaded" from another platform without the permission of the
|
||||
author or copyright holder, even with the appropriate licensing or other rights. This
|
||||
restriction does not apply to content within modpacks or to so called "forks" - that is,
|
||||
modified copies of a project which have diverged substantially enough from the original
|
||||
project, at the discretion of Modrinth's moderators.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Content must not infringe upon anyone's rights or intellectual property.
|
||||
</p>
|
||||
<p>Content must not infringe upon anyone's rights or intellectual property.</p>
|
||||
|
||||
<p>
|
||||
Content must abide by the laws which govern Rinth, Inc., i.e. those of the
|
||||
United States and of the State of Delaware.
|
||||
Content must abide by the laws which govern Rinth, Inc., i.e. those of the United States and
|
||||
of the State of Delaware.
|
||||
</p>
|
||||
|
||||
<h2 id="prohibited-content">5. Prohibited Content</h2>
|
||||
|
||||
<p>
|
||||
Content on Modrinth is meant to be appropriate for audiences 13 years of
|
||||
age and above.
|
||||
</p>
|
||||
<p>Content on Modrinth is meant to be appropriate for audiences 13 years of age and above.</p>
|
||||
|
||||
<p>This means that the following Content is not allowed:</p>
|
||||
<ul>
|
||||
<li>Content containing sexual or explicit material</li>
|
||||
<li>Content promoting or sharing harmful or hateful behavior</li>
|
||||
<li>
|
||||
Content themed around or containing real-life drugs or illicit
|
||||
substances
|
||||
</li>
|
||||
<li>Content themed around or containing real-life drugs or illicit substances</li>
|
||||
<li>Content with an excessive amount of profane language</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
export default defineNuxtComponent({
|
||||
head: {
|
||||
title: 'Rules - Modrinth',
|
||||
meta: [
|
||||
@@ -185,11 +158,11 @@ export default {
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/legal/rules`,
|
||||
content: 'https://modrinth.com/legal/rules',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<h1>Security Notice</h1>
|
||||
|
||||
<p>
|
||||
This is the security notice for all Modrinth repositories. The notice
|
||||
explains how vulnerabilities should be reported.
|
||||
This is the security notice for all Modrinth repositories. The notice explains how
|
||||
vulnerabilities should be reported.
|
||||
</p>
|
||||
<h2>Reporting a Vulnerability</h2>
|
||||
<p>
|
||||
If you've found a vulnerability, we would like to know so we can fix it
|
||||
before it is released publicly.
|
||||
If you've found a vulnerability, we would like to know so we can fix it before it is released
|
||||
publicly.
|
||||
<strong>Do not open a GitHub issue for a found vulnerability</strong>.
|
||||
</p>
|
||||
<p>
|
||||
@@ -17,15 +17,11 @@
|
||||
including:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
the website, page or repository where the vulnerability can be observed
|
||||
</li>
|
||||
<li>the website, page or repository where the vulnerability can be observed</li>
|
||||
<li>a brief description of the vulnerability</li>
|
||||
<li>
|
||||
optionally the type of vulnerability and any related
|
||||
<a
|
||||
href="https://www.owasp.org/index.php/Category:OWASP_Top_Ten_2017_Project"
|
||||
>
|
||||
<a href="https://www.owasp.org/index.php/Category:OWASP_Top_Ten_2017_Project">
|
||||
OWASP category
|
||||
</a>
|
||||
</li>
|
||||
@@ -36,17 +32,15 @@
|
||||
<p>The following vulnerabilities <strong>are not</strong> in scope:</p>
|
||||
<ul>
|
||||
<li>
|
||||
volumetric vulnerabilities, for example overwhelming a service with a
|
||||
high volume of requests
|
||||
volumetric vulnerabilities, for example overwhelming a service with a high volume of
|
||||
requests
|
||||
</li>
|
||||
<li>
|
||||
reports indicating that our services do not fully align with "best
|
||||
practice", for example missing security headers
|
||||
reports indicating that our services do not fully align with "best practice", for example
|
||||
missing security headers
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you aren't sure, you can still reach out via email or direct message.
|
||||
</p>
|
||||
<p>If you aren't sure, you can still reach out via email or direct message.</p>
|
||||
<hr />
|
||||
<p>
|
||||
This notice is inspired by the
|
||||
@@ -59,8 +53,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
export default defineNuxtComponent({
|
||||
head: {
|
||||
title: 'Security Notice - Modrinth',
|
||||
meta: [
|
||||
@@ -83,11 +76,11 @@ export default {
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/legal/security`,
|
||||
content: 'https://modrinth.com/legal/security',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -5,151 +5,132 @@
|
||||
<h2>1. Terms</h2>
|
||||
|
||||
<p>
|
||||
By accessing this Website, accessible from https://modrinth.com, you are
|
||||
agreeing to be bound by these Website Terms and Conditions of Use and
|
||||
agree that you are responsible for the agreement with any applicable local
|
||||
laws. If you disagree with any of these terms, you are prohibited from
|
||||
accessing this site. The materials contained in this Website are protected
|
||||
by copyright and trade mark law.
|
||||
By accessing this Website, accessible from https://modrinth.com, you are agreeing to be bound
|
||||
by these Website Terms and Conditions of Use and agree that you are responsible for the
|
||||
agreement with any applicable local laws. If you disagree with any of these terms, you are
|
||||
prohibited from accessing this site. The materials contained in this Website are protected by
|
||||
copyright and trade mark law.
|
||||
</p>
|
||||
|
||||
<h2>2. Use License</h2>
|
||||
|
||||
<p>
|
||||
Permission is granted to temporarily download one copy of the materials on
|
||||
Rinth, Inc.'s Website for personal, non-commercial transitory viewing
|
||||
only. This is the grant of a license, not a transfer of title, and under
|
||||
this license you may not:
|
||||
Permission is granted to temporarily download one copy of the materials on Rinth, Inc.'s
|
||||
Website for personal, non-commercial transitory viewing only. This is the grant of a license,
|
||||
not a transfer of title, and under this license you may not:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>modify or copy the materials;</li>
|
||||
<li>use the materials for any commercial purpose or for any public display;</li>
|
||||
<li>attempt to reverse engineer any software contained on Rinth, Inc.'s Website;</li>
|
||||
<li>remove any copyright or other proprietary notations from the materials; or</li>
|
||||
<li>
|
||||
use the materials for any commercial purpose or for any public display;
|
||||
</li>
|
||||
<li>
|
||||
attempt to reverse engineer any software contained on Rinth, Inc.'s
|
||||
Website;
|
||||
</li>
|
||||
<li>
|
||||
remove any copyright or other proprietary notations from the materials;
|
||||
or
|
||||
</li>
|
||||
<li>
|
||||
transferring the materials to another person or "mirror" the materials
|
||||
on any other server.
|
||||
transferring the materials to another person or "mirror" the materials on any other server.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
This will let Rinth, Inc. to terminate upon violations of any of these
|
||||
restrictions. Upon termination, your viewing right will also be terminated
|
||||
and you should destroy any downloaded materials in your possession whether
|
||||
it is printed or electronic format.
|
||||
This will let Rinth, Inc. to terminate upon violations of any of these restrictions. Upon
|
||||
termination, your viewing right will also be terminated and you should destroy any downloaded
|
||||
materials in your possession whether it is printed or electronic format.
|
||||
</p>
|
||||
|
||||
<h2>3. Disclaimer</h2>
|
||||
|
||||
<p>
|
||||
All the materials on Rinth, Inc.’s Website are provided "as is". Rinth,
|
||||
Inc. makes no warranties, may it be expressed or implied, therefore
|
||||
negates all other warranties. Furthermore, Rinth, Inc. does not make any
|
||||
representations concerning the accuracy or reliability of the use of the
|
||||
materials on its Website or otherwise relating to such materials or any
|
||||
sites linked to this Website.
|
||||
All the materials on Rinth, Inc.’s Website are provided "as is". Rinth, Inc. makes no
|
||||
warranties, may it be expressed or implied, therefore negates all other warranties.
|
||||
Furthermore, Rinth, Inc. does not make any representations concerning the accuracy or
|
||||
reliability of the use of the materials on its Website or otherwise relating to such materials
|
||||
or any sites linked to this Website.
|
||||
</p>
|
||||
|
||||
<h2>4. Limitations</h2>
|
||||
|
||||
<p>
|
||||
Rinth, Inc. or its suppliers will not be hold accountable for any damages
|
||||
that will arise with the use or inability to use the materials on Rinth,
|
||||
Inc.’s Website, even if Rinth, Inc. or an authorize representative of this
|
||||
Website has been notified, orally or written, of the possibility of such
|
||||
damage. Some jurisdiction does not allow limitations on implied warranties
|
||||
or limitations of liability for incidental damages, these limitations may
|
||||
not apply to you.
|
||||
Rinth, Inc. or its suppliers will not be hold accountable for any damages that will arise with
|
||||
the use or inability to use the materials on Rinth, Inc.’s Website, even if Rinth, Inc. or an
|
||||
authorize representative of this Website has been notified, orally or written, of the
|
||||
possibility of such damage. Some jurisdiction does not allow limitations on implied warranties
|
||||
or limitations of liability for incidental damages, these limitations may not apply to you.
|
||||
</p>
|
||||
|
||||
<h2>5. Revisions and Errata</h2>
|
||||
|
||||
<p>
|
||||
The materials appearing on Rinth, Inc.’s Website may include technical,
|
||||
typographical, or photographic errors. Rinth, Inc. will not promise that
|
||||
any of the materials in this Website are accurate, complete, or current.
|
||||
Rinth, Inc. may change the materials contained on its Website at any time
|
||||
without notice. Rinth, Inc. does not make any commitment to update the
|
||||
The materials appearing on Rinth, Inc.’s Website may include technical, typographical, or
|
||||
photographic errors. Rinth, Inc. will not promise that any of the materials in this Website
|
||||
are accurate, complete, or current. Rinth, Inc. may change the materials contained on its
|
||||
Website at any time without notice. Rinth, Inc. does not make any commitment to update the
|
||||
materials.
|
||||
</p>
|
||||
|
||||
<h2>6. Links</h2>
|
||||
|
||||
<p>
|
||||
Rinth, Inc. has not reviewed all of the sites linked to its Website and is
|
||||
not responsible for the contents of any such linked site. The presence of
|
||||
any link does not imply endorsement by Rinth, Inc. of the site. The use of
|
||||
any linked website is at the user’s own risk.
|
||||
Rinth, Inc. has not reviewed all of the sites linked to its Website and is not responsible for
|
||||
the contents of any such linked site. The presence of any link does not imply endorsement by
|
||||
Rinth, Inc. of the site. The use of any linked website is at the user’s own risk.
|
||||
</p>
|
||||
|
||||
<h2>7. Site Terms of Use Modifications</h2>
|
||||
|
||||
<p>
|
||||
Rinth, Inc. may revise these Terms of Use for its Website at any time
|
||||
without prior notice. By using this Website, you are agreeing to be bound
|
||||
by the current version of these Terms and Conditions of Use.
|
||||
Rinth, Inc. may revise these Terms of Use for its Website at any time without prior notice. By
|
||||
using this Website, you are agreeing to be bound by the current version of these Terms and
|
||||
Conditions of Use.
|
||||
</p>
|
||||
|
||||
<h2>8. Your Privacy</h2>
|
||||
|
||||
<p>
|
||||
Please read our
|
||||
<nuxt-link to="/legal/privacy"> Privacy Policy</nuxt-link>.
|
||||
<nuxt-link to="/legal/privacy"> Privacy Policy </nuxt-link>.
|
||||
</p>
|
||||
|
||||
<h2>9. Governing Law</h2>
|
||||
|
||||
<p>
|
||||
Any claim related to Rinth, Inc.'s Website shall be governed by the laws
|
||||
of us without regards to its conflict of law provisions.
|
||||
Any claim related to Rinth, Inc.'s Website shall be governed by the laws of us without regards
|
||||
to its conflict of law provisions.
|
||||
</p>
|
||||
|
||||
<h2>10. Content</h2>
|
||||
|
||||
<p>
|
||||
When you upload text, software, mods, scripts, graphics, photos, audio,
|
||||
videos, links, interactive features and other materials that may be viewed
|
||||
on or accessed through Modrinth, we refer to it as "Content".
|
||||
When you upload text, software, mods, scripts, graphics, photos, audio, videos, links,
|
||||
interactive features and other materials that may be viewed on or accessed through Modrinth,
|
||||
we refer to it as "Content".
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
You are responsible for all activity and Content that is uploaded under
|
||||
your Modrinth account.
|
||||
You are responsible for all activity and Content that is uploaded under your Modrinth
|
||||
account.
|
||||
</li>
|
||||
<li>
|
||||
You retain all of your ownership rights to your Content. We do not claim
|
||||
any ownership in or to any of your Content.
|
||||
You retain all of your ownership rights to your Content. We do not claim any ownership in or
|
||||
to any of your Content.
|
||||
</li>
|
||||
<li>
|
||||
To enable us to provide the services of Modrinth, you hereby grant us a
|
||||
worldwide, non-exclusive, royalty-free, and unrestricted license to use,
|
||||
reproduce, distribute copies, prepare derivative works of, or display
|
||||
Content in connection with Modrinth in any medium and for any purpose
|
||||
(including commercial purposes).
|
||||
To enable us to provide the services of Modrinth, you hereby grant us a worldwide,
|
||||
non-exclusive, royalty-free, and unrestricted license to use, reproduce, distribute copies,
|
||||
prepare derivative works of, or display Content in connection with Modrinth in any medium
|
||||
and for any purpose (including commercial purposes).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
All Content on Modrinth must obey the
|
||||
<nuxt-link to="/legal/rules">Content Rules</nuxt-link>. Please be aware of
|
||||
these Rules before uploading any Content to Modrinth.
|
||||
<nuxt-link to="/legal/rules"> Content Rules </nuxt-link>. Please be aware of these Rules
|
||||
before uploading any Content to Modrinth.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
auth: false,
|
||||
export default defineNuxtComponent({
|
||||
head: {
|
||||
title: 'Terms - Modrinth',
|
||||
meta: [
|
||||
@@ -172,11 +153,11 @@ export default {
|
||||
{
|
||||
hid: 'og:url',
|
||||
name: 'og:url',
|
||||
content: `https://modrinth.com/legal/terms`,
|
||||
content: 'https://modrinth.com/legal/terms',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -11,22 +11,21 @@
|
||||
<aside class="universal-card">
|
||||
<h1>Moderation</h1>
|
||||
<NavStack>
|
||||
<NavStackItem link="" label="All"> </NavStackItem>
|
||||
<NavStackItem link="/moderation" label="All" />
|
||||
<NavStackItem
|
||||
v-for="type in moderationTypes"
|
||||
:key="type"
|
||||
:link="'?type=' + type"
|
||||
:link="'/moderation/' + type"
|
||||
:label="$formatProjectType(type) + 's'"
|
||||
>
|
||||
</NavStackItem>
|
||||
/>
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<div class="project-list display-mode--list">
|
||||
<ProjectCard
|
||||
v-for="project in $route.query.type !== undefined
|
||||
? projects.filter((x) => x.project_type === $route.query.type)
|
||||
v-for="project in $route.params.type !== undefined
|
||||
? projects.filter((x) => x.project_type === $route.params.type)
|
||||
: projects"
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
@@ -47,56 +46,40 @@
|
||||
@click="
|
||||
setProjectStatus(
|
||||
project,
|
||||
project.requested_status
|
||||
? project.requested_status
|
||||
: 'approved'
|
||||
project.requested_status ? project.requested_status : 'approved'
|
||||
)
|
||||
"
|
||||
>
|
||||
<CheckIcon />
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="setProjectStatus(project, 'withheld')"
|
||||
>
|
||||
<button class="iconified-button" @click="setProjectStatus(project, 'withheld')">
|
||||
<UnlistIcon />
|
||||
Withhold
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="setProjectStatus(project, 'rejected')"
|
||||
>
|
||||
<button class="iconified-button" @click="setProjectStatus(project, 'rejected')">
|
||||
<CrossIcon />
|
||||
Reject
|
||||
</button>
|
||||
</ProjectCard>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
$route.query.type === 'report' || $route.query.type === undefined
|
||||
"
|
||||
v-if="$route.params.type === 'report' || $route.params.type === undefined"
|
||||
class="reports"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in reports"
|
||||
:key="index"
|
||||
class="card report"
|
||||
>
|
||||
<div v-for="(item, index) in reports" :key="index" class="card report">
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
<h3>
|
||||
{{ item.item_type }}
|
||||
<nuxt-link :to="item.url">{{ item.item_id }}</nuxt-link>
|
||||
<nuxt-link :to="item.url">
|
||||
{{ item.item_id }}
|
||||
</nuxt-link>
|
||||
</h3>
|
||||
reported by
|
||||
<a :href="`/user/${item.reporter}`">{{ item.reporter }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-highlightjs
|
||||
class="markdown-body"
|
||||
v-html="$xss($md.render(item.body))"
|
||||
/>
|
||||
<div class="markdown-body" v-html="renderHighlightedString(item.body)" />
|
||||
<Badge :type="`Marked as ${item.report_type}`" color="orange" />
|
||||
</div>
|
||||
<div class="actions">
|
||||
@@ -104,21 +87,17 @@
|
||||
<TrashIcon /> Delete report
|
||||
</button>
|
||||
<span
|
||||
v-tooltip="
|
||||
$dayjs(item.created).format(
|
||||
'[Created at] YYYY-MM-DD [at] HH:mm A'
|
||||
)
|
||||
"
|
||||
v-tooltip="$dayjs(item.created).format('[Created at] YYYY-MM-DD [at] HH:mm A')"
|
||||
class="stat"
|
||||
>
|
||||
<CalendarIcon />
|
||||
Created {{ $dayjs(item.created).fromNow() }}
|
||||
Created {{ fromNow(item.created) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="reports.length === 0 && projects.length === 0" class="error">
|
||||
<Security class="icon"></Security>
|
||||
<Security class="icon" />
|
||||
<br />
|
||||
<span class="text">You are up-to-date!</span>
|
||||
</div>
|
||||
@@ -131,18 +110,18 @@
|
||||
import ProjectCard from '~/components/ui/ProjectCard'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import UnlistIcon from '~/assets/images/utils/eye-off.svg?inline'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import Security from '~/assets/images/illustrations/security.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||
import UnlistIcon from '~/assets/images/utils/eye-off.svg'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg'
|
||||
import Security from '~/assets/images/illustrations/security.svg'
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
import ModalModeration from '~/components/ui/ModalModeration'
|
||||
import { renderHighlightedString } from '~/helpers/highlight'
|
||||
|
||||
export default {
|
||||
name: 'Moderation',
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
ModalModeration,
|
||||
NavStack,
|
||||
@@ -156,13 +135,17 @@ export default {
|
||||
TrashIcon,
|
||||
CalendarIcon,
|
||||
},
|
||||
async asyncData(data) {
|
||||
const [projects, reports] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`moderation/projects`, data.$defaultHeaders()),
|
||||
data.$axios.get(`report`, data.$defaultHeaders()),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
async setup() {
|
||||
const data = useNuxtApp()
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
const [projects, reports] = await Promise.all([
|
||||
useBaseFetch('moderation/projects', data.$defaultHeaders()),
|
||||
useBaseFetch('report', data.$defaultHeaders()),
|
||||
])
|
||||
|
||||
const newReports = await Promise.all(
|
||||
reports.map(async (report) => {
|
||||
@@ -173,48 +156,26 @@ export default {
|
||||
let url = ''
|
||||
|
||||
if (report.item_type === 'user') {
|
||||
const user = (
|
||||
await data.$axios.get(
|
||||
`user/${report.item_id}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
).data
|
||||
const user = await useBaseFetch(`user/${report.item_id}`, data.$defaultHeaders())
|
||||
url = `/user/${user.username}`
|
||||
report.item_id = user.username
|
||||
} else if (report.item_type === 'project') {
|
||||
const project = (
|
||||
await data.$axios.get(
|
||||
`project/${report.item_id}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
).data
|
||||
const project = await useBaseFetch(`project/${report.item_id}`, data.$defaultHeaders())
|
||||
report.item_id = project.slug || report.item_id
|
||||
url = `/${project.project_type}/${report.item_id}`
|
||||
} else if (report.item_type === 'version') {
|
||||
const version = (
|
||||
await data.$axios.get(
|
||||
`version/${report.item_id}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
).data
|
||||
const project = (
|
||||
await data.$axios.get(
|
||||
`project/${version.project_id}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
).data
|
||||
const version = await useBaseFetch(`version/${report.item_id}`, data.$defaultHeaders())
|
||||
const project = await useBaseFetch(
|
||||
`project/${version.project_id}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
report.item_id = version.version_number || report.item_id
|
||||
url = `/${project.project_type}/${
|
||||
project.slug || project.id
|
||||
}/version/${report.item_id}`
|
||||
url = `/${project.project_type}/${project.slug || project.id}/version/${report.item_id}`
|
||||
}
|
||||
|
||||
report.reporter = (
|
||||
await data.$axios.get(
|
||||
`user/${report.reporter}`,
|
||||
data.$defaultHeaders()
|
||||
)
|
||||
).data.username
|
||||
await useBaseFetch(`user/${report.reporter}`, data.$defaultHeaders())
|
||||
).username
|
||||
|
||||
return {
|
||||
...report,
|
||||
@@ -232,8 +193,8 @@ export default {
|
||||
)
|
||||
|
||||
return {
|
||||
projects,
|
||||
reports: newReports,
|
||||
projects: shallowRef(projects),
|
||||
reports: ref(newReports),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -261,6 +222,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
renderHighlightedString,
|
||||
setProjectStatus(project, status) {
|
||||
this.currentProject = project
|
||||
this.currentStatus = status
|
||||
@@ -275,28 +237,28 @@ export default {
|
||||
this.currentProject = null
|
||||
},
|
||||
async deleteReport(index) {
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`report/${this.reports[index].id}`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
|
||||
await useBaseFetch(`report/${this.reports[index].id}`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
this.reports.splice(index, 1)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
1
pages/moderation/[type].vue
Normal file
1
pages/moderation/[type].vue
Normal file
@@ -0,0 +1 @@
|
||||
<template><div /></template>
|
||||
@@ -4,24 +4,20 @@
|
||||
<aside class="universal-card">
|
||||
<h1>Notifications</h1>
|
||||
<NavStack>
|
||||
<NavStackItem link="" label="All"> </NavStackItem>
|
||||
<NavStackItem link="/notifications" label="All" :uses-query="true" />
|
||||
<NavStackItem
|
||||
v-for="type in notificationTypes"
|
||||
:key="type"
|
||||
:link="'?type=' + type"
|
||||
:link="'/notifications/' + type"
|
||||
:label="NOTIFICATION_TYPES[type]"
|
||||
>
|
||||
</NavStackItem>
|
||||
:uses-query="true"
|
||||
/>
|
||||
<h3>Manage</h3>
|
||||
<NavStackItem
|
||||
link="/settings/follows"
|
||||
label="Followed projects"
|
||||
chevron
|
||||
>
|
||||
<NavStackItem link="/settings/follows" label="Followed projects" chevron>
|
||||
<SettingsIcon />
|
||||
</NavStackItem>
|
||||
<NavStackItem
|
||||
v-if="$user.notifications.length > 0"
|
||||
v-if="user.notifications.length > 0"
|
||||
:action="clearNotifications"
|
||||
label="Clear all"
|
||||
danger
|
||||
@@ -34,30 +30,26 @@
|
||||
<div class="normal-page__content">
|
||||
<div class="notifications">
|
||||
<div
|
||||
v-for="notification in $route.query.type !== undefined
|
||||
? $user.notifications.filter((x) => x.type === $route.query.type)
|
||||
: $user.notifications"
|
||||
v-for="notification in $route.params.type !== undefined
|
||||
? user.notifications.filter((x) => x.type === $route.params.type)
|
||||
: user.notifications"
|
||||
:key="notification.id"
|
||||
class="universal-card adjacent-input"
|
||||
>
|
||||
<div class="label">
|
||||
<span class="label__title">
|
||||
<nuxt-link :to="notification.link">
|
||||
<h3 v-html="$xss($md.render(notification.title))" />
|
||||
<h3 v-html="renderString(notification.title)" />
|
||||
</nuxt-link>
|
||||
</span>
|
||||
<div class="label__description">
|
||||
<p>{{ notification.text }}</p>
|
||||
<span
|
||||
v-tooltip="
|
||||
$dayjs(notification.created).format(
|
||||
'MMMM D, YYYY [at] h:mm:ss A'
|
||||
)
|
||||
"
|
||||
v-tooltip="$dayjs(notification.created).format('MMMM D, YYYY [at] h:mm:ss A')"
|
||||
class="date"
|
||||
>
|
||||
<CalendarIcon />
|
||||
Received {{ $dayjs(notification.created).fromNow() }}</span
|
||||
Received {{ fromNow(notification.created) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,12 +58,8 @@
|
||||
v-for="(action, actionIndex) in notification.actions"
|
||||
:key="actionIndex"
|
||||
class="iconified-button"
|
||||
:class="`action-button-${action.title
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')}`"
|
||||
@click="
|
||||
performAction(notification, notificationIndex, actionIndex)
|
||||
"
|
||||
:class="`action-button-${action.title.toLowerCase().replaceAll(' ', '-')}`"
|
||||
@click="performAction(notification, notificationIndex, actionIndex)"
|
||||
>
|
||||
{{ action.title }}
|
||||
</button>
|
||||
@@ -84,8 +72,8 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$user.notifications.length === 0" class="error">
|
||||
<UpToDate class="icon"></UpToDate>
|
||||
<div v-if="user.notifications.length === 0" class="error">
|
||||
<UpToDate class="icon" />
|
||||
<br />
|
||||
<span class="text">You are up-to-date!</span>
|
||||
</div>
|
||||
@@ -95,14 +83,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClearIcon from '~/assets/images/utils/clear.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?inline'
|
||||
import ClearIcon from '~/assets/images/utils/clear.svg'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||
import CalendarIcon from '~/assets/images/utils/calendar.svg'
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg'
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
export default {
|
||||
name: 'Notifications',
|
||||
import { renderString } from '~/helpers/parse'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
NavStack,
|
||||
NavStackItem,
|
||||
@@ -111,8 +100,17 @@ export default {
|
||||
CalendarIcon,
|
||||
UpToDate,
|
||||
},
|
||||
async fetch() {
|
||||
await this.$store.dispatch('user/fetchNotifications')
|
||||
async setup() {
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
const user = await useUser()
|
||||
if (process.client) {
|
||||
await initUserNotifs()
|
||||
}
|
||||
|
||||
return { user: ref(user) }
|
||||
},
|
||||
head: {
|
||||
title: 'Notifications - Modrinth',
|
||||
@@ -121,9 +119,7 @@ export default {
|
||||
notificationTypes() {
|
||||
const obj = {}
|
||||
|
||||
for (const notification of this.$user.notifications.filter(
|
||||
(it) => it.type !== null
|
||||
)) {
|
||||
for (const notification of this.user.notifications.filter((it) => it.type !== null)) {
|
||||
obj[notification.type] = true
|
||||
}
|
||||
|
||||
@@ -138,58 +134,56 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
renderString,
|
||||
async clearNotifications() {
|
||||
try {
|
||||
const ids = this.$user.notifications.map((x) => x.id)
|
||||
const ids = this.user.notifications.map((x) => x.id)
|
||||
|
||||
await this.$axios.delete(
|
||||
`notifications?ids=${JSON.stringify(ids)}`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
|
||||
ids.forEach((x) => this.$store.dispatch('user/deleteNotification', x))
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
await useBaseFetch(`notifications?ids=${JSON.stringify(ids)}`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
}
|
||||
},
|
||||
async performAction(notification, notificationIndex, actionIndex) {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`notification/${notification.id}`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
|
||||
await this.$store.dispatch('user/deleteNotification', notification.id)
|
||||
|
||||
if (actionIndex !== null) {
|
||||
const config = {
|
||||
method:
|
||||
notification.actions[actionIndex].action_route[0].toLowerCase(),
|
||||
url: `${notification.actions[actionIndex].action_route[1]}`,
|
||||
headers: {
|
||||
Authorization: this.$auth.token,
|
||||
},
|
||||
}
|
||||
await this.$axios(config)
|
||||
for (const id of ids) {
|
||||
await userDeleteNotification(id)
|
||||
}
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async performAction(notification, _notificationIndex, actionIndex) {
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch(`notification/${notification.id}`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
|
||||
await userDeleteNotification(notification.id)
|
||||
|
||||
if (actionIndex !== null) {
|
||||
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
|
||||
method: notification.actions[actionIndex].action_route[0].toUpperCase(),
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -201,7 +195,7 @@ export default {
|
||||
align-items: baseline;
|
||||
margin-block-start: 0;
|
||||
|
||||
h3 ::v-deep {
|
||||
:deep(h3) {
|
||||
margin: 0;
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
1
pages/notifications/[type].vue
Normal file
1
pages/notifications/[type].vue
Normal file
@@ -0,0 +1 @@
|
||||
<template><div /></template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Datapacks',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Modpacks',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Mods',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Plugins',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ResourcePacks',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Shaders',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -23,31 +23,18 @@
|
||||
</aside>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<NuxtChild />
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import NavStack from '~/components/ui/NavStack'
|
||||
import NavStackItem from '~/components/ui/NavStackItem'
|
||||
|
||||
import PaintbrushIcon from '~/assets/images/utils/paintbrush.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import CurrencyIcon from '~/assets/images/utils/currency.svg?inline'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
components: {
|
||||
NavStack,
|
||||
NavStackItem,
|
||||
PaintbrushIcon,
|
||||
UserIcon,
|
||||
HeartIcon,
|
||||
CurrencyIcon,
|
||||
},
|
||||
}
|
||||
import PaintbrushIcon from '~/assets/images/utils/paintbrush.svg'
|
||||
import UserIcon from '~/assets/images/utils/user.svg'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg'
|
||||
import CurrencyIcon from '~/assets/images/utils/currency.svg'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
title="Are you sure you want to delete your account?"
|
||||
description="This will **immediately delete all of your user data and follows**. This will not delete your projects. Deleting your account cannot be reversed.<br><br>If you need help with your account, get support on the [Modrinth Discord](https://discord.gg/EUHuJHt)."
|
||||
proceed-label="Delete this account"
|
||||
:confirmation-text="$auth.user.username"
|
||||
:confirmation-text="auth.user.username"
|
||||
:has-to-type="true"
|
||||
@proceed="deleteAccount"
|
||||
/>
|
||||
@@ -13,18 +13,14 @@
|
||||
<Modal ref="modal_revoke_token" header="Revoke your Modrinth token">
|
||||
<div class="modal-revoke-token markdown-body">
|
||||
<p>
|
||||
Revoking your Modrinth token can have unintended consequences. Please
|
||||
be aware that the following could break:
|
||||
Revoking your Modrinth token can have unintended consequences. Please be aware that the
|
||||
following could break:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Any application that uses your token to access the API.</li>
|
||||
<li>Gradle - if Minotaur is given a incorrect token, your Gradle builds could fail.</li>
|
||||
<li>
|
||||
Gradle - if Minotaur is given a incorrect token, your Gradle builds
|
||||
could fail.
|
||||
</li>
|
||||
<li>
|
||||
GitHub - if you use a GitHub action that uses the Modrinth API, it
|
||||
will cause errors.
|
||||
GitHub - if you use a GitHub action that uses the Modrinth API, it will cause errors.
|
||||
</li>
|
||||
</ul>
|
||||
<p>If you are willing to continue, complete the following steps:</p>
|
||||
@@ -33,32 +29,23 @@
|
||||
<a
|
||||
href="https://github.com/settings/connections/applications/3acffb2e808d16d4b226"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
rel="noopener"
|
||||
>
|
||||
Head to the Modrinth Application page on GitHub.
|
||||
</a>
|
||||
Make sure to be logged into the GitHub account you used for
|
||||
Modrinth!
|
||||
</li>
|
||||
<li>
|
||||
Press the big red "Revoke Access" button next to the "Permissions"
|
||||
header.
|
||||
Make sure to be logged into the GitHub account you used for Modrinth!
|
||||
</li>
|
||||
<li>Press the big red "Revoke Access" button next to the "Permissions" header.</li>
|
||||
</ol>
|
||||
<p>
|
||||
Once you have completed those steps, press the continue button below.
|
||||
</p>
|
||||
<p>Once you have completed those steps, press the continue button below.</p>
|
||||
<p>
|
||||
<strong>
|
||||
This will log you out of Modrinth, however, when you log back in,
|
||||
your token will be regenerated.
|
||||
This will log you out of Modrinth, however, when you log back in, your token will be
|
||||
regenerated.
|
||||
</strong>
|
||||
</p>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_revoke_token.hide()"
|
||||
>
|
||||
<button class="iconified-button" @click="$refs.modal_revoke_token.hide()">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
@@ -73,7 +60,7 @@
|
||||
<section class="universal-card">
|
||||
<h2>User profile</h2>
|
||||
<p>Visit your user profile to edit your profile information.</p>
|
||||
<NuxtLink class="iconified-button" :to="`/user/${$auth.user.username}`">
|
||||
<NuxtLink class="iconified-button" :to="`/user/${auth.user.username}`">
|
||||
<UserIcon /> Visit your profile
|
||||
</NuxtLink>
|
||||
</section>
|
||||
@@ -83,13 +70,11 @@
|
||||
<p>Your account information is not displayed publicly.</p>
|
||||
<ul class="known-errors">
|
||||
<li v-if="hasMonetizationEnabled() && !email">
|
||||
You must have an email address set since you are enrolled in the
|
||||
Creator Monetization Program.
|
||||
You must have an email address set since you are enrolled in the Creator Monetization
|
||||
Program.
|
||||
</li>
|
||||
</ul>
|
||||
<label for="email-input"
|
||||
><span class="label__title">Email address</span>
|
||||
</label>
|
||||
<label for="email-input"><span class="label__title">Email address</span> </label>
|
||||
<input
|
||||
id="email-input"
|
||||
v-model="email"
|
||||
@@ -113,28 +98,18 @@
|
||||
<section class="universal-card">
|
||||
<h2>Authorization token</h2>
|
||||
<p>
|
||||
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!
|
||||
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!
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button"
|
||||
value="Copy to clipboard"
|
||||
@click="copyToken"
|
||||
>
|
||||
<button type="button" class="iconified-button" value="Copy to clipboard" @click="copyToken">
|
||||
<template v-if="copied">
|
||||
<CheckIcon />
|
||||
Copied token to clipboard
|
||||
</template>
|
||||
<template v-else><CopyIcon />Copy token to clipboard</template>
|
||||
<template v-else> <CopyIcon />Copy token to clipboard </template>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_revoke_token.show()"
|
||||
>
|
||||
<button type="button" class="iconified-button" @click="$refs.modal_revoke_token.show()">
|
||||
<SlashIcon />
|
||||
Revoke token
|
||||
</button>
|
||||
@@ -144,9 +119,8 @@
|
||||
<section id="delete-account" class="universal-card">
|
||||
<h2>Delete account</h2>
|
||||
<p>
|
||||
Once you delete your account, there is no going back. Deleting your
|
||||
account will remove all attached data, excluding projects, from our
|
||||
servers.
|
||||
Once you delete your account, there is no going back. Deleting your account will remove all
|
||||
attached data, excluding projects, from our servers.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
@@ -164,16 +138,16 @@
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm'
|
||||
import Modal from '~/components/ui/Modal'
|
||||
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import CopyIcon from '~/assets/images/utils/clipboard-copy.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import SlashIcon from '~/assets/images/utils/slash.svg?inline'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||
import UserIcon from '~/assets/images/utils/user.svg'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import CopyIcon from '~/assets/images/utils/clipboard-copy.svg'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||
import SlashIcon from '~/assets/images/utils/slash.svg'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Modal,
|
||||
ModalConfirm,
|
||||
@@ -186,10 +160,19 @@ export default {
|
||||
TrashIcon,
|
||||
SlashIcon,
|
||||
},
|
||||
async setup() {
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
return { auth }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
copied: false,
|
||||
email: this.$auth.user.email,
|
||||
email: this.auth.user.email,
|
||||
showKnownErrors: false,
|
||||
}
|
||||
},
|
||||
@@ -199,43 +182,41 @@ export default {
|
||||
methods: {
|
||||
async copyToken() {
|
||||
this.copied = true
|
||||
await navigator.clipboard.writeText(this.$auth.token)
|
||||
await navigator.clipboard.writeText(this.auth.token)
|
||||
},
|
||||
async deleteAccount() {
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
try {
|
||||
await this.$axios.delete(
|
||||
`user/${this.$auth.user.id}`,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await useBaseFetch(`user/${this.auth.user.id}`, {
|
||||
method: 'DELETE',
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
this.$cookies.set('auth-token-reset', true)
|
||||
alert(
|
||||
'Please note that logging back in with GitHub will create a new account.'
|
||||
)
|
||||
useCookie('auth-token').value = null
|
||||
alert('Please note that logging back in with GitHub will create a new account.')
|
||||
window.location.href = '/'
|
||||
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
},
|
||||
logout() {
|
||||
this.$refs.modal_revoke_token.hide()
|
||||
this.$cookies.set('auth-token-reset', true)
|
||||
useCookie('auth-token').value = null
|
||||
|
||||
window.location.href = `${this.$axios.defaults.baseURL}auth/init?url=${process.env.domain}`
|
||||
window.location.href = getAuthUrl()
|
||||
},
|
||||
hasMonetizationEnabled() {
|
||||
return (
|
||||
this.$auth.user.payout_data.payout_wallet &&
|
||||
this.$auth.user.payout_data.payout_wallet_type &&
|
||||
this.$auth.user.payout_data.payout_address
|
||||
this.auth.user.payout_data.payout_wallet &&
|
||||
this.auth.user.payout_data.payout_wallet_type &&
|
||||
this.auth.user.payout_data.payout_address
|
||||
)
|
||||
},
|
||||
async saveChanges() {
|
||||
@@ -243,32 +224,30 @@ export default {
|
||||
this.showKnownErrors = true
|
||||
return
|
||||
}
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
try {
|
||||
const data = {
|
||||
email: this.email ? this.email : null,
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}`,
|
||||
data,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
await useBaseFetch(`user/${this.auth.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await useAuth(this.auth.token)
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.modal-revoke-token {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="$user.follows.length > 0" class="project-list display-mode--list">
|
||||
<div v-if="user.follows.length > 0" class="project-list display-mode--list">
|
||||
<ProjectCard
|
||||
v-for="project in $user.follows"
|
||||
v-for="project in user.follows"
|
||||
:id="project.id"
|
||||
:key="project.id"
|
||||
:type="project.project_type"
|
||||
@@ -16,10 +16,7 @@
|
||||
:server-side="project.server_side"
|
||||
:color="project.color"
|
||||
>
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="$store.dispatch('user/unfollowProject', project)"
|
||||
>
|
||||
<button class="iconified-button" @click="userUnfollowProject(project)">
|
||||
<HeartIcon />
|
||||
Unfollow
|
||||
</button>
|
||||
@@ -30,30 +27,25 @@
|
||||
<br />
|
||||
<span class="text"
|
||||
>You don't have any followed projects. <br />
|
||||
Why don't you <nuxt-link class="link" to="/mods">search</nuxt-link> for
|
||||
new ones?</span
|
||||
Why don't you <nuxt-link class="link" to="/mods">search</nuxt-link> for new ones?</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import ProjectCard from '~/components/ui/ProjectCard'
|
||||
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import FollowIllustration from '~/assets/images/illustrations/follow_illustration.svg?inline'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg'
|
||||
import FollowIllustration from '~/assets/images/illustrations/follow_illustration.svg'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProjectCard,
|
||||
HeartIcon,
|
||||
FollowIllustration,
|
||||
},
|
||||
async fetch() {
|
||||
await this.$store.dispatch('user/fetchFollows')
|
||||
},
|
||||
head: {
|
||||
title: 'Followed projects - Modrinth',
|
||||
},
|
||||
const user = await useUser()
|
||||
if (process.client) {
|
||||
await initUserFollows()
|
||||
}
|
||||
|
||||
useHead({ title: 'Followed projects - Modrinth' })
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -5,57 +5,54 @@
|
||||
<div class="adjacent-input">
|
||||
<label for="theme-selector">
|
||||
<span class="label__title">Color theme</span>
|
||||
<span class="label__description"
|
||||
>Change the global site color theme.</span
|
||||
>
|
||||
<span class="label__description">Change the global site color theme.</span>
|
||||
</label>
|
||||
<Multiselect
|
||||
id="theme-selector"
|
||||
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"
|
||||
/>
|
||||
<div>
|
||||
<Multiselect
|
||||
id="theme-selector"
|
||||
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"
|
||||
@update:model-value="(value) => updateTheme(value, true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input small">
|
||||
<label for="search-layout-toggle">
|
||||
<span class="label__title">Search sidebar on the right</span>
|
||||
<span class="label__description"
|
||||
>Enabling this will put the search page's filters sidebar on the
|
||||
right side.</span
|
||||
>Enabling this will put the search page's filters sidebar on the right side.</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="search-layout-toggle"
|
||||
v-model="searchLayout"
|
||||
v-model="$cosmetics.searchLayout"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmeticSettings"
|
||||
@change="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<label for="project-layout-toggle">
|
||||
<span class="label__title">Project sidebar on the right</span>
|
||||
<span class="label__description"
|
||||
>Enabling this will put the project pages' info sidebars on the
|
||||
right side.</span
|
||||
>Enabling this will put the project pages' info sidebars on the right side.</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="project-layout-toggle"
|
||||
v-model="projectLayout"
|
||||
v-model="$cosmetics.projectLayout"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmeticSettings"
|
||||
@change="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -74,14 +71,14 @@
|
||||
</label>
|
||||
<Multiselect
|
||||
:id="projectType + '-search-display-mode'"
|
||||
:value="searchDisplayMode[projectType.id]"
|
||||
v-model="$cosmetics.searchDisplayMode[projectType.id]"
|
||||
:options="$tag.projectViewModes"
|
||||
:custom-label="$capitalizeString"
|
||||
:searchable="false"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@input="(value) => setSearchDisplayMode(projectType.id, value)"
|
||||
@update:model-value="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -91,49 +88,46 @@
|
||||
<label for="advanced-rendering">
|
||||
<span class="label__title">Advanced rendering</span>
|
||||
<span class="label__description"
|
||||
>Enables advanced rendering such as blur effects that may cause
|
||||
performance issues without hardware-accelerated rendering.</span
|
||||
>Enables advanced rendering such as blur effects that may cause performance issues
|
||||
without hardware-accelerated rendering.</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="advanced-rendering"
|
||||
v-model="advancedRendering"
|
||||
v-model="$cosmetics.advancedRendering"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmeticSettings"
|
||||
@change="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<label for="modpacks-alpha-notice">
|
||||
<span class="label__title">Modpacks alpha notice</span>
|
||||
<span class="label__description"
|
||||
>Shows a banner stating that modpacks are in alpha.</span
|
||||
>
|
||||
<span class="label__description">Shows a banner stating that modpacks are in alpha.</span>
|
||||
</label>
|
||||
<input
|
||||
id="modpacks-alpha-notice"
|
||||
v-model="modpacksAlphaNotice"
|
||||
v-model="$cosmetics.modpacksAlphaNotice"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmeticSettings"
|
||||
@change="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input small">
|
||||
<label for="external-links-new-tab">
|
||||
<span class="label__title">Open external links in new tab</span>
|
||||
<span class="label__description">
|
||||
Make links which go outside of Modrinth open in a new tab. No matter
|
||||
this setting, links on the same domain and in Markdown descriptions
|
||||
will open in the same tab, and links on ads and edit pages will open
|
||||
in a new tab.
|
||||
Make links which go outside of Modrinth open in a new tab. No matter this setting, links
|
||||
on the same domain and in Markdown descriptions will open in the same tab, and links on
|
||||
ads and edit pages will open in a new tab.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="external-links-new-tab"
|
||||
v-model="externalLinksNewTab"
|
||||
v-model="$cosmetics.externalLinksNewTab"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="saveCosmeticSettings"
|
||||
@change="saveCosmetics"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -143,37 +137,15 @@
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Multiselect,
|
||||
},
|
||||
auth: false,
|
||||
data() {
|
||||
return {
|
||||
searchLayout: false,
|
||||
projectLayout: false,
|
||||
modpacksAlphaNotice: true,
|
||||
advancedRendering: true,
|
||||
externalLinksNewTab: true,
|
||||
searchDisplayMode: {
|
||||
mod: 'list',
|
||||
plugin: 'list',
|
||||
resourcepack: 'gallery',
|
||||
modpack: 'list',
|
||||
shader: 'gallery',
|
||||
datapack: 'list',
|
||||
user: 'list',
|
||||
},
|
||||
searchDisplayMode: this.$cosmetics.searchDisplayMode,
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.searchLayout = this.$store.state.cosmetics.searchLayout
|
||||
this.projectLayout = this.$store.state.cosmetics.projectLayout
|
||||
this.modpacksAlphaNotice = this.$store.state.cosmetics.modpacksAlphaNotice
|
||||
this.advancedRendering = this.$store.state.cosmetics.advancedRendering
|
||||
this.externalLinksNewTab = this.$store.state.cosmetics.externalLinksNewTab
|
||||
this.searchDisplayMode = this.$store.state.cosmetics.searchDisplayMode
|
||||
},
|
||||
head: {
|
||||
title: 'Display settings - Modrinth',
|
||||
},
|
||||
@@ -183,10 +155,7 @@ export default {
|
||||
return {
|
||||
id: type.id,
|
||||
name: this.$formatProjectType(type.id) + ' search',
|
||||
display:
|
||||
'the ' +
|
||||
this.$formatProjectType(type.id).toLowerCase() +
|
||||
's search page',
|
||||
display: 'the ' + this.$formatProjectType(type.id).toLowerCase() + 's search page',
|
||||
}
|
||||
})
|
||||
types.push({
|
||||
@@ -197,40 +166,6 @@ export default {
|
||||
return types
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async saveCosmeticSettings() {
|
||||
await this.$store.dispatch('cosmetics/save', {
|
||||
searchLayout: this.searchLayout,
|
||||
projectLayout: this.projectLayout,
|
||||
modpacksAlphaNotice: this.modpacksAlphaNotice,
|
||||
advancedRendering: this.advancedRendering,
|
||||
externalLinksNewTab: this.externalLinksNewTab,
|
||||
searchDisplayMode: this.searchDisplayMode,
|
||||
$cookies: this.$cookies,
|
||||
})
|
||||
},
|
||||
async setSearchDisplayMode(projectType, value) {
|
||||
await this.$store.dispatch('cosmetics/saveSearchDisplayMode', {
|
||||
projectType,
|
||||
mode: value,
|
||||
$cookies: this.$cookies,
|
||||
})
|
||||
this.searchDisplayMode = this.$store.state.cosmetics.searchDisplayMode
|
||||
},
|
||||
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>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -9,11 +9,10 @@
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2 class="title">Enrollment</h2>
|
||||
<template v-if="!enrolled && !$auth.user.email">
|
||||
<template v-if="!enrolled && !auth.user.email">
|
||||
<p v-if="!enrolled">
|
||||
You are not currently enrolled in Modrinth's Creator Monetization
|
||||
Program. In order to enroll, you must first add a valid email address
|
||||
to your account.
|
||||
You are not currently enrolled in Modrinth's Creator Monetization Program. In order to
|
||||
enroll, you must first add a valid email address to your account.
|
||||
</p>
|
||||
<NuxtLink class="iconified-button" to="/settings/account">
|
||||
<SettingsIcon /> Visit account settings
|
||||
@@ -21,9 +20,8 @@
|
||||
</template>
|
||||
<template v-else-if="editing || !enrolled">
|
||||
<p v-if="!enrolled">
|
||||
You are not currently enrolled in Modrinth's Creator Monetization
|
||||
Program. Setup a method of receiving payments below to enable
|
||||
monetization.
|
||||
You are not currently enrolled in Modrinth's Creator Monetization Program. Setup a method
|
||||
of receiving payments below to enable monetization.
|
||||
</p>
|
||||
<div class="enroll universal-body">
|
||||
<Chips
|
||||
@@ -31,13 +29,13 @@
|
||||
:starting-value="selectedWallet"
|
||||
:items="wallets"
|
||||
:format-label="$formatWallet"
|
||||
@input="onChangeWallet()"
|
||||
@update:model-value="onChangeWallet()"
|
||||
/>
|
||||
|
||||
<p>
|
||||
Enter the information for the
|
||||
{{ $formatWallet(selectedWallet) }} account you would like to
|
||||
receive your revenue from the Creator Monetization Program:
|
||||
{{ $formatWallet(selectedWallet) }} account you would like to receive your revenue from
|
||||
the Creator Monetization Program:
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<Multiselect
|
||||
@@ -52,26 +50,20 @@
|
||||
|
||||
<label class="hidden" for="account-input"
|
||||
>{{ $formatWallet(selectedWallet) }}
|
||||
{{ formatAccountType(accountType).toLowerCase() }} input
|
||||
field</label
|
||||
{{ formatAccountType(accountType).toLowerCase() }} input field</label
|
||||
>
|
||||
<input
|
||||
id="account-input"
|
||||
v-model="account"
|
||||
:placeholder="`Enter your ${$formatWallet(
|
||||
selectedWallet
|
||||
)} ${formatAccountType(accountType).toLowerCase()}...`"
|
||||
:placeholder="`Enter your ${$formatWallet(selectedWallet)} ${formatAccountType(
|
||||
accountType
|
||||
).toLowerCase()}...`"
|
||||
:type="accountType === 'email' ? 'email' : ''"
|
||||
/>
|
||||
<span v-if="accountType === 'phone'">
|
||||
Format: +18888888888 or +1-888-888-8888
|
||||
</span>
|
||||
<span v-if="accountType === 'phone'"> Format: +18888888888 or +1-888-888-8888 </span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="updatePayoutData(false)"
|
||||
>
|
||||
<button class="iconified-button brand-button" @click="updatePayoutData(false)">
|
||||
<SaveIcon /> Save information
|
||||
</button>
|
||||
<button
|
||||
@@ -100,13 +92,13 @@
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import Chips from '~/components/ui/Chips'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import ChartIcon from '~/assets/images/utils/chart.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import TrashIcon from '~/assets/images/utils/trash.svg'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg'
|
||||
import ChartIcon from '~/assets/images/utils/chart.svg'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||
|
||||
export default {
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Multiselect,
|
||||
Chips,
|
||||
@@ -116,19 +108,24 @@ export default {
|
||||
ChartIcon,
|
||||
SettingsIcon,
|
||||
},
|
||||
async setup() {
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
const auth = await useAuth()
|
||||
return { auth }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
enrolled:
|
||||
this.$auth.user.payout_data.payout_wallet &&
|
||||
this.$auth.user.payout_data.payout_wallet_type &&
|
||||
this.$auth.user.payout_data.payout_address,
|
||||
this.auth.user.payout_data.payout_wallet &&
|
||||
this.auth.user.payout_data.payout_wallet_type &&
|
||||
this.auth.user.payout_data.payout_address,
|
||||
wallets: ['paypal', 'venmo'],
|
||||
selectedWallet: this.$auth.user.payout_data.payout_wallet ?? 'paypal',
|
||||
accountType:
|
||||
this.$auth.user.payout_data.payout_wallet_type ??
|
||||
this.getAccountTypes()[0],
|
||||
account: this.$auth.user.payout_data.payout_address ?? '',
|
||||
selectedWallet: this.auth.user.payout_data.payout_wallet ?? 'paypal',
|
||||
accountType: this.auth.user.payout_data.payout_wallet_type ?? this.getAccountTypes()[0],
|
||||
account: this.auth.user.payout_data.payout_address ?? '',
|
||||
}
|
||||
},
|
||||
head: {
|
||||
@@ -167,7 +164,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async updatePayoutData(unenroll) {
|
||||
this.$nuxt.$loading.start()
|
||||
startLoading()
|
||||
if (unenroll) {
|
||||
this.selectedWallet = 'paypal'
|
||||
this.accountType = this.getAccountTypes()[0]
|
||||
@@ -184,14 +181,12 @@ export default {
|
||||
},
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}`,
|
||||
data,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
await useBaseFetch(`user/${this.auth.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await useAuth(this.auth.token)
|
||||
|
||||
this.editing = false
|
||||
this.enrolled = !unenroll
|
||||
@@ -199,13 +194,13 @@ export default {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
stopLoading()
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
531
pages/user/[id].vue
Normal file
531
pages/user/[id].vue
Normal file
@@ -0,0 +1,531 @@
|
||||
<template>
|
||||
<div v-if="user">
|
||||
<Head>
|
||||
<Title>{{ user.username + ' - Modrinth' }}</Title>
|
||||
<Meta name="og:title" :content="user.username" />
|
||||
<Meta name="description" :content="metaDescription" />
|
||||
<Meta name="og:type" content="website" />
|
||||
<Meta name="apple-mobile-web-app-title" :content="metaDescription" />
|
||||
<Meta name="og:description" :content="metaDescription" />
|
||||
<Meta
|
||||
name="og:image"
|
||||
:content="user.avatar_url ? user.avatar_url : 'https://cdn.modrinth.com/placeholder.png'"
|
||||
/>
|
||||
</Head>
|
||||
<ModalCreation ref="modal_creation" />
|
||||
<ModalReport ref="modal_report" :item-id="user.id" item-type="user" />
|
||||
<div class="user-header-wrapper">
|
||||
<div class="user-header">
|
||||
<Avatar
|
||||
:src="previewImage ? previewImage : user.avatar_url"
|
||||
size="md"
|
||||
circle
|
||||
:alt="user.username"
|
||||
/>
|
||||
<h1 class="username">
|
||||
{{ user.username }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<div class="card sidebar">
|
||||
<h1 class="mobile-username">
|
||||
{{ user.username }}
|
||||
</h1>
|
||||
<div class="card__overlay">
|
||||
<FileInput
|
||||
v-if="isEditing"
|
||||
:max-size="262144"
|
||||
:show-icon="true"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image iconified-button"
|
||||
prompt="Upload avatar"
|
||||
@change="showPreviewImage"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
<button
|
||||
v-else-if="$auth.user && $auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
@click="isEditing = true"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
v-else-if="$auth.user"
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_report.show()"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
<a v-else class="iconified-button" :href="getAuthUrl()" rel="noopener nofollow">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</a>
|
||||
</div>
|
||||
<template v-if="isEditing">
|
||||
<div class="inputs universal-labels">
|
||||
<label for="user-username"><span class="label__title">Username</span></label>
|
||||
<input id="user-username" v-model="user.username" maxlength="39" type="text" />
|
||||
<label for="user-bio"><span class="label__title">Bio</span></label>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea id="user-bio" v-model="user.bio" maxlength="160" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
() => {
|
||||
isEditing = false
|
||||
user = JSON.parse(JSON.stringify($auth.user))
|
||||
previewImage = null
|
||||
icon = null
|
||||
}
|
||||
"
|
||||
>
|
||||
<CrossIcon /> Cancel
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="saveChanges">
|
||||
<SaveIcon /> Save
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="sidebar__item">
|
||||
<Badge v-if="$tag.staffRoles.includes(user.role)" :type="user.role" />
|
||||
<Badge v-else-if="projects.length > 0" type="creator" />
|
||||
</div>
|
||||
<span v-if="user.bio" class="sidebar__item bio">{{ user.bio }}</span>
|
||||
<hr class="card-divider" />
|
||||
<div class="primary-stat">
|
||||
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">{{ sumDownloads }}</span>
|
||||
downloads
|
||||
</div>
|
||||
</div>
|
||||
<div class="primary-stat">
|
||||
<HeartIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">{{ sumFollows }}</span>
|
||||
followers of projects
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<SunriseIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span
|
||||
v-tooltip="$dayjs(user.created).format('MMMM D, YYYY [at] h:mm:ss A')"
|
||||
class="secondary-stat__text date"
|
||||
>
|
||||
Joined {{ fromNow(user.created) }}
|
||||
</span>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<UserIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span class="secondary-stat__text"> User ID: <CopyCode :text="user.id" /> </span>
|
||||
</div>
|
||||
<a
|
||||
v-if="githubUrl"
|
||||
:href="githubUrl"
|
||||
:target="$external()"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="sidebar__item github-button iconified-button"
|
||||
>
|
||||
<GitHubIcon aria-hidden="true" />
|
||||
View GitHub profile
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<Promotion />
|
||||
<nav class="navigation-card">
|
||||
<NavRow
|
||||
:links="[
|
||||
{
|
||||
label: 'all',
|
||||
href: `/user/${user.username}`,
|
||||
},
|
||||
...projectTypes.map((x) => {
|
||||
return {
|
||||
label: $formatProjectType(x) + 's',
|
||||
href: `/user/${user.username}/${x}s`,
|
||||
}
|
||||
}),
|
||||
]"
|
||||
/>
|
||||
<div class="input-group">
|
||||
<NuxtLink
|
||||
v-if="$auth.user && $auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
to="/dashboard/projects"
|
||||
>
|
||||
<SettingsIcon />
|
||||
Manage projects
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-tooltip="$capitalizeString($cosmetics.searchDisplayMode.user) + ' view'"
|
||||
:aria-label="$capitalizeString($cosmetics.searchDisplayMode.user) + ' view'"
|
||||
class="square-button"
|
||||
@click="cycleSearchDisplayMode()"
|
||||
>
|
||||
<GridIcon v-if="$cosmetics.searchDisplayMode.user === 'grid'" />
|
||||
<ImageIcon v-else-if="$cosmetics.searchDisplayMode.user === 'gallery'" />
|
||||
<ListIcon v-else />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div
|
||||
v-if="projects.length > 0"
|
||||
class="project-list"
|
||||
:class="'display-mode--' + $cosmetics.searchDisplayMode.user"
|
||||
>
|
||||
<ProjectCard
|
||||
v-for="project in ($route.params.projectType !== undefined
|
||||
? projects.filter(
|
||||
(x) =>
|
||||
x.project_type ===
|
||||
$route.params.projectType.substr(0, $route.params.projectType.length - 1)
|
||||
)
|
||||
: projects
|
||||
)
|
||||
.slice()
|
||||
.sort((a, b) => b.downloads - a.downloads)"
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
:name="project.title"
|
||||
:display="$cosmetics.searchDisplayMode.user"
|
||||
:featured-image="
|
||||
project.gallery
|
||||
.slice()
|
||||
.sort((a, b) => b.featured - a.featured)
|
||||
.map((x) => x.url)[0]
|
||||
"
|
||||
:description="project.description"
|
||||
:created-at="project.published"
|
||||
:updated-at="project.updated"
|
||||
:downloads="project.downloads.toString()"
|
||||
:follows="project.followers.toString()"
|
||||
:icon-url="project.icon_url"
|
||||
:categories="project.categories"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:status="
|
||||
$auth.user && ($auth.user.id === user.id || $tag.staffRoles.includes($auth.user.role))
|
||||
? project.status
|
||||
: null
|
||||
"
|
||||
:type="project.project_type"
|
||||
:color="project.color"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<UpToDate class="icon" /><br />
|
||||
<span v-if="$auth.user && $auth.user.id === user.id" class="text">
|
||||
You don't have any projects.<br />
|
||||
Would you like to
|
||||
<a class="link" @click.prevent="$refs.modal_creation.show()"> create one</a>?
|
||||
</span>
|
||||
<span v-else class="text">This user has no projects!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import ProjectCard from '~/components/ui/ProjectCard'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
import Promotion from '~/components/ads/Promotion'
|
||||
|
||||
import GitHubIcon from '~/assets/images/utils/github.svg'
|
||||
import ReportIcon from '~/assets/images/utils/report.svg'
|
||||
import SunriseIcon from '~/assets/images/utils/sunrise.svg'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg'
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg'
|
||||
import UserIcon from '~/assets/images/utils/user.svg'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import GridIcon from '~/assets/images/utils/grid.svg'
|
||||
import ListIcon from '~/assets/images/utils/list.svg'
|
||||
import ImageIcon from '~/assets/images/utils/image.svg'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
import ModalReport from '~/components/ui/ModalReport'
|
||||
import ModalCreation from '~/components/ui/ModalCreation'
|
||||
import NavRow from '~/components/ui/NavRow'
|
||||
import CopyCode from '~/components/ui/CopyCode'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
|
||||
const data = useNuxtApp()
|
||||
const route = useRoute()
|
||||
|
||||
let user, projects
|
||||
try {
|
||||
;[{ data: user }, { data: projects }] = await Promise.all([
|
||||
useAsyncData(`user/${route.params.id}`, () =>
|
||||
useBaseFetch(`user/${route.params.id}`, data.$defaultHeaders())
|
||||
),
|
||||
useAsyncData(
|
||||
`user/${route.params.id}/projects`,
|
||||
() => useBaseFetch(`user/${route.params.id}/projects`, data.$defaultHeaders()),
|
||||
{
|
||||
transform: (projects) => {
|
||||
for (const project of projects) {
|
||||
project.categories = project.categories.concat(project.loaders)
|
||||
project.project_type = data.$getProjectTypeForUrl(
|
||||
project.project_type,
|
||||
project.categories
|
||||
)
|
||||
}
|
||||
|
||||
return projects
|
||||
},
|
||||
}
|
||||
),
|
||||
])
|
||||
} catch {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: 'User not found',
|
||||
})
|
||||
}
|
||||
|
||||
if (!user.value) {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: 'User not found',
|
||||
})
|
||||
}
|
||||
|
||||
let githubUrl
|
||||
try {
|
||||
const githubUser = await $fetch(`https://api.github.com/user/` + user.value.github_id)
|
||||
githubUrl = ref(githubUser.html_url)
|
||||
} catch {}
|
||||
|
||||
if (user.value.username !== route.params.id) {
|
||||
await navigateTo(`/user/${user.value.username}`, { redirectCode: 301 })
|
||||
}
|
||||
|
||||
const metaDescription = ref(
|
||||
user.value.bio
|
||||
? `${user.value.bio} - Download ${user.value.username}'s projects on Modrinth`
|
||||
: `Download ${user.value.username}'s projects on Modrinth`
|
||||
)
|
||||
|
||||
const projectTypes = computed(() => {
|
||||
const obj = {}
|
||||
|
||||
for (const project of projects.value) {
|
||||
obj[project.project_type] = true
|
||||
}
|
||||
|
||||
return Object.keys(obj)
|
||||
})
|
||||
const sumDownloads = computed(() => {
|
||||
let sum = 0
|
||||
|
||||
for (const project of projects.value) {
|
||||
sum += project.downloads
|
||||
}
|
||||
|
||||
return data.$formatNumber(sum)
|
||||
})
|
||||
const sumFollows = computed(() => {
|
||||
let sum = 0
|
||||
|
||||
for (const project of projects.value) {
|
||||
sum += project.followers
|
||||
}
|
||||
|
||||
return data.$formatNumber(sum)
|
||||
})
|
||||
|
||||
const isEditing = ref(false)
|
||||
const icon = shallowRef(null)
|
||||
const previewImage = shallowRef(null)
|
||||
|
||||
function showPreviewImage(files) {
|
||||
const reader = new FileReader()
|
||||
icon.value = files[0]
|
||||
reader.readAsDataURL(icon.value)
|
||||
reader.onload = (event) => {
|
||||
previewImage.value = event.target.result
|
||||
}
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
startLoading()
|
||||
try {
|
||||
if (icon.value) {
|
||||
await useBaseFetch(
|
||||
`user/${data.$auth.user.id}/icon?ext=${
|
||||
icon.value.type.split('/')[icon.value.type.split('/').length - 1]
|
||||
}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: icon.value,
|
||||
...data.$defaultHeaders(),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const reqData = {
|
||||
email: user.value.email,
|
||||
bio: user.value.bio,
|
||||
}
|
||||
if (user.value.username !== data.$auth.user.username) {
|
||||
reqData.username = user.value.username
|
||||
}
|
||||
|
||||
await useBaseFetch(`user/${data.$auth.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: reqData,
|
||||
...data.$defaultHeaders(),
|
||||
})
|
||||
await useAuth(data.$auth.token)
|
||||
|
||||
isEditing.value = false
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
data.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
function cycleSearchDisplayMode() {
|
||||
data.$cosmetics.searchDisplayMode.user = data.$cycleValue(
|
||||
data.$cosmetics.searchDisplayMode.user,
|
||||
data.$tag.projectViewModes
|
||||
)
|
||||
saveCosmetics()
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
export default defineNuxtComponent({
|
||||
methods: {},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-header-wrapper {
|
||||
display: flex;
|
||||
margin: 0 auto -1.5rem;
|
||||
max-width: 80rem;
|
||||
|
||||
.user-header {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-username {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 501px) {
|
||||
.mobile-username {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-header-wrapper .user-header .username {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding-top: 2.5rem;
|
||||
}
|
||||
|
||||
.sidebar__item:not(:last-child) {
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.profile-picture {
|
||||
border-radius: var(--size-rounded-lg);
|
||||
height: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.bio {
|
||||
display: block;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.secondary-stat {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.secondary-stat__icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.secondary-stat__text {
|
||||
margin-left: 0.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.date {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.github-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
input {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group:first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
height: 10rem;
|
||||
}
|
||||
</style>
|
||||
1
pages/user/[id]/[projectType].vue
Normal file
1
pages/user/[id]/[projectType].vue
Normal file
@@ -0,0 +1 @@
|
||||
<template><div /></template>
|
||||
@@ -1,637 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<ModalCreation ref="modal_creation" />
|
||||
<ModalReport ref="modal_report" :item-id="user.id" item-type="user" />
|
||||
<div class="user-header-wrapper">
|
||||
<div class="user-header">
|
||||
<Avatar
|
||||
:src="previewImage ? previewImage : user.avatar_url"
|
||||
size="md"
|
||||
circle
|
||||
:alt="user.username"
|
||||
/>
|
||||
<h1 class="username">{{ user.username }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page">
|
||||
<div class="normal-page__sidebar">
|
||||
<div class="card sidebar">
|
||||
<h1 class="mobile-username">{{ user.username }}</h1>
|
||||
<div class="card__overlay">
|
||||
<FileInput
|
||||
v-if="isEditing"
|
||||
:max-size="262144"
|
||||
:show-icon="true"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="choose-image iconified-button"
|
||||
prompt="Upload avatar"
|
||||
@change="showPreviewImage"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
<button
|
||||
v-else-if="$auth.user && $auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
@click="isEditing = true"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
v-else-if="$auth.user"
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_report.show()"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
<a
|
||||
v-else
|
||||
class="iconified-button"
|
||||
:href="authUrl"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</a>
|
||||
</div>
|
||||
<template v-if="isEditing">
|
||||
<div class="inputs universal-labels">
|
||||
<label for="user-username"
|
||||
><span class="label__title">Username</span></label
|
||||
>
|
||||
<input
|
||||
id="user-username"
|
||||
v-model="user.username"
|
||||
maxlength="39"
|
||||
type="text"
|
||||
/>
|
||||
<label for="user-bio"
|
||||
><span class="label__title">Bio</span></label
|
||||
>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea
|
||||
id="user-bio"
|
||||
v-model="user.bio"
|
||||
maxlength="160"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="iconified-button"
|
||||
@click="
|
||||
isEditing = false
|
||||
user = JSON.parse(JSON.stringify($auth.user))
|
||||
previewImage = null
|
||||
icon = null
|
||||
"
|
||||
>
|
||||
<CrossIcon /> Cancel
|
||||
</button>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="saveChanges"
|
||||
>
|
||||
<SaveIcon /> Save
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="sidebar__item">
|
||||
<Badge
|
||||
v-if="$tag.staffRoles.includes(user.role)"
|
||||
:type="user.role"
|
||||
/>
|
||||
<Badge v-else-if="projects.length > 0" type="creator" />
|
||||
</div>
|
||||
<span v-if="user.bio" class="sidebar__item bio">{{
|
||||
user.bio
|
||||
}}</span>
|
||||
<hr class="card-divider" />
|
||||
<div class="primary-stat">
|
||||
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">{{ sumDownloads() }}</span>
|
||||
downloads
|
||||
</div>
|
||||
</div>
|
||||
<div class="primary-stat">
|
||||
<HeartIcon class="primary-stat__icon" aria-hidden="true" />
|
||||
<div class="primary-stat__text">
|
||||
<span class="primary-stat__counter">{{ sumFollows() }}</span>
|
||||
followers of projects
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<SunriseIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span
|
||||
v-tooltip="
|
||||
$dayjs(user.created).format('MMMM D, YYYY [at] h:mm:ss A')
|
||||
"
|
||||
class="secondary-stat__text date"
|
||||
>
|
||||
Joined {{ $dayjs(user.created).fromNow() }}
|
||||
</span>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
<div class="stats-block__item secondary-stat">
|
||||
<UserIcon class="secondary-stat__icon" aria-hidden="true" />
|
||||
<span class="secondary-stat__text">
|
||||
User ID: <CopyCode :text="user.id" />
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
v-if="githubUrl"
|
||||
:href="githubUrl"
|
||||
:target="$external()"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="sidebar__item github-button iconified-button"
|
||||
>
|
||||
<GitHubIcon aria-hidden="true" />
|
||||
View GitHub profile
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<Advertisement type="banner" small-screen="square" />
|
||||
<nav class="navigation-card">
|
||||
<NavRow
|
||||
query="type"
|
||||
:links="[
|
||||
{
|
||||
label: 'all',
|
||||
href: '',
|
||||
},
|
||||
...projectTypes.map((x) => {
|
||||
return {
|
||||
label: $formatProjectType(x) + 's',
|
||||
href: x,
|
||||
}
|
||||
}),
|
||||
]"
|
||||
/>
|
||||
<div class="input-group">
|
||||
<NuxtLink
|
||||
v-if="$auth.user && $auth.user.id === user.id"
|
||||
class="iconified-button"
|
||||
to="/dashboard/projects"
|
||||
>
|
||||
<SettingsIcon />
|
||||
Manage projects
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-tooltip="
|
||||
$capitalizeString($cosmetics.searchDisplayMode.user) + ' view'
|
||||
"
|
||||
:aria-label="
|
||||
$capitalizeString($cosmetics.searchDisplayMode.user) + ' view'
|
||||
"
|
||||
class="square-button"
|
||||
@click="cycleSearchDisplayMode()"
|
||||
>
|
||||
<GridIcon v-if="$cosmetics.searchDisplayMode.user === 'grid'" />
|
||||
<ImageIcon
|
||||
v-else-if="$cosmetics.searchDisplayMode.user === 'gallery'"
|
||||
/>
|
||||
<ListIcon v-else />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div
|
||||
v-if="projects.length > 0"
|
||||
class="project-list"
|
||||
:class="'display-mode--' + $cosmetics.searchDisplayMode.user"
|
||||
>
|
||||
<ProjectCard
|
||||
v-for="project in ($route.query.type !== undefined
|
||||
? projects.filter((x) => x.project_type === $route.query.type)
|
||||
: projects
|
||||
)
|
||||
.slice()
|
||||
.sort((a, b) => b.downloads - a.downloads)"
|
||||
:id="project.slug || project.id"
|
||||
:key="project.id"
|
||||
:name="project.title"
|
||||
:display="$cosmetics.searchDisplayMode.user"
|
||||
:featured-image="
|
||||
project.gallery
|
||||
.slice()
|
||||
.sort((a, b) => b.featured - a.featured)
|
||||
.map((x) => x.url)[0]
|
||||
"
|
||||
:description="project.description"
|
||||
:created-at="project.published"
|
||||
:updated-at="project.updated"
|
||||
:downloads="project.downloads.toString()"
|
||||
:follows="project.followers.toString()"
|
||||
:icon-url="project.icon_url"
|
||||
:categories="project.categories"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:status="
|
||||
$auth.user &&
|
||||
($auth.user.id === user.id ||
|
||||
$tag.staffRoles.includes($auth.user.role))
|
||||
? project.status
|
||||
: null
|
||||
"
|
||||
:type="project.project_type"
|
||||
:color="project.color"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<UpToDate class="icon" /><br />
|
||||
<span v-if="$auth.user && $auth.user.id === user.id" class="text">
|
||||
You don't have any projects.<br />
|
||||
Would you like to
|
||||
<a class="link" @click.prevent="$refs.modal_creation.show()">
|
||||
create one</a
|
||||
>?
|
||||
</span>
|
||||
<span v-else class="text">This user has no projects!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProjectCard from '~/components/ui/ProjectCard'
|
||||
import Badge from '~/components/ui/Badge'
|
||||
import Advertisement from '~/components/ads/Advertisement'
|
||||
|
||||
import GitHubIcon from '~/assets/images/utils/github.svg?inline'
|
||||
import ReportIcon from '~/assets/images/utils/report.svg?inline'
|
||||
import SunriseIcon from '~/assets/images/utils/sunrise.svg?inline'
|
||||
import DownloadIcon from '~/assets/images/utils/download.svg?inline'
|
||||
import SettingsIcon from '~/assets/images/utils/settings.svg?inline'
|
||||
import UpToDate from '~/assets/images/illustrations/up_to_date.svg?inline'
|
||||
import UserIcon from '~/assets/images/utils/user.svg?inline'
|
||||
import EditIcon from '~/assets/images/utils/edit.svg?inline'
|
||||
import HeartIcon from '~/assets/images/utils/heart.svg?inline'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg?inline'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg?inline'
|
||||
import GridIcon from '~/assets/images/utils/grid.svg?inline'
|
||||
import ListIcon from '~/assets/images/utils/list.svg?inline'
|
||||
import ImageIcon from '~/assets/images/utils/image.svg?inline'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg?inline'
|
||||
import FileInput from '~/components/ui/FileInput'
|
||||
import ModalReport from '~/components/ui/ModalReport'
|
||||
import ModalCreation from '~/components/ui/ModalCreation'
|
||||
import NavRow from '~/components/ui/NavRow'
|
||||
import CopyCode from '~/components/ui/CopyCode'
|
||||
import Avatar from '~/components/ui/Avatar'
|
||||
|
||||
export default {
|
||||
auth: false,
|
||||
components: {
|
||||
Avatar,
|
||||
CopyCode,
|
||||
NavRow,
|
||||
ModalCreation,
|
||||
ModalReport,
|
||||
FileInput,
|
||||
ProjectCard,
|
||||
SunriseIcon,
|
||||
DownloadIcon,
|
||||
GitHubIcon,
|
||||
ReportIcon,
|
||||
Badge,
|
||||
SettingsIcon,
|
||||
UpToDate,
|
||||
UserIcon,
|
||||
EditIcon,
|
||||
Advertisement,
|
||||
HeartIcon,
|
||||
CrossIcon,
|
||||
SaveIcon,
|
||||
GridIcon,
|
||||
ListIcon,
|
||||
ImageIcon,
|
||||
UploadIcon,
|
||||
},
|
||||
async asyncData(data) {
|
||||
try {
|
||||
const [user, projects] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`user/${data.params.id}`, data.$defaultHeaders()),
|
||||
data.$axios.get(
|
||||
`user/${data.params.id}/projects`,
|
||||
data.$defaultHeaders()
|
||||
),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
|
||||
if (user.username !== data.params.id) {
|
||||
data.redirect(301, `/user/${user.username}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let gitHubUser = {}
|
||||
let versions = []
|
||||
try {
|
||||
const [gitHubUserData, versionsData] = (
|
||||
await Promise.all([
|
||||
data.$axios.get(`https://api.github.com/user/` + user.github_id),
|
||||
data.$axios.get(
|
||||
`versions?ids=${JSON.stringify(
|
||||
[].concat.apply(
|
||||
[],
|
||||
projects.map((x) => x.versions)
|
||||
)
|
||||
)}`
|
||||
),
|
||||
])
|
||||
).map((it) => it.data)
|
||||
gitHubUser = gitHubUserData
|
||||
versions = versionsData
|
||||
} catch {}
|
||||
|
||||
for (const version of versions) {
|
||||
const projectIndex = projects.findIndex(
|
||||
(x) => x.id === version.project_id
|
||||
)
|
||||
if (projects[projectIndex].loaders) {
|
||||
for (const loader of version.loaders) {
|
||||
if (!projects[projectIndex].loaders.includes(loader)) {
|
||||
projects[projectIndex].loaders.push(loader)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
projects[projectIndex].loaders = version.loaders
|
||||
}
|
||||
}
|
||||
for (const project of projects) {
|
||||
project.categories = project.categories.concat(project.loaders)
|
||||
project.project_type = data.$getProjectTypeForUrl(
|
||||
project.project_type,
|
||||
project.categories
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
projects,
|
||||
githubUrl: gitHubUser.html_url,
|
||||
}
|
||||
} catch {
|
||||
data.error({
|
||||
statusCode: 404,
|
||||
message: 'User not found',
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
icon: null,
|
||||
previewImage: null,
|
||||
}
|
||||
},
|
||||
head() {
|
||||
const description = this.user.bio
|
||||
? `${this.user.bio} - Download ${this.user.username}'s projects on Modrinth`
|
||||
: `Download ${this.user.username}'s projects on Modrinth`
|
||||
|
||||
return {
|
||||
title: this.user.username + ' - Modrinth',
|
||||
meta: [
|
||||
{
|
||||
hid: 'og:type',
|
||||
name: 'og:type',
|
||||
content: 'website',
|
||||
},
|
||||
{
|
||||
hid: 'og:title',
|
||||
name: 'og:title',
|
||||
content: this.user.username,
|
||||
},
|
||||
{
|
||||
hid: 'apple-mobile-web-app-title',
|
||||
name: 'apple-mobile-web-app-title',
|
||||
content: description,
|
||||
},
|
||||
{
|
||||
hid: 'og:description',
|
||||
name: 'og:description',
|
||||
content: description,
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: `${this.user.bio} - Download ${this.user.username}'s projects on Modrinth`,
|
||||
},
|
||||
{
|
||||
hid: 'og:image',
|
||||
name: 'og:image',
|
||||
content:
|
||||
this.user.avatar_url || 'https://cdn.modrinth.com/placeholder.png',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
authUrl() {
|
||||
return `${process.env.authURLBase}auth/init?url=${process.env.domain}${this.$route.path}`
|
||||
},
|
||||
projectTypes() {
|
||||
const obj = {}
|
||||
|
||||
for (const project of this.projects) {
|
||||
obj[project.project_type] = true
|
||||
}
|
||||
|
||||
return Object.keys(obj)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sumDownloads() {
|
||||
let sum = 0
|
||||
|
||||
for (const projects of this.projects) {
|
||||
sum += projects.downloads
|
||||
}
|
||||
|
||||
return this.$formatNumber(sum)
|
||||
},
|
||||
sumFollows() {
|
||||
let sum = 0
|
||||
|
||||
for (const projects of this.projects) {
|
||||
sum += projects.followers
|
||||
}
|
||||
|
||||
return this.$formatNumber(sum)
|
||||
},
|
||||
showPreviewImage(files) {
|
||||
const reader = new FileReader()
|
||||
this.icon = files[0]
|
||||
reader.readAsDataURL(this.icon)
|
||||
reader.onload = (event) => {
|
||||
this.previewImage = event.target.result
|
||||
}
|
||||
},
|
||||
async saveChanges() {
|
||||
this.$nuxt.$loading.start()
|
||||
try {
|
||||
if (this.icon) {
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}/icon?ext=${
|
||||
this.icon.type.split('/')[this.icon.type.split('/').length - 1]
|
||||
}`,
|
||||
this.icon,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
}
|
||||
|
||||
const data = {
|
||||
email: this.user.email,
|
||||
bio: this.user.bio,
|
||||
}
|
||||
if (this.user.username !== this.$auth.user.username) {
|
||||
data.username = this.user.username
|
||||
}
|
||||
|
||||
await this.$axios.patch(
|
||||
`user/${this.$auth.user.id}`,
|
||||
data,
|
||||
this.$defaultHeaders()
|
||||
)
|
||||
await this.$store.dispatch('auth/fetchUser', {
|
||||
token: this.$auth.token,
|
||||
})
|
||||
|
||||
this.isEditing = false
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.response.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
this.$nuxt.$loading.finish()
|
||||
},
|
||||
async cycleSearchDisplayMode() {
|
||||
const value = this.$cosmetics.searchDisplayMode.user
|
||||
const newValue = this.$cycleValue(value, this.$tag.projectViewModes)
|
||||
await this.$store.dispatch('cosmetics/saveSearchDisplayMode', {
|
||||
projectType: 'user',
|
||||
mode: newValue,
|
||||
$cookies: this.$cookies,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-header-wrapper {
|
||||
display: flex;
|
||||
margin: 0 auto -1.5rem;
|
||||
max-width: 80rem;
|
||||
|
||||
.user-header {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-username {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 501px) {
|
||||
.mobile-username {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-header-wrapper .user-header .username {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding-top: 2.5rem;
|
||||
}
|
||||
|
||||
.sidebar__item:not(:last-child) {
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.profile-picture {
|
||||
border-radius: var(--size-rounded-lg);
|
||||
height: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.bio {
|
||||
display: block;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.secondary-stat {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.secondary-stat__icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.secondary-stat__text {
|
||||
margin-left: 0.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.date {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.github-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
input {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group:first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
height: 10rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user