mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
71 lines
2.7 KiB
TypeScript
71 lines
2.7 KiB
TypeScript
/**
|
|
* S32 — the pure view model of the anonymous card-download counters of the
|
|
* staff home. One absolute counter per card model, in the natural catalog
|
|
* order; no total, no percentage, no series: the numbers count downloads,
|
|
* never people (there is no visitor identity to count).
|
|
*
|
|
* Client-safe and free of Payload/Next: the loader feeds it the aggregate rows
|
|
* (or `null` when the read failed) and the component renders the view.
|
|
*/
|
|
|
|
import { CARD_MODELS, type CardModelId } from '@/lib/cardModels'
|
|
import type { ContentEventAggregateRow } from '@/lib/contentEvents'
|
|
|
|
export const CARD_DOWNLOAD_COUNTS_TITLE = 'Cards'
|
|
export const CARD_DOWNLOAD_COUNTS_SUBTITLE = 'Downloads anônimos acumulados desde o lançamento.'
|
|
export const CARD_DOWNLOAD_COUNTS_CAPTION =
|
|
'Os números contam downloads, não pessoas ou visitantes únicos.'
|
|
export const CARD_DOWNLOAD_COUNTS_EMPTY_TITLE = 'Nenhum download ainda'
|
|
export const CARD_DOWNLOAD_COUNTS_EMPTY_BODY =
|
|
'Os contadores aparecem quando alguém baixar um card.'
|
|
export const CARD_DOWNLOAD_COUNTS_EMPTY_CAPTION =
|
|
'A contagem será anônima e mostrará downloads por modelo, nunca pessoas.'
|
|
|
|
/** The one event type counted per card model (S32 v1). */
|
|
const CARD_DOWNLOAD_EVENT_TYPE = 'download'
|
|
|
|
export type CardDownloadCount = {
|
|
modelId: CardModelId
|
|
label: string
|
|
count: number
|
|
}
|
|
|
|
export type CardDownloadCountsView =
|
|
| { state: 'data'; counts: CardDownloadCount[] }
|
|
| { state: 'empty' }
|
|
| { state: 'unavailable' }
|
|
|
|
/**
|
|
* Counts per model id, ignoring anything that is not the card-download event
|
|
* and any id outside the catalog (the mapping below is the catalog itself).
|
|
*/
|
|
export const cardDownloadCountsFromRows = (
|
|
rows: readonly ContentEventAggregateRow[],
|
|
): CardDownloadCount[] => {
|
|
const countByModelId = new Map<string, number>()
|
|
|
|
for (const row of rows) {
|
|
if (row.type !== CARD_DOWNLOAD_EVENT_TYPE) continue
|
|
const count = Number.isFinite(row.count) ? Math.max(0, Math.trunc(row.count)) : 0
|
|
countByModelId.set(row.subjectId, (countByModelId.get(row.subjectId) ?? 0) + count)
|
|
}
|
|
|
|
return CARD_MODELS.map((model) => ({
|
|
modelId: model.id,
|
|
label: model.label,
|
|
count: countByModelId.get(model.id) ?? 0,
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* `null` means the aggregate read failed — the view is `unavailable`, which
|
|
* never renders as "no downloads yet" (that state is only for real zeros).
|
|
*/
|
|
export const resolveCardDownloadCounts = (
|
|
rows: readonly ContentEventAggregateRow[] | null,
|
|
): CardDownloadCountsView => {
|
|
if (rows === null) return { state: 'unavailable' }
|
|
|
|
const counts = cardDownloadCountsFromRows(rows)
|
|
return counts.some((entry) => entry.count > 0) ? { state: 'data', counts } : { state: 'empty' }
|
|
}
|