PageSourceSearch

https://tnw-mvp-app.vercel.app/src/screens/challengeDailyHistoryScreen.js

js tnw-mvp-app.vercel.app collected 2026-10-03 07:53:32 UTC 5,839 bytes, 185 lines download raw bytes

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, "&amp;")
182    .replace(/</g, "&lt;")
183    .replace(/>/g, "&gt;")
184    .replace(/"/g, "&quot;");
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.