PageSourceSearch

https://railsblocks.com/assets/controllers/newsletter_controller-5…5d3c537c306682a8e7911ec7ffd3f73c571f80de.js

js railsblocks.com collected 2026-09-25 20:23:45 UTC 5,133 bytes, 169 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2
3export default class extends Controller {
4  static targets = ["email", "form", "successMessage", "submitButton"];
5
6  async submit(event) {
7    event.preventDefault();
8
9    const email = this.emailTarget.value.trim();
10
11    if (!email) {
12      this.showError("Please enter your email address");
13      return;
14    }
15
16    if (!this.isValidEmail(email)) {
17      this.showError("Please enter a valid email address");
18      return;
19    }
20
21    this.setLoading(true);
22
23    // Get Turnstile controller
24    const turnstileElement = this.element.querySelector('[data-controller*="turnstile"]');
25    const turnstileController = turnstileElement
26      ? this.application.getControllerForElementAndIdentifier(turnstileElement, "turnstile")
27      : null;
28
29    // Check if we already have a token
30    let turnstileToken = turnstileController?.getToken();
31
32    // If no token, execute the challenge and wait for it
33    if (!turnstileToken && turnstileController) {
34      // Execute the challenge
35      turnstileController.execute();
36
37      // Wait for the turnstile:success event
38      try {
39        await this.waitForTurnstile(turnstileElement);
40        turnstileToken = turnstileController.getToken();
41      } catch (error) {
42        console.error("Turnstile verification failed:", error);
43        this.setLoading(false);
44        this.showError("Verification failed. Please try again.");
45        return;
46      }
47    }
48
49    // Fallback: check input field
50    if (!turnstileToken) {
51      const tokenInput = this.element.querySelector('input[name="cf-turnstile-response"]');
52      turnstileToken = tokenInput?.value;
53    }
54
55    if (!turnstileToken) {
56      console.error("No Turnstile token available");
57      this.setLoading(false);
58      this.showError("Verification failed. Please try again.");
59      return;
60    }
61
62    try {
63      const response = await fetch("/newsletter", {
64        method: "POST",
65        headers: {
66          "Content-Type": "application/json",
67          "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')?.content,
68        },
69        body: JSON.stringify({
70          email: email,
71          "cf-turnstile-response": turnstileToken,
72        }),
73      });
74
75      const data = await response.json();
76
77      if (response.ok) {
78        this.showSuccess();
79
80        // Track successful newsletter signup
81        if (typeof fathom !== "undefined") {
82          fathom.trackEvent("newsletter signup");
83        }
84
85        // Track with PostHog
86        if (typeof posthog !== "undefined") {
87          posthog.capture("newsletter signup", {
88            email: email,
89          });
90        }
91      } else {
92        this.showError(data.error || "Something went wrong. Please try again.");
93      }
94    } catch (error) {
95      console.error("Newsletter subscription error:", error);
96      this.showError("Something went wrong. Please try again.");
97    } finally {
98      this.setLoading(false);
99    }
100  }
101
102  showSuccess() {
103    this.formTarget.style.display = "none";
104    this.successMessageTarget.style.display = "block";
105  }
106
107  showError(message) {
108    // Remove any existing error messages
109    const existingError = this.element.querySelector(".error-message");
110    if (existingError) {
111      existingError.remove();
112    }
113
114    // Create and show error message
115    const errorDiv = document.createElement("div");
116    errorDiv.className = "error-message mt-2 text-sm text-red-600 dark:text-red-400";
117    errorDiv.textContent = message;
118    this.formTarget.appendChild(errorDiv);
119
120    // Remove error message after 5 seconds
121    setTimeout(() => {
122      if (errorDiv.parentNode) {
123        errorDiv.remove();
124      }
125    }, 5000);
126  }
127
128  setLoading(loading) {
129    if (loading) {
130      this.submitButtonTarget.disabled = true;
131      this.submitButtonTarget.textContent = "Subscribing...";
132      this.submitButtonTarget.classList.add("opacity-50", "cursor-not-allowed");
133    } else {
134      this.submitButtonTarget.disabled = false;
135      this.submitButtonTarget.textContent = "Subscribe";
136      this.submitButtonTarget.classList.remove("opacity-50", "cursor-not-allowed");
137    }
138  }
139
140  isValidEmail(email) {
141    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
142    return emailRegex.test(email);
143  }
144
145  waitForTurnstile(element, timeout = 10000) {
146    return new Promise((resolve, reject) => {
147      const timer = setTimeout(() => {
148        reject(new Error("Turnstile verification timeout"));
149      }, timeout);
150
151      const successHandler = () => {
152        clearTimeout(timer);
153        element.removeEventListener("turnstile:success", successHandler);
154        element.removeEventListener("turnstile:error", errorHandler);
155        resolve();
156      };
157
158      const errorHandler = () => {
159        clearTimeout(timer);
160        element.removeEventListener("turnstile:success", successHandler);
161        element.removeEventListener("turnstile:error", errorHandler);
162        reject(new Error("Turnstile verification error"));
163      };
164
165      element.addEventListener("turnstile:success", successHandler);
166      element.addEventListener("turnstile:error", errorHandler);
167    });
168  }
169};

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.