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.