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:
Prospector
2023-07-15 20:39:33 -07:00
committed by GitHub
parent 1a2b45eebd
commit a5613ebb10
67 changed files with 3613 additions and 776 deletions

View 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>

View File

@@ -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>

View 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>

View File

@@ -0,0 +1 @@
<template><div /></template>

View File

@@ -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 {

View 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>

View 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>

View File

@@ -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>

View File

@@ -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 {