You've already forked AstralRinth
fix: analytics post release bugs (#6291)
* fix: previous period data was included in the table * fix: revenue displaying stale data when viewing it from different metric and grouped by 6 hour or 1 hour * fix: remove staletime on analytics query so switching tabs does not refersh query * feat: add monetization alert * fix-small: missing space in tooltip * fix: incorrect y-axis formatting for trailing decimal 0s * fix: switching tabs resets table series selection due to other refetches * fix: always show month first in chart tooltip * fix: change all time start date to be project published date * fix: increase length on project name column * fix: unknown download source data points not showing for download source breakdown * fix: double unknown for loader * fix: no data on country labeling incorrectly as "Unknown" instead of "Other" * fix: date picker number inputs showing arrows * fix: stat card showing enormous percentage for prev period by switching it to absolute value difference after 1000% * fix: decimal values for playtime being rounded badly, resulting in 0.04 becoming 0.0 * fix: chips having stroke * refactor: pnpm prepr * fix: spacing in annoucement link * fix: legend scroll shadow on top of event tooltip
This commit is contained in:
+2
-2
@@ -10,7 +10,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="showLegendTopFade"
|
||||
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-5 bg-gradient-to-b from-surface-3 to-transparent"
|
||||
class="z-1 pointer-events-none absolute left-0 right-0 top-0 h-5 bg-gradient-to-b from-surface-3 to-transparent"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="showLegendBottomFade"
|
||||
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-5 bg-gradient-to-t from-surface-3 to-transparent"
|
||||
class="z-1 pointer-events-none absolute bottom-0 left-0 right-0 h-5 bg-gradient-to-t from-surface-3 to-transparent"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -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) }}
|
||||
<ExternalIcon class="size-3.5" aria-hidden="true" />
|
||||
|
||||
+8
-7
@@ -18,7 +18,7 @@
|
||||
({{ durationLabel }})
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="previousRangeLabel" class="min-w-0 truncate text-xs text-primary">
|
||||
<span v-if="previousRangeLabel" class="min-w-0 space-x-1 truncate text-xs text-primary">
|
||||
<span class="font-medium">{{ previousRangeLabel }}</span>
|
||||
<span class="font-normal text-secondary">
|
||||
{{ 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}`
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -40,7 +40,6 @@ export function useAnalyticsChartEvents(
|
||||
placeholderData: [],
|
||||
refetchOnMount: 'always',
|
||||
retry: false,
|
||||
staleTime: 0,
|
||||
})
|
||||
|
||||
const localAnalyticsChartEvents = computed(() => analyticsEvents.value ?? [])
|
||||
|
||||
+11
-4
@@ -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(
|
||||
|
||||
@@ -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}',
|
||||
|
||||
+2
@@ -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%',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -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,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
+17
-1
@@ -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<string, string>()
|
||||
const projectDisplayValues = new Map<string, string>()
|
||||
@@ -119,9 +122,22 @@ export function buildAnalyticsTableRows({
|
||||
}
|
||||
|
||||
function getCombinedBreakdownDisplay(displays: AnalyticsTableBreakdownDisplayValues) {
|
||||
const unknownBreakdownLabel = formatMessage(analyticsMessages.unknown)
|
||||
let hasUnknownBreakdownLabel = false
|
||||
|
||||
return selectedBreakdowns
|
||||
.map((breakdown) => displays[breakdown])
|
||||
.filter((displayValue): displayValue is string => Boolean(displayValue))
|
||||
.filter((displayValue) => {
|
||||
if (displayValue !== unknownBreakdownLabel) {
|
||||
return true
|
||||
}
|
||||
if (hasUnknownBreakdownLabel) {
|
||||
return false
|
||||
}
|
||||
hasUnknownBreakdownLabel = true
|
||||
return true
|
||||
})
|
||||
.join(COMBINED_BREAKDOWN_LABEL_SEPARATOR)
|
||||
}
|
||||
|
||||
@@ -184,7 +200,7 @@ export function buildAnalyticsTableRows({
|
||||
}
|
||||
}
|
||||
|
||||
timeSlices.forEach((slice, sliceIndex) => {
|
||||
currentTimeSlices.forEach((slice, sliceIndex) => {
|
||||
const bucketLabel = includeDate ? getBucketLabel(sliceIndex) : undefined
|
||||
|
||||
for (const point of slice) {
|
||||
|
||||
@@ -30,9 +30,10 @@ export function getAnalyticsBreakdownValue(
|
||||
case 'user_agent': {
|
||||
const downloadSource = normalizeBreakdownValue(
|
||||
'user_agent' in point ? point.user_agent : undefined,
|
||||
UNKNOWN_BREAKDOWN_VALUE,
|
||||
)
|
||||
return downloadSource === ALL_BREAKDOWN_VALUE
|
||||
? ALL_BREAKDOWN_VALUE
|
||||
return downloadSource === UNKNOWN_BREAKDOWN_VALUE
|
||||
? UNKNOWN_BREAKDOWN_VALUE
|
||||
: getDownloadSourceLabel(downloadSource, formatMessage)
|
||||
}
|
||||
case 'download_reason':
|
||||
@@ -98,5 +99,12 @@ function normalizeBreakdownValue(
|
||||
fallback = ALL_BREAKDOWN_VALUE,
|
||||
): string {
|
||||
const normalized = value?.trim()
|
||||
const normalizedLowercase = normalized?.toLowerCase()
|
||||
if (
|
||||
fallback === UNKNOWN_BREAKDOWN_VALUE &&
|
||||
(normalizedLowercase === 'unknown' || normalizedLowercase === 'other')
|
||||
) {
|
||||
return fallback
|
||||
}
|
||||
return normalized && normalized.length > 0 ? normalized : fallback
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ const {
|
||||
selectedCustomTimeframeEndDate,
|
||||
selectedGroupBy,
|
||||
queryRefreshTimestamp,
|
||||
analyticsAllTimeStartDate,
|
||||
refreshAnalyticsQuery,
|
||||
} = injectAnalyticsDashboardContext()
|
||||
|
||||
@@ -107,6 +108,7 @@ function handleTimeframeDraftChange(selection: TimeFramePickerSelection) {
|
||||
customStartDate: selection.customStartDate,
|
||||
customEndDate: selection.customEndDate,
|
||||
nowTimestamp: queryRefreshTimestamp.value,
|
||||
allTimeStartDate: analyticsAllTimeStartDate.value,
|
||||
})
|
||||
const { start, end } = ensureMinimumTimeRange(range.start, range.end)
|
||||
const durationMinutes = Math.max(1, Math.floor((end.getTime() - start.getTime()) / 60000))
|
||||
|
||||
@@ -894,12 +894,6 @@ function isRevenueHourlyGroupBy(groupBy: AnalyticsGroupByPreset): boolean {
|
||||
return groupBy === '1h' || groupBy === '6h'
|
||||
}
|
||||
|
||||
function getAllTimeYearGroupStart(end: Date): Date {
|
||||
const start = new Date(end)
|
||||
start.setFullYear(2021)
|
||||
return start
|
||||
}
|
||||
|
||||
const groupByOptions = computed<ComboboxOption<AnalyticsGroupByPreset>[]>(() => {
|
||||
const timeframeMinutes = selectedTimeframeDurationMinutes.value
|
||||
const options = groupByPresetOptions.map((option) => {
|
||||
@@ -1159,13 +1153,7 @@ function buildMetricFilters(
|
||||
|
||||
const fetchRequest = computed<Labrinth.Analytics.v3.FetchRequest>(() => {
|
||||
const rawRange = selectedTimeRange.value
|
||||
const rawStart =
|
||||
selectedTimeframeMode.value === 'preset' &&
|
||||
selectedTimeframe.value === 'all_time' &&
|
||||
selectedGroupBy.value === 'year'
|
||||
? getAllTimeYearGroupStart(rawRange.end)
|
||||
: rawRange.start
|
||||
const { start, end } = ensureMinimumTimeRange(rawStart, rawRange.end)
|
||||
const { start, end } = ensureMinimumTimeRange(rawRange.start, rawRange.end)
|
||||
|
||||
const groupByMs = getAnalyticsGroupByPresetMinutes(selectedGroupBy.value) * 60 * 1000
|
||||
const desiredSlices = Math.max(1, Math.floor((end.getTime() - start.getTime()) / groupByMs))
|
||||
|
||||
@@ -84,6 +84,7 @@ function subtractCalendarMonths(date: Date, months: number): Date {
|
||||
export function getTimeRangeForPreset(
|
||||
preset: AnalyticsTimeframePreset,
|
||||
nowTimestamp: number,
|
||||
allTimeStartDate: Date = new Date(Date.UTC(2023, 0, 1, 0, 0, 0, 0)),
|
||||
): AnalyticsTimeRange {
|
||||
const now = getRoundedNow(nowTimestamp)
|
||||
const end = new Date(now)
|
||||
@@ -130,7 +131,7 @@ export function getTimeRangeForPreset(
|
||||
}
|
||||
case 'all_time':
|
||||
return {
|
||||
start: new Date(Date.UTC(2023, 0, 1, 0, 0, 0, 0)),
|
||||
start: new Date(allTimeStartDate),
|
||||
end,
|
||||
}
|
||||
default:
|
||||
@@ -193,6 +194,7 @@ export function getAnalyticsTimeRange({
|
||||
customStartDate,
|
||||
customEndDate,
|
||||
nowTimestamp,
|
||||
allTimeStartDate,
|
||||
}: {
|
||||
mode: AnalyticsTimeframeMode
|
||||
preset: AnalyticsTimeframePreset
|
||||
@@ -201,6 +203,7 @@ export function getAnalyticsTimeRange({
|
||||
customStartDate: string
|
||||
customEndDate: string
|
||||
nowTimestamp: number
|
||||
allTimeStartDate?: Date
|
||||
}): AnalyticsTimeRange {
|
||||
switch (mode) {
|
||||
case 'last':
|
||||
@@ -211,7 +214,7 @@ export function getAnalyticsTimeRange({
|
||||
return getTimeRangeForCustomDateTimeRange(customStartDate, customEndDate)
|
||||
case 'preset':
|
||||
default:
|
||||
return getTimeRangeForPreset(preset, nowTimestamp)
|
||||
return getTimeRangeForPreset(preset, nowTimestamp, allTimeStartDate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +272,7 @@ export function useSelectedAnalyticsTimeRange() {
|
||||
selectedCustomTimeframeStartDate,
|
||||
selectedCustomTimeframeEndDate,
|
||||
queryRefreshTimestamp,
|
||||
analyticsAllTimeStartDate,
|
||||
} = injectAnalyticsDashboardContext()
|
||||
|
||||
const selectedTimeRange = computed(() =>
|
||||
@@ -280,6 +284,7 @@ export function useSelectedAnalyticsTimeRange() {
|
||||
customStartDate: selectedCustomTimeframeStartDate.value,
|
||||
customEndDate: selectedCustomTimeframeEndDate.value,
|
||||
nowTimestamp: queryRefreshTimestamp.value,
|
||||
allTimeStartDate: analyticsAllTimeStartDate.value,
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<StatCard
|
||||
v-for="card in statCards"
|
||||
:key="card.key"
|
||||
:label="card.label"
|
||||
:stat-label="card.statLabel"
|
||||
:vs-prev-period-percent="card.vsPrevPeriodPercent"
|
||||
:icon="card.icon"
|
||||
:active="activeStat === card.key"
|
||||
:disabled="card.disabled"
|
||||
@click="setActiveStat(card.key)"
|
||||
/>
|
||||
<div class="flex w-full flex-col gap-3">
|
||||
<Admonition
|
||||
v-if="showMonetizationBanner"
|
||||
type="info"
|
||||
:header="formatMessage(analyticsStatCardMessages.monetizationBannerTitle)"
|
||||
show-actions-underneath
|
||||
dismissible
|
||||
@dismiss="dismissMonetizationBanner"
|
||||
>
|
||||
<div class="text-primary">
|
||||
{{ formatMessage(analyticsStatCardMessages.monetizationBannerBody) }}
|
||||
</div>
|
||||
<template #actions>
|
||||
<ButtonStyled color="blue">
|
||||
<a href="https://modrinth.com/legal/cmp-info" target="_blank" class="w-fit !px-4">
|
||||
{{ formatMessage(analyticsStatCardMessages.monetizationBannerLearnMore) }}
|
||||
<RightArrowIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</Admonition>
|
||||
<div class="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<StatCard
|
||||
v-for="card in statCards"
|
||||
:key="card.key"
|
||||
:label="card.label"
|
||||
:stat-label="card.statLabel"
|
||||
:vs-prev-period-percent="card.vsPrevPeriodPercent"
|
||||
:icon="card.icon"
|
||||
:active="activeStat === card.key"
|
||||
:disabled="card.disabled"
|
||||
@click="setActiveStat(card.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFormatNumber, useVIntl } from '@modrinth/ui'
|
||||
import { RightArrowIcon } from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled, useFormatNumber, useVIntl } from '@modrinth/ui'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
type AnalyticsDashboardStat,
|
||||
@@ -25,10 +49,13 @@ import {
|
||||
import { analyticsStatCardMessages, formatAnalyticsStatLabel } from '../analytics-messages.ts'
|
||||
import StatCard from './StatCard.vue'
|
||||
|
||||
const MONETIZATION_BANNER_DISMISSED_KEY = 'analytics-monetization-banner-dismissed'
|
||||
|
||||
const {
|
||||
activeStat,
|
||||
setActiveStat,
|
||||
currentTotals,
|
||||
previousTotals,
|
||||
percentChanges,
|
||||
hasPreviousPeriodComparison,
|
||||
selectedBreakdowns,
|
||||
@@ -36,6 +63,11 @@ const {
|
||||
} = injectAnalyticsDashboardContext()
|
||||
const formatNumber = useFormatNumber()
|
||||
const { formatMessage } = useVIntl()
|
||||
const monetizationBannerDismissed = useLocalStorage(MONETIZATION_BANNER_DISMISSED_KEY, false)
|
||||
const showMonetizationBanner = computed(
|
||||
() => selectedBreakdowns.value.includes('monetization') && !monetizationBannerDismissed.value,
|
||||
)
|
||||
const MAX_PREVIOUS_PERIOD_PERCENT_DISPLAY = 1000
|
||||
|
||||
const compactNumberFormatter = computed(
|
||||
() =>
|
||||
@@ -57,16 +89,79 @@ function formatStatNumber(value: number): string {
|
||||
|
||||
function formatPercent(value: number): string {
|
||||
const rounded = Math.round(value * 10) / 10
|
||||
if (rounded === 0) {
|
||||
return '0%'
|
||||
}
|
||||
|
||||
const signPrefix = rounded > 0 ? '+' : ''
|
||||
return `${signPrefix}${rounded.toFixed(1)}%`
|
||||
}
|
||||
|
||||
function formatPreviousPeriodPercent(value: number): string | null {
|
||||
function formatSignedStatNumber(value: number): string {
|
||||
const signPrefix = value > 0 ? '+' : ''
|
||||
return `${signPrefix}${formatStatNumber(value)}`
|
||||
}
|
||||
|
||||
function formatSignedRevenue(value: number): string {
|
||||
const signPrefix = value > 0 ? '+' : value < 0 ? '-' : ''
|
||||
return `${signPrefix}${formatMessage(analyticsStatCardMessages.revenueValue, {
|
||||
value: formatStatNumber(Math.abs(value)),
|
||||
})}`
|
||||
}
|
||||
|
||||
function formatSignedPlaytimeHours(value: number): string {
|
||||
const rounded = Math.round(value * 10) / 10
|
||||
if (rounded === 0) {
|
||||
return '0'
|
||||
}
|
||||
|
||||
if (Math.abs(rounded) >= 1000) {
|
||||
const signPrefix = rounded > 0 ? '+' : ''
|
||||
return `${signPrefix}${compactNumberFormatter.value.format(rounded)}`
|
||||
}
|
||||
|
||||
const signPrefix = rounded > 0 ? '+' : ''
|
||||
return `${signPrefix}${rounded.toFixed(1)}`
|
||||
}
|
||||
|
||||
function formatSignedPlaytime(value: number): string {
|
||||
return formatMessage(analyticsStatCardMessages.playtimeHours, {
|
||||
hours: formatSignedPlaytimeHours(value / 3600),
|
||||
})
|
||||
}
|
||||
|
||||
function formatPreviousPeriodComparison(
|
||||
stat: AnalyticsDashboardStat,
|
||||
percentChange: number,
|
||||
currentValue: number,
|
||||
previousValue: number,
|
||||
): string | null {
|
||||
if (!hasPreviousPeriodComparison.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return formatPercent(value)
|
||||
const delta = currentValue - previousValue
|
||||
if (previousValue === 0 && currentValue === 0) {
|
||||
return formatPercent(percentChange)
|
||||
}
|
||||
|
||||
if (previousValue !== 0 && Math.abs(percentChange) <= MAX_PREVIOUS_PERIOD_PERCENT_DISPLAY) {
|
||||
return formatPercent(percentChange)
|
||||
}
|
||||
|
||||
switch (stat) {
|
||||
case 'revenue':
|
||||
return formatSignedRevenue(delta)
|
||||
case 'playtime':
|
||||
return formatSignedPlaytime(delta)
|
||||
case 'views':
|
||||
case 'downloads':
|
||||
return formatSignedStatNumber(delta)
|
||||
}
|
||||
}
|
||||
|
||||
function dismissMonetizationBanner() {
|
||||
monetizationBannerDismissed.value = true
|
||||
}
|
||||
|
||||
const statCards = computed<
|
||||
@@ -83,7 +178,12 @@ const statCards = computed<
|
||||
key: 'views',
|
||||
label: formatAnalyticsStatLabel('views', formatMessage),
|
||||
statLabel: formatStatNumber(currentTotals.value.views),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodPercent(percentChanges.value.views),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodComparison(
|
||||
'views',
|
||||
percentChanges.value.views,
|
||||
currentTotals.value.views,
|
||||
previousTotals.value.views,
|
||||
),
|
||||
icon: 'eye',
|
||||
disabled: !isAnalyticsDashboardStatRelevant('views', selectedBreakdowns.value),
|
||||
},
|
||||
@@ -91,7 +191,12 @@ const statCards = computed<
|
||||
key: 'downloads',
|
||||
label: formatAnalyticsStatLabel('downloads', formatMessage),
|
||||
statLabel: formatStatNumber(currentTotals.value.downloads),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodPercent(percentChanges.value.downloads),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodComparison(
|
||||
'downloads',
|
||||
percentChanges.value.downloads,
|
||||
currentTotals.value.downloads,
|
||||
previousTotals.value.downloads,
|
||||
),
|
||||
icon: 'download',
|
||||
disabled: !isAnalyticsDashboardStatRelevant('downloads', selectedBreakdowns.value),
|
||||
},
|
||||
@@ -101,7 +206,12 @@ const statCards = computed<
|
||||
statLabel: formatMessage(analyticsStatCardMessages.revenueValue, {
|
||||
value: formatStatNumber(currentTotals.value.revenue),
|
||||
}),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodPercent(percentChanges.value.revenue),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodComparison(
|
||||
'revenue',
|
||||
percentChanges.value.revenue,
|
||||
currentTotals.value.revenue,
|
||||
previousTotals.value.revenue,
|
||||
),
|
||||
icon: 'dollar',
|
||||
disabled: !isAnalyticsDashboardStatRelevant('revenue', selectedBreakdowns.value),
|
||||
},
|
||||
@@ -111,7 +221,12 @@ const statCards = computed<
|
||||
statLabel: formatMessage(analyticsStatCardMessages.playtimeHours, {
|
||||
hours: formatStatNumber(currentTotals.value.playtime / 3600),
|
||||
}),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodPercent(percentChanges.value.playtime),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodComparison(
|
||||
'playtime',
|
||||
percentChanges.value.playtime,
|
||||
currentTotals.value.playtime,
|
||||
previousTotals.value.playtime,
|
||||
),
|
||||
icon: 'clock',
|
||||
disabled: !isAnalyticsDashboardStatRelevant('playtime', selectedBreakdowns.value),
|
||||
},
|
||||
|
||||
@@ -368,6 +368,15 @@
|
||||
"analytics.stat.downloads": {
|
||||
"message": "Downloads"
|
||||
},
|
||||
"analytics.stat.monetization-banner.body": {
|
||||
"message": "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."
|
||||
},
|
||||
"analytics.stat.monetization-banner.learn-more": {
|
||||
"message": "Learn more"
|
||||
},
|
||||
"analytics.stat.monetization-banner.title": {
|
||||
"message": "How does monetization work?"
|
||||
},
|
||||
"analytics.stat.playtime": {
|
||||
"message": "Playtime"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
|
||||
const ANALYTICS_START_TIMESTAMP = '2023-01-01T00:00:00.000Z'
|
||||
export const ANALYTICS_START_DATE_INPUT_VALUE = ANALYTICS_START_TIMESTAMP.slice(0, 10)
|
||||
const ANALYTICS_START_TIME = new Date(ANALYTICS_START_TIMESTAMP).getTime()
|
||||
export const ANALYTICS_START_TIME = new Date(ANALYTICS_START_TIMESTAMP).getTime()
|
||||
export const REVENUE_MIN_TIMEFRAME_MS = 1 * 24 * 60 * 60 * 1000 // need at least 1 day in timeframe range to show revenue
|
||||
const ANALYTICS_DAY_MS = 24 * 60 * 60 * 1000
|
||||
const ANALYTICS_MAX_TIME_SLICES = 256 // controls granularity allowed in "group by" for timeframe ranges
|
||||
@@ -32,6 +32,7 @@ function isProjectAnalyticsPoint(
|
||||
|
||||
export function buildComparisonFetchRequest(
|
||||
fetchRequest: Labrinth.Analytics.v3.FetchRequest | null,
|
||||
minStartTime = ANALYTICS_START_TIME,
|
||||
): AnalyticsProjectFetchRequest | null {
|
||||
if (!isAnalyticsFetchRequestReady(fetchRequest)) {
|
||||
return null
|
||||
@@ -47,7 +48,7 @@ export function buildComparisonFetchRequest(
|
||||
|
||||
const previousStart = new Date(startTimestamp - duration)
|
||||
|
||||
if (previousStart.getTime() < ANALYTICS_START_TIME) {
|
||||
if (previousStart.getTime() < minStartTime) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -93,8 +94,12 @@ function getAnalyticsTimeSliceCount(
|
||||
export function splitAnalyticsTimeSlices(
|
||||
timeSlices: Labrinth.Analytics.v3.TimeSlice[],
|
||||
fetchRequest: Labrinth.Analytics.v3.FetchRequest | null,
|
||||
minStartTime = ANALYTICS_START_TIME,
|
||||
): AnalyticsTimeSliceSplit {
|
||||
if (!isAnalyticsFetchRequestReady(fetchRequest) || !buildComparisonFetchRequest(fetchRequest)) {
|
||||
if (
|
||||
!isAnalyticsFetchRequestReady(fetchRequest) ||
|
||||
!buildComparisonFetchRequest(fetchRequest, minStartTime)
|
||||
) {
|
||||
return {
|
||||
currentTimeSlices: timeSlices,
|
||||
previousTimeSlices: [],
|
||||
@@ -339,6 +344,7 @@ export function getAnalyticsTimeframeDurationMs({
|
||||
customStartDate,
|
||||
customEndDate,
|
||||
nowTimestamp,
|
||||
allTimeStartTimestamp = ANALYTICS_START_TIME,
|
||||
}: {
|
||||
mode: AnalyticsTimeframeMode
|
||||
preset: AnalyticsTimeframePreset
|
||||
@@ -347,6 +353,7 @@ export function getAnalyticsTimeframeDurationMs({
|
||||
customStartDate: string
|
||||
customEndDate: string
|
||||
nowTimestamp: number
|
||||
allTimeStartTimestamp?: number
|
||||
}): number {
|
||||
if (mode === 'preset') {
|
||||
switch (preset) {
|
||||
@@ -370,7 +377,7 @@ export function getAnalyticsTimeframeDurationMs({
|
||||
return now.getTime() - yearStart.getTime()
|
||||
}
|
||||
case 'all_time': {
|
||||
const allTimeDurationMs = nowTimestamp - ANALYTICS_START_TIME
|
||||
const allTimeDurationMs = nowTimestamp - allTimeStartTimestamp
|
||||
return Math.max(0, allTimeDurationMs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export function toAnalyticsDashboardProject(
|
||||
iconUrl: project.icon_url ?? undefined,
|
||||
downloads: project.downloads ?? 0,
|
||||
status: getProjectStatusFilterValue(project.status),
|
||||
publishedAt: project.published ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ export type AnalyticsDashboardProjectSource = ProjectTypeMetadata & {
|
||||
icon_url?: string | null
|
||||
downloads?: number | null
|
||||
status?: string | null
|
||||
published?: string | null
|
||||
}
|
||||
|
||||
export type AnalyticsProjectVersionSource = {
|
||||
@@ -121,6 +122,7 @@ export interface AnalyticsDashboardProject {
|
||||
iconUrl?: string
|
||||
downloads: number
|
||||
status: ProjectStatusFilterValue
|
||||
publishedAt?: string
|
||||
}
|
||||
|
||||
export interface AnalyticsDashboardProjectGroup {
|
||||
|
||||
@@ -36,6 +36,7 @@ import type { OrganizationContext } from '../organization-context'
|
||||
import {
|
||||
addVersionIdsFromTimeSlices,
|
||||
addVersionProjectNamesFromTimeSlices,
|
||||
ANALYTICS_START_TIME,
|
||||
areAnalyticsFetchRequestsEqual,
|
||||
buildAnalyticsCurrentTimeSlicesQueryKey,
|
||||
buildAnalyticsFacetsRequest,
|
||||
@@ -68,6 +69,7 @@ import {
|
||||
sortStringValues,
|
||||
} from './analytics-filter-utils'
|
||||
import {
|
||||
getProjectIdsMatchingStatusFilter,
|
||||
getProjectOrganizationId,
|
||||
getSingleQueryValue,
|
||||
getUniqueAnalyticsDashboardProjects,
|
||||
@@ -131,6 +133,17 @@ const ANALYTICS_TIME_SLICES_GC_TIME_MS = 30 * 1000
|
||||
const ANALYTICS_PREFETCH_GC_TIME_MS = 15 * 1000
|
||||
const ANALYTICS_FILTER_OPTIONS_GC_TIME_MS = 60 * 1000
|
||||
const ANALYTICS_MOBILE_LAYOUT_QUERY = '(pointer: coarse), (max-width: 800px)'
|
||||
const ANALYTICS_ALL_TIME_START_OFFSET_MONTHS = 2
|
||||
|
||||
function subtractAnalyticsCalendarMonths(date: Date, months: number): Date {
|
||||
const nextDate = new Date(date)
|
||||
const day = nextDate.getDate()
|
||||
nextDate.setDate(1)
|
||||
nextDate.setMonth(nextDate.getMonth() - months)
|
||||
const daysInMonth = new Date(nextDate.getFullYear(), nextDate.getMonth() + 1, 0).getDate()
|
||||
nextDate.setDate(Math.min(day, daysInMonth))
|
||||
return nextDate
|
||||
}
|
||||
|
||||
function getAnalyticsFetchErrorMessage(error: unknown): string {
|
||||
if (error && typeof error === 'object') {
|
||||
@@ -164,6 +177,7 @@ export interface AnalyticsDashboardContextValue {
|
||||
selectedCustomTimeframeStartDate: Ref<string>
|
||||
selectedCustomTimeframeEndDate: Ref<string>
|
||||
selectedGroupBy: Ref<AnalyticsGroupByPreset>
|
||||
analyticsAllTimeStartDate: ComputedRef<Date>
|
||||
selectedBreakdowns: Ref<AnalyticsSelectedBreakdowns>
|
||||
selectedFilters: Ref<AnalyticsSelectedFilters>
|
||||
queryRefreshTimestamp: Ref<number>
|
||||
@@ -368,6 +382,7 @@ export function createAnalyticsDashboardContext(
|
||||
},
|
||||
enabled: computed(() => shouldFetchEffectiveUser.value && hasCompletedAnalyticsLoading.value),
|
||||
placeholderData: null,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
const effectiveUsername = computed(() => {
|
||||
if (effectiveUserId.value === options.auth.value.user?.id) {
|
||||
@@ -407,6 +422,7 @@ export function createAnalyticsDashboardContext(
|
||||
}
|
||||
},
|
||||
enabled: shouldFetchDashboardAllProjects,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const areProjectsLoaded = computed(() => {
|
||||
@@ -547,6 +563,63 @@ export function createAnalyticsDashboardContext(
|
||||
|
||||
return getAnalyticsVersionIdsFromProjects(projects, sortedSelectedProjectIds.value)
|
||||
})
|
||||
const { data: filterOptionProjectVersions, isFetched: hasFetchedFilterOptionProjectVersions } =
|
||||
useQuery({
|
||||
queryKey: computed(() => [
|
||||
'analytics',
|
||||
'dashboard',
|
||||
analyticsQueryUserId.value,
|
||||
'filter-options',
|
||||
'versions',
|
||||
filterOptionVersionIds.value,
|
||||
]),
|
||||
queryFn: () =>
|
||||
fetchAnalyticsVersionMetadataByIds(filterOptionVersionIds.value, (ids) =>
|
||||
client.labrinth.versions_v3.getVersions(ids),
|
||||
),
|
||||
enabled: computed(
|
||||
() =>
|
||||
filterOptionProjectSources.value !== null && sortedSelectedProjectIds.value.length > 0,
|
||||
),
|
||||
placeholderData: [],
|
||||
gcTime: ANALYTICS_FILTER_OPTIONS_GC_TIME_MS,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const projectsById = computed(
|
||||
() => new Map(projects.value.map((project) => [project.id, project])),
|
||||
)
|
||||
const analyticsAllTimeStartDate = computed(() => {
|
||||
const fallbackStartDate = new Date(ANALYTICS_START_TIME)
|
||||
const filteredProjectIds = getProjectIdsMatchingStatusFilter(
|
||||
selectedProjectIds.value.length > 0 ? selectedProjectIds.value : availableProjectIds.value,
|
||||
projectStatusById.value,
|
||||
selectedFilters.value,
|
||||
)
|
||||
let startTime = Number.POSITIVE_INFINITY
|
||||
|
||||
for (const projectId of filteredProjectIds) {
|
||||
const publishedAt = projectsById.value.get(projectId)?.publishedAt
|
||||
if (!publishedAt) {
|
||||
continue
|
||||
}
|
||||
|
||||
const projectStartTime = new Date(publishedAt).getTime()
|
||||
if (Number.isFinite(projectStartTime)) {
|
||||
startTime = Math.min(startTime, projectStartTime)
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(startTime)) {
|
||||
return fallbackStartDate
|
||||
}
|
||||
|
||||
const offsetStartDate = subtractAnalyticsCalendarMonths(
|
||||
new Date(startTime),
|
||||
ANALYTICS_ALL_TIME_START_OFFSET_MONTHS,
|
||||
)
|
||||
return new Date(Math.max(offsetStartDate.getTime(), ANALYTICS_START_TIME))
|
||||
})
|
||||
const hasExplicitProjectSelectionQuery = computed(() =>
|
||||
hasAnalyticsProjectSelectionQuery(route.query),
|
||||
)
|
||||
@@ -598,6 +671,7 @@ export function createAnalyticsDashboardContext(
|
||||
customStartDate: selectedCustomTimeframeStartDate.value,
|
||||
customEndDate: selectedCustomTimeframeEndDate.value,
|
||||
nowTimestamp: queryRefreshTimestamp.value,
|
||||
allTimeStartTimestamp: analyticsAllTimeStartDate.value.getTime(),
|
||||
}) > REVENUE_MIN_TIMEFRAME_MS,
|
||||
)
|
||||
|
||||
@@ -867,7 +941,17 @@ export function createAnalyticsDashboardContext(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const comparisonFetchRequest = computed(() => buildComparisonFetchRequest(fetchRequest.value))
|
||||
const analyticsComparisonStartTime = computed(() => {
|
||||
if (selectedTimeframeMode.value === 'preset' && selectedTimeframe.value === 'all_time') {
|
||||
const fetchRequestStart = fetchRequest.value?.time_range.start
|
||||
return new Date(fetchRequestStart ?? analyticsAllTimeStartDate.value).getTime()
|
||||
}
|
||||
|
||||
return ANALYTICS_START_TIME
|
||||
})
|
||||
const comparisonFetchRequest = computed(() =>
|
||||
buildComparisonFetchRequest(fetchRequest.value, analyticsComparisonStartTime.value),
|
||||
)
|
||||
const analyticsTimeSlicesFetchRequest = computed(
|
||||
() => comparisonFetchRequest.value ?? fetchRequest.value,
|
||||
)
|
||||
@@ -901,6 +985,7 @@ export function createAnalyticsDashboardContext(
|
||||
)
|
||||
},
|
||||
enabled: computed(() => isAnalyticsFetchRequestReady(analyticsTimeSlicesFetchRequest.value)),
|
||||
refetchOnWindowFocus: false,
|
||||
gcTime: ANALYTICS_TIME_SLICES_GC_TIME_MS,
|
||||
})
|
||||
watch(currentAnalyticsError, (error) => {
|
||||
@@ -956,7 +1041,10 @@ export function createAnalyticsDashboardContext(
|
||||
}
|
||||
|
||||
const dailyFetchRequest = buildDailyAnalyticsFetchRequest(fetchRequest.value)
|
||||
return buildComparisonFetchRequest(dailyFetchRequest) ?? dailyFetchRequest
|
||||
return (
|
||||
buildComparisonFetchRequest(dailyFetchRequest, analyticsComparisonStartTime.value) ??
|
||||
dailyFetchRequest
|
||||
)
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -984,7 +1072,7 @@ export function createAnalyticsDashboardContext(
|
||||
nextQueryRefreshTimestamp,
|
||||
),
|
||||
queryFn: () =>
|
||||
fetchAnalyticsTimeSlices(nextFetchRequest, (request) =>
|
||||
fetchAnalyticsData(nextFetchRequest, (request) =>
|
||||
client.labrinth.analytics_v3.fetch(request),
|
||||
),
|
||||
gcTime: ANALYTICS_PREFETCH_GC_TIME_MS,
|
||||
@@ -1057,6 +1145,7 @@ export function createAnalyticsDashboardContext(
|
||||
isAnalyticsFetchRequestReady(analyticsFacetsRequest.value),
|
||||
),
|
||||
gcTime: ANALYTICS_FILTER_OPTIONS_GC_TIME_MS,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const { data: analyticsDownloadCountTimeSlices } = useQuery({
|
||||
@@ -1086,30 +1175,9 @@ export function createAnalyticsDashboardContext(
|
||||
),
|
||||
placeholderData: [],
|
||||
gcTime: ANALYTICS_FILTER_OPTIONS_GC_TIME_MS,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const { data: filterOptionProjectVersions, isFetched: hasFetchedFilterOptionProjectVersions } =
|
||||
useQuery({
|
||||
queryKey: computed(() => [
|
||||
'analytics',
|
||||
'dashboard',
|
||||
analyticsQueryUserId.value,
|
||||
'filter-options',
|
||||
'versions',
|
||||
filterOptionVersionIds.value,
|
||||
]),
|
||||
queryFn: () =>
|
||||
fetchAnalyticsVersionMetadataByIds(filterOptionVersionIds.value, (ids) =>
|
||||
client.labrinth.versions_v3.getVersions(ids),
|
||||
),
|
||||
enabled: computed(
|
||||
() =>
|
||||
filterOptionProjectSources.value !== null && sortedSelectedProjectIds.value.length > 0,
|
||||
),
|
||||
placeholderData: [],
|
||||
gcTime: ANALYTICS_FILTER_OPTIONS_GC_TIME_MS,
|
||||
})
|
||||
|
||||
const analyticsFacetsFilterOptionSummary = computed(() =>
|
||||
getAnalyticsFacetsFilterOptionSummary(analyticsFacetsData.value?.facets),
|
||||
)
|
||||
@@ -1213,6 +1281,7 @@ export function createAnalyticsDashboardContext(
|
||||
const splitTimeSlices = splitAnalyticsTimeSlices(
|
||||
nextAnalyticsData.metrics,
|
||||
fetchRequest.value,
|
||||
analyticsComparisonStartTime.value,
|
||||
)
|
||||
timeSlices.value = splitTimeSlices.currentTimeSlices
|
||||
previousTimeSlices.value = splitTimeSlices.previousTimeSlices
|
||||
@@ -1269,6 +1338,7 @@ export function createAnalyticsDashboardContext(
|
||||
enabled: computed(() => analyticsVersionIds.value.length > 0),
|
||||
placeholderData: [],
|
||||
gcTime: ANALYTICS_FILTER_OPTIONS_GC_TIME_MS,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const allVersionMetadata = computed(() => {
|
||||
@@ -1488,6 +1558,7 @@ export function createAnalyticsDashboardContext(
|
||||
selectedCustomTimeframeStartDate,
|
||||
selectedCustomTimeframeEndDate,
|
||||
selectedGroupBy,
|
||||
analyticsAllTimeStartDate,
|
||||
selectedBreakdowns,
|
||||
selectedFilters,
|
||||
queryRefreshTimestamp,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="col-start-2 flex min-w-0 flex-1 flex-col gap-2">
|
||||
<div
|
||||
v-if="header || $slots.header || normalizedTimestamp"
|
||||
class="flex flex-wrap items-center gap-2 text-lg font-bold leading-6"
|
||||
class="flex flex-wrap items-center gap-2 text-lg font-semibold leading-6"
|
||||
>
|
||||
<slot name="header">{{ header }}</slot>
|
||||
<span
|
||||
|
||||
@@ -75,7 +75,7 @@ function toggleItem(item: T) {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.btn {
|
||||
border: 1px solid var(--surface-5);
|
||||
border: 1px solid transparent;
|
||||
&.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@@ -87,7 +87,6 @@ function toggleItem(item: T) {
|
||||
|
||||
&:focus-visible {
|
||||
outline: 0.25rem solid var(--color-focus-ring);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1580,6 +1580,15 @@ defineExpose({
|
||||
.modrinth-date-picker :deep(.flatpickr-current-month input.cur-year) {
|
||||
@apply min-w-[76px] px-2 text-center;
|
||||
}
|
||||
.modrinth-date-picker :deep(input[type='number']) {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
.modrinth-date-picker :deep(input[type='number']::-webkit-inner-spin-button),
|
||||
.modrinth-date-picker :deep(input[type='number']::-webkit-outer-spin-button) {
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.modrinth-date-picker
|
||||
:deep(.flatpickr-current-month .numInputWrapper:has(input.cur-year:disabled)) {
|
||||
@apply cursor-not-allowed;
|
||||
|
||||
Reference in New Issue
Block a user