You've already forked AstralRinth
forked from didirus/AstralRinth
New organizations (#1488)
* [WIP] Transfer organizations to own branch * push progress * Setup organizations page * Add organizations grid to user profile * Remove debug * Add error handling for failed organization fetch * Refactor organization page and settings * Restructure to composition setup api * checklist completion * Apply suggestions from code review Co-authored-by: Emma Alexia <emma@modrinth.com> * Update pages/[type]/[id]/settings/index.vue Co-authored-by: Emma Alexia <emma@modrinth.com> * Update pages/[type]/[id]/settings/index.vue Co-authored-by: Emma Alexia <emma@modrinth.com> * Update pages/[type]/[id]/settings/index.vue Co-authored-by: Emma Alexia <emma@modrinth.com> * Update pages/[type]/[id]/settings/index.vue Co-authored-by: Emma Alexia <emma@modrinth.com> * Clean up org state management * Refactor useClientTry to simplify code * Remove unused code and update dependencies * Refactor bulkEditLinks event handler * Refactor organization management functions * Update heading from "Creators" to "Members" * Refactor team member invitation * Refactor member management functions * Implement validation on clientside for org names * Name sanitization for fun characters * Update onInviteTeamMember function parameters * Remove name * sidebar * random rendering issue * Conform to org removal * Org no projects conditional * Update organization links in dashboard * Update Cards to universal-cards * Refactor gallery upload permissions * Refactor to sidebar pattern * Update button classes in gallery and versions components * Finish (most) * almost finish * Finish orgs :D * Fix lint * orgs fixes * fix most things * project settings * convert grid to cards * clean up unused test class * Settings -> Manage * add org view to org management * Fix prop mounting issue * fix analytics grid layout overflow * fix multiselect breaking layout * Refactor chart selection logic in ChartDisplay.vue * Add transfer modal --------- Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Emma Alexia <emma@modrinth.com>
This commit is contained in:
@@ -4,14 +4,6 @@
|
||||
<div class="markdown-body">
|
||||
<p>New projects are created as drafts and can be found under your profile page.</p>
|
||||
</div>
|
||||
<label for="project-type">
|
||||
<span class="label__title">Project type<span class="required">*</span></span>
|
||||
</label>
|
||||
<Chips
|
||||
id="project-type"
|
||||
v-model="projectType"
|
||||
:items="tags.projectTypes.map((x) => x.display)"
|
||||
/>
|
||||
<label for="name">
|
||||
<span class="label__title">Name<span class="required">*</span></span>
|
||||
</label>
|
||||
@@ -28,9 +20,7 @@
|
||||
<span class="label__title">URL<span class="required">*</span></span>
|
||||
</label>
|
||||
<div class="text-input-wrapper">
|
||||
<div class="text-input-wrapper__before">
|
||||
https://modrinth.com/{{ getProjectType() ? getProjectType().id : '???' }}/
|
||||
</div>
|
||||
<div class="text-input-wrapper__before">https://modrinth.com/project/</div>
|
||||
<input
|
||||
id="slug"
|
||||
v-model="slug"
|
||||
@@ -40,6 +30,25 @@
|
||||
@input="manualSlug = true"
|
||||
/>
|
||||
</div>
|
||||
<label for="visibility">
|
||||
<span class="label__title">Visibility<span class="required">*</span></span>
|
||||
<span class="label__description">
|
||||
The visibility of your project after it has been approved.
|
||||
</span>
|
||||
</label>
|
||||
<multiselect
|
||||
id="visibility"
|
||||
v-model="visibility"
|
||||
:options="visibilities"
|
||||
track-by="actual"
|
||||
label="display"
|
||||
:multiple="false"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:show-labels="false"
|
||||
placeholder="Choose visibility.."
|
||||
open-direction="bottom"
|
||||
/>
|
||||
<label for="additional-information">
|
||||
<span class="label__title">Summary<span class="required">*</span></span>
|
||||
<span class="label__description"
|
||||
@@ -64,26 +73,23 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import CheckIcon from '~/assets/images/utils/right-arrow.svg'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import Chips from '~/components/ui/Chips.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Chips,
|
||||
CrossIcon,
|
||||
CheckIcon,
|
||||
Modal,
|
||||
Multiselect,
|
||||
},
|
||||
props: {
|
||||
itemType: {
|
||||
organizationId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
@@ -93,80 +99,68 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projectType: this.tags.projectTypes[0].display,
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
manualSlug: false,
|
||||
visibilities: [
|
||||
{
|
||||
actual: 'approved',
|
||||
display: 'Public',
|
||||
},
|
||||
{
|
||||
actual: 'private',
|
||||
display: 'Private',
|
||||
},
|
||||
{
|
||||
actual: 'unlisted',
|
||||
display: 'Unlisted',
|
||||
},
|
||||
],
|
||||
visibility: {
|
||||
actual: 'approved',
|
||||
display: 'Public',
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
getProjectType() {
|
||||
return this.tags.projectTypes.find((x) => this.projectType === x.display)
|
||||
},
|
||||
getClientSide() {
|
||||
switch (this.getProjectType().id) {
|
||||
case 'plugin':
|
||||
return 'unsupported'
|
||||
case 'resourcepack':
|
||||
return 'required'
|
||||
case 'shader':
|
||||
return 'required'
|
||||
case 'datapack':
|
||||
return 'optional'
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
},
|
||||
getServerSide() {
|
||||
switch (this.getProjectType().id) {
|
||||
case 'plugin':
|
||||
return 'required'
|
||||
case 'resourcepack':
|
||||
return 'unsupported'
|
||||
case 'shader':
|
||||
return 'unsupported'
|
||||
case 'datapack':
|
||||
return 'required'
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
},
|
||||
async createProject() {
|
||||
startLoading()
|
||||
|
||||
const projectType = this.getProjectType()
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
formData.append(
|
||||
'data',
|
||||
JSON.stringify({
|
||||
title: this.name.trim(),
|
||||
project_type: projectType.actual,
|
||||
slug: this.slug,
|
||||
description: this.description.trim(),
|
||||
body: '',
|
||||
initial_versions: [],
|
||||
team_members: [
|
||||
{
|
||||
user_id: auth.value.user.id,
|
||||
name: auth.value.user.username,
|
||||
role: 'Owner',
|
||||
},
|
||||
],
|
||||
categories: [],
|
||||
client_side: this.getClientSide(),
|
||||
server_side: this.getServerSide(),
|
||||
license_id: 'LicenseRef-Unknown',
|
||||
is_draft: true,
|
||||
})
|
||||
)
|
||||
const projectData = {
|
||||
title: this.name.trim(),
|
||||
project_type: 'mod',
|
||||
slug: this.slug,
|
||||
description: this.description.trim(),
|
||||
body: '',
|
||||
requested_status: this.visibility.actual,
|
||||
initial_versions: [],
|
||||
team_members: [
|
||||
{
|
||||
user_id: auth.value.user.id,
|
||||
name: auth.value.user.username,
|
||||
role: 'Owner',
|
||||
},
|
||||
],
|
||||
categories: [],
|
||||
client_side: 'required',
|
||||
server_side: 'required',
|
||||
license_id: 'LicenseRef-Unknown',
|
||||
is_draft: true,
|
||||
}
|
||||
|
||||
if (this.organizationId) {
|
||||
projectData.organization_id = this.organizationId
|
||||
}
|
||||
|
||||
formData.append('data', JSON.stringify(projectData))
|
||||
|
||||
try {
|
||||
await useBaseFetch('project', {
|
||||
@@ -181,12 +175,9 @@ export default {
|
||||
await this.$router.push({
|
||||
name: 'type-id',
|
||||
params: {
|
||||
type: projectType.id,
|
||||
type: 'project',
|
||||
id: this.slug,
|
||||
},
|
||||
state: {
|
||||
overrideProjectType: projectType.id,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
|
||||
@@ -20,6 +20,13 @@
|
||||
<nuxt-link v-if="project" :to="getProjectLink(project)" tabindex="-1">
|
||||
<Avatar size="xs" :src="project.icon_url" :raised="raised" no-shadow />
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-else-if="organization"
|
||||
:to="`/organization/${organization.slug}`"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Avatar size="xs" :src="organization.icon_url" :raised="raised" no-shadow />
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else-if="user" :to="getUserLink(user)" tabindex="-1">
|
||||
<Avatar size="xs" :src="user.avatar_url" :raised="raised" no-shadow />
|
||||
</nuxt-link>
|
||||
@@ -31,6 +38,10 @@
|
||||
class="moderation-color"
|
||||
/>
|
||||
<InvitationIcon v-else-if="type === 'team_invite' && project" class="creator-color" />
|
||||
<InvitationIcon
|
||||
v-else-if="type === 'organization_invite' && organization"
|
||||
class="creator-color"
|
||||
/>
|
||||
<VersionIcon v-else-if="type === 'project_update' && project && version" />
|
||||
<NotificationIcon v-else />
|
||||
</template>
|
||||
@@ -54,6 +65,19 @@
|
||||
>.
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="type === 'organization_invite' && organization">
|
||||
<nuxt-link :to="`/user/${invitedBy.username}`" class="iconified-link title-link">
|
||||
<Avatar :src="invitedBy.avatar_url" circle size="xxs" no-shadow :raised="raised" />
|
||||
<span class="space"> </span>
|
||||
<span>{{ invitedBy.username }}</span>
|
||||
</nuxt-link>
|
||||
<span>
|
||||
has invited you to join
|
||||
<nuxt-link :to="`/organization/${organization.slug}`" class="title-link">
|
||||
{{ organization.name }} </nuxt-link
|
||||
>.
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="type === 'status_change' && project">
|
||||
<nuxt-link :to="getProjectLink(project)" class="title-link">
|
||||
{{ project.title }}
|
||||
@@ -154,7 +178,7 @@
|
||||
</span>
|
||||
</span>
|
||||
<div v-if="compact" class="notification__actions">
|
||||
<template v-if="type === 'team_invite'">
|
||||
<template v-if="type === 'team_invite' || type === 'organization_invite'">
|
||||
<button
|
||||
v-tooltip="`Accept`"
|
||||
class="iconified-button square-button brand-button button-transparent"
|
||||
@@ -191,7 +215,9 @@
|
||||
</div>
|
||||
<div v-else class="notification__actions">
|
||||
<div v-if="type !== null" class="input-group">
|
||||
<template v-if="type === 'team_invite' && !notification.read">
|
||||
<template
|
||||
v-if="(type === 'team_invite' || type === 'organization_invite') && !notification.read"
|
||||
>
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
@click="
|
||||
@@ -322,6 +348,7 @@ const report = computed(() => props.notification.extra_data.report)
|
||||
const project = computed(() => props.notification.extra_data.project)
|
||||
const version = computed(() => props.notification.extra_data.version)
|
||||
const user = computed(() => props.notification.extra_data.user)
|
||||
const organization = computed(() => props.notification.extra_data.organization)
|
||||
const invitedBy = computed(() => props.notification.extra_data.invited_by)
|
||||
|
||||
const threadLink = computed(() => {
|
||||
|
||||
138
components/ui/OrganizationCreateModal.vue
Normal file
138
components/ui/OrganizationCreateModal.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<Modal ref="modal" header="Create an organization">
|
||||
<div class="universal-modal modal-creation universal-labels">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
Organizations can be found under your profile page. You will be set as its owner, but you
|
||||
can invite other members and transfer ownership at any time.
|
||||
</p>
|
||||
</div>
|
||||
<label for="name">
|
||||
<span class="label__title">Name<span class="required">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="name"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
:placeholder="`Enter organization name...`"
|
||||
autocomplete="off"
|
||||
@input="updateSlug()"
|
||||
/>
|
||||
<label for="slug">
|
||||
<span class="label__title">URL<span class="required">*</span></span>
|
||||
</label>
|
||||
<div class="text-input-wrapper">
|
||||
<div class="text-input-wrapper__before">https://modrinth.com/organization/</div>
|
||||
<input
|
||||
id="slug"
|
||||
v-model="slug"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
@input="manualSlug = true"
|
||||
/>
|
||||
</div>
|
||||
<label for="additional-information">
|
||||
<span class="label__title">Summary<span class="required">*</span></span>
|
||||
<span class="label__description">This will appear on your organization's page.</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea id="additional-information" v-model="description" maxlength="256" />
|
||||
</div>
|
||||
<div class="push-right input-group">
|
||||
<Button @click="modal.hide()">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="primary" @click="createProject">
|
||||
<CheckIcon />
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { XIcon as CrossIcon, CheckIcon, Modal, Button } from 'omorphia'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const name = ref('')
|
||||
const slug = ref('')
|
||||
const description = ref('')
|
||||
const manualSlug = ref(false)
|
||||
|
||||
const modal = ref()
|
||||
|
||||
async function createProject() {
|
||||
startLoading()
|
||||
try {
|
||||
const value = {
|
||||
name: name.value.trim(),
|
||||
description: description.value.trim(),
|
||||
slug: slug.value.trim().replace(/ +/g, ''),
|
||||
}
|
||||
|
||||
const result = await useBaseFetch('organization', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(value),
|
||||
apiVersion: 3,
|
||||
})
|
||||
|
||||
modal.value.hide()
|
||||
|
||||
await router.push(`/organization/${result.slug}`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
function show() {
|
||||
name.value = ''
|
||||
description.value = ''
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
function updateSlug() {
|
||||
if (!manualSlug.value) {
|
||||
slug.value = name.value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')
|
||||
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
|
||||
.replaceAll(/--+/gm, '-')
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-creation {
|
||||
input {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.text-input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-top: var(--gap-md);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
255
components/ui/OrganizationProjectTransferModal.vue
Normal file
255
components/ui/OrganizationProjectTransferModal.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div>
|
||||
<Modal ref="modalOpen" header="Transfer Projects">
|
||||
<div class="universal-modal items">
|
||||
<div class="table">
|
||||
<div class="table-row table-head">
|
||||
<div class="table-cell check-cell">
|
||||
<Checkbox
|
||||
:model-value="selectedProjects.length === props.projects.length"
|
||||
@update:model-value="toggleSelectedProjects()"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-cell">Icon</div>
|
||||
<div class="table-cell">Name</div>
|
||||
<div class="table-cell">ID</div>
|
||||
<div class="table-cell">Type</div>
|
||||
<div class="table-cell" />
|
||||
</div>
|
||||
<div v-for="project in props.projects" :key="`project-${project.id}`" class="table-row">
|
||||
<div class="table-cell check-cell">
|
||||
<Checkbox
|
||||
: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.push(project)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-cell">
|
||||
<nuxt-link tabindex="-1" :to="`/project/${project.slug ? project.slug : project.id}`">
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
aria-hidden="true"
|
||||
:alt="'Icon for ' + project.name"
|
||||
no-shadow
|
||||
/>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
||||
<div class="table-cell">
|
||||
<span class="project-title">
|
||||
<nuxt-link
|
||||
class="hover-link wrap-as-needed"
|
||||
:to="`/project/${project.slug ? project.slug : project.id}`"
|
||||
>
|
||||
{{ project.title }}
|
||||
</nuxt-link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-cell">
|
||||
<CopyCode :text="project.id" />
|
||||
</div>
|
||||
|
||||
<div class="table-cell">
|
||||
<BoxIcon />
|
||||
<span>{{
|
||||
$formatProjectType(
|
||||
$getProjectTypeForDisplay(
|
||||
project.project_types?.[0] ?? 'project',
|
||||
project.loaders
|
||||
)
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="table-cell">
|
||||
<nuxt-link
|
||||
class="btn icon-only"
|
||||
:to="`/project/${project.slug ? project.slug : project.id}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="push-right input-group">
|
||||
<Button @click="$refs.modalOpen?.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button :disabled="!selectedProjects?.length" color="primary" @click="onSubmitHandler()">
|
||||
<TransferIcon />
|
||||
<span>
|
||||
Transfer
|
||||
<span>
|
||||
{{
|
||||
selectedProjects.length === props.projects.length
|
||||
? 'All'
|
||||
: selectedProjects.length
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ ' ' }}
|
||||
{{ selectedProjects.length === 1 ? 'project' : 'projects' }}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Button @click="$refs.modalOpen?.show()">
|
||||
<TransferIcon />
|
||||
<span>Transfer projects</span>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Checkbox,
|
||||
CopyCode,
|
||||
Avatar,
|
||||
BoxIcon,
|
||||
SettingsIcon,
|
||||
Button,
|
||||
Modal,
|
||||
TransferIcon,
|
||||
XIcon,
|
||||
} from 'omorphia'
|
||||
|
||||
const modalOpen = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
projects: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
// define emit for submission
|
||||
const emit = defineEmits(['submit'])
|
||||
|
||||
const selectedProjects = ref([])
|
||||
|
||||
const toggleSelectedProjects = () => {
|
||||
if (selectedProjects.value.length === props.projects.length) {
|
||||
selectedProjects.value = []
|
||||
} else {
|
||||
selectedProjects.value = props.projects
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmitHandler = () => {
|
||||
if (selectedProjects.value.length === 0) {
|
||||
return
|
||||
}
|
||||
emit('submit', selectedProjects.value)
|
||||
selectedProjects.value = []
|
||||
modalOpen.value?.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table {
|
||||
display: grid;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
margin-top: var(--gap-md);
|
||||
border: 1px solid var(--color-button-bg);
|
||||
background-color: var(--color-raised-bg);
|
||||
|
||||
.table-row {
|
||||
grid-template-columns: 2.75rem 3.75rem 2fr 1fr 1fr 3.5rem;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--gap-xs);
|
||||
padding: var(--gap-md);
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.check-cell {
|
||||
padding-left: var(--gap-md);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name type settings' 'checkbox icon id type settings';
|
||||
grid-template-columns:
|
||||
min-content min-content minmax(min-content, 2fr)
|
||||
minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(1) {
|
||||
grid-area: checkbox;
|
||||
}
|
||||
|
||||
:nth-child(2) {
|
||||
grid-area: icon;
|
||||
}
|
||||
|
||||
:nth-child(3) {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
:nth-child(4) {
|
||||
grid-area: id;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:nth-child(5) {
|
||||
grid-area: type;
|
||||
}
|
||||
|
||||
:nth-child(6) {
|
||||
grid-area: settings;
|
||||
}
|
||||
}
|
||||
|
||||
.table-head {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
|
||||
:nth-child(2),
|
||||
:nth-child(3),
|
||||
:nth-child(4),
|
||||
:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 560px) {
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template: 'checkbox icon name settings' 'checkbox icon id settings' 'checkbox icon type settings' 'checkbox icon status settings';
|
||||
grid-template-columns: min-content min-content minmax(min-content, 1fr) min-content;
|
||||
|
||||
:nth-child(5) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-head {
|
||||
grid-template: 'checkbox settings';
|
||||
grid-template-columns: min-content minmax(min-content, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
padding: var(--gap-md);
|
||||
}
|
||||
</style>
|
||||
@@ -42,6 +42,7 @@
|
||||
class="tags"
|
||||
>
|
||||
<EnvironmentIndicator
|
||||
v-if="clientSide && serverSide"
|
||||
:type-only="moderation"
|
||||
:client-side="clientSide"
|
||||
:server-side="serverSide"
|
||||
|
||||
@@ -54,7 +54,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!collapsed" class="grid-display width-16">
|
||||
<div v-for="nag in nags.filter((x) => x.condition)" :key="nag.id" class="grid-display__item">
|
||||
<div
|
||||
v-for="nag in nags.filter((x) => x.condition && !x.hide)"
|
||||
:key="nag.id"
|
||||
class="grid-display__item"
|
||||
>
|
||||
<span class="label">
|
||||
<RequiredIcon
|
||||
v-if="nag.status === 'required'"
|
||||
@@ -101,7 +105,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { formatProjectType } from '~/plugins/shorthands.js'
|
||||
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
||||
import DropdownIcon from '~/assets/images/utils/dropdown.svg'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||
@@ -110,309 +116,266 @@ import RequiredIcon from '~/assets/images/utils/asterisk.svg'
|
||||
import SuggestionIcon from '~/assets/images/utils/lightbulb.svg'
|
||||
import ModerationIcon from '~/assets/images/sidebar/admin.svg'
|
||||
import SendIcon from '~/assets/images/utils/send.svg'
|
||||
import { acceptTeamInvite, removeSelfFromTeam } from '~/helpers/teams.js'
|
||||
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChevronRightIcon,
|
||||
DropdownIcon,
|
||||
CheckIcon,
|
||||
CrossIcon,
|
||||
RequiredIcon,
|
||||
SuggestionIcon,
|
||||
ModerationIcon,
|
||||
SendIcon,
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
allMembers: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isSettings: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
routeName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
tags: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
setProcessing: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'setProcessing function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
toggleCollapsed: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'toggleCollapsed function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
updateMembers: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'updateMembers function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
featuredGalleryImage() {
|
||||
return this.project.gallery.find((img) => img.featured)
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
allMembers: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isSettings: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
routeName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
tags: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
setProcessing: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'setProcessing function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
nags() {
|
||||
return [
|
||||
{
|
||||
condition:
|
||||
this.project.body === '' || this.project.body.startsWith('# Placeholder description'),
|
||||
title: 'Add a description',
|
||||
id: 'add-description',
|
||||
description:
|
||||
"A description that clearly describes the project's purpose and function is required.",
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: 'Visit description settings',
|
||||
hide: this.routeName === 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: !this.project.icon_url,
|
||||
title: 'Add an icon',
|
||||
id: 'add-icon',
|
||||
description:
|
||||
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: 'Visit general settings',
|
||||
hide: this.routeName === 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: !this.featuredGalleryImage,
|
||||
title: 'Feature a gallery image',
|
||||
id: 'feature-gallery-image',
|
||||
description: 'Featured gallery images may be the first impression of many users.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'gallery',
|
||||
title: 'Visit gallery page',
|
||||
hide: this.routeName === 'type-id-gallery',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: this.versions.length < 1,
|
||||
title: 'Upload a version',
|
||||
id: 'upload-version',
|
||||
description: 'At least one version is required for a project to be submitted for review.',
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'versions',
|
||||
title: 'Visit versions page',
|
||||
hide: this.routeName === 'type-id-versions',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: this.project.categories.length < 1,
|
||||
title: 'Select tags',
|
||||
id: 'select-tags',
|
||||
description: 'Select all tags that apply to your project.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: 'Visit tag settings',
|
||||
hide: this.routeName === 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: !(
|
||||
this.project.issues_url ||
|
||||
this.project.source_url ||
|
||||
this.project.wiki_url ||
|
||||
this.project.discord_url ||
|
||||
this.project.donation_urls.length > 0
|
||||
),
|
||||
title: 'Add external links',
|
||||
id: 'add-links',
|
||||
description:
|
||||
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: 'Visit links settings',
|
||||
hide: this.routeName === 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
hide:
|
||||
this.project.project_type === 'resourcepack' ||
|
||||
this.project.project_type === 'plugin' ||
|
||||
this.project.project_type === 'shader' ||
|
||||
this.project.project_type === 'datapack',
|
||||
condition:
|
||||
this.project.client_side === 'unknown' || this.project.server_side === 'unknown',
|
||||
title: 'Select supported environments',
|
||||
id: 'select-environments',
|
||||
description: `Select if the ${this.$formatProjectType(
|
||||
this.project.project_type
|
||||
).toLowerCase()} functions on the client-side and/or server-side.`,
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: 'Visit general settings',
|
||||
hide: this.routeName === 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: this.project.license.id === 'LicenseRef-Unknown',
|
||||
title: 'Select license',
|
||||
id: 'select-license',
|
||||
description: `Select the license your ${this.$formatProjectType(
|
||||
this.project.project_type
|
||||
).toLowerCase()} is distributed under.`,
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'settings/license',
|
||||
title: 'Visit license settings',
|
||||
hide: this.routeName === 'type-id-settings-license',
|
||||
},
|
||||
},
|
||||
{
|
||||
hide: this.project.status !== 'draft',
|
||||
condition: true,
|
||||
title: 'Submit for review',
|
||||
id: 'submit-for-review',
|
||||
description:
|
||||
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
|
||||
status: 'review',
|
||||
link: null,
|
||||
action: {
|
||||
onClick: this.submitForReview,
|
||||
title: 'Submit for review',
|
||||
disabled: () =>
|
||||
this.nags.filter((x) => x.condition && x.status === 'required').length > 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
hide: !this.tags.rejectedStatuses.includes(this.project.status),
|
||||
condition: true,
|
||||
title: 'Resubmit for review',
|
||||
id: 'resubmit-for-review',
|
||||
description: `Your project has been ${this.project.status} by
|
||||
},
|
||||
toggleCollapsed: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'toggleCollapsed function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
updateMembers: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: 'updateMembers function not found',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured))
|
||||
|
||||
const nags = computed(() => [
|
||||
{
|
||||
condition: props.versions.length < 1,
|
||||
title: 'Upload a version',
|
||||
id: 'upload-version',
|
||||
description: 'At least one version is required for a project to be submitted for review.',
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'versions',
|
||||
title: 'Visit versions page',
|
||||
hide: props.routeName === 'type-id-versions',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition:
|
||||
props.project.body === '' || props.project.body.startsWith('# Placeholder description'),
|
||||
title: 'Add a description',
|
||||
id: 'add-description',
|
||||
description:
|
||||
"A description that clearly describes the project's purpose and function is required.",
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: 'Visit description settings',
|
||||
hide: props.routeName === 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: !props.project.icon_url,
|
||||
title: 'Add an icon',
|
||||
id: 'add-icon',
|
||||
description:
|
||||
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: 'Visit general settings',
|
||||
hide: props.routeName === 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.project.gallery.length === 0 || !featuredGalleryImage,
|
||||
title: 'Feature a gallery image',
|
||||
id: 'feature-gallery-image',
|
||||
description: 'Featured gallery images may be the first impression of many users.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'gallery',
|
||||
title: 'Visit gallery page',
|
||||
hide: props.routeName === 'type-id-gallery',
|
||||
},
|
||||
},
|
||||
{
|
||||
hide: props.project.versions.length === 0,
|
||||
condition: props.project.categories.length < 1,
|
||||
title: 'Select tags',
|
||||
id: 'select-tags',
|
||||
description: 'Select all tags that apply to your project.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'settings/tags',
|
||||
title: 'Visit tag settings',
|
||||
hide: props.routeName === 'type-id-settings-tags',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: !(
|
||||
props.project.issues_url ||
|
||||
props.project.source_url ||
|
||||
props.project.wiki_url ||
|
||||
props.project.discord_url ||
|
||||
props.project.donation_urls.length > 0
|
||||
),
|
||||
title: 'Add external links',
|
||||
id: 'add-links',
|
||||
description:
|
||||
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.',
|
||||
status: 'suggestion',
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: 'Visit links settings',
|
||||
hide: props.routeName === 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
hide:
|
||||
props.project.versions.length === 0 ||
|
||||
props.project.project_type === 'resourcepack' ||
|
||||
props.project.project_type === 'plugin' ||
|
||||
props.project.project_type === 'shader' ||
|
||||
props.project.project_type === 'datapack',
|
||||
condition:
|
||||
props.project.client_side === 'unknown' ||
|
||||
props.project.server_side === 'unknown' ||
|
||||
(props.project.client_side === 'unsupported' && props.project.server_side === 'unsupported'),
|
||||
title: 'Select supported environments',
|
||||
id: 'select-environments',
|
||||
description: `Select if the ${formatProjectType(
|
||||
props.project.project_type
|
||||
).toLowerCase()} functions on the client-side and/or server-side.`,
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: 'Visit general settings',
|
||||
hide: props.routeName === 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.project.license.id === 'LicenseRef-Unknown',
|
||||
title: 'Select license',
|
||||
id: 'select-license',
|
||||
description: `Select the license your ${formatProjectType(
|
||||
props.project.project_type
|
||||
).toLowerCase()} is distributed under.`,
|
||||
status: 'required',
|
||||
link: {
|
||||
path: 'settings/license',
|
||||
title: 'Visit license settings',
|
||||
hide: props.routeName === 'type-id-settings-license',
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.project.status === 'draft',
|
||||
title: 'Submit for review',
|
||||
id: 'submit-for-review',
|
||||
description:
|
||||
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
|
||||
status: 'review',
|
||||
link: null,
|
||||
action: {
|
||||
onClick: submitForReview,
|
||||
title: 'Submit for review',
|
||||
disabled: () => nags.value.filter((x) => x.condition && x.status === 'required').length > 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
condition: props.tags.rejectedStatuses.includes(props.project.status),
|
||||
title: 'Resubmit for review',
|
||||
id: 'resubmit-for-review',
|
||||
description: `Your project has been ${props.project.status} by
|
||||
Modrinth's staff. In most cases, you can resubmit for review after
|
||||
addressing the staff's message.`,
|
||||
status: 'review',
|
||||
link: {
|
||||
path: 'moderation',
|
||||
title: 'Visit moderation page',
|
||||
hide: this.routeName === 'type-id-moderation',
|
||||
},
|
||||
},
|
||||
]
|
||||
.filter((x) => !x.hide)
|
||||
.sort((a, b) =>
|
||||
this.sortByTrue(
|
||||
!a.condition,
|
||||
!b.condition,
|
||||
this.sortByTrue(
|
||||
a.status === 'required',
|
||||
b.status === 'required',
|
||||
this.sortByFalse(a.status === 'review', b.status === 'review')
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
showInvitation() {
|
||||
if (this.allMembers && this.auth) {
|
||||
const member = this.allMembers.find((x) => x.user.id === this.auth.user.id)
|
||||
return member && !member.accepted
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
acceptInvite() {
|
||||
acceptTeamInvite(this.project.team)
|
||||
this.updateMembers()
|
||||
},
|
||||
declineInvite() {
|
||||
removeSelfFromTeam(this.project.team)
|
||||
this.updateMembers()
|
||||
},
|
||||
sortByTrue(a, b, ifEqual = 0) {
|
||||
if (a === b) {
|
||||
return ifEqual
|
||||
} else if (a) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
},
|
||||
sortByFalse(a, b, ifEqual = 0) {
|
||||
if (a === b) {
|
||||
return ifEqual
|
||||
} else if (b) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
},
|
||||
async submitForReview() {
|
||||
if (
|
||||
!this.acknowledgedMessage ||
|
||||
this.nags.filter((x) => x.condition && x.status === 'required').length === 0
|
||||
) {
|
||||
await this.setProcessing()
|
||||
}
|
||||
status: 'review',
|
||||
link: {
|
||||
path: 'moderation',
|
||||
title: 'Visit moderation page',
|
||||
hide: props.routeName === 'type-id-moderation',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const showInvitation = computed(() => {
|
||||
if (props.allMembers && props.auth) {
|
||||
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id)
|
||||
return member && !member.accepted
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const acceptInvite = () => {
|
||||
acceptTeamInvite(props.project.team)
|
||||
props.updateMembers()
|
||||
}
|
||||
|
||||
const declineInvite = () => {
|
||||
removeTeamMember(props.project.team, props.auth.user.id)
|
||||
props.updateMembers()
|
||||
}
|
||||
|
||||
const submitForReview = async () => {
|
||||
if (
|
||||
!props.acknowledgedMessage ||
|
||||
nags.value.filter((x) => x.condition && x.status === 'required').length === 0
|
||||
) {
|
||||
await props.setProcessing()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -370,10 +370,11 @@ svg {
|
||||
|
||||
.bar-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="analytics.error.value">
|
||||
{{ analytics.error.value }}
|
||||
<div v-if="analytics.error.value" class="universal-card">
|
||||
<h2>
|
||||
<span class="label__title">Error</span>
|
||||
</h2>
|
||||
<div>
|
||||
{{ analytics.error.value }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="graphs">
|
||||
<div class="graphs__vertical-bar">
|
||||
@@ -18,7 +23,7 @@
|
||||
:class="`clickable button-base ${
|
||||
selectedChart === 'downloads' ? 'button-base__selected' : ''
|
||||
}`"
|
||||
:onclick="() => (selectedChart = 'downloads')"
|
||||
:onclick="() => setSelectedChart('downloads')"
|
||||
role="button"
|
||||
/>
|
||||
</client-only>
|
||||
@@ -35,7 +40,7 @@
|
||||
:class="`clickable button-base ${
|
||||
selectedChart === 'views' ? 'button-base__selected' : ''
|
||||
}`"
|
||||
:onclick="() => (selectedChart = 'views')"
|
||||
:onclick="() => setSelectedChart('views')"
|
||||
role="button"
|
||||
/>
|
||||
</client-only>
|
||||
@@ -52,7 +57,7 @@
|
||||
:class="`clickable button-base ${
|
||||
selectedChart === 'revenue' ? 'button-base__selected' : ''
|
||||
}`"
|
||||
:onclick="() => (selectedChart = 'revenue')"
|
||||
:onclick="() => setSelectedChart('revenue')"
|
||||
role="button"
|
||||
/>
|
||||
</client-only>
|
||||
@@ -118,7 +123,9 @@
|
||||
<div class="country-data">
|
||||
<Card
|
||||
v-if="
|
||||
analytics.formattedData.value?.downloadsByCountry && selectedChart === 'downloads'
|
||||
analytics.formattedData.value?.downloadsByCountry &&
|
||||
selectedChart === 'downloads' &&
|
||||
analytics.formattedData.value.downloadsByCountry.data.length > 0
|
||||
"
|
||||
class="country-downloads"
|
||||
>
|
||||
@@ -169,7 +176,11 @@
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
v-if="analytics.formattedData.value?.viewsByCountry && selectedChart === 'views'"
|
||||
v-if="
|
||||
analytics.formattedData.value?.viewsByCountry &&
|
||||
selectedChart === 'views' &&
|
||||
analytics.formattedData.value.viewsByCountry.data.length > 0
|
||||
"
|
||||
class="country-downloads"
|
||||
>
|
||||
<label>
|
||||
@@ -183,14 +194,20 @@
|
||||
>
|
||||
<div class="country-flag-container">
|
||||
<img
|
||||
:src="`https://flagcdn.com/h240/${name.toLowerCase()}.png`"
|
||||
:alt="name"
|
||||
:src="
|
||||
name.toLowerCase() === 'xx' || !name
|
||||
? 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
: countryCodeToFlag(name)
|
||||
"
|
||||
alt="Hidden country"
|
||||
class="country-flag"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="country-text">
|
||||
<strong class="country-name">{{ countryCodeToName(name) }}</strong>
|
||||
<strong class="country-name">
|
||||
<template v-if="name.toLowerCase() === 'xx' || !name">Hidden</template>
|
||||
<template v-else>{{ countryCodeToName(name) }}</template>
|
||||
</strong>
|
||||
<span class="data-point">{{ formatNumber(count) }}</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -225,6 +242,8 @@ import dayjs from 'dayjs'
|
||||
import { defineProps, ref, computed } from 'vue'
|
||||
import { UiChartsCompactChart as CompactChart, UiChartsChart as Chart } from '#components'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
projects?: any[]
|
||||
@@ -247,7 +266,18 @@ const selectableRanges = Object.entries(props.ranges).map(([duration, extra]) =>
|
||||
res: typeof extra === 'string' ? Number(duration) : extra[1],
|
||||
}))
|
||||
|
||||
const selectedChart = ref('downloads')
|
||||
// const selectedChart = ref('downloads')
|
||||
const selectedChart = computed(() => {
|
||||
return (router.currentRoute.value.query?.chart as string | undefined) || 'downloads'
|
||||
})
|
||||
const setSelectedChart = (chart: string) => {
|
||||
router.push({
|
||||
query: {
|
||||
...router.currentRoute.value.query,
|
||||
chart,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Chart refs
|
||||
const downloadsChart = ref()
|
||||
|
||||
Reference in New Issue
Block a user