PageSourceSearch

https://www.picklebeastpickleball.com/assets/controllers/bracket_s…66a3ba7c325674dfcbf6419681167dca09eee391.js

js picklebeastpickleball.com collected 2026-09-24 14:17:26 UTC 7,639 bytes, 205 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2
3// Tournament Admin — Bracket Seed List
4// =============================================================
5// Stimulus controller for the "Seed Teams" card on the
6// admin_bracket_viewer partial (brackets_page).
7//
8// Surfaces:
9//   - Inline seed_number editing per team
10//   - "Auto-seed by registration order" button (1..N in row order)
11//   - "Clear seeds" button (sets all to nil)
12//
13// All writes go through teams_v2#update_team via PATCH. The URL template
14// has a literal `TEAM_ID` placeholder that we substitute per row — that
15// pattern is the one we use elsewhere in the V2 teams UI so the controller
16// stays Turbo-Frame-safe.
17//
18// Why a Stimulus controller (not inline <script>): inline scripts inside
19// the Turbo Frame on brackets_page survive the initial load but are NOT
20// re-executed when the frame re-renders after a division switch. Stimulus
21// auto-connects on every DOM insertion.
22
23export default class extends Controller {
24  static targets = ["list", "row", "seedInput", "status"];
25  static values = {
26    updateUrlTemplate: String,
27    csrf: String,
28    divisionId: String,
29  };
30
31  connect() {
32    // Initial render is server-rendered. Drag state lives here.
33    this.draggingId = null;
34  }
35
36  // ── Drag and drop ──────────────────────────────────────────────
37  // HTML5 native DnD. On drop we (a) reorder the DOM, (b) re-assign
38  // 1..N seeds in the new visual order, (c) PATCH each team in
39  // sequence. No external library — keeps the controller small and
40  // Turbo-Frame-safe.
41
42  onDragStart(event) {
43    const row = event.target.closest("[data-team-id]");
44    if (!row) return;
45    this.draggingId = row.dataset.teamId;
46    row.classList.add("is-dragging");
47    if (event.dataTransfer) {
48      event.dataTransfer.effectAllowed = "move";
49      // Some browsers require a non-empty dataTransfer payload for
50      // drag to fire properly.
51      event.dataTransfer.setData("text/plain", this.draggingId);
52    }
53  }
54
55  onDragEnd(event) {
56    const row = event.target.closest("[data-team-id]");
57    if (row) row.classList.remove("is-dragging");
58    this.draggingId = null;
59    // Clear any lingering drop-target highlights.
60    this.listTarget.querySelectorAll(".is-drop-target").forEach((el) => el.classList.remove("is-drop-target"));
61  }
62
63  onDragEnter(event) {
64    const row = event.target.closest("[data-team-id]");
65    if (!row || row.dataset.teamId === this.draggingId) return;
66    row.classList.add("is-drop-target");
67  }
68
69  onDragLeave(event) {
70    const row = event.target.closest("[data-team-id]");
71    if (!row) return;
72    // The relatedTarget can be a child element of the same row during
73    // sub-element drag-enter/leave — only clear when truly leaving the
74    // row's bounding box.
75    if (event.relatedTarget && row.contains(event.relatedTarget)) return;
76    row.classList.remove("is-drop-target");
77  }
78
79  onDragOver(event) {
80    // Required to allow drop. preventDefault enables the drop event.
81    event.preventDefault();
82    if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
83  }
84
85  onDrop(event) {
86    event.preventDefault();
87    if (!this.draggingId) return;
88    const draggingRow = this.listTarget.querySelector(`[data-team-id="${this.draggingId}"]`);
89    const targetRow = event.target.closest("[data-team-id]");
90    if (!draggingRow || !targetRow || draggingRow === targetRow) {
91      this.#clearDropTargets();
92      return;
93    }
94
95    // Insert above or below based on the cursor position within the
96    // target row. Above-half → insert before; below-half → insert after.
97    const rect = targetRow.getBoundingClientRect();
98    const cursorY = event.clientY;
99    const isAbove = cursorY < rect.top + rect.height / 2;
100    if (isAbove) {
101      this.listTarget.insertBefore(draggingRow, targetRow);
102    } else {
103      this.listTarget.insertBefore(draggingRow, targetRow.nextSibling);
104    }
105    this.#clearDropTargets();
106
107    // Re-seed every row 1..N based on the new visual order.
108    this.#reseedAllInOrder();
109  }
110
111  #clearDropTargets() {
112    this.listTarget.querySelectorAll(".is-drop-target").forEach((el) => el.classList.remove("is-drop-target"));
113  }
114
115  // Assigns sequential seeds (1..N) by visual order and PATCHes each
116  // row whose seed actually changed.
117  #reseedAllInOrder() {
118    const rows = Array.from(this.listTarget.querySelectorAll("[data-team-id]"));
119    rows.forEach((row, idx) => {
120      const teamId = row.dataset.teamId;
121      const seed = idx + 1;
122      const input = row.querySelector("[data-bracket-seed-list-target='seedInput']");
123      const currentVal = input ? parseInt(input.value, 10) : NaN;
124      if (input) input.value = seed;
125      if (currentVal !== seed) {
126        this.#patchSeed(teamId, seed, row);
127      }
128    });
129  }
130
131  // Called when a seed input changes (or blurs).
132  onSeedChange(event) {
133    const input = event.target;
134    const row = input.closest("[data-team-id]");
135    if (!row) return;
136    const teamId = row.dataset.teamId;
137    const raw = input.value.trim();
138    const seedNumber = raw === "" ? null : parseInt(raw, 10);
139    if (raw !== "" && (Number.isNaN(seedNumber) || seedNumber < 1)) {
140      this.#setStatus(row, "error", "Invalid");
141      return;
142    }
143    this.#patchSeed(teamId, seedNumber, row);
144  }
145
146  // Set seeds 1..N in current row order.
147  autoSeedByCreatedAt(event) {
148    event.preventDefault();
149    const rows = Array.from(this.listTarget.querySelectorAll("[data-team-id]"));
150    rows.forEach((row, idx) => {
151      const teamId = row.dataset.teamId;
152      const seed = idx + 1;
153      const input = row.querySelector("[data-bracket-seed-list-target='seedInput']");
154      if (input) input.value = seed;
155      this.#patchSeed(teamId, seed, row);
156    });
157  }
158
159  // Clear every team's seed (set to null).
160  clearSeeds(event) {
161    event.preventDefault();
162    if (!window.confirm("Clear seed numbers for every team in this division?")) return;
163    const rows = Array.from(this.listTarget.querySelectorAll("[data-team-id]"));
164    rows.forEach((row) => {
165      const teamId = row.dataset.teamId;
166      const input = row.querySelector("[data-bracket-seed-list-target='seedInput']");
167      if (input) input.value = "";
168      this.#patchSeed(teamId, null, row);
169    });
170  }
171
172  // ── private ────────────────────────────────────────────────────
173
174  #patchSeed(teamId, seedNumber, row) {
175    const url = this.updateUrlTemplateValue.replace("TEAM_ID", teamId);
176    this.#setStatus(row, "saving", "Saving…");
177    fetch(url, {
178      method: "PATCH",
179      headers: {
180        "Content-Type": "application/json",
181        Accept: "application/json",
182        "X-CSRF-Token": this.csrfValue,
183      },
184      body: JSON.stringify({ seed_number: seedNumber }),
185    })
186      .then((r) => r.json().then((data) => ({ ok: r.ok, data })))
187      .then(({ ok, data }) => {
188        if (ok) {
189          this.#setStatus(row, "saved", "Saved");
190          setTimeout(() => this.#setStatus(row, "", ""), 900);
191        } else {
192          this.#setStatus(row, "error", data?.error || "Failed");
193        }
194      })
195      .catch(() => this.#setStatus(row, "error", "Network"));
196  }
197
198  #setStatus(row, kind, text) {
199    const status = row.querySelector("[data-bracket-seed-list-target='status']");
200    if (!status) return;
201    status.textContent = text;
202    status.className = "tap-seed-row__status";
203    if (kind) status.classList.add(`tap-seed-row__status--${kind}`);
204  }
205};

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.