Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
204 commits
Select commit Hold shift + click to select a range
0301f4a
feat: implement analytics route in api client
tdgao Apr 21, 2026
1db663b
remove: delete current analytics implementation
tdgao Apr 22, 2026
e09845b
feat: wire up shared analytics dashboard page
tdgao Apr 22, 2026
1c368d8
feat: initial implementation of analytics DI, query builder component…
tdgao Apr 22, 2026
661765d
feat: style consistency updates
tdgao Apr 22, 2026
5234270
feat: implement analytics chart
tdgao Apr 22, 2026
31f13a2
feat: improve query builder styles
tdgao Apr 22, 2026
e52d642
feat: add query to url params for query builder
tdgao Apr 22, 2026
97fde95
feat: implement analytics table and breakdown
tdgao Apr 23, 2026
5297b17
fix: date display to show time conditionally
tdgao Apr 23, 2026
fbc996b
fix: query builder disable group-by options if not relavant
tdgao Apr 23, 2026
6ce8be3
feat: style improvements
tdgao Apr 23, 2026
270e73f
remove: events toggle button for now since it does nothing
tdgao Apr 23, 2026
fd07f8e
fix: type error
tdgao Apr 23, 2026
8840a13
refactor: pnpm prepr
tdgao Apr 23, 2026
3daa527
feat: improve query builder styles and timeframes
tdgao Apr 23, 2026
b126bc4
feat: add table empty state
tdgao Apr 23, 2026
d5239bd
feat: implement disabled statcard for non-applicable ones
tdgao Apr 23, 2026
bfdb386
refactor: object destructure for context
tdgao Apr 23, 2026
f17b978
feat: filter server projects
tdgao Apr 24, 2026
ec723c7
feat: style improvements to project select
tdgao Apr 27, 2026
4231250
feat: separate out query filter component
tdgao Apr 27, 2026
34b392b
fix: type
tdgao Apr 27, 2026
43abf27
fix: triangle safe area for query filter sub menus
tdgao Apr 28, 2026
ff0968b
implement header slot for table
tdgao Apr 28, 2026
5cf6941
feat: implement multiselect input content slow
tdgao Apr 28, 2026
329cc3b
feat: use mutliselect for active filtered by options and use table he…
tdgao Apr 28, 2026
40d8007
Merge branch 'main' into truman/analytics
tdgao Apr 28, 2026
8694971
refactor: pnpm prepr
tdgao Apr 28, 2026
8c7e485
fix: broken lock file
tdgao Apr 28, 2026
34b736f
feat: implement adding project id analytics
tdgao Apr 28, 2026
df8aff2
feat: hide/show specific series in graph, formatted legend labels, lo…
tdgao Apr 28, 2026
849edf0
fix: queries not caching properly
tdgao Apr 28, 2026
53a04cf
feat: update columns widths
tdgao Apr 28, 2026
628712e
refactor: pnpm prepr
tdgao Apr 28, 2026
0cedf43
feat: add dropdown width and min dropdown width on combobox and multi…
tdgao Apr 29, 2026
0c99170
fix: QA Issues
tdgao Apr 29, 2026
c6a4749
feat: improve query filter menu styles and switch out of Menu
tdgao Apr 29, 2026
4414ea8
feat: implement better chart tooltip menu anchoring and dragging on m…
tdgao Apr 29, 2026
a0144f5
feat: small style improvements
tdgao Apr 29, 2026
2eee097
feat: improve query filter and how it commits changes
tdgao Apr 29, 2026
4b31e46
fix: remove projects from filters
tdgao Apr 29, 2026
05cd300
feat: update combobox active item to green
tdgao Apr 29, 2026
c8ad19c
fix: version_id breakdown incorrectly named
tdgao Apr 29, 2026
b3eea2c
feat: add anchored line to analytics chart and remove darkening points
tdgao Apr 29, 2026
6f2117c
feat: implement bottom slot in combobox and multiselect dropdowns
tdgao Apr 30, 2026
31476c2
feat: implement country filter
tdgao Apr 30, 2026
56876cc
feat: implement custom timeframe pickers and projects above num downl…
tdgao Apr 30, 2026
7d0c04a
refactor: separate out query builder timeframe inputs
tdgao Apr 30, 2026
3c366d1
fix: custom timeframe UX and styles
tdgao Apr 30, 2026
560a442
refactor: move query filter components into co-located folder
tdgao Apr 30, 2026
4a61322
feat: implement version number display and also searchable category f…
tdgao Apr 30, 2026
fdf4ac6
feat: small style improvements
tdgao Apr 30, 2026
88c2a30
feat: implement loader type options and remove "All" option
tdgao Apr 30, 2026
99e2d65
fix: filter sub menu first opens badly
tdgao Apr 30, 2026
0ec0714
feat: add category filter sorting and get download source filter options
tdgao Apr 30, 2026
109cb05
feat: add query filter sub menu empty state
tdgao Apr 30, 2026
0b4ac38
feat: hide filters as needed based on breakdown
tdgao Apr 30, 2026
1c2d8c3
feat: implement filter by game version downloads above value
tdgao Apr 30, 2026
424bf6b
fix: empty state y axis
tdgao May 1, 2026
c7908fb
fix: spacing
tdgao May 1, 2026
f8c93ec
feat: implement tabs component
tdgao May 1, 2026
12c18d6
feat: use tabs component in graph
tdgao May 1, 2026
aa868e3
fix: graph loading state on refresh
tdgao May 1, 2026
e432861
refactor: use chips and button component in table
tdgao May 1, 2026
dcab62d
feat: implement include date
tdgao May 1, 2026
3b3fcc1
fix: table breakdown column not actual breakdown name
tdgao May 1, 2026
a3b2836
refactor: pnpm prepr
tdgao May 1, 2026
e1d3b6f
feat: update stat card component styles
tdgao May 1, 2026
48c652d
feat: more concise axis labels
tdgao May 1, 2026
de192d6
feat: add pinned icon in chart tooltip
tdgao May 1, 2026
872c9de
fix: analytics query bugs
tdgao May 1, 2026
3a55afb
fix: include date disabled in analytics table
tdgao May 1, 2026
646f803
feat; change how the query filter options are fetched
tdgao May 1, 2026
47b41e9
refactor: pnpm prepr
tdgao May 1, 2026
1ddb8b2
feat: implement independant all projects select and num selected row
tdgao May 4, 2026
27b3cc1
fix: qa issues
tdgao May 4, 2026
d7cbe17
feat: all switch between seeing only release or all game versions + Q…
tdgao May 4, 2026
ab1183c
feat: update analytics query to match new format
tdgao May 4, 2026
b1e3d09
feat: only displays loaders and game versions relevant to project + q…
tdgao May 4, 2026
bcb8566
feat: improve how time frame picker opens for custom timeframe
tdgao May 4, 2026
b63db68
Merge branch 'main' into truman/analytics
tdgao May 4, 2026
cfb1fbf
feat: hook up backend for new metrics
tdgao May 4, 2026
1e41ae1
feat: improve graph UI
tdgao May 4, 2026
5be9d24
feat: wire up downloads.monetized
tdgao May 4, 2026
45d697e
fix: dropdown cancel button
tdgao May 4, 2026
662075c
fix: small style update
tdgao May 4, 2026
b2067bc
fix: back fill 0 when there is no data at a point in time
tdgao May 4, 2026
8aba8bd
feat: implement pretty loading state
tdgao May 4, 2026
dbcf267
feat: improve filter bar styles
tdgao May 6, 2026
2482ea5
feat: implement generic DropdownFilterBar component
tdgao May 6, 2026
7e324ae
Merge branch 'main' into truman/analytics
tdgao May 7, 2026
6a71e77
feat: implement calendar only date picker and custom range timeframe …
tdgao May 7, 2026
4fdf7d7
fix: QA issues - show year if relevant, press enter to run query, hid…
tdgao May 7, 2026
067d307
fix: remove spinner
tdgao May 7, 2026
3ef95b6
fix: graph offset
tdgao May 7, 2026
6114d17
feat: add total to tooltip and handle overflow
tdgao May 7, 2026
722feb9
feat: add country downloads above... filter
tdgao May 7, 2026
6ad653a
feat: implement above num downloads in preview dropdown
tdgao May 7, 2026
f77788f
fix: exclude draft projects from analytics
tdgao May 7, 2026
45c8af9
feat: default 30 days by day, and shift click to in legend to show al…
tdgao May 7, 2026
8fb1961
feat: implement hover guide on graph
tdgao May 7, 2026
0c5a3fa
feat: implement better x-axis and tooltip showing timeframe
tdgao May 8, 2026
1749158
fix: stat card overflow
tdgao May 8, 2026
5392577
feat: add loader colors for graph
tdgao May 8, 2026
40cfcaf
feat: disable toggle line if only one exists, and show graph as soon …
tdgao May 8, 2026
b78f16e
refactor: pnpm prepr
tdgao May 8, 2026
9fea83b
fix: graph and table seeding 0s from filter options instead of backen…
tdgao May 8, 2026
c7e141c
fix: switch breakdown/filter selection if one already exists
tdgao May 8, 2026
e479917
feat: dont show prev period for all time/ when no prev period
tdgao May 8, 2026
089b37d
fix: graph colors to be unique
tdgao May 8, 2026
8181b05
feat: implement virtualized table
tdgao May 8, 2026
1c5b37b
feat: update date/no date sorting
tdgao May 8, 2026
8b5cf04
feat: implement better no projects empty state
tdgao May 8, 2026
eac9daa
feat: implement filter by project status
tdgao May 8, 2026
d8c311e
feat: implement ratio mode
tdgao May 8, 2026
5d20585
feat: implement cancel/apply for custom timeframe range picker
tdgao May 8, 2026
2e3d6a0
feat: implement dot for showing todays date
tdgao May 8, 2026
37f9564
feat: add max date to be today and show todays date
tdgao May 8, 2026
df196cc
feat: if ratio mode, dont show total
tdgao May 8, 2026
843529b
feat: implement show more batching excess lines into "Other" bucket
tdgao May 8, 2026
aad7b13
refactor: pnpm prepr
tdgao May 8, 2026
cba07e4
feat: add pick and plop for date range start/end dates
tdgao May 11, 2026
92ad6e2
feat: implement reset query button
tdgao May 12, 2026
e38d7af
feat: clear button to clear breakdown
tdgao May 12, 2026
3c406c8
feat: more aggressively trim allowed minimum group by option
tdgao May 12, 2026
28d56e8
fix: dont show project status filter when from project settings/analy…
tdgao May 12, 2026
c939701
fix: clear selected X above number when appropriate
tdgao May 12, 2026
635ae77
feat: graph style updates and dont show year in x axis unless more th…
tdgao May 13, 2026
ae0cea0
fix: loading state to include legend in blur
tdgao May 13, 2026
02aa76a
feat: add project icon to project select
tdgao May 13, 2026
4788369
feat: filter out draft projects from analytics
tdgao May 13, 2026
8261b8b
feat: implement multiselect sections headers, project select org sect…
tdgao May 13, 2026
5fc9f47
feat: implement click and drag to select date range
tdgao May 13, 2026
5a7a6e6
feat: implement windows history for query builder
tdgao May 13, 2026
077cc60
revert: no longer switch breakdown/filter option if same category
tdgao May 13, 2026
48c957d
feat: implement showing project for project version breakdown/filter …
tdgao May 13, 2026
3714cb3
feat: implement modrinth sided events
tdgao May 13, 2026
626d55c
Merge branch 'main' into truman/analytics
tdgao May 13, 2026
dda6960
fix: border radius
tdgao May 13, 2026
228bf74
feat: implement analytics range highlight
tdgao May 13, 2026
a74316c
fix: loading state showing empty state text
tdgao May 13, 2026
2c5d478
refactor: pnpm prepr
tdgao May 13, 2026
816349c
feat: improve dropdown filter bar and multiselect performance
tdgao May 14, 2026
4b28961
fix: multiselect keyboard use
tdgao May 14, 2026
99a8a67
fix: graph overflow issues
tdgao May 14, 2026
309f0fc
fix: loading state text on table
tdgao May 14, 2026
38664c1
feat: implement tooltip scroll
tdgao May 14, 2026
b15afcb
fix: adjust charts event tooltip
tdgao May 14, 2026
04110fc
feat: shorten time to not repeat am/pm
tdgao May 15, 2026
4f607d8
feat: implement query params for graph component settings
tdgao May 15, 2026
ebebec7
fix: qa
tdgao May 15, 2026
ccc2ff8
feat: add reset timeframe button
tdgao May 15, 2026
9664eb1
fix: legend colors moving between metric by determining color based o…
tdgao May 15, 2026
a4172cd
feat: implement auto switching temporarily to group by day for renven…
tdgao May 15, 2026
10f2c7f
fix: change to > 1 day
tdgao May 15, 2026
1786b37
fix: custom timeframe picker
tdgao May 15, 2026
b93aa1d
feat: implement big performance improvement for table
tdgao May 15, 2026
5c9b9f8
feat: implement hover on legend to highlight graph
tdgao May 15, 2026
60f4ca1
fix: defer commit in query builder/filter and style fixes
tdgao May 15, 2026
0e4b384
feat: more performance optimization to analytics dashboard state, cha…
tdgao May 15, 2026
c5c199a
feat: add tooltip for other item
tdgao May 15, 2026
8bf9658
feat: improve custom time frame range select
tdgao May 15, 2026
8d06d46
feat: implement analytics events admin page
tdgao May 15, 2026
72940f3
fix: switch column order
tdgao May 15, 2026
46d66c9
pnpm prepr
tdgao May 15, 2026
42ec8a8
feat: implement mock analytics events
tdgao May 15, 2026
79a5ef4
feat: improve analytics events admin page
tdgao May 15, 2026
f3911a4
feat: focus title input on analytics create event modal
tdgao May 15, 2026
ec2c1bd
fix: remove labels annoying
tdgao May 15, 2026
4b2d1b6
feat: hook up analytics events backend
tdgao May 16, 2026
29ea3ff
fix: type error
tdgao May 18, 2026
ba6c8be
feat: reduce combobox padding
tdgao May 18, 2026
aa493a3
feat: reduce padding on multiselect
tdgao May 18, 2026
51a1daa
feat: add overlay scrollbar for combobox
tdgao May 18, 2026
5dcae6b
feat: a bunch of style fixes to combobox, multiselect and dropdown fi…
tdgao May 18, 2026
c1afb22
feat: MORE PADDING fixes
tdgao May 18, 2026
d6dc8a9
feat: use user_agent for download source
tdgao May 18, 2026
9c66aa5
Revert "feat: use user_agent for download source"
tdgao May 18, 2026
7da4730
fix: query filter project version lag and borked virtualization
tdgao May 18, 2026
30ac937
feat: rename breakdown "none" to "project"
tdgao May 19, 2026
f509967
feat: implement right side checkmark for multiselect
tdgao May 19, 2026
d07d184
feat: keep crossed out legend items still shown in tooltip but also c…
tdgao May 19, 2026
77f7ac9
fix: focus styles
tdgao May 19, 2026
7e928cf
fix: focus styles pt2
tdgao May 19, 2026
5f6cfaf
feat: implement filter by top 8
tdgao May 19, 2026
94ee519
Merge branch 'main' into truman/analytics
tdgao May 19, 2026
1a62683
fix: preview is incorrect when selecting same date in range date picker
tdgao May 19, 2026
78b5c83
feat (playtest): cross out legend items in tooltip and allow hide/sho…
tdgao May 19, 2026
96ff380
feat (playtest): table component controls what graph shows
tdgao May 19, 2026
bcad49a
feat: change download source to use user_agent
tdgao May 19, 2026
428056e
feat: fix click to cross out in legend
tdgao May 19, 2026
06e2bcf
feat: add hover legend item to highlight line in tooltip
tdgao May 19, 2026
28f59d3
fix: export csv to always be dropdown
tdgao May 19, 2026
724570b
feat: implement breakdown = none
tdgao May 19, 2026
a414e2f
performance: frontend memory reduction
tdgao May 19, 2026
0aa1634
performance: reduce memory usage from project versions query by keepi…
tdgao May 19, 2026
fb3497e
fix: table checked items not in graph if 0
tdgao May 19, 2026
a999795
feat: add shift click to select a range in table
tdgao May 19, 2026
175da90
performance: add caching for metric types so switching between them i…
tdgao May 19, 2026
bfc1520
performance: batch analytics requests by 15 project ids, with 150 ms …
tdgao May 19, 2026
a9526b8
feat: add analytics table search
tdgao May 19, 2026
683476a
Merge branch 'main' into truman/analytics
tdgao May 19, 2026
02f2163
refactor: pnpm prepr
tdgao May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@vueuse/core": "^11.1.0",
"ace-builds": "^1.36.2",
"ansi-to-html": "^0.7.2",
"chart.js": "^4.5.1",
"dayjs": "^1.11.7",
"dompurify": "^3.1.7",
"floating-vue": "^5.2.2",
Expand Down
70 changes: 70 additions & 0 deletions apps/frontend/src/components/analytics/AnalyticsDashboard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<template>
<div class="flex flex-col gap-4 pb-20 lg:pl-4 lg:pt-1.5">
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between gap-2">
<span class="text-xl font-semibold text-contrast md:text-2xl">Analytics</span>
<div class="flex flex-wrap items-center justify-end gap-2">
<ButtonStyled type="transparent">
<button
type="button"
:disabled="isAnalyticsQueryBuilderDefault"
@click="resetAnalyticsQueryBuilder"
>
Reset
</button>
</ButtonStyled>
<ButtonStyled type="outlined">
<button
type="button"
:disabled="projects.length === 0 || !fetchRequest || isRefetching"
@click="refreshAnalyticsQuery"
>
<RefreshCwIcon :class="isRefetching ? 'animate-spin' : ''" />
Refresh
</button>
</ButtonStyled>
</div>
</div>
<QueryBuilder />
</div>
<StatCards />
<AnalyticsGraph />
<AnalyticsTable />
</div>
</template>

