diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue index 22c9580aed..c5c4d3112a 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue @@ -10,7 +10,7 @@ >
@@ -99,7 +99,7 @@ > diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartEvents.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartEvents.vue index cd3a957660..7ee3e7d7af 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartEvents.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartEvents.vue @@ -132,7 +132,7 @@ :href="event.announcement_url" target="_blank" rel="noopener noreferrer" - class="mt-1.5 inline-flex items-center gap-1 text-sm font-medium text-primary underline !transition-all hover:text-contrast" + class="my-0.5 inline-flex items-center gap-1 text-xs font-medium text-primary underline !transition-all hover:text-contrast" > {{ formatMessage(analyticsChartMessages.seeAnnouncement) }} diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue index 8f70acc3b7..c51fda5d59 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue @@ -18,7 +18,7 @@ ({{ durationLabel }}) - + {{ previousRangeLabel }} {{ formatMessage(analyticsChartMessages.previousPeriodShort) }} @@ -197,6 +197,7 @@ function getEntryAriaLabel(entry: AnalyticsChartTooltipEntry) { const ONE_DAY_MS = 24 * 60 * 60 * 1000 const ONE_HOUR_MS = 60 * 60 * 1000 const ONE_MINUTE_MS = 60 * 1000 +const DATE_LOCALE = 'en-US' function formatRangeLabel( start: Date, @@ -225,13 +226,13 @@ function formatRangeLabel( } if (includeTime) { - const startLabel = new Intl.DateTimeFormat(undefined, startOptions).format(start) - const endLabel = new Intl.DateTimeFormat(undefined, timeOptions).format(end) + const startLabel = new Intl.DateTimeFormat(DATE_LOCALE, startOptions).format(start) + const endLabel = new Intl.DateTimeFormat(DATE_LOCALE, timeOptions).format(end) const range = `${startLabel}–${endLabel}` if (!showTrailingYear) return range - const yearLabel = new Intl.DateTimeFormat(undefined, { year: 'numeric' }).format(end) + const yearLabel = new Intl.DateTimeFormat(DATE_LOCALE, { year: 'numeric' }).format(end) return `${range}, ${yearLabel}` } @@ -244,13 +245,13 @@ function formatRangeLabel( endOptions = { day: 'numeric' } } - const startLabel = new Intl.DateTimeFormat(undefined, startOptions).format(start) - const endLabel = new Intl.DateTimeFormat(undefined, endOptions).format(end) + const startLabel = new Intl.DateTimeFormat(DATE_LOCALE, startOptions).format(start) + const endLabel = new Intl.DateTimeFormat(DATE_LOCALE, endOptions).format(end) const range = `${startLabel}–${endLabel}` if (!showTrailingYear) return range - const yearLabel = new Intl.DateTimeFormat(undefined, { year: 'numeric' }).format(end) + const yearLabel = new Intl.DateTimeFormat(DATE_LOCALE, { year: 'numeric' }).format(end) return `${range}, ${yearLabel}` } diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-events.ts b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-events.ts index 4ac5d3e9d2..c0ca6489c1 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-events.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-events.ts @@ -40,7 +40,6 @@ export function useAnalyticsChartEvents( placeholderData: [], refetchOnMount: 'always', retry: false, - staleTime: 0, }) const localAnalyticsChartEvents = computed(() => analyticsEvents.value ?? []) diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts index 2e5420755f..a472c532c0 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts @@ -109,7 +109,7 @@ function getRegionDisplayNames(locale: string): Intl.DisplayNames | null { function formatCountryCode(countryCode: string, formatMessage: FormatMessage): string { const normalized = countryCode.trim().toUpperCase() if (normalized === OTHER_COUNTRY_CODE) { - return formatMessage(analyticsMessages.unknown) + return formatMessage(analyticsMessages.other) } if (!REGION_CODE_PATTERN.test(normalized)) { @@ -146,6 +146,9 @@ export function formatBreakdownLabel( normalizedLowercaseValue === 'other' || normalizedLowercaseValue === 'unknown' ) { + if (selectedBreakdown === 'country') { + return formatMessage(analyticsMessages.other) + } return formatMessage(analyticsMessages.unknown) } if (selectedBreakdown === 'country') { @@ -753,7 +756,7 @@ export function formatMetricValue( case 'playtime': { const hours = value / 3600 return formatMessage(analyticsStatCardMessages.playtimeHours, { - hours: hours.toFixed(1), + hours: Math.abs(hours) < 1 ? hours.toFixed(2) : hours.toFixed(1), }) } case 'views': @@ -770,7 +773,11 @@ function formatSmallAxisNumber(value: number): string { } const formattedValue = Math.abs(value) < 1 ? value.toFixed(2) : value.toFixed(1) - return formattedValue.replace(/\.?0+$/, '') + return trimTrailingFractionZeros(formattedValue) +} + +function trimTrailingFractionZeros(value: string): string { + return value.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '') } const COMPACT_AXIS_UNITS = [ @@ -814,7 +821,7 @@ function formatCompactAxisValue(value: number): string { return String(truncatedValue) } - return roundedValue.toFixed(fractionDigitCount).replace(/\.?0+$/, '') + return trimTrailingFractionZeros(roundedValue.toFixed(fractionDigitCount)) } export function formatAxisValue( diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts b/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts index 460151bde8..e16df0e504 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts @@ -224,6 +224,19 @@ export const analyticsGraphTitleMessages = defineMessages({ }) export const analyticsStatCardMessages = defineMessages({ + monetizationBannerTitle: { + id: 'analytics.stat.monetization-banner.title', + defaultMessage: 'How does monetization work?', + }, + monetizationBannerBody: { + id: 'analytics.stat.monetization-banner.body', + defaultMessage: + 'Only views and downloads made through Modrinth are eligible for monetization and must pass fraud-prevention filtering. Modrinth App downloads also require the user to be logged in. Because all projects have a similar ratio of monetized downloads, your revenue would not meaningfully change if all downloads were counted.', + }, + monetizationBannerLearnMore: { + id: 'analytics.stat.monetization-banner.learn-more', + defaultMessage: 'Learn more', + }, revenueValue: { id: 'analytics.stat.revenue-value', defaultMessage: '${value}', diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts index 2ed74ec540..1f5f414d02 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts @@ -66,6 +66,7 @@ export function buildAnalyticsTableColumns({ key: getAnalyticsTableBreakdownColumnKey(breakdown), label: getAnalyticsTableBreakdownColumnLabel(breakdown, formatMessage), enableSorting: true, + width: breakdown === 'project' ? '25%' : undefined, }) } } @@ -75,6 +76,7 @@ export function buildAnalyticsTableColumns({ key: 'project', label: formatAnalyticsBreakdownLabel('project', formatMessage), enableSorting: true, + width: '25%', }) } diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-formatting.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-formatting.ts index e233ef544d..256d57c543 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-formatting.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-formatting.ts @@ -40,10 +40,12 @@ export function formatAnalyticsTableCompactPlaytime( formatMessage: FormatMessage, ): string { const totalSeconds = Math.max(0, Math.round(value)) + const hours = totalSeconds / SECONDS_PER_HOUR + const fractionDigits = hours < 1 ? 2 : 1 return formatMessage(analyticsStatCardMessages.playtimeHours, { - hours: (totalSeconds / SECONDS_PER_HOUR).toLocaleString(undefined, { - minimumFractionDigits: 1, - maximumFractionDigits: 1, + hours: hours.toLocaleString(undefined, { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits, }), }) } diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts index 714506a0c3..04c7d0a12e 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts @@ -12,6 +12,7 @@ import { getSliceCount, } from '../analytics-chart/analytics-chart-utils' import type { FormatMessage } from '../analytics-messages' +import { analyticsMessages } from '../analytics-messages' import { ALL_BREAKDOWN_VALUE, COMBINED_BREAKDOWN_LABEL_SEPARATOR, @@ -64,6 +65,8 @@ export function buildAnalyticsTableRows({ const timeRange = fetchRequest.time_range const sliceCount = getSliceCount(timeRange, timeSlices.length) + const currentTimeSlices = + timeSlices.length > sliceCount ? timeSlices.slice(timeSlices.length - sliceCount) : timeSlices const includeDate = mode === 'date_breakdown' const breakdownDisplayValues = new Map