mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 17:43:04 +00:00
Papel communicator com nav própria e gate speechCatalog; acervo em /campanha/comunicacao/acervo com busca normalizada (destaque no trecho), filtros de ano/tema/alcance/fase/município/duração em URL canônica e paginação por discurso via speech.searchText (GIN trgm + backfill). Detalhe com player nativo no trecho, transcrição clicável, baixar/abrir fonte e fallback sem VOD. Resolve o débito do C153 (Sollinha fora da vertical) e corrige o parser de keywords oficiais no dono. Closes #956
182 lines
6.2 KiB
TypeScript
182 lines
6.2 KiB
TypeScript
/**
|
|
* Speech acervo URL contract (C154): state, param parse/canonicalize and
|
|
* hrefs, same shape as `peopleListUrl`/`supporterUi` over the shared
|
|
* `campaignListUrl` helpers. Facet enums are exhaustive (selecting every
|
|
* member equals no filter); year/phase/municipality are data-driven and
|
|
* validated structurally.
|
|
*/
|
|
import { CAMPAIGN_COMMUNICATION_ACERVO } from '@/lib/campaignPaths'
|
|
import { isContactSearchQueryReady } from '@/lib/contactSearchQuery'
|
|
import {
|
|
SPEECH_SCOPES,
|
|
SPEECH_TOPICS,
|
|
type SpeechScope,
|
|
type SpeechTopic,
|
|
} from '@/lib/speechFacets'
|
|
import {
|
|
allParamValues,
|
|
buildListHref,
|
|
firstValue,
|
|
normalizedText,
|
|
parseExhaustiveEnumParam,
|
|
resolveListUrl,
|
|
strictDecimalInteger,
|
|
type RawSearchParams,
|
|
} from '@/utilities/campaignListUrl'
|
|
|
|
export const speechPageSize = 25
|
|
|
|
/** Duration buckets are ranges over `durationSeconds`; `sem_duracao` is null. */
|
|
export const SPEECH_DURATION_BUCKETS = [
|
|
{ value: 'curta', label: 'Até 2 min' },
|
|
{ value: 'media', label: '2 a 5 min' },
|
|
{ value: 'longa', label: 'Mais de 5 min' },
|
|
{ value: 'sem_duracao', label: 'Sem duração' },
|
|
] as const
|
|
|
|
export type SpeechDurationBucket = (typeof SPEECH_DURATION_BUCKETS)[number]['value']
|
|
|
|
export const speechDurationLabels = Object.fromEntries(
|
|
SPEECH_DURATION_BUCKETS.map(({ value, label }) => [value, label]),
|
|
) as Record<SpeechDurationBucket, string>
|
|
|
|
export const speechTopicLabels = Object.fromEntries(
|
|
SPEECH_TOPICS.map(({ value, label }) => [value, label]),
|
|
) as Record<SpeechTopic, string>
|
|
|
|
export const speechScopeLabels = Object.fromEntries(
|
|
SPEECH_SCOPES.map(({ value, label }) => [value, label]),
|
|
) as Record<SpeechScope, string>
|
|
|
|
export type SpeechListState = {
|
|
page: number
|
|
q?: string
|
|
years?: number[]
|
|
topics?: SpeechTopic[]
|
|
scopes?: SpeechScope[]
|
|
phases?: string[]
|
|
municipalities?: number[]
|
|
durations?: SpeechDurationBucket[]
|
|
}
|
|
|
|
/** Server-loaded filter options (years/phases present, cited municipalities). */
|
|
export type SpeechFilterOptions = {
|
|
years: number[]
|
|
phases: string[]
|
|
municipalities: { value: string; label: string }[]
|
|
}
|
|
|
|
type RawSpeechListParams = RawSearchParams
|
|
|
|
const speechListParamNames = [
|
|
'q',
|
|
'year',
|
|
'topic',
|
|
'scope',
|
|
'phase',
|
|
'municipality',
|
|
'duration',
|
|
'page',
|
|
] as const
|
|
|
|
const speechListParamNameSet = new Set<string>(speechListParamNames)
|
|
const speechTopicSet = new Set<string>(SPEECH_TOPICS.map(({ value }) => value))
|
|
const speechScopeSet = new Set<string>(SPEECH_SCOPES.map(({ value }) => value))
|
|
const speechDurationSet = new Set<string>(SPEECH_DURATION_BUCKETS.map(({ value }) => value))
|
|
const MAX_PHASE_LENGTH = 80
|
|
|
|
const parseYearValues = (raw: string | string[] | undefined): number[] =>
|
|
allParamValues(raw)
|
|
.map((token) => strictDecimalInteger(token))
|
|
.filter((year): year is number => year !== undefined && year >= 1900 && year <= 2100)
|
|
|
|
const parseMunicipalityValues = (raw: string | string[] | undefined): number[] =>
|
|
allParamValues(raw)
|
|
.map((token) => strictDecimalInteger(token))
|
|
.filter((id): id is number => id !== undefined && id > 0)
|
|
|
|
export const parseSpeechListParams = (params: RawSpeechListParams): SpeechListState => {
|
|
const rawPage = strictDecimalInteger(firstValue(params.page))
|
|
const rawQ = normalizedText(firstValue(params.q))
|
|
const q = rawQ && isContactSearchQueryReady(rawQ) ? rawQ : undefined
|
|
const years = parseYearValues(params.year)
|
|
const topics = parseExhaustiveEnumParam<SpeechTopic>(params.topic, speechTopicSet)
|
|
const scopes = parseExhaustiveEnumParam<SpeechScope>(params.scope, speechScopeSet)
|
|
const phases = allParamValues(params.phase).filter((token) => token.length <= MAX_PHASE_LENGTH)
|
|
const municipalities = parseMunicipalityValues(params.municipality)
|
|
const durations = parseExhaustiveEnumParam<SpeechDurationBucket>(
|
|
params.duration,
|
|
speechDurationSet,
|
|
)
|
|
|
|
return {
|
|
page: rawPage ?? 1,
|
|
...(q ? { q } : {}),
|
|
...(years.length ? { years } : {}),
|
|
...(topics.length ? { topics } : {}),
|
|
...(scopes.length ? { scopes } : {}),
|
|
...(phases.length ? { phases } : {}),
|
|
...(municipalities.length ? { municipalities } : {}),
|
|
...(durations.length ? { durations } : {}),
|
|
}
|
|
}
|
|
|
|
const speechListStateToRawParams = (
|
|
state: SpeechListState,
|
|
page = state.page,
|
|
): RawSpeechListParams => ({
|
|
page: String(page),
|
|
q: state.q,
|
|
year: state.years?.map(String),
|
|
topic: state.topics,
|
|
scope: state.scopes,
|
|
phase: state.phases,
|
|
municipality: state.municipalities?.map(String),
|
|
duration: state.durations,
|
|
})
|
|
|
|
/** Expects already-canonical state (from parse or a rule-preserving toggle). */
|
|
export const serializeCanonicalSpeechListSearchParams = (
|
|
canonicalState: SpeechListState,
|
|
): URLSearchParams => {
|
|
const params = new URLSearchParams()
|
|
if (canonicalState.q) params.set('q', canonicalState.q)
|
|
for (const year of canonicalState.years ?? []) params.append('year', String(year))
|
|
for (const topic of canonicalState.topics ?? []) params.append('topic', topic)
|
|
for (const scope of canonicalState.scopes ?? []) params.append('scope', scope)
|
|
for (const phase of canonicalState.phases ?? []) params.append('phase', phase)
|
|
for (const municipality of canonicalState.municipalities ?? []) {
|
|
params.append('municipality', String(municipality))
|
|
}
|
|
for (const duration of canonicalState.durations ?? []) params.append('duration', duration)
|
|
if (canonicalState.page > 1) params.set('page', String(canonicalState.page))
|
|
return params
|
|
}
|
|
|
|
const buildSpeechListSearchParams = (state: SpeechListState, page = state.page): URLSearchParams =>
|
|
serializeCanonicalSpeechListSearchParams(
|
|
parseSpeechListParams(speechListStateToRawParams(state, page)),
|
|
)
|
|
|
|
export const buildSpeechListHref = (state: SpeechListState, page: number): string =>
|
|
buildListHref(state, buildSpeechListSearchParams, CAMPAIGN_COMMUNICATION_ACERVO, page)
|
|
|
|
export const buildSpeechFiltersKey = (state: SpeechListState): string =>
|
|
buildSpeechListSearchParams(state).toString()
|
|
|
|
export const resolveSpeechListUrl = (
|
|
params: RawSpeechListParams,
|
|
totalPages?: number,
|
|
): {
|
|
state: SpeechListState
|
|
href: string
|
|
redirectHref?: string
|
|
} =>
|
|
resolveListUrl({
|
|
params,
|
|
paramNameSet: speechListParamNameSet,
|
|
parse: parseSpeechListParams,
|
|
buildSearchParams: buildSpeechListSearchParams,
|
|
basePath: CAMPAIGN_COMMUNICATION_ACERVO,
|
|
totalPages,
|
|
})
|