<script setup lang="ts">
import { RefreshCwIcon } from '@modrinth/assets'
import { ButtonStyled, injectProjectPageContext } from '@modrinth/ui'

import {
createAnalyticsDashboardContext,
provideAnalyticsDashboardContext,
} from '~/providers/analytics/analytics'
import { injectOrganizationContext } from '~/providers/organization-context'

import AnalyticsGraph from './graph/AnalyticsGraph.vue'
import QueryBuilder from './query-builder/QueryBuilder.vue'
import StatCards from './stat-cards/StatCards.vue'
import AnalyticsTable from './table/AnalyticsTable.vue'

const auth = await useAuth()
const projectPageContext = injectProjectPageContext(null)
const organizationContext = injectOrganizationContext(null)

const analyticsDashboardContext = createAnalyticsDashboardContext({
auth,
projectPageContext,
organizationContext,
})
const {
fetchRequest,
isAnalyticsQueryBuilderDefault,
isRefetching,
projects,
refreshAnalyticsQuery,
resetAnalyticsQueryBuilder,
} = analyticsDashboardContext

provideAnalyticsDashboardContext(analyticsDashboardContext)
</script>
152 changes: 152 additions & 0 deletions apps/frontend/src/components/analytics/AnalyticsLoadingBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<template>
<div class="analytics-loading-bar" :style="{ opacity: isVisible ? 1 : 0 }" aria-hidden="true">
<div
class="analytics-loading-bar__track"
:style="{
width: `${progress}%`,
transition: !isTransitioning
? 'none'
: isFinishing
? 'width 0.1s ease-in-out'
: isCreeping
? 'width 2s linear'
: 'width 0.9s ease-in-out',
}"
/>
</div>
</template>

