You've already forked AstralRinth
forked from didirus/AstralRinth
Threads and more! (#1232)
* Begin UI for threads and moderation overhaul * Hide close button on non-report threads * Fix review age coloring * Add project count * Remove action buttons from queue page and add queued date to project page * Hook up to actual data * Remove unused icon * Get up to 1000 projects in queue * prettier * more prettier * Changed all the things * lint * rebuild * Add omorphia * Workaround formatjs bug in ThreadSummary.vue * Fix notifications page on prod * Fix a few notifications and threads bugs * lockfile * Fix duplicate button styles * more fixes and polishing * More fixes * Remove legacy pages * More bugfixes * Add some error catching for reports and notifications * More error handling * fix lint * Add inbox links * Remove loading component and rename member header * Rely on threads always existing * Handle if project update notifs are not grouped * oops * Fix chips on notifications page * Import ModalModeration * finish threads --------- Co-authored-by: triphora <emma@modrinth.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
51
pages/dashboard/follows.vue
Normal file
51
pages/dashboard/follows.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div v-if="user.follows.length > 0" class="project-list display-mode--list">
|
||||
<ProjectCard
|
||||
v-for="project in user.follows"
|
||||
:id="project.id"
|
||||
:key="project.id"
|
||||
:type="project.project_type"
|
||||
:categories="project.categories"
|
||||
:created-at="project.published"
|
||||
:updated-at="project.updated"
|
||||
:description="project.description"
|
||||
:downloads="project.downloads ? project.downloads.toString() : '0'"
|
||||
:icon-url="project.icon_url"
|
||||
:name="project.title"
|
||||
:client-side="project.client_side"
|
||||
:server-side="project.server_side"
|
||||
:color="project.color"
|
||||
>
|
||||
<button class="iconified-button" @click="userUnfollowProject(project)">
|
||||
<HeartIcon />
|
||||
Unfollow
|
||||
</button>
|
||||
</ProjectCard>
|
||||
</div>
|
||||
<div v-else class="error">
|
||||
<FollowIllustration class="icon" />
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ProjectCard from '~/components/ui/ProjectCard.vue'
|
||||
|
||||
import HeartIcon from 'assets/images/utils/heart.svg'
|
||||
import FollowIllustration from 'assets/images/illustrations/follow_illustration.svg'
|
||||
|
||||
const user = await useUser()
|
||||
if (process.client) {
|
||||
await initUserFollows()
|
||||
}
|
||||
|
||||
useHead({ title: 'Followed review - Modrinth' })
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,7 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Overview</h2>
|
||||
<div class="dashboard-overview">
|
||||
<section class="universal-card dashboard-header">
|
||||
<Avatar :src="$auth.user.avatar_url" size="md" circle :alt="$auth.user.username" />
|
||||
<div class="username">
|
||||
<h1>
|
||||
{{ $auth.user.username }}
|
||||
</h1>
|
||||
<NuxtLink class="goto-link" :to="`/user/${$auth.user.username}`">
|
||||
Visit your profile
|
||||
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
<section class="universal-card dashboard-notifications">
|
||||
<div class="header__row">
|
||||
<h2 class="header__title">Notifications</h2>
|
||||
<nuxt-link v-if="notifications.length > 0" class="goto-link" to="/dashboard/notifications">
|
||||
See all <ChevronRightIcon />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<template v-if="notifications.length > 0">
|
||||
<NotificationItem
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
v-model:notifications="allNotifs"
|
||||
class="universal-card recessed"
|
||||
:notification="notification"
|
||||
raised
|
||||
compact
|
||||
/>
|
||||
<nuxt-link
|
||||
v-if="extraNotifs > 0"
|
||||
class="goto-link view-more-notifs"
|
||||
to="/dashboard/notifications"
|
||||
>
|
||||
View {{ extraNotifs }} more notification{{ extraNotifs === 1 ? '' : 's' }}
|
||||
<ChevronRightIcon />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<div v-else class="universal-body">
|
||||
<p>You have no unread notifications.</p>
|
||||
<nuxt-link class="iconified-button" to="/dashboard/notifications">
|
||||
<HistoryIcon /> View notification history
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</section>
|
||||
<section class="universal-card dashboard-analytics">
|
||||
<h2>Analytics</h2>
|
||||
<div class="grid-display">
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Total downloads</div>
|
||||
@@ -32,25 +77,13 @@
|
||||
}}</span
|
||||
></span
|
||||
>
|
||||
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
|
||||
<!-- >View breakdown-->
|
||||
<!-- <ChevronRightIcon-->
|
||||
<!-- class="featured-header-chevron"-->
|
||||
<!-- aria-hidden="true"-->
|
||||
<!-- /></NuxtLink>-->
|
||||
</div>
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Total revenue</div>
|
||||
<div class="value">
|
||||
{{ $formatMoney(payouts.all_time, true) }}
|
||||
</div>
|
||||
<span>{{ $formatMoney(payouts.last_month, true) }} this month</span>
|
||||
<!-- <NuxtLink class="goto-link" to="/dashboard/analytics"-->
|
||||
<!-- >View breakdown-->
|
||||
<!-- <ChevronRightIcon-->
|
||||
<!-- class="featured-header-chevron"-->
|
||||
<!-- aria-hidden="true"-->
|
||||
<!-- /></NuxtLink>-->
|
||||
<span>{{ $formatMoney(payouts.last_month, true) }} in the last month</span>
|
||||
</div>
|
||||
<div class="grid-display__item">
|
||||
<div class="label">Current balance</div>
|
||||
@@ -69,32 +102,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<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!
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
||||
import HistoryIcon from '~/assets/images/utils/history.svg'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
import NotificationItem from '~/components/ui/NotificationItem.vue'
|
||||
import { fetchNotifications, groupNotifications } from '~/helpers/notifications.js'
|
||||
|
||||
useHead({
|
||||
title: 'Creator dashboard - Modrinth',
|
||||
title: 'Dashboard - Modrinth',
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
const app = useNuxtApp()
|
||||
|
||||
const [rawProjects, rawPayouts] = await Promise.all([
|
||||
useBaseFetch(`user/${auth.value.user.id}/projects`, app.$defaultHeaders()),
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`, app.$defaultHeaders()),
|
||||
const [{ data: projects }, { data: payouts }] = await Promise.all([
|
||||
useAsyncData(`user/${auth.value.user.id}/projects`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/projects`, app.$defaultHeaders())
|
||||
),
|
||||
useAsyncData(`user/${auth.value.user.id}/payouts`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`, app.$defaultHeaders())
|
||||
),
|
||||
])
|
||||
|
||||
const projects = shallowRef(rawProjects)
|
||||
const payouts = ref(rawPayouts)
|
||||
const minWithdraw = ref(0.26)
|
||||
|
||||
const downloadsProjectCount = computed(
|
||||
@@ -103,4 +135,75 @@ const downloadsProjectCount = computed(
|
||||
const followersProjectCount = computed(
|
||||
() => projects.value.filter((project) => project.followers > 0).length
|
||||
)
|
||||
|
||||
const allNotifs = groupNotifications(await fetchNotifications())
|
||||
|
||||
const notifications = computed(() => allNotifs.slice(0, 3))
|
||||
const extraNotifs = computed(() => allNotifs.length - notifications.value.length)
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.dashboard-overview {
|
||||
display: grid;
|
||||
grid-template:
|
||||
'header header'
|
||||
'notifications analytics' / 1fr 1fr;
|
||||
gap: var(--spacing-card-md);
|
||||
|
||||
> .universal-card {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-notifications {
|
||||
grid-area: notifications;
|
||||
//display: flex;
|
||||
//flex-direction: column;
|
||||
//gap: var(--spacing-card-md);
|
||||
|
||||
a.view-more-notifs {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-analytics {
|
||||
grid-area: analytics;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
gap: var(--spacing-card-bg);
|
||||
grid-area: header;
|
||||
|
||||
.username {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-sm);
|
||||
justify-content: center;
|
||||
word-break: break-word;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.avatar {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
h1 {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
133
pages/dashboard/notifications.vue
Normal file
133
pages/dashboard/notifications.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<Breadcrumbs
|
||||
v-if="history"
|
||||
current-title="History"
|
||||
:link-stack="[{ href: `/dashboard/notifications`, label: 'Notifications' }]"
|
||||
/>
|
||||
<div class="header__row">
|
||||
<div class="header__title">
|
||||
<h2 v-if="history">Notification history</h2>
|
||||
<h2 v-else>Notifications</h2>
|
||||
</div>
|
||||
<template v-if="!history">
|
||||
<Button v-if="allNotifs && allNotifs.some((notif) => notif.read)" @click="updateRoute()">
|
||||
<HistoryIcon /> View history
|
||||
</Button>
|
||||
<Button v-if="notifications.length > 0" color="danger" @click="readAll()">
|
||||
<CheckCheckIcon /> Mark all as read
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="notifications.length > 0">
|
||||
<Chips
|
||||
v-if="notifTypes.length > 1"
|
||||
v-model="selectedType"
|
||||
:items="notifTypes"
|
||||
:format-label="
|
||||
(x) => (x === 'all' ? 'All' : $formatProjectType(x).replace('_', ' ') + 's')
|
||||
"
|
||||
:capitalize="false"
|
||||
/>
|
||||
<NotificationItem
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
v-model:notifications="allNotifs"
|
||||
class="universal-card recessed"
|
||||
:notification="notification"
|
||||
raised
|
||||
/>
|
||||
</template>
|
||||
<p v-else>You don't have any unread notifications.</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, HistoryIcon } from 'omorphia'
|
||||
import { fetchNotifications, groupNotifications, markAsRead } from '~/helpers/notifications.js'
|
||||
import NotificationItem from '~/components/ui/NotificationItem.vue'
|
||||
import Chips from '~/components/ui/Chips.vue'
|
||||
import CheckCheckIcon from '~/assets/images/utils/check-check.svg'
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
|
||||
useHead({
|
||||
title: 'Notifications - Modrinth',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const history = computed(() => {
|
||||
return route.name === 'dashboard-notifications-history'
|
||||
})
|
||||
|
||||
const allNotifs = ref(null)
|
||||
|
||||
const notifTypes = computed(() => {
|
||||
if (allNotifs.value === null) {
|
||||
return []
|
||||
}
|
||||
const types = [
|
||||
...new Set(
|
||||
allNotifs.value
|
||||
.filter((notification) => {
|
||||
return history.value || !notification.read
|
||||
})
|
||||
.map((notif) => notif.type)
|
||||
),
|
||||
]
|
||||
return types.length > 1 ? ['all', ...types] : types
|
||||
})
|
||||
|
||||
const notifications = computed(() => {
|
||||
if (allNotifs.value === null) {
|
||||
return []
|
||||
}
|
||||
const groupedNotifs = groupNotifications(allNotifs.value, history.value)
|
||||
return groupedNotifs.filter(
|
||||
(notif) => selectedType.value === 'all' || notif.type === selectedType.value
|
||||
)
|
||||
})
|
||||
|
||||
const selectedType = ref('all')
|
||||
|
||||
await fetchNotifications().then((result) => {
|
||||
allNotifs.value = result
|
||||
})
|
||||
|
||||
function updateRoute() {
|
||||
if (history.value) {
|
||||
router.push('/dashboard/notifications')
|
||||
} else {
|
||||
router.push('/dashboard/notifications/history')
|
||||
}
|
||||
}
|
||||
|
||||
async function readAll() {
|
||||
const ids = notifications.value.flatMap((notification) => [
|
||||
notification.id,
|
||||
...(notification.grouped_notifs ? notification.grouped_notifs.map((notif) => notif.id) : []),
|
||||
])
|
||||
|
||||
const updateNotifs = await markAsRead(ids)
|
||||
allNotifs.value = updateNotifs(allNotifs.value)
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.read-toggle-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-md);
|
||||
|
||||
.label__title {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header__title {
|
||||
h2 {
|
||||
margin: 0 auto 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1
pages/dashboard/notifications/history.vue
Normal file
1
pages/dashboard/notifications/history.vue
Normal file
@@ -0,0 +1 @@
|
||||
<template><div /></template>
|
||||
@@ -183,7 +183,7 @@
|
||||
</button>
|
||||
<div class="push-right">
|
||||
<div class="labeled-control-row">
|
||||
Sort By
|
||||
Sort by
|
||||
<Multiselect
|
||||
v-model="sortBy"
|
||||
:searchable="false"
|
||||
@@ -194,8 +194,13 @@
|
||||
:allow-empty="false"
|
||||
@update:model-value="projects = updateSort(projects, sortBy, descending)"
|
||||
/>
|
||||
<button class="square-button" @click="updateDescending()">
|
||||
<ArrowIcon :transform="`rotate(${descending ? -90 : 90})`" />
|
||||
<button
|
||||
v-tooltip="descending ? 'Descending' : 'Ascending'"
|
||||
class="square-button"
|
||||
@click="updateDescending()"
|
||||
>
|
||||
<DescendingIcon v-if="descending" />
|
||||
<AscendingIcon v-else />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -311,7 +316,8 @@ 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'
|
||||
import ArrowIcon from '~/assets/images/utils/left-arrow.svg'
|
||||
import AscendingIcon from '~/assets/images/utils/sort-asc.svg'
|
||||
import DescendingIcon from '~/assets/images/utils/sort-desc.svg'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
@@ -329,7 +335,8 @@ export default defineNuxtComponent({
|
||||
ModalCreation,
|
||||
Multiselect,
|
||||
CopyCode,
|
||||
ArrowIcon,
|
||||
AscendingIcon,
|
||||
DescendingIcon,
|
||||
},
|
||||
async setup() {
|
||||
const user = await useUser()
|
||||
@@ -387,13 +394,7 @@ export default defineNuxtComponent({
|
||||
switch (sort) {
|
||||
case 'Name':
|
||||
sortedArray = projects.slice().sort((a, b) => {
|
||||
if (a.title < b.title) {
|
||||
return -1
|
||||
}
|
||||
if (a.title > b.title) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
return a.title.localeCompare(b.title)
|
||||
})
|
||||
break
|
||||
case 'Status':
|
||||
@@ -633,6 +634,7 @@ export default defineNuxtComponent({
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-md);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.small-select {
|
||||
|
||||
15
pages/dashboard/report/[id].vue
Normal file
15
pages/dashboard/report/[id].vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<ReportView
|
||||
:report-id="route.params.id"
|
||||
:breadcrumbs-stack="[{ href: '/dashboard/reports', label: 'Active reports' }]"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import ReportView from '~/components/ui/report/ReportView.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
title: `Report ${route.params.id} - Modrinth`,
|
||||
})
|
||||
</script>
|
||||
16
pages/dashboard/reports.vue
Normal file
16
pages/dashboard/reports.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Reports you've filed</h2>
|
||||
<ReportsList />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import ReportsList from '~/components/ui/report/ReportsList.vue'
|
||||
|
||||
useHead({
|
||||
title: 'Active reports - Modrinth',
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -20,19 +20,6 @@
|
||||
>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()">
|
||||
<TransferIcon /> Transfer to
|
||||
{{ $formatWallet(auth.user.payout_data.payout_wallet) }}
|
||||
</button>
|
||||
<NuxtLink class="iconified-button" to="/dashboard/revenue/transfers">
|
||||
<HistoryIcon /> View transfer history
|
||||
</NuxtLink>
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Monetization settings
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else-if="auth.user.payout_data.balance > 0">
|
||||
You have made
|
||||
@@ -49,6 +36,22 @@
|
||||
<SettingsIcon /> Enroll in the Creator Monetization Program
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div v-if="enrolled" class="input-group">
|
||||
<button
|
||||
v-if="auth.user.payout_data.balance >= minWithdraw"
|
||||
class="iconified-button brand-button"
|
||||
@click="$refs.modal_transfer.show()"
|
||||
>
|
||||
<TransferIcon /> Transfer to
|
||||
{{ $formatWallet(auth.user.payout_data.payout_wallet) }}
|
||||
</button>
|
||||
<NuxtLink class="iconified-button" to="/dashboard/revenue/transfers">
|
||||
<HistoryIcon /> View transfer history
|
||||
</NuxtLink>
|
||||
<NuxtLink class="iconified-button" to="/settings/monetization">
|
||||
<SettingsIcon /> Monetization settings
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</section>
|
||||
<section class="universal-card">
|
||||
<h2>Processing fees</h2>
|
||||
|
||||
@@ -41,11 +41,9 @@ useHead({
|
||||
const auth = await useAuth()
|
||||
const app = useNuxtApp()
|
||||
|
||||
const [raw] = await Promise.all([
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`, app.$defaultHeaders()),
|
||||
])
|
||||
|
||||
const payouts = ref(raw)
|
||||
const { data: payouts } = await useAsyncData(`user/${auth.value.user.id}/payouts`, () =>
|
||||
useBaseFetch(`user/${auth.value.user.id}/payouts`, app.$defaultHeaders())
|
||||
)
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.grid-table {
|
||||
|
||||
Reference in New Issue
Block a user