Next.js app keeps getting phantom hits when student laptops in charging carts—how do I stop it?

I’ve built a Next.js web app (hosted on Vercel, with a Neon Postgres database) that students open on school laptops. When they place those laptops in a charging cart that alternates power banks every 10–15 minutes (and with the tab still on the website), each bank switch briefly “wakes” the browser and triggers a network request to my app’s middleware/DB. Over a full day in the cart, this ends up firing a request every 10 minutes—even though the students aren’t actually using the page—drastically increasing my Neon usage and hitting Vercel unnecessarily.

What I’ve tried so far:

A “visibilitychange + focus” client component in Next.js that increments a counter and redirects after 4 wakes. I added a debouncing window (up to 8 minutes) so that back-to-back visibilitychange and focus events don’t double-count.

Here’s the client component I wrote that is suppose to redirect the user to a separate static webpage hosted on Github pages in order to stop making hits to my Next.js middleware and turning on my Neon database:

// components/AbsentUserChecker.tsx placed in the rootLayout
"use client";

import { useEffect } from "react";
import { usePathname } from "next/navigation";

const MAX_VISITS = process.env.NODE_ENV === "development" ? 1000 : 4;
const REDIRECT_URL = "https:www.jotter-blog-still-there/";

// Minimum gap (ms) between two counted wakes.
// If visibilitychange and focus fire within this window, we only count once.
const DEDUPE_WINDOW_MS = 7 * 60 * 1000; // 8 minutes

export default function AbsentUserChecker() {
    const pathname = usePathname();

    useEffect(() => {
        // On mount or when pathname changes, reset if needed:
        const storedPath = localStorage.getItem("lastPath");
        if (storedPath !== pathname) {
            localStorage.setItem("lastPath", pathname);
            localStorage.setItem("visitCount", "0");
            // Also clear any previous “lastIncrementTS” so we start fresh:
            localStorage.setItem("lastIncrementTS", "0");
        }

        const handleWake = () => {
            // Only count if page is actually visible
            if (document.visibilityState !== "visible") {
                return;
            }

            const now = Date.now();
            // Check the last time we incremented:
            const lastInc = parseInt(
                localStorage.getItem("lastIncrementTS") || "0",
                10
            );
            if (now - lastInc < DEDUPE_WINDOW_MS) {
                // If it’s been less than DEDUPE_WINDOW_MS since the last counted wake,
                // abort. This prevents double‐count when visibility+focus fire in quick succession.
                return;
            }

            // Record that we are now counting a new wake at time = now
            localStorage.setItem("lastIncrementTS", now.toString());

            const storedPath2 = localStorage.getItem("lastPath");
            let visitCount = parseInt(
                localStorage.getItem("visitCount") || "0",
                10
            );

            // If the user actually navigated to a different URL/pathname, reset to 1
            if (storedPath2 !== pathname) {
                localStorage.setItem("lastPath", pathname);
                localStorage.setItem("visitCount", "1");
                return;
            }

            // Otherwise, same path → increment
            visitCount += 1;
            localStorage.setItem("visitCount", visitCount.toString());

            // If we reach MAX_VISITS, clear and redirect
            if (visitCount >= MAX_VISITS) {
                localStorage.removeItem("visitCount");
                localStorage.removeItem("lastPath");
                localStorage.removeItem("lastIncrementTS");
                window.location.href = REDIRECT_URL;
            }
        };

        document.addEventListener("visibilitychange", handleWake);
        window.addEventListener("focus", handleWake);

        return () => {
            document.removeEventListener("visibilitychange", handleWake);
            window.removeEventListener("focus", handleWake);
        };
    }, [pathname]);

    return null;
}

The core issue:
Charging-cart bank switches either (a) don’t toggle visibilityState in some OS/browser combos, or (b) fully freeze/suspend the tab with no “resume” event until a human opens the lid. As a result, my client logic never sees a “wake” event—and so the counter never increments and no redirect happens. Meanwhile, the cart’s brief power fluctuation still wakes the network layer enough to hit my server.

What I’m looking for:
Is there any reliable, cross-browser event or API left that will fire when a laptop’s power source changes (AC ↔ battery) or when the OS briefly re-enables the network—even if the tab never “becomes visible” or “gains focus”? If not, what other strategies can I use to prevent these phantom hits without accidentally logging students out or redirecting them when they’re legitimately interacting? Any ideas or workarounds would be hugely appreciated!