<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue'

const props = defineProps<{
loading: boolean
}>()

const progress = ref(0)
const isVisible = ref(false)
const isFinishing = ref(false)
const isCreeping = ref(false)
const isTransitioning = ref(false)

let startFrame: number | null = null
let showFrame: number | null = null
let creepTimeout: ReturnType<typeof setTimeout> | null = null
let hideTimeout: ReturnType<typeof setTimeout> | null = null
let resetTimeout: ReturnType<typeof setTimeout> | null = null

function clearTimers() {
if (showFrame !== null && typeof window !== 'undefined') {
window.cancelAnimationFrame(showFrame)
}
if (startFrame !== null && typeof window !== 'undefined') {
window.cancelAnimationFrame(startFrame)
}
if (creepTimeout) clearTimeout(creepTimeout)
if (hideTimeout) clearTimeout(hideTimeout)
if (resetTimeout) clearTimeout(resetTimeout)
showFrame = null
startFrame = null
creepTimeout = null
hideTimeout = null
resetTimeout = null
}

function start() {
clearTimers()
isVisible.value = false
progress.value = 0
isFinishing.value = false
isCreeping.value = false
isTransitioning.value = false

if (typeof window === 'undefined') {
progress.value = 98
return
}

showFrame = window.requestAnimationFrame(() => {
isVisible.value = true
showFrame = null
startFrame = window.requestAnimationFrame(() => {
isTransitioning.value = true
progress.value = 85
startFrame = null
})
})
creepTimeout = setTimeout(() => {
isCreeping.value = true
progress.value = 98
creepTimeout = null
}, 900)
}

