PageSourceSearch

https://calendariociclismo.app/js/services/race-presentation.js?v=20260912cxactionicons

js calendariociclismo.app collected 2026-09-24 10:44:12 UTC 7,733 bytes, 118 lines download raw bytes

1export const startlistCyclistHtml = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:-0.15em"><circle cx="18.5" cy="17.5" r="3.5"/><circle cx="5.5" cy="17.5" r="3.5"/><circle cx="15" cy="5" r="1"/><path d="M12 17.5V14l-3-3 4-3 2 3h2"/></svg>';
2export const resultsTrophyHtml = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:-0.15em"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2z"/></svg>';
3export const CLASS_ORDER = ['stage', 'gc', 'points', 'kom', 'youth', 'teams'];
4export const CLASS_LABELS = {
5  stage:{ es:'Etapa', en:'Stage' }, gc:{ es:'General', en:'GC' }, points:{ es:'Puntos', en:'Points' },
6  kom:{ es:'Montaña', en:'KOM' }, youth:{ es:'Jóvenes', en:'Youth' }, teams:{ es:'Equipos', en:'Teams' },
7};
8
9export function classificationInventory(config = [], stages = []) {
10  const map = new Map(config.map(row => [row.classKind, { ...row }]));
11  for (const stage of stages) if (!map.has(stage.classKind)) map.set(stage.classKind, { classKind:stage.classKind });
12  return [...map.values()].sort((a,b) => (a.position ?? fallbackOrder(a.classKind)) - (b.position ?? fallbackOrder(b.classKind)) || a.classKind.localeCompare(b.classKind));
13}
14function fallbackOrder(kind) { const index = CLASS_ORDER.indexOf(kind); return index < 0 ? 10 : index; }
15export function classificationLabel(row, lang = 'es') {
16  return row?.[lang === 'en' ? 'labelEn' : 'labelEs'] || CLASS_LABELS[row?.classKind]?.[lang] || row?.classKind || '';
17}
18export function classificationColor(row) {
19  if (row?.classKind === 'stage') return null;
20  return /^#[0-9a-f]{6}$/i.test(row?.colorHex || '') ? row.colorHex : null;
21}
22export function visibleStageClassifications(rows = [], inventory = []) {
23  const position = new Map(inventory.map((row,index) => [row.classKind,index]));
24  return [...rows].sort((a,b) => (position.get(a.classKind) ?? Infinity) - (position.get(b.classKind) ?? Infinity));
25}
26export async function loadFeaturedRaces(client, dates) {
27  if (!dates.length) return new Map();
28  const { data, error } = await client.rpc('featured_races_for_dates', { date_keys:[...new Set(dates)] });
29  if (error) throw error;
30  const byDate = new Map();
31  for (const row of data || []) {
32    if (!byDate.has(row.dateKey)) byDate.set(row.dateKey,new Set());
33    byDate.get(row.dateKey).add(row.raceId);
34  }
35  return byDate;
36}
37export function todayRaceState(day, now = Date.now()) {
38  if (day.isCancelledDay) return 'cancelled';
39  if (day.isRestDay) return 'rest';
40  if (day._hasInhouse) return 'results';
41  if (day.raceStatus === 'finished') return 'waiting';
42  const finish = Date.parse(day.estimatedFinishTimeUtc);
43  if (Number.isFinite(finish) && now >= finish) return 'waiting';
44  const start = Date.parse(day.neutralStartTimeUtc);
45  return day.raceStatus === 'running' || (Number.isFinite(start) && now >= start) ? 'running' : 'scheduled';
46}
47export function waitingResultsHtml(lang = 'es', element = 'div') {
48  const tag = element === 'span' ? 'span' : 'div';
49  const label = lang === 'en' ? 'Awaiting results' : 'Esperando resultados';
50  return `<${tag} class="race-card__schedule race-card__schedule--waiting" role="status" aria-label="${label}">
51    <svg class="race-card__waiting-flag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
52      <path d="M4 21V3"/>
53      <path d="M4 5c3-1.8 5.7-1.45 8.3-.52 3.05 1.1 5.85 1.18 8.7-.58v9.6c-2.85 1.76-5.65 1.68-8.7.58-2.6-.93-5.3-1.28-8.3.52V5Z"/>
54      <path d="M4.85 4.58c1.35-.55 2.67-.63 3.95-.48v4.75c-1.28-.15-2.6-.07-3.95.48V4.58Zm7.45-.1c1.52.55 2.98.82 4.4.63v4.74c-1.42.19-2.88-.08-4.4-.63V4.48ZM8.8 8.85c1.14.13 2.3.43 3.5.86v4.75c-1.2-.43-2.36-.73-3.5-.86V8.85Zm7.9 1c1.45-.19 2.87-.68 4.3-1.56v4.75c-1.43.88-2.85 1.37-4.3 1.56V9.85Z" fill="currentColor" stroke="none"/>
55    </svg>
56    <svg class="race-card__waiting-dots" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="4" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="20" cy="12" r="2"/></svg>
57  </${tag}>`;
58}
59export function profileProgress(day, now = Date.now()) {
60  if (day.isCancelledDay || day.isRestDay || ['itt','ttt'].includes(day.primaryType)) return 0;
61  if (day._hasInhouse || day.raceStatus === 'finished') return 1;
62  const start = Date.parse(day.realStartTimeUtc || day.neutralStartTimeUtc), finish = Date.parse(day.estimatedFinishTimeUtc);
63  return Number.isFinite(start) && finish > start ? Math.max(0, Math.min(1, (now-start)/(finish-start))) : 0;
64}
65export function hasValidTimeLimit(day) {
66  if (!Number.isFinite(Number(day?.timeLimitSeconds)) || Number(day.timeLimitSeconds)<=0 || !Number.isFinite(Date.parse(day?.timeLimitBasis?.verifiedAt))) return false;
67  try { return ['http:','https:'].includes(new URL(day.timeLimitBasis.sourceUrl).protocol); }
68  catch { return false; }
69}
70
71export function formatDurationSeconds(value) {
72  const total = Number(value);
73  if (!Number.isFinite(total) || total <= 0) return '';
74  const hours = Math.floor(total / 3600);
75  const minutes = Math.floor((total - hours * 3600) / 60);
76  const seconds = Number((total - hours * 3600 - minutes * 60).toFixed(3));
77  const [wholeSeconds, fraction] = String(seconds).split('.');
78  const secondsText = `${wholeSeconds.padStart(2, '0')}${fraction ? `.${fraction}` : ''}`;
79  return `${hours}:${String(minutes).padStart(2, '0')}:${secondsText}`;
80}
81
82export function parseDurationText(value) {
83  const match = String(value ?? '').trim().match(/^(\d+):([0-5]\d):([0-5]\d(?:[.,]\d+)?)$/);
84  if (!match) return null;
85  const total = Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3].replace(',', '.'));
86  return Number.isFinite(total) && total > 0 ? total : null;
87}
88
89export function classificationIsUpdating(row, now = Date.now()) {
90  return row?.publicationStatus === 'provisional' && row.updating === true
91    && Number.isFinite(Date.parse(row.updatingUntil)) && now < Date.parse(row.updatingUntil);
92}
93
94export function isTttStageClassification({
95  rows = [], classKind, isTeams = false, raceDay = null,
96  stageNumber = null, isOneDay = false, stageRaceType = null,
97} = {}) {
98  const eligible = classKind === 'stage'
99    || (classKind === 'gc' && stageNumber == null && isOneDay);
100  if (isTeams || !eligible) return false;
101
102  const sourceType = String(stageRaceType || '').toUpperCase();
103  if (raceDay && raceDay.primaryType !== 'ttt') return false;
104  if (!raceDay && ['ITT', 'IRR'].includes(sourceType)) return false;
105
106  const rowIrm = row => String(row.irm || row.resultValue || row.timeText || '').toUpperCase();
107  const classified = rows.filter(row => !/^(DNS|DNF|OTL|DSQ|ABD|DF|NR)$/.test(rowIrm(row)));
108  const counts = new Map();
109  for (const row of classified) if (row.rank != null) counts.set(row.rank, (counts.get(row.rank) || 0) + 1);
110  let sharedRanks = 0;
111  for (const count of counts.values()) if (count >= 2) sharedRanks++;
112  const nullRanks = classified.filter(row => row.rank == null).length;
113  const structural = sharedRanks >= 2 || nullRanks >= 2;
114  if (!structural) return false;
115
116  if (raceDay?.primaryType === 'ttt' || sourceType === 'TTT') return true;
117  return sharedRanks >= 3 || nullRanks >= 6;
118}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.