1import { fetchChallengeDailyHistory } from "../js/challengeLeaderboardApi.js"; 2import { formatEuro, formatMs } from "../js/challengeEconomy.js"; 3import { formatChallengeDayLabel } from "../js/challengeDay.js"; 4 5const HISTORY_NOTE = 6 "Daily History zeigt echte Challenge-Tage aus dem MVP-Testbetrieb. Score, Zeit und Pot-Werte dienen der Produktvalidierung; keine echten Käufe, keine Auszahlung, kein Gewinnanspruch."; 7 8export function renderChallengeDailyHistoryScreen(state) { 9 void state; 10 11 return ` 12 <article class="screen app-screen app-screen--brand challenge-daily-history-screen"> 13 <header class="screen__header challenge-daily-history-hero"> 14 <h1>Daily History</h1> 15 <p>Letzte echte Challenge-Tage. Pot-Werte sind Simulation. Keine echten Käufe. Keine Auszahlung. Kein Gewinnanspruch.</p> 16 </header> 17 18 <div class="screen__content challenge-daily-history-content"> 19 <section class="glass-card challenge-daily-history-card" aria-live="polite" data-challenge-daily-history> 20 ${renderLoadingState()} 21 </section> 22 23 <div class="screen-actions"> 24 <button class="btn btn--primary" type="button" data-nav="challengeLeaderboard"> 25 Leaderboard 26 </button> 27 <button class="btn btn--ghost" type="button" data-nav="challengeIntro"> 28 Home 29 </button> 30 </div> 31 </div> 32 </article> 33 `; 34} 35 36export function mountChallengeDailyHistoryScreen(container, state) { 37 void state; 38 39 const historyEl = container.querySelector("[data-challenge-daily-history]"); 40 if (!historyEl) return () => {}; 41 42 let isMounted = true; 43 44 historyEl.innerHTML = renderLoadingState(); 45 46 fetchChallengeDailyHistory({ limit: 7 }) 47 .then((response) => { 48 if (!isMounted) return; 49 50 if (!response.ok) { 51 historyEl.innerHTML = renderErrorState(response.error); 52 return; 53 } 54 55 const days = Array.isArray(response.days) ? response.days : []; 56 if (days.length === 0) { 57 historyEl.innerHTML = renderEmptyState(); 58 return; 59 } 60 61 historyEl.innerHTML = renderHistoryContent(days); 62 }) 63 .catch((error) => { 64 if (!isMounted) return; 65 historyEl.innerHTML = renderErrorState( 66 error instanceof Error && error.message 67 ? error.message 68 : "Challenge-Tageshistorie konnte nicht geladen werden." 69 ); 70 }); 71 72 return function unmount() { 73 isMounted = false; 74 }; 75} 76 77function renderLoadingState() { 78 return ` 79 <div class="challenge-leaderboard-state"> 80 <p class="challenge-leaderboard-state__title">Tageshistorie wird geladen...</p> 81 <p>Wir laden die letzten echten Challenge-Tage.</p> 82 </div> 83 `; 84} 85 86function renderErrorState(error) { 87 const message = normalizeError(error); 88 return ` 89 <div class="challenge-leaderboard-state challenge-leaderboard-state--error"> 90 <p class="challenge-leaderboard-state__title">Tageshistorie gerade nicht erreichbar.</p> 91 <p>${escapeHtml(message)}</p> 92 </div> 93 `; 94} 95 96function renderEmptyState() { 97 return ` 98 <div class="challenge-leaderboard-state"> 99 <p class="challenge-leaderboard-state__title">Noch keine Tageshistorie vorhanden.</p> 100 <p>Sobald echte Challenge-Tage abgeschlossen sind, erscheinen sie hier.</p> 101 </div> 102 `; 103} 104 105function renderHistoryContent(days) { 106 const safeDays = days.slice(0, 7); 107 108 return ` 109 <div class="challenge-daily-history-stack"> 110 <ol class="challenge-daily-history-list"> 111 ${safeDays.map(renderHistoryRow).join("")} 112 </ol> 113 <p class="challenge-daily-history-note">${escapeHtml(HISTORY_NOTE)}</p> 114 </div> 115 `; 116} 117 118function renderHistoryRow(day) { 119 const challengeDay = formatChallengeDayLabel(day?.challenge_day); 120 const participantCount = formatInteger(day?.participant_count); 121 const topNickname = String(day?.top_nickname ?? "â").trim() || "â"; 122 const topScore = formatScore(day?.top_score); 123 const topTime = formatMs(day?.top_time_ms); 124 const dailyPot = formatEuro(day?.daily_pot_eur); 125 const simulatedTopPrize = formatEuro(day?.simulated_top_prize_eur); 126 127 return ` 128 <li class="challenge-daily-history-row"> 129 <div class="challenge-daily-history-meta"> 130 <strong class="challenge-day-label">${escapeHtml(challengeDay)}</strong>
131 <span>Top-Platz · ${escapeHtml(topNickname)}</span> 132 </div> 133 <dl class="challenge-daily-history-stats"> 134 <div> 135 <dt>Teilnehmerzahl</dt> 136 <dd>${escapeHtml(participantCount)}</dd> 137 </div> 138 <div> 139 <dt>Top Score</dt> 140 <dd>${escapeHtml(topScore)}</dd> 141 </div> 142 <div> 143 <dt>Top Zeit</dt> 144 <dd>${escapeHtml(topTime)}</dd> 145 </div> 146 <div> 147 <dt>Daily Pot</dt> 148 <dd>${escapeHtml(dailyPot)}</dd> 149 </div> 150 <div> 151 <dt>Simulierter Top-Preis</dt> 152 <dd>${escapeHtml(simulatedTopPrize)}</dd> 153 </div> 154 </dl> 155 </li> 156 `; 157} 158 159function normalizeError(error) { 160 const message = String(error ?? "").trim().replace(/supabase/gi, "Server"); 161 if (!message) return "Challenge-Tageshistorie konnte nicht geladen werden."; 162 if (message.includes("Konfiguration fehlt")) return "Tageshistorie-Konfiguration fehlt."; 163 return message; 164} 165 166function formatScore(value) { 167 const number = Number(value ?? 0); 168 if (!Number.isFinite(number)) return "0"; 169 return Math.max(0, Math.trunc(number)).toLocaleString("de-DE"); 170} 171 172function formatInteger(value) { 173 const number = Number(value ?? 0); 174 if (!Number.isFinite(number)) return "0"; 175 return Math.max(0, Math.trunc(number)).toLocaleString("de-DE"); 176} 177 178function escapeHtml(text) { 179 const s = String(text ?? ""); 180 return s 181 .replace(/&/g, "&") 182 .replace(/</g, "<") 183 .replace(/>/g, ">") 184 .replace(/"/g, """); 185}
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.