PageSourceSearch

https://www.picklebeastpickleball.com/assets/controllers/tournamen…f672c612fcec53253300cc19b600274105ee4a47.js

js picklebeastpickleball.com collected 2026-09-24 14:18:26 UTC 7,413 bytes, 234 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = [
5    "recipientSelect", "recipientCount",
6    "messageTypeSection", "messageTypeRadio",
7    "messageSection", "messageInput", "charCount",
8    "sendBtn", "sendText",
9    "successState", "successText"
10  ]
11
12  static values = {
13    tournamentId: Number
14  }
15
16  connect() {
17    console.log("Tournament broadcast controller connected")
18    this.updateCharCount()
19    // Register global method for triggering from outside controller scope
20    window.openTournamentBroadcast = () => this.open()
21  }
22
23  disconnect() {
24    delete window.openTournamentBroadcast
25  }
26
27  open() {
28    this.element.style.display = "flex"
29    document.body.style.overflow = "hidden"
30    this.resetForm()
31    // Focus message input after animation
32    setTimeout(() => {
33      if (this.hasMessageInputTarget) {
34        this.messageInputTarget.focus()
35      }
36    }, 100)
37  }
38
39  close() {
40    this.element.style.display = "none"
41    document.body.style.overflow = ""
42    this.resetForm()
43  }
44
45  resetForm() {
46    if (this.hasMessageInputTarget) {
47      this.messageInputTarget.value = ""
48    }
49    if (this.hasRecipientSelectTarget) {
50      this.recipientSelectTarget.value = "all"
51    }
52    if (this.hasMessageTypeSectionTarget) {
53      this.messageTypeSectionTarget.style.display = "none"
54    }
55    if (this.hasMessageSectionTarget) {
56      this.messageSectionTarget.style.display = "block"
57    }
58    if (this.hasSuccessStateTarget) {
59      this.successStateTarget.style.display = "none"
60    }
61    // Reset message type radios to custom
62    this.messageTypeRadioTargets.forEach(radio => {
63      radio.checked = radio.value === "custom"
64    })
65    this.updateCharCount()
66    this.updateRecipientCount()
67    this.validateForm()
68  }
69
70  updateRecipientCount() {
71    if (!this.hasRecipientSelectTarget || !this.hasRecipientCountTarget) return
72
73    const selectedOption = this.recipientSelectTarget.selectedOptions[0]
74    const count = selectedOption?.dataset.count || "0"
75    this.recipientCountTarget.textContent = count
76
77    // Show/hide message type section based on selection
78    const isAllParticipants = this.recipientSelectTarget.value === "all"
79    if (this.hasMessageTypeSectionTarget) {
80      this.messageTypeSectionTarget.style.display = isAllParticipants ? "none" : "block"
81    }
82
83    // Update send button text
84    if (this.hasSendTextTarget) {
85      this.sendTextTarget.textContent = `Send to ${count}`
86    }
87
88    this.validateForm()
89  }
90
91  toggleMessageType() {
92    const selectedType = this.messageTypeRadioTargets.find(r => r.checked)?.value || "custom"
93
94    if (this.hasMessageSectionTarget) {
95      // Show message input only for custom messages
96      this.messageSectionTarget.style.display = selectedType === "custom" ? "block" : "none"
97    }
98
99    this.validateForm()
100  }
101
102  updateCharCount() {
103    if (!this.hasMessageInputTarget || !this.hasCharCountTarget) return
104
105    const length = this.messageInputTarget.value.length
106    this.charCountTarget.textContent = length
107
108    // Change color when approaching limit
109    if (length > 180) {
110      this.charCountTarget.parentElement.style.color = "var(--pb-color-warning)"
111    } else {
112      this.charCountTarget.parentElement.style.color = ""
113    }
114
115    this.validateForm()
116  }
117
118  validateForm() {
119    if (!this.hasSendBtnTarget) return
120
121    const isAllParticipants = this.recipientSelectTarget?.value === "all"
122    const selectedType = this.messageTypeRadioTargets.find(r => r.checked)?.value || "custom"
123
124    // For all participants, always require custom message
125    // For divisions, require message only if custom type selected
126    const needsMessage = isAllParticipants || selectedType === "custom"
127    const hasMessage = this.messageInputTarget?.value.trim().length > 0
128
129    const isValid = !needsMessage || hasMessage
130
131    this.sendBtnTarget.disabled = !isValid
132  }
133
134  async send() {
135    if (!this.hasSendBtnTarget || this.sendBtnTarget.disabled) return
136
137    const sendBtn = this.sendBtnTarget
138    const originalText = this.sendTextTarget?.textContent || "Send"
139
140    // Show loading state
141    sendBtn.disabled = true
142    if (this.hasSendTextTarget) {
143      this.sendTextTarget.innerHTML = `
144        <svg class="spinning" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
145          <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
146        </svg>
147        Sending...
148      `
149    }
150
151    try {
152      const isAllParticipants = this.recipientSelectTarget?.value === "all"
153      const divisionId = isAllParticipants ? null : this.recipientSelectTarget?.value
154      const message = this.messageInputTarget?.value.trim()
155      const messageType = this.messageTypeRadioTargets.find(r => r.checked)?.value || "custom"
156
157      let response
158      if (isAllParticipants) {
159        // Broadcast to all tournament participants
160        response = await fetch(`/api/tournaments/${this.tournamentIdValue}/broadcast`, {
161          method: "POST",
162          headers: {
163            "Content-Type": "application/json",
164            "Accept": "application/json",
165            "X-CSRF-Token": this.getCSRFToken()
166          },
167          body: JSON.stringify({
168            message: message,
169            include_pending: true
170          })
171        })
172      } else {
173        // Broadcast to specific division
174        response = await fetch(`/api/tournaments/${this.tournamentIdValue}/divisions/${divisionId}/broadcast`, {
175          method: "POST",
176          headers: {
177            "Content-Type": "application/json",
178            "Accept": "application/json",
179            "X-CSRF-Token": this.getCSRFToken()
180          },
181          body: JSON.stringify({
182            message_type: messageType,
183            custom_message: messageType === "custom" ? message : null,
184            include_pending: true
185          })
186        })
187      }
188
189      const data = await response.json()
190
191      if (!response.ok) {
192        throw new Error(data.error || "Failed to send broadcast")
193      }
194
195      // Show success state
196      this.showSuccess(data.sent_count || data.total_participants || 0)
197
198    } catch (error) {
199      console.error("Error sending broadcast:", error)
200      alert(error.message || "Failed to send broadcast")
201
202      // Reset button
203      sendBtn.disabled = false
204      if (this.hasSendTextTarget) {
205        this.sendTextTarget.textContent = originalText
206      }
207    }
208  }
209
210  showSuccess(sentCount) {
211    // Hide form elements
212    const formElements = this.element.querySelectorAll(
213      ".tournament-broadcast-modal__recipient, " +
214      ".tournament-broadcast-modal__message-type, " +
215      ".tournament-broadcast-modal__message, " +
216      ".tournament-broadcast-modal__warning, " +
217      ".tournament-broadcast-modal__footer"
218    )
219    formElements.forEach(el => el.style.display = "none")
220
221    // Show success state
222    if (this.hasSuccessStateTarget) {
223      this.successStateTarget.style.display = "flex"
224    }
225    if (this.hasSuccessTextTarget) {
226      this.successTextTarget.textContent = `Your message was sent to ${sentCount} participant${sentCount !== 1 ? "s" : ""}.`
227    }
228  }
229
230  getCSRFToken() {
231    const meta = document.querySelector('meta[name="csrf-token"]')
232    return meta ? meta.content : ""
233  }
234};

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.