PageSourceSearch

https://railsblocks.com/assets/controllers/banner_controller-89c7d…06948e33368ad440d5c4e3a45a82274105a599ac.js

js railsblocks.com collected 2026-09-25 20:22:50 UTC 7,838 bytes, 228 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2
3// Connects to data-controller="banner"
4export default class extends Controller {
5  static targets = ["days", "hours", "minutes", "seconds"];
6  static values = {
7    cookieName: { type: String, default: "banner_dismissed" }, // The name of the cookie to store the banner dismissal
8    cookieDays: { type: Number, default: 0 }, // How long the cookie should persist (-1 = no cookie, 0 = session only, >0 = days)
9    countdownEndTime: { type: String, default: "" }, // ISO 8601 date string (e.g., "2024-12-31T23:59:59")
10    autoHide: { type: Boolean, default: false }, // Auto hide after countdown expires (I recommend setting this to true by default)
11  };
12
13  connect() {
14    // Check if banner was previously dismissed - do this BEFORE showing anything
15    if (this.isBannerDismissed()) {
16      // Don't show the banner at all, just remove from DOM
17      return;
18    }
19
20    // Check if countdown has already expired (only if countdown is configured)
21    if (this.countdownEndTimeValue) {
22      const endTime = new Date(this.countdownEndTimeValue);
23      if (!isNaN(endTime.getTime()) && endTime <= new Date()) {
24        // Countdown already expired, don't show banner
25        return;
26      }
27    }
28
29    // Check if banner has been shown before (to skip animation on subsequent page loads)
30    const hasBeenShown = this.hasBannerBeenShown();
31
32    // Banner should be shown - remove hidden class
33    this.element.classList.remove("hidden");
34
35    if (hasBeenShown) {
36      // Already shown before - skip animation and show immediately
37      this.element.classList.remove("opacity-0", "-translate-y-full", "translate-y-full");
38      this.element.classList.add("opacity-100", "translate-y-0");
39    } else {
40      // First time showing - animate in
41      requestAnimationFrame(() => {
42        this.element.classList.remove("opacity-0");
43        // Remove only the y-axis translations, preserve x-axis (for centered banners)
44        this.element.classList.remove("-translate-y-full", "translate-y-full");
45        this.element.classList.add("opacity-100", "translate-y-0");
46      });
47      // Mark banner as shown (session cookie so it persists across page navigations)
48      this.markBannerAsShown();
49    }
50
51    // Initialize countdown if we have a target end time
52    if (this.countdownEndTimeValue) {
53      this.initializeCountdown();
54    }
55  }
56
57  disconnect() {
58    if (this.countdownTimer) {
59      clearInterval(this.countdownTimer);
60    }
61  }
62
63  initializeCountdown() {
64    // Parse the end time as ISO 8601 date string
65    const endTime = new Date(this.countdownEndTimeValue);
66
67    // Validate the date
68    if (isNaN(endTime.getTime())) {
69      console.error(
70        "Invalid countdown end time. Please provide a valid ISO 8601 date string (e.g., '2024-12-31T23:59:59'):",
71        this.countdownEndTimeValue,
72      );
73      return;
74    }
75
76    this.endTime = endTime;
77
78    // Update immediately
79    this.updateCountdown();
80
81    // Update every second
82    this.countdownTimer = setInterval(() => {
83      this.updateCountdown();
84    }, 1000);
85  }
86
87  updateCountdown() {
88    const now = new Date();
89    const timeRemaining = this.endTime - now;
90
91    if (timeRemaining <= 0) {
92      // Countdown expired
93      this.handleCountdownExpired();
94      return;
95    }
96
97    // Calculate days, hours, minutes, seconds
98    const days = Math.floor(timeRemaining / (1000 * 60 * 60 * 24));
99    const hours = Math.floor((timeRemaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
100    const minutes = Math.floor((timeRemaining % (1000 * 60 * 60)) / (1000 * 60));
101    const seconds = Math.floor((timeRemaining % (1000 * 60)) / 1000);
102
103    // Update targets if they exist
104    if (this.hasDaysTarget) {
105      this.daysTarget.textContent = String(days).padStart(2, "0");
106    }
107    if (this.hasHoursTarget) {
108      this.hoursTarget.textContent = String(hours).padStart(2, "0");
109    }
110    if (this.hasMinutesTarget) {
111      this.minutesTarget.textContent = String(minutes).padStart(2, "0");
112    }
113    if (this.hasSecondsTarget) {
114      this.secondsTarget.textContent = String(seconds).padStart(2, "0");
115    }
116  }
117
118  handleCountdownExpired() {
119    // Clear the interval
120    if (this.countdownTimer) {
121      clearInterval(this.countdownTimer);
122      this.countdownTimer = null;
123    }
124
125    // Display zeros
126    if (this.hasDaysTarget) this.daysTarget.textContent = "00";
127    if (this.hasHoursTarget) this.hoursTarget.textContent = "00";
128    if (this.hasMinutesTarget) this.minutesTarget.textContent = "00";
129    if (this.hasSecondsTarget) this.secondsTarget.textContent = "00";
130
131    // Auto hide if configured
132    if (this.autoHideValue) {
133      setTimeout(() => {
134        this.hide();
135      }, 2000); // Wait 2 seconds before hiding
136    }
137  }
138
139  hide(event) {
140    if (event) event.preventDefault();
141
142    this.#performHide();
143  }
144
145  // Hide without preventing default - use this for links that should navigate
146  hideAndNavigate(event) {
147    // Set cookie immediately (before navigation) so it persists
148    if (this.cookieDaysValue !== -1) {
149      this.setCookie(this.cookieNameValue, "true", this.cookieDaysValue);
150    }
151
152    // Don't prevent default - allow links to navigate
153    this.#performHide(true); // Pass true to skip cookie setting in setTimeout
154  }
155
156  #performHide(skipCookie = false) {
157    // Add exit animation using Tailwind classes
158    this.element.classList.remove("opacity-100", "translate-y-0");
159    this.element.classList.add("opacity-0");
160
161    // Add the appropriate slide out based on position
162    if (this.element.classList.contains("top-0")) {
163      this.element.classList.add("-translate-y-full");
164    } else if (this.element.classList.contains("bottom-0")) {
165      this.element.classList.add("translate-y-full");
166    }
167
168    // Wait for animation to complete before removing
169    setTimeout(() => {
170      // Set cookie to remember dismissal (only if not already set)
171      if (!skipCookie && this.cookieDaysValue !== -1) {
172        this.setCookie(this.cookieNameValue, "true", this.cookieDaysValue);
173      }
174
175      // Remove element from DOM when using cookies, otherwise just hide it
176      if (this.cookieDaysValue !== -1) {
177        this.element.remove();
178      } else {
179        // When cookieDays is -1, keep element in DOM but hide it for refresh functionality
180        this.element.classList.add("hidden");
181      }
182    }, 300); // Match the transition duration
183  }
184
185  // Cookie management methods
186  setCookie(name, value, days) {
187    if (days === 0) {
188      // Session cookie (no expiration - lasts until browser closes)
189      document.cookie = name + "=" + value + ";path=/;SameSite=Lax";
190    } else {
191      // Persistent cookie with expiration
192      const date = new Date();
193      date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
194      const expires = "expires=" + date.toUTCString();
195      document.cookie = name + "=" + value + ";" + expires + ";path=/;SameSite=Lax";
196    }
197  }
198
199  getCookie(name) {
200    const nameEQ = name + "=";
201    const cookies = document.cookie.split(";");
202    for (let i = 0; i < cookies.length; i++) {
203      let cookie = cookies[i];
204      while (cookie.charAt(0) === " ") {
205        cookie = cookie.substring(1, cookie.length);
206      }
207      if (cookie.indexOf(nameEQ) === 0) {
208        return cookie.substring(nameEQ.length, cookie.length);
209      }
210    }
211    return null;
212  }
213
214  isBannerDismissed() {
215    return this.getCookie(this.cookieNameValue) === "true";
216  }
217
218  hasBannerBeenShown() {
219    const shownCookieName = this.cookieNameValue + "_shown";
220    return this.getCookie(shownCookieName) === "true";
221  }
222
223  markBannerAsShown() {
224    const shownCookieName = this.cookieNameValue + "_shown";
225    // Use session cookie (0 days) so it persists across page navigations but clears when browser closes
226    this.setCookie(shownCookieName, "true", 0);
227  }
228};

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.