function finish() {
clearTimers()
isVisible.value = true
isFinishing.value = true
isCreeping.value = false
isTransitioning.value = true
progress.value = 100

if (typeof window === 'undefined') {
isVisible.value = false
progress.value = 0
isFinishing.value = false
isCreeping.value = false
isTransitioning.value = false
return
}

hideTimeout = setTimeout(() => {
isVisible.value = false
resetTimeout = setTimeout(() => {
isTransitioning.value = false
progress.value = 0
isFinishing.value = false
isCreeping.value = false
}, 400)
}, 350)
}

watch(
() => props.loading,
(loading) => {
if (loading) {
start()
} else if (
isVisible.value ||
progress.value > 0 ||
showFrame !== null ||
startFrame !== null ||
creepTimeout !== null
) {
finish()
}
},
{ immediate: true },
)

onBeforeUnmount(clearTimers)
</script>

<style scoped>
.analytics-loading-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 20;
height: 2px;
overflow: hidden;
background: color-mix(in srgb, var(--color-brand) 18%, transparent);
pointer-events: none;
transition: opacity 0.4s;
}

.analytics-loading-bar__track {
height: 100%;
border-radius: 999px;
background: var(--loading-bar-gradient);
}
</style>
77 changes: 77 additions & 0 deletions apps/frontend/src/components/analytics/breakdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Labrinth } from '@modrinth/api-client'

