feat: various analytics updates (#6330)

* feat: add button to view user analytics

* feat: add "Your projects" preset selection

* feat: fix revenue rounding for values under 1 and show full values for all statcards with tooltip

* fix: sum rounded value instead of raw value for tooltip total if it's under 1

* fix: show decimal in playtime statcard if under 1 hrs

* feat: disable playtime statcard for purely plugin projects

* refactor: pnpm prepr
This commit is contained in:
Truman Gao
2026-06-08 22:03:28 +00:00
committed by GitHub
parent a92b5b08df
commit 2c9bf58d1f
12 changed files with 491 additions and 51 deletions
@@ -38,7 +38,8 @@
<div class="flex flex-col gap-2.5">
<div
class="text-2xl font-semibold leading-none md:text-4xl"
v-tooltip="!disabled ? statTooltip : undefined"
class="w-fit text-2xl font-semibold leading-none md:text-4xl"
:class="{
'text-primary': disabled,
'text-contrast': !disabled,
@@ -114,6 +115,7 @@ import { analyticsStatCardMessages } from '../analytics-messages'
const props = defineProps<{
label: string
statLabel: string
statTooltip?: string
vsPrevPeriodPercent: string | null
icon: string
active?: boolean
@@ -26,6 +26,7 @@
:key="card.key"
:label="card.label"
:stat-label="card.statLabel"
:stat-tooltip="card.statTooltip"
:vs-prev-period-percent="card.vsPrevPeriodPercent"
:icon="card.icon"
:active="activeStat === card.key"
@@ -47,6 +48,7 @@ import {
} from '~/providers/analytics/analytics'
import { analyticsStatCardMessages, formatAnalyticsStatLabel } from '../analytics-messages.ts'
import { formatAnalyticsTableFullPlaytime } from '../analytics-table/analytics-table-formatting.ts'
import StatCard from './StatCard.vue'
const MONETIZATION_BANNER_DISMISSED_KEY = 'analytics-monetization-banner-dismissed'
@@ -77,6 +79,38 @@ const compactNumberFormatter = computed(
}),
)
const underDollarRevenueFormatter = computed(
() =>
new Intl.NumberFormat(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}),
)
const preciseRevenueFormatter = computed(
() =>
new Intl.NumberFormat(undefined, {
minimumFractionDigits: 5,
maximumFractionDigits: 5,
}),
)
const tooltipRevenueFormatter = computed(
() =>
new Intl.NumberFormat(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}),
)
const underHourPlaytimeFormatter = computed(
() =>
new Intl.NumberFormat(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}),
)
function formatStatNumber(value: number): string {
const rounded = Math.round(value)
@@ -87,6 +121,45 @@ function formatStatNumber(value: number): string {
return formatNumber(rounded)
}
function formatFullStatNumber(value: number): string {
return formatNumber(Math.round(value))
}
function formatRevenueNumber(value: number): string {
if (Math.abs(value) > 0 && Math.abs(value) < 1) {
return underDollarRevenueFormatter.value.format(value)
}
return formatStatNumber(value)
}
function formatRevenueValue(value: number): string {
return formatMessage(analyticsStatCardMessages.revenueValue, {
value: formatRevenueNumber(value),
})
}
function formatPreciseRevenueValue(value: number): string {
return formatMessage(analyticsStatCardMessages.revenueValue, {
value:
Math.abs(value) < 1
? preciseRevenueFormatter.value.format(value)
: tooltipRevenueFormatter.value.format(value),
})
}
function formatPlaytimeTooltip(value: number): string {
return formatAnalyticsTableFullPlaytime(value, formatMessage)
}
function formatPlaytimeNumber(value: number): string {
if (Math.abs(value) > 0 && Math.abs(value) < 1) {
return underHourPlaytimeFormatter.value.format(value)
}
return formatStatNumber(value)
}
function formatPercent(value: number): string {
const rounded = Math.round(value * 10) / 10
if (rounded === 0) {
@@ -105,7 +178,7 @@ function formatSignedStatNumber(value: number): string {
function formatSignedRevenue(value: number): string {
const signPrefix = value > 0 ? '+' : value < 0 ? '-' : ''
return `${signPrefix}${formatMessage(analyticsStatCardMessages.revenueValue, {
value: formatStatNumber(Math.abs(value)),
value: formatRevenueNumber(Math.abs(value)),
})}`
}
@@ -169,6 +242,7 @@ const statCards = computed<
key: AnalyticsDashboardStat
label: string
statLabel: string
statTooltip?: string
vsPrevPeriodPercent: string | null
icon: string
disabled: boolean
@@ -178,6 +252,7 @@ const statCards = computed<
key: 'views',
label: formatAnalyticsStatLabel('views', formatMessage),
statLabel: formatStatNumber(currentTotals.value.views),
statTooltip: formatFullStatNumber(currentTotals.value.views),
vsPrevPeriodPercent: formatPreviousPeriodComparison(
'views',
percentChanges.value.views,
@@ -191,6 +266,7 @@ const statCards = computed<
key: 'downloads',
label: formatAnalyticsStatLabel('downloads', formatMessage),
statLabel: formatStatNumber(currentTotals.value.downloads),
statTooltip: formatFullStatNumber(currentTotals.value.downloads),
vsPrevPeriodPercent: formatPreviousPeriodComparison(
'downloads',
percentChanges.value.downloads,
@@ -203,9 +279,8 @@ const statCards = computed<
{
key: 'revenue',
label: formatAnalyticsStatLabel('revenue', formatMessage),
statLabel: formatMessage(analyticsStatCardMessages.revenueValue, {
value: formatStatNumber(currentTotals.value.revenue),
}),
statLabel: formatRevenueValue(currentTotals.value.revenue),
statTooltip: formatPreciseRevenueValue(currentTotals.value.revenue),
vsPrevPeriodPercent: formatPreviousPeriodComparison(
'revenue',
percentChanges.value.revenue,
@@ -219,8 +294,9 @@ const statCards = computed<
key: 'playtime',
label: formatAnalyticsStatLabel('playtime', formatMessage),
statLabel: formatMessage(analyticsStatCardMessages.playtimeHours, {
hours: formatStatNumber(currentTotals.value.playtime / 3600),
hours: formatPlaytimeNumber(currentTotals.value.playtime / 3600),
}),
statTooltip: formatPlaytimeTooltip(currentTotals.value.playtime),
vsPrevPeriodPercent: formatPreviousPeriodComparison(
'playtime',
percentChanges.value.playtime,