PageSourceSearch

https://my.pocketvision.de/assets/js/my-sleeper-vision/dashboard/weekly-performance-card-DiNeC4d.js

js pocketvision.de collected 2026-10-08 09:42:29 UTC 10,852 bytes, 270 lines download raw bytes

1import domtoimage from 'dom-to-image-more';
2
3/**
4 * Weekly-Performance-Kachel (PV-167): zeichnet nur die Gruppen, die der
5 * Server in `totals` liefert (Gruppen ohne Liga fehlen dort). Reihenfolge
6 * spiegelt WeeklyPerformanceCalculator::GROUPS. Chopped hat eine eigene
7 * Kachelform (Ueberleben statt Bilanz); nicht gewertete Wochen zeigen einen
8 * expliziten Pending-Zustand, nie 0-0-Unentschieden.
9 */
10export default class WeeklyPerformanceCard {
11  static FORMATS = {
12    redraft:  { label: 'Redraft' },
13    dynasty:  { label: 'Dynasty' },
14    keeper:   { label: 'Keeper' },
15    bestball: { label: 'Bestball' },
16    chopped:  { label: 'Chopped' }
17  };
18
19  static ORDER = ['redraft', 'dynasty', 'keeper', 'bestball', 'chopped'];
20
21  constructor(rootSelector) {
22    this.root = document.querySelector(rootSelector);
23    if (!this.root) return;
24
25    this.body = this.root.querySelector('#weekly-performance-body');
26    this.weekSelector = this.root.querySelector('#weekly-week-selector');
27    this.shareBtn = this.root.querySelector('#weekly-share-btn');
28
29    this.currentWeek = parseInt(this.root.dataset.currentWeek, 10);
30  }
31
32  init() {
33    if (!this.root) return;
34    this.bindEvents();
35    this.loadWeekData(this.weekSelector.value);
36  }
37
38  bindEvents() {
39    this.weekSelector.addEventListener('change', (e) => this.loadWeekData(e.target.value));
40    this.shareBtn.addEventListener('click', () => this.shareImage());
41  }
42
43  apiUrl(week) {
44    return this.root.dataset.url + `?week=${week}`;
45  }
46
47  async loadWeekData(week) {
48    this.body.innerHTML = `<p class="sv-text-dim">Loading …</p>`;
49    try {
50      const res = await fetch(this.apiUrl(week), { credentials: 'same-origin' });
51      if (!res.ok) throw new Error(`HTTP ${res.status}`);
52      const data = await res.json();
53      this.render(data);
54    } catch (err) {
55      this.body.innerHTML = `<div class="sv-alert sv-alert--danger">Could not load data. Your leagues may still be syncing in the background – please try again in a few minutes.</div>`;
56    }
57  }
58
59  render(data) {
60    if (!data || typeof data.totals !== 'object' || data.totals === null) {
61      this.body.innerHTML = `<div class="sv-alert sv-alert--danger">Unexpected response – no data available.</div>`;
62      return;
63    }
64
65    const groups = WeeklyPerformanceCard.ORDER.filter(
66      (g) => data.totals[g] && this.safeNum(data.totals[g].leagues) > 0
67    );
68
69    if (groups.length === 0) {
70      this.body.innerHTML = `
71        <div class="sv-empty">
72          <div class="sv-empty__title">No synced leagues</div>
73          <div class="sv-empty__hint">Enable sync for at least one active league under “My Leagues” to see your weekly record here.</div>
74        </div>`;
75      return;
76    }
77
78    const tiles = groups
79      .map((g) => (g === 'chopped' ? this.renderChoppedTile(data.totals[g]) : this.renderTile(g, data.totals[g])))
80      .join('');
81    const dense = groups.length > 3 ? ' sv-wp-grid--many' : '';
82    this.body.innerHTML = `<div class="sv-wp-grid${dense}" style="--sv-wp-count:${groups.length};">${tiles}</div>`;
83  }
84
85  renderTile(type, stats) {
86    const win = this.safeNum(stats.win);
87    const loss = this.safeNum(stats.loss);
88    const tie = this.safeNum(stats.tie);
89    const leagues = this.safeNum(stats.leagues);
90    const pending = this.safeNum(stats.pending);
91    const games = win + loss + tie;
92    const fmt = WeeklyPerformanceCard.FORMATS[type] ?? { label: type };
93
94    let cls, pctLabel, record;
95    if (games > 0) {
96      const pct = win / games;
97      cls = this.classifyRatio(pct).cls;
98      pctLabel = `${Math.round(pct * 100)}%`;
99      record = `${win}-${loss}-${tie}`;
100    } else {
101      cls = 'pending';
102      pctLabel = '–';
103      record = pending > 0 ? 'Not scored yet' : 'No games';
104    }
105    const p = games > 0 ? win / games : 0;
106
107    return `
108      <div class="sv-wp-tile sv-wp-tile--${cls}">
109        <div class="sv-wp-tile__head">
110          <span class="sv-wp-tile__title">${fmt.label}</span>
111          <span class="sv-badge sv-badge--neutral">${leagues} ${leagues === 1 ? 'League' : 'Leagues'}</span>
112        </div>
113
114        <div class="sv-wp-gauge" style="--p:${p};">
115          <div class="sv-wp-gauge__label">${pctLabel}</div>
116          <div class="sv-wp-gauge__sub">${record}</div>
117        </div>
118
119        <div class="sv-wp-pills">
120          <span class="sv-badge sv-badge--neutral" title="Points for – deine Punkte im Head-to-Head dieser Woche, über alle Ligen der Kachel summiert."><strong>PF</strong>&nbsp;${this.safeNum(stats.pointsFor).toFixed(1)}</span>
121          <span class="sv-badge sv-badge--neutral" title="Points against – die Punkte deiner Gegner im Head-to-Head dieser Woche."><strong>PA</strong>&nbsp;${this.safeNum(stats.pointsAgainst).toFixed(1)}</span>
122          ${pending > 0 && games > 0 ? `<span class="sv-badge sv-badge--neutral" title="Ligen, deren Woche Sleeper noch nicht gewertet hat.">${pending} pending</span>` : ''}
123        </div>
124      </div>
125    `;
126  }
127
128  renderChoppedTile(stats) {
129    const survived = this.safeNum(stats.survived);
130    const chopped = this.safeNum(stats.chopped);
131    const out = this.safeNum(stats.out);
132    const pending = this.safeNum(stats.pending);
133    const leagues = this.safeNum(stats.leagues);
134    const decided = survived + chopped;
135
136    // Kein Donut: die Ueberlebenszahlen brauchen Platz, den der Ring nicht hat.
137    // Stattdessen zwei KPI-Zahlen (Design-System-Molekuel) ueber dem
138    // Fortschrittsbalken als Ueberlebensanteil.
139    let cls, pct, caption, survivedValue, choppedValue;
140    if (decided > 0) {
141      pct = survived / decided;
142      cls = this.classifyRatio(pct).cls;
143      caption = `${Math.round(pct * 100)}% survived`;
144      survivedValue = String(survived);
145      choppedValue = String(chopped);
146    } else {
147      pct = 0;
148      cls = 'pending';
149      caption = pending > 0 ? 'Not scored yet' : 'Already out';
150      survivedValue = '–';
151      choppedValue = '–';
152    }
153    const barCls = decided === 0 ? '' : (chopped === 0 ? 'is-ok' : 'is-err');
154
155    const margin = typeof stats.cutMargin === 'number' && !isNaN(stats.cutMargin) ? stats.cutMargin : null;
156    const marginLabel = margin === null ? '–' : `${margin > 0 ? '+' : ''}${margin.toFixed(1)}`;
157
158    return `
159      <div class="sv-wp-tile sv-wp-tile--${cls}">
160        <div class="sv-wp-tile__head">
161          <span class="sv-wp-tile__title">Chopped</span>
162          <span class="sv-badge sv-badge--neutral">${leagues} ${leagues === 1 ? 'League' : 'Leagues'}</span>
163        </div>
164
165        <div class="sv-wp-survival">
166          <div class="sv-kpi-grid sv-wp-survival__kpis">
167            <div class="sv-kpi sv-kpi--ok" title="Chopped-Ligen, in denen du diese Woche nicht das niedrigste Team warst.">
168              <div class="sv-kpi__value">${survivedValue}</div>
169              <div class="sv-kpi__label">Survived</div>
170            </div>
171            <div class="sv-kpi sv-kpi--err" title="Chopped-Ligen, in denen du diese Woche ausgeschieden bist.">
172              <div class="sv-kpi__value">${choppedValue}</div>
173              <div class="sv-kpi__label">Chopped</div>
174            </div>
175          </div>
176          <div class="sv-progress" role="progressbar" aria-label="Survival rate" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${Math.round(pct * 100)}">
177            <div class="sv-progress__bar ${barCls}" style="width:${Math.round(pct * 100)}%;"></div>
178          </div>
179          <div class="sv-wp-survival__caption">${caption}</div>
180        </div>
181
182        <div class="sv-wp-pills">
183          <span class="sv-badge sv-badge--neutral" title="Points for – deine Punkte in dieser Woche, über alle Chopped-Ligen summiert, in denen du noch dabei warst."><strong>PF</strong>&nbsp;${this.safeNum(stats.pointsFor).toFixed(1)}</span>
184          <span class="sv-badge sv-badge--neutral" title="Knappster Abstand zum Beil: deine Punkte minus das niedrigste andere noch lebende Team – negativ heißt, du warst das niedrigste."><strong>Cut</strong>&nbsp;${marginLabel}</span>
185          ${out > 0 ? `<span class="sv-badge sv-badge--neutral" title="Ligen, aus denen du schon vor dieser Woche ausgeschieden bist.">${out} out</span>` : ''}
186          ${pending > 0 && decided > 0 ? `<span class="sv-badge sv-badge--neutral" title="Ligen, deren Woche Sleeper noch nicht gewertet hat.">${pending} pending</span>` : ''}
187        </div>
188      </div>
189    `;
190  }
191
192  classifyRatio(p) {
193    // Akzentfarbe kommt aus dem Tile-Modifier (CSS), nicht inline.
194    if (p >= 0.6) return { cls: 'good' };
195    if (p >= 0.45) return { cls: 'ok' };
196    return { cls: 'bad' };
197  }
198
199  safeNum(n, d = 0) {
200    return (typeof n === 'number' && !isNaN(n)) ? n : d;
201  }
202
203  async shareImage(mode = 'wide') {
204    const presets = { wide: { w: 1200, h: 630 }, tall: { w: 1080, h: 1350 } };
205    const { w, h } = presets[mode] || presets.wide;
206
207    this.shareBtn.disabled = true;
208    const originalHtml = this.shareBtn.innerHTML;
209    this.shareBtn.textContent = 'Creating image …';
210
211    const prev = {
212      width: this.root.style.width,
213      height: this.root.style.height,
214      padding: this.root.style.padding,
215      dataShare: this.root.getAttribute('data-share'),
216      hadShare: this.root.classList.contains('wp-share'),
217    };
218
219    try {
220      this.root.classList.add('wp-share');
221      this.root.setAttribute('data-share', mode);
222      this.root.style.width = w + 'px';
223      this.root.style.height = h + 'px';
224      this.root.style.padding = '24px';
225
226      // Einen Frame warten, bis Layout stabil ist
227      await new Promise(r => requestAnimationFrame(() => r()));
228
229      const bg =
230        getComputedStyle(this.root).backgroundColor ||
231        getComputedStyle(document.body).backgroundColor || '#ffffff';
232
233      const blob = await domtoimage.toBlob(this.root, {
234        width: w,
235        height: h,
236        bgcolor: bg,
237        quality: 1,
238        cacheBust: true,
239        // WICHTIG: harte Styles auf dem Klon
240        style: {
241          width: w + 'px',
242          height: h + 'px',
243          overflow: 'hidden',
244          border: '0',
245          boxShadow: 'none',
246          margin: '0',
247        },
248      });
249
250      const url = URL.createObjectURL(blob);
251      const a = document.createElement('a');
252      a.href = url;
253      a.download = `my-pocketvision-weekly-performance-${mode}.png`;
254      a.click();
255    } catch (e) {
256      console.error(e);
257      alert('Error generating image');
258    } finally {
259      if (!prev.hadShare) this.root.classList.remove('wp-share');
260      if (prev.dataShare) this.root.setAttribute('data-share', prev.dataShare);
261      else this.root.removeAttribute('data-share');
262      this.root.style.width = prev.width;
263      this.root.style.height = prev.height;
264      this.root.style.padding = prev.padding;
265      this.shareBtn.disabled = false;
266      this.shareBtn.innerHTML = originalHtml;
267    }
268  }
269
270}

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.