import type { AnalyticsBreakdownPreset } from '~/providers/analytics/analytics'

export const ALL_BREAKDOWN_VALUE = 'All'
export const UNKNOWN_BREAKDOWN_VALUE = 'Unknown'

export function getAnalyticsBreakdownValue(
point: Labrinth.Analytics.v3.ProjectAnalytics,
selectedBreakdown: AnalyticsBreakdownPreset,
): string {
switch (selectedBreakdown) {
case 'none':
return ALL_BREAKDOWN_VALUE
case 'project':
return normalizeBreakdownValue('source_project' in point ? point.source_project : undefined)
case 'country':
return normalizeBreakdownValue('country' in point ? point.country?.toUpperCase() : undefined)
case 'monetization': {
if ('monetized' in point && typeof point.monetized === 'boolean') {
return point.monetized ? 'monetized' : 'unmonetized'
}
return ALL_BREAKDOWN_VALUE
}
case 'user_agent':
return getDownloadSourceLabel(
normalizeBreakdownValue('user_agent' in point ? point.user_agent : undefined),
)
case 'download_reason':
return normalizeBreakdownValue(
'reason' in point ? point.reason : undefined,
UNKNOWN_BREAKDOWN_VALUE,
)
case 'version_id':
return normalizeBreakdownValue('version_id' in point ? point.version_id : undefined)
case 'loader':
return normalizeBreakdownValue(
'loader' in point ? point.loader : undefined,
UNKNOWN_BREAKDOWN_VALUE,
)
case 'game_version':
return normalizeBreakdownValue(
'game_version' in point ? point.game_version : undefined,
UNKNOWN_BREAKDOWN_VALUE,
)
default:
return ALL_BREAKDOWN_VALUE
}
}

export function getDownloadSourceLabel(value: string): string {
const normalized = value.trim()
const normalizedLowercase = normalized.toLowerCase()
if (normalizedLowercase === 'website') {
return 'Modrinth Website'
}
if (normalizedLowercase === 'modrinth_app') {
return 'Modrinth App'
}
if (!normalized.includes('_')) {
return normalized
}

return normalizedLowercase
.split('_')
.filter((part) => part.length > 0)
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
.join(' ')
}

function normalizeBreakdownValue(
value: string | undefined,
fallback = ALL_BREAKDOWN_VALUE,
): string {
const normalized = value?.trim()
return normalized && normalized.length > 0 ? normalized : fallback
}
Loading
Loading