Cannot find getReactNativePersistence when using Firebase Auth with AsyncStorage in React Native (Expo)

I’m developing a React Native app using Expo and trying to persist Firebase Auth state across sessions with AsyncStorage.

Here is my firebaseConfig.ts file:

import { initializeApp } from "firebase/app";
import { initializeAuth, getReactNativePersistence } from "firebase/auth";
import ReactNativeAsyncStorage from '@react-native-async-storage/async-storage';

const firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "..."
};

export const app = initializeApp(firebaseConfig);
export const auth = initializeAuth(app, {
  persistence: getReactNativePersistence(ReactNativeAsyncStorage) 
});

When trying to import getReactNativePersistence, I get this error:

Module ‘”firebase/auth”‘ has no exported member ‘getReactNativePersistence’.

Is there any way to properly use getReactNativePersistence in this setup, or another recommended approach to persist Firebase Auth state in Expo without ejecting or using a custom dev client?

Any help would be appreciated!

What I’ve already tried:

Installed dependencies:

npm install firebase
npm install @react-native-async-storage/async-storage

My package.json includes:

"@react-native-async-storage/async-storage": "^2.1.2",
"firebase": "^11.8.1"

Tried adding this to tsconfig.json based on suggestions I found:

{
  "compilerOptions": {
    "paths": {
      "@firebase/auth": ["./node_modules/@firebase/auth/dist/index.rn.d.ts"]
    }
  },
  "extends": "expo/tsconfig.base"
}

However, this did not solve the issue. Also, when I check my node_modules/@firebase directory, there is no auth folder at all.

$(…).jstree is not a function, depending upon where/when called

I’m trying to get some code working that will fetch jsTree configuration data from an AJAX source. Although I am able to get the needed data from the backend I am getting a “TypeError: $(…).jstree is not a function” error when trying configure jsTree.

And when I drill down on this I see the configuration data being fetched is irrelevant. I get the same error with a hard coded config. It seems like it’s the timing of configuration that is the issue.

Note the test case below. When the jstree() call #1 is enabled I get the error. When call #2 is enabled everything works fine. Both of the jstree() calls are the same.

<div id="jstree_demo_div"></div>

<script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.3/jstree.min.js"></script>

<script>
    $(document).ready(function() {
        $.ajax({
            type: "get",
            url: "https://localhost:44366/Dashboard/JsTree?handler=ArrayData",
            contentType: "application/x-www-form-urlencoded",
            success: function (data) {
                // 1 - This does not work although it is identical to line as #2 below.
                $('#jstree_demo_div').jstree({ 'core' : { 'data' : [ 'First', 'Second' ] } });
            }
        });
    });

    // 2 - When this is enabled.  It works.
    //$('#jstree_demo_div').jstree({ 'core' : { 'data' : [ 'First', 'Second' ] } });
</script>

I have also tried using $(window).load() instead of $(document).ready() but that made no difference.

Has anyone else come across this issue?

Why am I unable to copy text to the clipboard using the Clipboard API?

I’m trying to make an HTML element where, when clicked, copies some text to the clipboard. I was originally using the Clipboard API, but it doesn’t work.

Does anybody know how I can do a similar thing that would work on all modern browsers (assuming they are fully up to date)? It’s for a website of mine, and I want it to copy my email when clicked. It could be a button, link, whatever.

I tried using a button that ran a JS function called copyText(text), but nothing actually got copied.

function copyText(text) {
    navigator.clipboard.writeText(text)
    console.log("${text} copied successfully!")
}

When I ran the above code using <button onclick="copyText(copy)" target="_blank">copy</button> , I get this error (in Chrome):

Uncaught ReferenceError: copy is not defined
    at HTMLButtonElement.onclick (linkWork.html:16:63)

How to delete a file from Nostr Blossom server using NDK?

I am currently studying Nostr and trying to delete a file I previously uploaded to a Blossom server.

I executed the following JavaScript code using @nostr-dev-kit/ndk and @nostr-dev-kit/ndk-blossom:

import NDK, { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { NDKBlossom } from "@nostr-dev-kit/ndk-blossom";

const signer = new NDKPrivateKeySigner("nsec..."); // Replaced with my actual nsec

const ndk = new NDK({
    explicitRelayUrls: ["wss://nos.lol"],
    signer: signer
});
await ndk.connect();

const blossom = new NDKBlossom(ndk);
const serverList = await blossom.getServerList();
console.log("serverList:", serverList.servers);

const hash = "hash value"; // Replaced with the actual hash of the file I want to delete
const result = await blossom.deleteBlob(hash);
console.log("result:", result);

 
When I execute this code, I get the following output:

serverList: [ "https://cdn.nostrcheck.me" ]
result: false

 
The serverList correctly identifies https://cdn.nostrcheck.me (which I believe is a Blossom server), but the deleteBlob method returns false, indicating that the deletion failed.

I’ve confirmed that the hash value is correct for the file I want to delete.
I verified this by using blossom.listBlobs() and checking the returned list of blobs.

 
Here’s my environment:

  • bun: 1.2.15
  • @nostr-dev-kit/ndk: 2.14.24
  • @nostr-dev-kit/ndk-blossom: 0.1.23

 
Could someone please point me in the right direction or tell me what might be wrong with my code or understanding?

Thank you in advance!

How to keep my dialog from moving my page to the top

I have this dialog:

<dialog id="image-popup" class="xyzzy">

   <form method="dialog">
        <button class="large-button" id="close" aria-label="close" formnovalidate><b>Close</b></button>
   </form> 
  <img src="#" alt="" width="560" height="420"/>
</dialog>

and this script:

<script>
  function showPopupImage(alt, src) {
    const popup = document.getElementById('image-popup')
    popup.querySelector('img').src = src
    popup.querySelector('img').alt = alt
    popup.showModal()
  }

</SCRIPT>

which I call like this:

<a href="#" onclick="showPopupImage('keys', 'keys.jpg')">keys</a> 

All this work fine, except if I click one of these links further down the page, the page scrolls to the top when the dialog opens.

I’ve spent most of the afternoon googling this but no joy. Thanks in advance.

Validation messages not clearing with resetForm() or $(“#myForm”)[0].reset()

I’m trying to understand how input validation works. In the HTML below, btnconfirm triggers the validation errors and resetForm resets the form.

resetForm clears the input boxes, but doesn’t clear the validation messages.

The first validation of “username” works, but the message is not shown (ie. I don’t want a popup).

The only one that works is the second one, but it doesn’t clear with “Reset”.

@page
@model Products.Pages.IndexModel
@{
    //var product = Model.product;
}
@Html.AntiForgeryToken()

    <form id="myForm">

        <input type="text" id="username" required/>
        <label for="username">Username required</label>
        <br />

        <input type="text" id="firstname" name="firstname" data-val="true" data-val-required="First Name REQUIRED">
        <span data-valmsg-for="firstname" data-valmsg-replace="true"></span>
        <br />

        <input type="text" id="lastname" name="lastname" required>
        <span for="lastname">last name required</span>
        <br />

        <button id="btnconfirm" class="button" type="submit">Check inputs</button>
            <input type="reset" id="resetForm" value="Reset">
    </form>

@section Scripts {
    @{
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }

    <script>
        $("#resetForm").click(function(){
            var validator = $( "#myForm" ).validate();
            validator.resetForm();

            $("#myForm")[0].reset();

            alert("reset");
        });
    </script>
}

How to change the text orientation of the letters in to upright or normal?

Hovering over the <div class="menu-item"> or the numbers should show the letters inside of <div class="submenu"> <buttons> <span> in upright orientation. But for some reason it’s not happening (except for “1”, in which case by default the orientation is upright). I have also asked ChatGPT about this, nothing helped.

const menuItems = document.querySelectorAll(".menu-item");
const itemCount = menuItems.length;
const radius = 100;

menuItems.forEach((item, index) => {
  const angle = (index / itemCount) * (2 * Math.PI);
  const x = radius * Math.cos(angle);
  const y = radius * Math.sin(angle);
  item.style.left = `calc(50% + ${x}px)`;
  item.style.top = `calc(50% + ${y}px)`;
  item.style.transform = `translate(-50%, -50%) rotate(${angle}rad)`;

  const text = item.querySelector("span");
  if (text) {
    text.style.transform = `rotate(${-angle}rad)`;
    text.style.display = "inline-block";
  }
});

document.querySelectorAll(".submenu").forEach((submenu) => {
  const buttons = submenu.querySelectorAll("button");
  const btnCount = buttons.length;
  const btnRadius = 60;

  buttons.forEach((btn, i) => {
    const angle = (i / btnCount) * (2 * Math.PI);
    const x = btnRadius * Math.cos(angle);
    const y = btnRadius * Math.sin(angle);
    btn.style.left = `calc(50% + ${x}px)`;
    btn.style.top = `calc(50% + ${y}px)`;
    btn.style.setProperty("--angle", `${angle}rad`);
    btn.style.transform = `translate(-50%, -50%) rotate(${angle}rad)`;

    const text = btn.querySelector("span");
    if (text) {
      text.style.transform = `rotate(${-angle}rad)`;
      text.style.display = "inline-block";
    }
  });
});

/* Previous code for positioning menu items and submenus

const menuItems = document.querySelectorAll(".menu-item");
const itemCount = menuItems.length;
const radius = 100;

menuItems.forEach((item, index) => {
  const angle = (index / itemCount) * (2 * Math.PI);
  const x = radius * Math.cos(angle);
  const y = radius * Math.sin(angle);
  item.style.left = `calc(50% + ${x}px)`;
  item.style.top = `calc(50% + ${y}px)`;
  item.style.transform = `translate(-50%, -50%) rotate(${angle}rad)`;
});

// Position submenu buttons in a circle around each menu item
document.querySelectorAll(".submenu").forEach((submenu) => {
  const buttons = submenu.querySelectorAll("button");
  const btnCount = buttons.length;
  const btnRadius = 60;

  buttons.forEach((btn, i) => {
    const angle = (i / btnCount) * (2 * Math.PI);
    const x = btnRadius * Math.cos(angle);
    const y = btnRadius * Math.sin(angle);
    btn.style.left = `calc(50% + ${x}px)`;
    btn.style.top = `calc(50% + ${y}px)`;
    btn.style.transform = `translate(-50%, -50%) rotate(${angle}rad)`;
  });
});*/
body {
  background: linear-gradient(135deg, #1e1e2f, #292940);
  height: 100vh;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  font-family: "Segoe UI", sans-serif;
}

.menu-center {
  position: relative;
  width: 220px;
  height: 220px;
}

.central-button {
  position: absolute;
  width: 90px;
  height: 90px;
  background: linear-gradient(135deg, #3498db, #2980b9);
  border-radius: 50%;
  color: white;
  font-weight: bold;
  font-size: 16px;
  display: flex;
  justify-content: center;
  align-items: center;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  cursor: pointer;
  box-shadow: 0 0 15px rgba(52, 152, 219, 0.4);
  z-index: 2;
  transition: transform 0.3s ease;
}

.central-button:hover {
  transform: translate(-50%, -50%) scale(1.1);
  box-shadow: 0 0 20px rgba(52, 152, 219, 0.8);
}

.menu-item {
  position: absolute;
  width: 20px;
  height: 20px;
  top: 40%;
  left: 40%;
  background: linear-gradient(135deg, #f39c12, #e67e22);
  transform-origin: center center;
  border-radius: 50%;
  color: white;
  font-weight: bold;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: all 0.4s ease;
  box-shadow: 0 0 10px rgba(243, 156, 18, 0.4);
}

.menu-label,
button span {
  display: inline-block;
  transform-origin: center;
}

.menu-item span,
.submenu button span {
  display: inline-block;
  transform-origin: center center;
  position: absolute;
  white-space: nowrap;
}

.menu-center:hover .menu-item {
  opacity: 1;
  pointer-events: auto;
}

.menu-center:hover .menu-item:nth-child(2) {
  transition-delay: 0s;
}

.menu-center:hover .menu-item:nth-child(3) {
  transition-delay: 0.1s;
}

.menu-center:hover .menu-item:nth-child(4) {
  transition-delay: 0.2s;
}

.menu-center:hover .menu-item:nth-child(5) {
  transition-delay: 0.3s;
}

.menu-center:hover .menu-item:nth-child(6) {
  transition-delay: 0.4s;
}

.menu-center:hover .menu-item:nth-child(7) {
  transition-delay: 0.5s;
}

.menu-center:hover .menu-item:nth-child(8) {
  transition-delay: 0.6s;
}

.menu-center:hover .menu-item:nth-child(9) {
  transition-delay: 0.7s;
}

.menu-item:hover {
  transform: scale(1.15);
  box-shadow: 0 0 15px rgba(243, 156, 18, 0.8);
  z-index: 1;
}

.submenu {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
}

.menu-item:hover .submenu button {
  opacity: 1;
  pointer-events: auto;
}

.menu-item:hover .submenu button:nth-child(1) {
  transition-delay: 0s;
}

.menu-item:hover .submenu button:nth-child(2) {
  transition-delay: 0.1s;
}

.menu-item:hover .submenu button:nth-child(3) {
  transition-delay: 0.2s;
}

.menu-item:hover .submenu button:nth-child(4) {
  transition-delay: 0.3s;
}

.menu-item:hover .submenu button:nth-child(5) {
  transition-delay: 0.4s;
}

.menu-item:hover .submenu button:nth-child(6) {
  transition-delay: 0.5s;
}

.menu-item:hover .submenu button:nth-child(7) {
  transition-delay: 0.6s;
}

.menu-item:hover .submenu button:nth-child(8) {
  transition-delay: 0.7s;
}

.submenu button {
  position: absolute;
  width: 30px;
  height: 30px;
  background: linear-gradient(135deg, #2ecc71, #27ae60);
  top: 45%;
  left: 45%;
  transform-origin: center center;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  color: white;
  font-size: 10px;
  font-weight: bold;
  opacity: 0;
  transition: all 0.4s ease;
  transition-delay: 0s;
  opacity: 0;
  box-shadow: 0 0 8px rgba(46, 204, 113, 0.5);
}

.submenu button:hover {
  transform: translate(-50%, -50%) rotate(var(--angle, 0rad)) scale(1.2);
  box-shadow: 0 0 12px rgba(46, 204, 113, 0.9);
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Radial Menu</title>
  <link rel="stylesheet" href="style.css" />
</head>

<body>
  <div class="menu-center">
    <div class="central-button">Menu</div>
    <div class="menu-item">
      <span class="menu-label">1</span>
      <div class="submenu">
        <button><span>A</span></button>
        <button><span>B</span></button>
        <button><span>C</span></button>
        <button><span>D</span></button>
        <button><span>E</span></button>
        <button><span>F</span></button>
        <button><span>G</span></button>
        <button><span>H</span></button>
      </div>
    </div>
    <div class="menu-item">
      <span class="menu-label">2</span>
      <div class="submenu">
        <button><span>A</span></button>
        <button><span>B</span></button>
        <button><span>C</span></button>
        <button><span>D</span></button>
        <button><span>E</span></button>
        <button><span>F</span></button>
        <button><span>G</span></button>
        <button><span>H</span></button>
      </div>
    </div>
    <div class="menu-item">
      <span class="menu-label">3</span>
      <div class="submenu">
        <button><span>A</span></button>
        <button><span>B</span></button>
        <button><span>C</span></button>
        <button><span>D</span></button>
        <button><span>E</span></button>
        <button><span>F</span></button>
        <button><span>G</span></button>
        <button><span>H</span></button>
      </div>
    </div>
    <div class="menu-item">
      <span class="menu-label">4</span>
      <div class="submenu">
        <button><span>A</span></button>
        <button><span>B</span></button>
        <button><span>C</span></button>
        <button><span>D</span></button>
        <button><span>E</span></button>
        <button><span>F</span></button>
        <button><span>G</span></button>
        <button><span>H</span></button>
      </div>
    </div>
    <div class="menu-item">
      <span class="menu-label">5</span>
      <div class="submenu">
        <button><span>A</span></button>
        <button><span>B</span></button>
        <button><span>C</span></button>
        <button><span>D</span></button>
        <button><span>E</span></button>
        <button><span>F</span></button>
        <button><span>G</span></button>
        <button><span>H</span></button>
      </div>
    </div>
    <div class="menu-item">
      <span class="menu-label">6</span>
      <div class="submenu">
        <button><span>A</span></button>
        <button><span>B</span></button>
        <button><span>C</span></button>
        <button><span>D</span></button>
        <button><span>E</span></button>
        <button><span>F</span></button>
        <button><span>G</span></button>
        <button><span>H</span></button>
      </div>
    </div>
    <div class="menu-item">
      <span class="menu-label">7</span>
      <div class="submenu">
        <button><span>A</span></button>
        <button><span>B</span></button>
        <button><span>C</span></button>
        <button><span>D</span></button>
        <button><span>E</span></button>
        <button><span>F</span></button>
        <button><span>G</span></button>
        <button><span>H</span></button>
      </div>
    </div>
    <div class="menu-item">
      <span class="menu-label">8</span>
      <div class="submenu">
        <button><span>A</span></button>
        <button><span>B</span></button>
        <button><span>C</span></button>
        <button><span>D</span></button>
        <button><span>E</span></button>
        <button><span>F</span></button>
        <button><span>G</span></button>
        <button><span>H</span></button>
      </div>
    </div>
  </div>
  <script src="script.js"></script>
</body>

</html>

how to play audio in an html5 page when opened in android facebook webview?

I have an html5 application that uses three.js library for some 3d wireframe graphics
and a background music (mp3).

The page works perfectly in chrome.
But If I send to to someone on facebook and they click on the link, the link opens in messenger’s webview and there is no audio.

The page unmutes and plays the audio on user gesture (onclick event on a start button).

But still there is no audio.

But if I use an embed of a google drive audio or video, the video HAS audio even in the webview.

So there must be a way.

As of now my code does this:

document.getElementById('startButton').addEventListener('click', () => {
[...]
            const startButton = document.getElementById('startButton');
            startButton.style.display = 'none'; // Hide the button

            const audio = document.getElementById('background-music');
            audio.muted = false; // Ensure not muted
            audio.play().catch(e => console.error("Error playing audio:", e));
[...]
}

what can I do?

Clear input required validation message data-val-required

I have a modal popup with an input field with a required validator. The user can close the modal popup and reopen.

I’m including the html below. The validation works fine in the sense that the “Required field” message is displayed when the user doesn’t enter a value.

The issue is that when the user closes the modal popup and re-opens, the message is still there.

How can I reset the validation or set the text to an empty string when the user opens modal popup?

<input type="text" id="fullname" name="fullname" data-val="true" data-val-required="Required field">
<span style="font-size: small; color: red" data-valmsg-for="fullname" data-valmsg-replace="true"></span>

How to create a repeater field and save it with WooCommerce Settings API?

I have created a repeater form in the WooCommerce settings (custom settings page).

Repeater form settings

The form works great but I’ve come into an issue with how to save and load it back in with the WP Option. The current solution I’ve been working with is a javascript object that updates a hidden input based on what is currently in the form. But this doesn’t work that well without bugs. I’m assuming there may be an easier way to update it.

Here is my current code for what would pass the JSON into the database:

add_action( 'woocommerce_settings_save_qwc_quote_settings', 'qwc_save_repeater_form' );
function qwc_save_repeater_form() {
  $form_field_options = ! empty( $_REQUEST[ 'form-options' ] ) ?   json_decode($_REQUEST[ 'form-options' ])  : '';
  if($form_field_options !== '') {
    update_option( 'qwc_form_fields', $form_field_options );
  }
}

The “form-options” is the hidden input that is updated via javascript. Is there a more efficient way to make it so this form creates a PHP array in the options table and loads it back in without having to use a custom built javascript workflow?

Here is also my current code for the repeater table.

<div class="qwc_form">
    <table class="qwc_repeater_options">
      <thead>
        <tr>
          <th>Name</th>
          <th>Type</th>
          <th>Class</th>
          <th>Placeholder</th>
          <th>Validation</th>
          <th>Required</th>
          <th>Connect to</th>
          <th>Remove</th>
        </tr></thead>
      <tbody>
      <tr class="hidden">
                <td><input name="name" placeholder="Enter field name..." type="text" value="" /></td>
                <td><select name="type">
                  <?php
                    foreach($type_options as $option) {
                      ?><option value="<?php echo $option; ?>"><?php echo ucwords($option); ?></option><?php
                    }
                  ?>
                </select></td>
                <td><input name="classes" type="text" placeholder="Enter class names without commas, seperate by space." value="" /></td>
                <td><input name="placeholder" type="text" placeholder="Field placeholder" value="" /></td>
                <td><input name="validation" type="checkbox" value="" /></td>
                <td><input name="required" type="checkbox" value="" /></td>
                <td class="connect"><select name="connection" value="">
                    <?php
                      foreach($connection_options as $meta => $option) {
                        ?>
                          <option value="<?php echo $meta; ?>"><?php echo $option; ?></option>  
                        <?php
                      }
                    ?>
                </select>
                <input type="text" class="cf_connect" style="display:none;" />
                </td>
                <td><button class="remove">X</button></td>
              </tr>
        <?php
          foreach($form_fields as $row) {
            ?>
              <tr data-id="0">
                <td><input name="name" placeholder="Enter field name..." type="<?php echo $row['type'] ?>" value="<?php echo $row['name'] ?>" /></td>
                <td><select name="type">
                  <?php
                    foreach($type_options as $option) {
                      ?><option value="<?php echo $option; ?>"><?php echo ucwords($option); ?></option><?php
                    }
                  ?>
                </select></td>
                <td><input name="classes" type="text" placeholder="Enter class names without commas, seperate by space." value="<?php echo $row['class'] ?>" /></td>
                <td><input name="placeholder" type="text" placeholder="Field placeholder" value="<?php echo $row['placeholder'] ?>" /></td>
                <td><input name="validation" type="checkbox" value="<?php echo $row['validation'] ?>" /></td>
                <td><input name="required" type="checkbox" value="<?php echo $row['required'] ?>" /></td>
                <td class="connect"><select name="connection" value="<?php echo $row['connection'] ?>">
                    <?php
                      foreach($connection_options as $meta => $option) {
                        ?>
                          <option value="<?php echo $meta; ?>"><?php echo $option; ?></option>  
                        <?php
                      }
                    ?>
                </select>
                <input type="text" class="cf_connect" style="display:none;" />
                </td>
                <td><button class="remove">X</button></td>
              </tr>
            <?php
          }
        ?>
      </tbody>
  </table>
  <button class="button addrow">Add Row</button>
  <input id="form-options" name="form-options" value="" />
  </div>

How to make random 404 pages?

I’m hosting a website on Neocities, and I want to make a random 404 webpage. So basically when you reload the 404 page it loads a different one every time or changes images and text on an existing one.

I think Javascript is needed and I never used Javascript, so can you explain it very detailed for me?

javascript : display the words “accept” or “deny”

I want to display the words “accept” or “deny” in the “observation” column from the “average” column line by line, but the problem is that my code displays the first line.

Here’s my PHP code:

<table name="cart" class="table table-bordered">
    <tr style='background: whitesmoke;'>
        <th>Evaluation</th>
        <th>Devoir</th>
        <th>Examen</th>
        <th>moyennne</th>
        <th>observation</th>
    </tr>
    <?php
    $query = "SELECT * FROM s3  limit 10";
    $result = mysqli_query($conn,$query);

    while($row = mysqli_fetch_array($result) ){
        $id = $row['id'];
  
        $numstudent = $row['numstudent'];
        $evaluation = $row['evaluation'];
        $devoir = $row['devoir'];
        $examen = $row['examen']; 
    ?>
        <tr name="line_items">
            <td><input class="decimal form-control" type='text' name='evaluation_<?= $id ?>' value='<?= $evaluation ?>' ></td>
            <td><input class="decimal form-control" type='text' name='devoir_<?= $id ?>' value='<?= $devoir ?>' ></td>
            <td><input class="decimal form-control" type='text'  name='examen_<?= $id ?>' value='<?= $examen ?>' ></td>
            <td><input class="decimal form-control" type='text' id="fname" name='item_total' jAutoCalc="((({evaluation_<?= $id ?>} + {devoir_<?= $id ?>})/2)+({examen_<?= $id ?>}*2))/3" >
             
            </td>
            <td><input class="form-control" type='text' id="fname1"  readonly></td>
        </tr>
    <?php
    }
    ?>
</table>

and javascript code:

document.getElementById("fname").onchange = function() {
  myFunction()
};

function myFunction() {
  var x = document.getElementById("fname");
  var y = document.getElementById("fname1");
  var val = "";
  var z = x.value;
  if (z >= 10.00) {
    val = "admis";
  } else {
    val = "ajourné";
  }

  y.value = val;;
}

When entering grades, the average and observation are automatically changed for a single line.
I want to enter them line by line, and the average and observation are automatically changed.
How do I do this?

Express-validator invokes catch block of the controller

I use express-validator to check email from req.body. Validation works fine, but when I enter proper email (errors array of express-validator is empty) catch block (errorHandler – “Network error”) of the controller kicks in. When I remove validation logic from controller everything works fine. Why?

errorHandler:

    const errorHandler = (error, req, res, next) => {
  if (error instanceof AppError) {
    return res.status(error.statusCode).json({
      message: error.message,
      statusCode: error.statusCode,
    });
  }

  return res.status(500).send({ message: "Network error." });
};
module.exports = errorHandler;

controller:

    exports.sendForgotPasswordEmail = async (req, res, next) => {
  try {
    const result = validationResult(req);
    if (!result.isEmpty()) {
      throw new AppError(result.array()[0].msg, 400);
    }

    const data = matchedData(req);
    const { email } = data;
    
    const emailHash = bcrypt.hashSync(email, 8);
    const token =
      Math.random().toString(36).substring(7) +
      Math.round(new Date().getTime() / 1000) +
      emailHash;
    const user = await User.findOne({ email });
    if (!user) {
      throw new AppError("User doesn't exists.", 400);
    }  
      await ResetPassword.updateOne(
        {
          email,
        },
        {
          $set: {
            email,
            token,
            siteUrl: process.env.SITE_URL,
            timestamp: Date.now(),
          },
        },
        {
          upsert: true,
        }
      );
      res.send({
        message:
          "Reset Password Link sent to the registered email successfully.",
      });
    
  } catch (error) {
    return next(error);
  }
};

route:

    router.post(
  "/forgot-password",
  body("email")
    .trim()
    .notEmpty()
    .withMessage("Email can not be empty")
    .isEmail()
    .withMessage("Email must contain @ character."),
  sendForgotPasswordEmail
);

Inconsistent JavaScript module execution on Shopify dev store

I’m working on a custom JavaScript setup for a Shopify development store. I use Vite to bundle everything into a single JS file that gets included via:

{{ 'bundle.js' | asset_url | script_tag }}

The Problem
On some page loads, all logs and functionality behave as expected.

On other loads (without code changes), my module logs still appear (showing the script ran), but core functionality breaks silently.

Most commonly, event listeners like click and change don’t fire, or DOM-dependent code fails to execute.

This only happens sometimes, seemingly depending on when and how Shopify renders DOM elements, or how third-party apps modify them.

I’ve wrapped my initialization in DOMContentLoaded, but it doesn’t reliably fix the issue.

Tried Solutions

  • Removing Vite entirely and using plain tags – same issue.
  • Switching to import maps – no improvement.
  • Adding a version parameter to the script URL (?v=123) and dynamically parsing it to bypass Shopify caching – no effect.

Example 1: Overall JS Initialization Logic
I initialize many features and utilities from different modules, like so:

import { initA, initB } from './module-a.js';
import { initC, initD } from './module-b.js';
import { setupLightbox } from './lightbox.js';

const initialize = () => {
  initA();
  initB();
  initC();
  initD();

  if (window.location.href.includes('custom-config')) {
    initCustomFeature();
  }
};

document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM loaded');
  initialize();

  setupLightbox({
    selector: '.lightbox',
    loop: true,
    touchNavigation: true,
  });
});

Even though DOMContentLoaded fires and logs appear, some modules fail to work properly — especially ones relying on DOM elements added by Shopify sections or apps.

Module with Click + Change Event Listeners
This is a simplified version of a module where the issue appears:

export const exampleFeature = {
  initialize() {
    this.sizeContainer = document.querySelector('.size-options');
    this.countSelector = document.querySelector('.count-selector');

    this.setupEventListeners();
    this.updateCountOptions('4');
  },

  setupEventListeners() {
    this.sizeContainer?.addEventListener('click', (e) => {
      const selected = e.target.closest('.size-option');
      if (!selected) return;

      document.querySelectorAll('.size-option').forEach((el) => el.classList.remove('active'));
      selected.classList.add('active');

      this.updateCountOptions(selected.dataset.size);
    });

    this.countSelector?.addEventListener('change', (e) => {
      const value = e.target.value;
      this.updateSelectionUI(value);
    });
  },

  updateCountOptions(size) {
    // Logic to update count options
  },

  updateSelectionUI(value) {
    // Update UI or internal state
  },
};

This module often fails silently: either the event listeners don’t fire, or the DOM elements return null when selected — despite DOMContentLoaded supposedly guaranteeing that the DOM is ready.