Why do only some sites work when running Synchronous JavaScript XMLHTTPRequests in a data:text/html URI?

Quick Note before I begin: While I discuss Apple Shortcuts to explain the context behind my question, this is still a web/JavaScript problem. The only thing that you need to know is that I cannot run asynchronous or callback JavaScript code. If you are curious about the details behind the exact implementation/limitations of the JavaScript code made in this post, see Running JavaScript in Shortcuts. An example shortcut displaying my problem (which goes over the exact same things talked about in this post) can be found here


Once upon a time, I had some intentionally terrible and outdated JavaScript that I was forced to use as a workaround for a limitation on a block-coding automation app on my iPhone called “Shortcuts.” You see, there are times when I need the automations I write for Shortcuts to access an API or send some API calls. Shortcuts has an internal API Request Library called “Get Contents of URL”, but it’s borderline useless because it only returns response bodies. Since I needed additional data like the request’s status code, I made my own mediocre API Requester replacement in JavaScript. This is what it looked like in shortcuts:

data:text/html,<body/><script>o={error:"NetworkError",message:"You%20are%20offline!",stack:null};if(navigator.onLine){r=new%20XMLHttpRequest();r.open("GET","https://jsonplaceholder.typicode.com/users",false);try{r.send();o={"Status":r.status,"Response":r.responseText}}catch(e){o={error:e?.name??"UnknownError",message:e?.message??String(e),stack:e?.stack??null};}};document.write(JSON.stringify(o));</script>

And here it is as a more human-legible snippet:

<body />
<script>
  o = {
    error: "NetworkError",
    message: "You are offline!",
    stack: null
  };

  if (navigator.onLine) {
    r = new XMLHttpRequest();
    r.open("GET", "https://jsonplaceholder.typicode.com/users", false);
  
    try {
      r.send();
      o = {
        "Status": r.status,
        "Response": r.responseText
      }
    } catch (e) {
      o = {
        error: e?.name ?? "UnknownError",
        message: e?.message ?? String(e),
        stack: e?.stack ?? null
      };
    }
  };
  document.write(JSON.stringify(o));
</script>

I would then extract the text from the JavaScript webpage, then move on with my life. However, I’m finding that some URLs passed into this just flat out don’t work. For instance, take https://zenquotes.io/api/random, a REST API GET request that’s supposed to return a random quote:

<body />
<script>
  o = {
    error: "NetworkError",
    message: "You are offline!",
    stack: null
  };

  if (navigator.onLine) {
    r = new XMLHttpRequest();
    r.open("GET", "https://zenquotes.io/api/random", false);
  
    try {
      r.send();
      o = {
        "Status": r.status,
        "Response": r.responseText
      }
    } catch (e) {
      o = {
        error: e?.name ?? "UnknownError",
        message: e?.message ?? String(e),
        stack: e?.stack ?? null
      };
    }
  };
  document.write(JSON.stringify(o));
</script>

And here’s its pasteable URL equivalent:

data:text/html,<body/><script>o={error:"NetworkError",message:"You%20are%20offline!",stack:null};if(navigator.onLine){r=new%20XMLHttpRequest();r.open("GET","https://zenquotes.io/api/random",false);try{r.send();o={"Status":r.status,"Response":r.responseText}}catch(e){o={error:e?.name??"UnknownError",message:e?.message??String(e),stack:e?.stack??null};}};document.write(JSON.stringify(o));</script>

On mobile, all it does is give a vague “NetworkError” error explaining that it couldn’t connect to the internet. Thankfully, I get a more verbose error when I run the code on my PC saying that it “failed to load [URL].” No idea what that means, why it’s happening, or how to fix it (if that’s even possible). All I did was change the URL that it’s requesting, so I’m assuming that my code as-is isn’t all-encompassing enough or that I’m missing some important information. I think this because when I try requesting the URL through Shortcut’s native Request Library, it works fine. Any ideas? If you want to see what this problem looks like where I’m running it, check out this demo shortcut I made (all it does is go over everything I just talked about here and lets you test your own JS code if you have an idea for a solution)

detect when state update is from user input in field instead of props passed in

I have an css animation I want to fire on a text input field. I’m doing this by assigning the value to the key of the component, and keyframing the animation(not shown here):

function Input({value}) {
  return <input key={value}/>
}

The form that holds this field can be auto-filled with a button click. In that case, I do want to fire the animation, but I don’t want to fire the animation when the user is typing in the field. The problem is the top-level state is updated on a change callback for the field.

function Input({value, changeHandler}) {
  return <input key={value} onChange={changeHandler}/>
}

I’ve been playing around with ref and useEffect and setting a static 'input' to the ref, the issue is once the user makes a change through the callback, we never update the animation again.

function Input({value, changeHandler}) {

  const source = useRef(null);
  const [val, setVal] = useState(value);
  useEffect(() => {
    if (source.current !== "input") {
      setVal(value)
    }
  });

  const handler = (e) => {
    source.current = "input";
    changeHandler(e.target.value)
  };

  return <input key={value} onChange={handler}/>
}

I feel like there’s something I could do with useCallback or useMemo, but I can’t get my head around when to update the state.

como podría mejorar mi pseudocodigo? [closed]

Tengo este código sobre los pasos a seguir para crear una cita del parte del docente a un padre de familia, que más se le podría agregar a este código para que funcione bien y que se muestre en pantalla lo solicitado

Inicio
Mostrar pantalla “Inicio/Login”

Solicitar: usuario, contraseña
Si usuario y contraseña son válidos Entonces
    Redirigir a "Menú principal"
Sino
    Mostrar mensaje: "Credenciales inválidas"
    Volver a solicitar login
FinSi

Mientras usuario no cierre sesión Hacer
    Mostrar "Menú principal"
    Mostrar opciones:
        1. Registrar nueva citación
        2. Consultar citaciones
        3. Cerrar sesión

    Leer opción

    Según opción Hacer
        Caso 1:
            Ir a "Registrar citación"
            Solicitar:
                - Nombre del estudiante
                - Curso/Paralelo
                - Motivo de la citación
                - Fecha y hora
                - Observaciones
            Si campos obligatorios están completos Entonces
                Guardar citación en base de datos
                Mostrar mensaje: "Citación guardada exitosamente"
                Volver a menú
            Sino
                Mostrar mensaje: "Complete todos los campos"
            FinSi
        FinCaso

        Caso 2:
            Ir a "Consultar citaciones"
            Mostrar filtros:
                - Por estudiante
                - Por fecha
                - Por curso
            Mostrar tabla de resultados según filtros
            Si usuario da clic en “Ver detalle” Entonces
                Mostrar detalle completo de la citación
            FinSi
            Si usuario presiona “REGRESAR” Entonces
                Volver a "Menú principal"
            FinSi
        FinCaso

        Caso 3:
            Confirmar cierre de sesión
            Si confirma Entonces
                Terminar sesión
                Volver a pantalla de "Login"
            FinSi
        FinCaso
        Otro:
            Mostrar mensaje: "Opción inválida"
    FinSegún
FinMientras

Fin

Google Sheets Custom Function Receiving Literal Values Instead of Range Objects (Persistent Issue)

I’m experiencing an extremely frustrating and persistent issue with a Google Apps Script custom function in my Google Sheet. Despite extensive troubleshooting, my custom function (designed to sum cells by background color) is consistently receiving literal cell values (e.g., “1268.74,,263.98,…”) as arguments, instead of actual Range objects (e.g., C33:C45). This prevents the script from executing correctly.

Problem Summary:
My custom function’s signature (function SUM_UNCOLORED_CELLS(…ranges)) is correctly defined and the JSDoc /** @param {…GoogleAppsScript.Spreadsheet.Range} ranges … */ is in place. When I type the formula in the sheet, the helper tooltip correctly displays ranges (Range, …). However, upon execution, the Apps Script logs show:
Skipping invalid or empty range argument at index 0: 1268.74,,263.98,100,,48,,231.86,123,0,0,0,0
(and similar lines for all arguments), with a — Final sum: 0 —.

Goal: Sum numbers in non-contiguous ranges (e.g., C33:C45, C48:C54, …) that have no fill color (white/transparent background).

Formula in Sheet:
=SUM_UNCOLORED_CELLS(C33:C45,C48:C54,C57:C64,C67:C70,C73:C90,C93:C97,C100:C108,C111:C116)

Apps Script Code (Code.gs):

JavaScript


/**
 * Sums the values in multiple ranges that do NOT have a custom fill color.
 * It considers both truly transparent backgrounds ("") and default white backgrounds ("#ffffff") as "no fill".
 *
 * @param {...GoogleAppsScript.Spreadsheet.Range} ranges A list of ranges to sum (e.g., C1:C10, D1:D10).
 * @return {Number} The sum of cells without a custom background color across all provided ranges.
 * @customfunction
 */
function SUM_UNCOLORED_CELLS(...ranges) {
  var sum = 0;
  Logger.log('--- Starting SUM_UNCOLORED_CELLS execution ---');

  for (var k = 0; k < ranges.length; k++) {
    var currentRange = ranges[k];
    if (currentRange && typeof currentRange.getValues === 'function') {
      // This block should execute, but based on logs, currentRange is not a Range object
      Logger.log('Processing range: ' + currentRange.getA1Notation()); // This line is never reached
      var values = currentRange.getValues();
      var backgrounds = currentRange.getBackgrounds();
      for (var i = 0; i < values.length; i++) {
        for (var j = 0; j < values[0].length; j++) {
          var cellValue = values[i][j];
          var cellBackground = backgrounds[i][j];
          if ((cellBackground === '' || cellBackground === '#ffffff') && typeof cellValue === 'number' && !isNaN(cellValue)) {
            sum += cellValue;
          }
        }
      }
    } else {
          // THIS IS THE PROBLEM SEEN IN MY LOGS:
          Logger.log('Skipping invalid or empty range argument at index ' + k + ': '   + currentRange);
        }
      }
      return sum;
    }

What I’ve Tried (All Unsuccessful):

Ensured script code is accurate, authorized, and uses correct JSDoc.

Cleared browser cache and cookies (all time).

Refreshed/reopened the Google Sheet/browser multiple times.

Deleted and re-entered the formula from scratch.

Renamed the custom function (from SUM_NO_COLOR to SUM_UNCOLORED_CELLS) to force re-indexing by Google Sheets.

Tried testing with a single range (e.g., =SUM_UNCOLORED_CELLS(C33:C40)), which also failed with the same “Skipping invalid argument” error in logs.

Confirmed the script is correctly bound to the specific Google Sheet.

(Also previously got an “only takes one argument” error message in the sheet, despite …ranges.)

My Conclusion:
It appears there’s a specific, persistent issue within my Google Sheets environment (potentially a bug or deep caching problem) that prevents range references from being passed as Range objects to custom functions, even when the function’s signature seems to be recognized by the sheet’s tooltip.

Any help or insights on why Sheets might be passing literal values instead of Range objects for custom functions would be greatly appreciated. Thank you!”

Attempted to use a custom Google Apps Script function: (SUM_UNCOLORED_CELLS) to sum cells based on background color.

Confirmed custom function script code is correct and designed to accept Range objects.

Confirmed the script is authorized for my Google account.

Observed contradictory behavior:

The formula helper tooltip in Google Sheets correctly shows the function expects (Range, …) arguments.

However, Apps Script execution logs consistently show the function is receiving literal values (e.g., “1268.74,,263.98,…”) instead of actual Range objects from the sheet.

This leads to Skipping invalid or empty range argument in logs and a final sum of 0.

Performed standard browser/sheet troubleshooting:

Cleared browser cache and cookies (all time).

Refreshed the Google Sheet page (F5/Ctrl+F5) multiple times.

Closed and reopened the Google Sheet tab and browser.

Deleted and re-entered the formula from scratch in the sheet.

Renamed the custom function (to SUM_UNCOLORED_CELLS) to force Google Sheets to re-index its signature.

Tested the function with a single, simple range (e.g., =SUM_UNCOLORED_CELLS(C33:C40)) which also failed with the same argument-passing error.

Attempted to get help through official channels: Tried to post to the Google Sheets Help Community forum but was blocked due to a “community policy” violation.

Datepicker input box in form doesnt work when I dublicate the html [closed]

when I dublicate a datepicker in html it doesnt show right again

this is the code

<div class="col-md-6">
<div id="date" class="relative input-group date" data-date-format="dd-mm-yyyy">
<i class="absolute top-0 end-0 id-color pt-3 pe-3 icofont-calendar"></i>
<input class="form-control" name="date" type="text" >
<span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span>
</div>
</div>

This is the JS behind the form

$date = $_POST['date'];
$('.date-field').datepicker();


$headers = "MIME-Version: 1.1";
$headers .= "Content-type: text/html; charset=iso-8859-1";

$message .= 'Date & Time : ' . $date. " " . $time . "n";

PayPal Checkout with GooglePay button in advanced integration – dynamic total price does not work

I used this integration for placing GooglePay button in checkout: https://github.com/paypal-examples/googlepay/tree/main/advanced-integration

It works well. I also change “totalPrice” variable in app.js, line 151: https://github.com/paypal-examples/googlepay/blob/main/advanced-integration/public/app.js to show price dynamically (depending on what the customer sets in checkout).

But then I am not able to set the price that will be displayed at the final payment (when the payment confirmation window opens). Variable “purchaseAmount” in paypal-api.js, line 11: https://github.com/paypal-examples/googlepay/blob/main/advanced-integration/paypal-api.js

For example, I set services in checkout for $15 (from app.js, line 151). Clicked GooglePay button for payment, Google payment window opened with price $15, clicked to pay, window of my bank opened and price is still $0.10 (from paypal-api.js, line 11).

I tried modifying the code in paypal-api.js to load the price dynamically, but it didn’t work for me. There is a note “TODO: pull prices from a database”, but I don’t want to load prices from database (I don’t have anything like that). I want to load price from my HTML form.

Has anyone solved this issue? Or does anyone know how to do it? I am unable to set variable in paypal-api.js, line 11.

Thanks guys.

Why does this code not work? I want to replicate the .join method of java threads in JS

import fs from 'node:fs';


function readFilePromise(fileRoute, encoding) {
    return new Promise(resolve => resolve(fs.readFileSync(fileRoute, encoding)));
}
let word;

readFilePromise('./data/word.txt', 'utf8')
    .then(content => word = content.trim())
    .then(console.log('Finished'));

while(word === undefined) {}

console.log(word);

I tried wrapping the readFileSync function to make it async.
./data/word.txt is a file that contains a single word, stored in utf8.
In this code, the .then chain ends because I see the “Finished” log in the console, but the “word” variable’s value doesn’t change, so the while loop never ends.

Why?
Maybe this question is very dumb, I know I can add a third .then to the chain and do whatever I want with the content of the file, but I want to replicate the .join method of java threads.
Is there any way to do that with JS promises?

Animate element along an svg based on user scrolling

I’m trying to animate an element along an SVG path, but based on scroll rather than time.
The idea is:

  • The element should follow a predefined SVG path.
  • It should stay centered in the viewport as the user scrolls.
  • The path animation should be scroll-driven (not time-based).
  • The page has multiple sections, and I want the element to pause
    movement whenever a full section is visible in the viewport.

So far, animating along an SVG path with CSS alone is easy when it’s time-based, but I’m not sure how to:

  1. Tie the element’s position along the path to the scroll position.
  2. Detect when a section is fully visible and pause the animation at
    that point.

What’s the best way to implement this? Is there a library or technique that makes this easier? Any tips or example code would be appreciated.

const circle = document.getElementById('circle');
const path = document.getElementById('myPath');
const pathLength = path.getTotalLength();

circle.style.position = 'absolute';

function updateCirclePosition() {
  const scrollPercentage = window.scrollY / (document.body.scrollHeight - window.innerHeight);
  const pathPoint = path.getPointAtLength(scrollPercentage * pathLength);

  circle.style.left = pathPoint.x - 10 + 'px';
  circle.style.top = pathPoint.y - 10 + 'px';
}

window.addEventListener('scroll', updateCirclePosition);
updateCirclePosition();
body {
  height: 200vh;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

#container {
  position: relative;
  width: 400px;
  height: 200px;
}

#myPath {
  fill: none;
  stroke: black;
  stroke-width: 2;
}

#circle {
  position: absolute;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background-color: red;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Scroll-Based SVG Animation</title>

</head>

<body>
  <div id="container">
    <svg width="400" height="200">
            <path id="myPath" d="M20,20 Q100,100 200,20 Q300, -60, 380, 180" />
        </svg>
    <div id="circle"></div>
  </div>
</body>

</html>

while this example works, there is some issues, I can’t stop the svg at the middle of the screen when I want to, the element is not at the middle of the screen while animation is going

Javascript adding days to date doesn’t work but subtracting does [duplicate]

I have a form with 2 fields called “From Date” and “To Date”, both with jQuery Datepicker widgets. These date fields are user selected and represent the start and end dates of a report. I also have a “Day Range” value which is the maximum number of days between these 2 dates. When a date field changes I re-calculate the “minDate” and “maxDate” parameters for the Datepicker widget on the other date field, limiting what the user can select. These calculations involves adding/subtracting days from a date.

I have found that adding days to a date fails to work correctly, however subtracting works fine. I have googled how to add/subtract days and each example suggests the setDate() function, i.e;

newDate.setDate(newDate.getDate() + 7);

Yet I can’t seem to get this to work, here are some examples (my date format is DD/MM/YYYY);

01/01/2025 + 0 days  = 10/01/2025 = + 9 days   = Wrong
01/01/2025 + 1 days  = 11/01/2025 = + 10 days  = Wrong
01/01/2025 + 9 days  = 19/01/2025 = + 18 days  = Wrong
01/01/2025 + 10 days = 20/04/2025 = + 109 days = Wrong

01/01/2025 - 0 days  = 01/01/2025 = - 0 days  = Correct
01/01/2025 - 1 days  = 31/12/2024 = - 1 days  = Correct
01/01/2025 - 9 days  = 23/12/2024 = - 9 days  = Correct
01/01/2025 - 10 days = 22/12/2024 = - 10 days = Correct

I have created a simple example to demonstrate the problem;
(I apologise in advance for how terrible it is, first time using JS)

var dateFormat = "dd/mm/yy";

$(document).ready(function() {
  $("#AddBtn").click(function() {
    var myDate = $.datepicker.parseDate(
      dateFormat,
      document.getElementById("V1_Date").value
    );
    var myDays = document.getElementById("DayRange").value;
    myDate = addDays(myDate, myDays);
    var formattedDate = $.datepicker.formatDate(dateFormat, myDate);
    document.getElementById("ResultDate").value = formattedDate;
  });

  $("#SubBtn").click(function() {
    var myDate = $.datepicker.parseDate(
      dateFormat,
      document.getElementById("V1_Date").value
    );
    var myDays = document.getElementById("DayRange").value;
    myDate = subDays(myDate, myDays);
    var formattedDate = $.datepicker.formatDate(dateFormat, myDate);
    document.getElementById("ResultDate").value = formattedDate;
  });
});

var xDate = $("#V1_Date").datepicker({
  changeYear: true,
  changeMonth: true,
  dateFormat: dateFormat,
});

function addDays(date, days) {
  const newDate = new Date(date);
  newDate.setDate(newDate.getDate() + days);
  return newDate;
}

function subDays(date, days) {
  const newDate = new Date(date);
  newDate.setDate(newDate.getDate() - days);
  return newDate;
}
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.0/themes/base/jquery-ui.css" />
<script src="http://code.jquery.com/jquery-1.8.3.js"></script>
<script src="http://code.jquery.com/ui/1.10.0/jquery-ui.js"></script>

<p>Days: <input type="text" value="10" id="DayRange" /></p>
<p>Date: <input type="text" id="V1_Date" autocomplete="off" /></p>
<input type="button" value="Add days" id="AddBtn" />
<input type="button" value="Sub days" id="SubBtn" />
<p>Result: <input type="text" id="ResultDate" value="" disabled /></p>

Inaccurate & Unreliable Barcode Scanning on iOS Web App (React/TypeScript) – BarcodeDetector / ZXing

I’m developing a React/TypeScript web application (deployed as a PWA-like experience) that includes a barcode scanner feature. Users can scan product barcodes (primarily EAN-13, UPC-A) to look up product information.

My current implementation attempts to use the browser’s native BarcodeDetector API when available. If BarcodeDetector is not supported or fails to initialize, it falls back to zxing/library for software-based decoding.

The core issue is that the barcode scanner is significantly less reliable and often produces inaccurate scans (misreads or no reads at all) on iPhones (iOS Safari/WebKit-based browsers) compared to Android devices, where it works quite well.

Observed Behavior:

  • Android Devices: Scanning is generally fast, accurate, and reliable across various lighting conditions and barcode qualities.
  • iPhones (iOS):
    • Frequent “No Read”: The scanner often fails to detect any barcode, even when the barcode is clearly visible in the viewfinder.
    • Inaccurate Reads: When a scan does occur, it sometimes returns an incorrect barcode (e.g., a few digits off, or a completely wrong code).
    • Focusing Issues: On newer iPhone models (e.g., iPhone 13 Pro, 14 Pro, 15 Pro), the camera struggles to focus on close-up barcodes, resulting in blurry images that are difficult to decode. Users instinctively try to get close, which seems to worsen the problem.
    • General Slowness: Even when it eventually scans, the process feels much slower and more frustrating than on Android.

I configured getUserMedia to request the environment (rear-facing) camera and specific ideal and min resolutions (e.g., 1280×720) to ensure a clear video stream for barcode detection.

I was expecting the barcode scanner to perform consistently and accurately across modern mobile devices, providing a user experience comparable to what I observe on Android web browsers, or at least a high degree of reliability in scanning standard EAN/UPC barcodes.

What actually resulted is that while the scanner works largely as expected on Android devices (fast, accurate, reliable), its performance on iPhones (iOS Safari/WebKit) is severely degraded. I frequently experience the scanner failing to detect barcodes at all, even when they are clearly visible in the camera feed. When a scan does occur on an iPhone, it is often inaccurate, returning a misread or a completely incorrect barcode value. Furthermore, the camera on newer iPhone models struggles significantly with focusing on close-up barcodes, leading to blurry images that are impossible for the decoder to process accurately. This results in a frustratingly slow and unreliable experience for iOS users compared to the smooth operation on Android.

My Current Scanner Implementation:

import { useState, useRef, useCallback, useEffect } from 'react';
import { BrowserMultiFormatReader, IScannerControls } from '@zxing/library';

interface BarcodeResult {
  code: string;
  format: string;
}

export function useBarcodeScanner() {
  const [state, setState] = useState<any>({
    isScanning: false,
    error: null,
    lastResult: null
  });

  const videoRef = useRef<HTMLVideoElement>(null);
  const zxingControlsRef = useRef<IScannerControls | null>(null);
  const isInitializedRef = useRef(false);
  const codeReaderRef = useRef<BrowserMultiFormatReader | null>(null);

  const getCodeReader = useCallback(() => {
    if (!codeReaderRef.current) {
      codeReaderRef.current = new BrowserMultiFormatReader();
    }
    return codeReaderRef.current;
  }, []);

  const stopScanning = useCallback(() => {
    if (zxingControlsRef.current) {
      zxingControlsRef.current.stop();
      zxingControlsRef.current = null;
    } else if (videoRef.current && videoRef.current.srcObject) {
      const tracks = (videoRef.current.srcObject as MediaStream).getTracks();
      tracks.forEach(track => { if (track.readyState === 'live') track.stop(); });
    }
    isInitializedRef.current = false;
    setState(prev => ({ ...prev, isScanning: false }));
  }, []);

  const startScanning = useCallback(async (onResult: (result: BarcodeResult) => void) => {
    if (isInitializedRef.current) return;

    setState(prev => ({ ...prev, isScanning: true, error: null }));
    isInitializedRef.current = true;

    try {
      if ('BarcodeDetector' in window) {
        // --- Native BarcodeDetector Logic ---
        const stream = await navigator.mediaDevices.getUserMedia({
          video: {
            facingMode: 'environment',
            width: { ideal: 1280, min: 640 },
            height: { ideal: 720, min: 480 }
          }
        });
        if (videoRef.current) {
          videoRef.current.srcObject = stream;
          videoRef.current.setAttribute('playsinline', 'true');
          await videoRef.current.play();

          const barcodeDetector = new (window as any).BarcodeDetector({
            formats: ['ean_13', 'ean_8', 'code_128', 'code_39', 'upc_a', 'upc_e']
          });

          let scanLoopInterval: NodeJS.Timeout;
          const scanLoop = async () => {
            if (!videoRef.current || videoRef.current.paused || videoRef.current.ended || !isInitializedRef.current) {
              if (scanLoopInterval) clearInterval(scanLoopInterval);
              return;
            }
            try {
              const barcodes = await barcodeDetector.detect(videoRef.current);
              if (barcodes.length > 0) {
                const barcode = barcodes[0];
                const result: BarcodeResult = { code: barcode.rawValue, format: barcode.format };
                setState(prev => ({ ...prev, lastResult: result }));
                onResult(result);
                stopScanning(); // Stop after successful scan
              }
            } catch (err) {  }
          };
          scanLoopInterval = setInterval(scanLoop, 300);
        }
      } else {
        // --- ZXing Fallback Logic ---
        const codeReader = getCodeReader();
        const videoElement = videoRef.current;
        if (!videoElement) throw new Error('Video element not found for ZXing scanner.');

        codeReader.decodeFromVideoDevice(undefined, videoElement, (result, error, controls) => {
          if (result) {
            const barcodeData: BarcodeResult = { code: result.getText(), format: result.getBarcodeFormat().toString() };
            setState(prev => ({ ...prev, lastResult: barcodeData }));
            onResult(barcodeData);
            controls.stop();
            zxingControlsRef.current = null;
            isInitializedRef.current = false;
          }
          if (error && !error.message.includes('No MultiFormat Readers were able to decode')) {
            setState(prev => ({ ...prev, error: `Camera-Error: ${error.message}` }));
            stopScanning();
          }
        })
        .then(controls => { zxingControlsRef.current = controls; })
        .catch(err => {
          setState(prev => ({ ...prev, isScanning: false, error: err.message || 'Fehler beim Starten des Scanners mit ZXing.' }));
          stopScanning();
        });
      }
    } catch (err: any) {
      setState(prev => ({ ...prev, isScanning: false, error: err.message || 'Kamera-Zugriff oder Wiedergabe fehlgeschlagen' }));
      stopScanning();
    }
  }, [stopScanning, getCodeReader]);

  useEffect(() => { return () => stopScanning(); }, [stopScanning]);

  return { ...state, videoRef, startScanning, stopScanning };
}

The useBarcodeScanner hook is used in a modal component (BarcodeScanner.tsx) that renders a element for the camera feed.

import React, { useEffect, useState } from 'react';
import { X, Camera, Loader2, CheckCircle, AlertCircle } from 'lucide-react';
import { useBarcodeScanner } from '../hooks/useBarcodeScanner';
import { useOpenFoodFacts } from '../hooks/useOpenFoodFacts';

interface BarcodeScannerProps {
  onProductFound: (productData: any) => void;
  onClose: () => void;
}

const BarcodeScanner: React.FC<BarcodeScannerProps> = ({ onProductFound, onClose }) => {
  const { isScanning, error: scanError, videoRef, startScanning, stopScanning } = useBarcodeScanner();
  const { loading: fetchingProduct, error: fetchError, fetchProduct, clearError } = useOpenFoodFacts();
  const [scanResult, setScanResult] = useState<string | null>(null);
  const [productFound, setProductFound] = useState(false);

  useEffect(() => {
    if (!scanResult && !isScanning) {
      startScanning(async (result) => {
        setScanResult(result.code);
        const productData = await fetchProduct(result.code);
        if (productData) {
          setProductFound(true);
          setTimeout(() => onProductFound(productData), 1500);
        }
      });
    }
    return () => stopScanning();
  }, [startScanning, stopScanning, fetchProduct, onProductFound, scanResult, isScanning]);

 ......

export default BarcodeScanner;

This issue of inconsistent and inaccurate barcode scanning on iOS web apps, particularly compared to Android, seems to be a common pain point for many developers.

Are there specific getUserMedia constraints or video processing techniques (e.g., specific resolutions, frame rates, or image manipulation before passing to the detector) that are known to improve barcode scanning accuracy on iOS web apps?

Given my current BarcodeDetector / zxing/library fallback approach, are there any known optimizations or best practices I might be missing for iOS? (I’m aware of html5-qrcode and its BarcodeDetector integration, but I’m trying to understand the root cause and potential improvements at a lower level if possible, or if html5-qrcode truly offers a significant advantage beyond just wrapping).

Any insights, code examples, or explanations of the technical limitations specific to iOS would be greatly appreciated. Thank you!

How to get rid of double quote missing error in aria snapshot?

I’m using the method toMatchAriaSnapshot. I have dynanic text so I’m generating my snapshot text like that:

await expect(page.locator('#contract')).toMatchAriaSnapshot(`
     - dialog:
       - document:
         - heading "Add" [level=5]
         - button "Close"

         - text: "` + (error["login"] ?? "") + messages["login"] + ` *"
         - textbox
`);

But, sometimes it will require the quote, and some other time, it will not. It will result in this:

-     - text: "Identifiant *"
+     - text: Identifiant *

the error I'm getting

Sometimes, when error["login"] contains something, I will get message like this: Missing information. Error code : A-002 Identifiant * which requires the double quotes.

When not using quotes, and when I have the full message, here is the result:

Error: expect.toMatchAriaSnapshot: Nested mappings are not allowed in compact mappings at line 13, column 13:

    - text: Missing information. Error code : A-002 Identifiant *
            ^

How can I make playwright ignore the error of missing quote, or too many quotes ?

Inaccurate & Unreliable Barcode Scanning on iOS Web App (React/TypeScript) – BarcodeDetector / ZXing

I’m developing a React/TypeScript web application (deployed as a PWA-like experience) that includes a barcode scanner feature. Users can scan product barcodes (primarily EAN-13, UPC-A) to look up product information.

My current implementation attempts to use the browser’s native BarcodeDetector API when available. If BarcodeDetector is not supported or fails to initialize, it falls back to zxing/library for software-based decoding.

The core issue is that the barcode scanner is significantly less reliable and often produces inaccurate scans (misreads or no reads at all) on iPhones (iOS Safari/WebKit-based browsers) compared to Android devices, where it works quite well.

Observed Behavior:

  • Android Devices: Scanning is generally fast, accurate, and reliable across various lighting conditions and barcode qualities.
  • iPhones (iOS):
    • Frequent “No Read”: The scanner often fails to detect any barcode, even when the barcode is clearly visible in the viewfinder.
    • Inaccurate Reads: When a scan does occur, it sometimes returns an incorrect barcode (e.g., a few digits off, or a completely wrong code).
    • Focusing Issues: On newer iPhone models (e.g., iPhone 13 Pro, 14 Pro, 15 Pro), the camera struggles to focus on close-up barcodes, resulting in blurry images that are difficult to decode. Users instinctively try to get close, which seems to worsen the problem.
    • General Slowness: Even when it eventually scans, the process feels much slower and more frustrating than on Android.

I configured getUserMedia to request the environment (rear-facing) camera and specific ideal and min resolutions (e.g., 1280×720) to ensure a clear video stream for barcode detection.

I was expecting the barcode scanner to perform consistently and accurately across modern mobile devices, providing a user experience comparable to what I observe on Android web browsers, or at least a high degree of reliability in scanning standard EAN/UPC barcodes.

What actually resulted is that while the scanner works largely as expected on Android devices (fast, accurate, reliable), its performance on iPhones (iOS Safari/WebKit) is severely degraded. I frequently experience the scanner failing to detect barcodes at all, even when they are clearly visible in the camera feed. When a scan does occur on an iPhone, it is often inaccurate, returning a misread or a completely incorrect barcode value. Furthermore, the camera on newer iPhone models struggles significantly with focusing on close-up barcodes, leading to blurry images that are impossible for the decoder to process accurately. This results in a frustratingly slow and unreliable experience for iOS users compared to the smooth operation on Android.

My Current Scanner Implementation:

import { useState, useRef, useCallback, useEffect } from 'react';
import { BrowserMultiFormatReader, IScannerControls } from '@zxing/library';

interface BarcodeResult {
  code: string;
  format: string;
}

export function useBarcodeScanner() {
  const [state, setState] = useState<any>({
    isScanning: false,
    error: null,
    lastResult: null
  });

  const videoRef = useRef<HTMLVideoElement>(null);
  const zxingControlsRef = useRef<IScannerControls | null>(null);
  const isInitializedRef = useRef(false);
  const codeReaderRef = useRef<BrowserMultiFormatReader | null>(null);

  const getCodeReader = useCallback(() => {
    if (!codeReaderRef.current) {
      codeReaderRef.current = new BrowserMultiFormatReader();
    }
    return codeReaderRef.current;
  }, []);

  const stopScanning = useCallback(() => {
    if (zxingControlsRef.current) {
      zxingControlsRef.current.stop();
      zxingControlsRef.current = null;
    } else if (videoRef.current && videoRef.current.srcObject) {
      const tracks = (videoRef.current.srcObject as MediaStream).getTracks();
      tracks.forEach(track => { if (track.readyState === 'live') track.stop(); });
    }
    isInitializedRef.current = false;
    setState(prev => ({ ...prev, isScanning: false }));
  }, []);

  const startScanning = useCallback(async (onResult: (result: BarcodeResult) => void) => {
    if (isInitializedRef.current) return;

    setState(prev => ({ ...prev, isScanning: true, error: null }));
    isInitializedRef.current = true;

    try {
      if ('BarcodeDetector' in window) {
        // --- Native BarcodeDetector Logic ---
        const stream = await navigator.mediaDevices.getUserMedia({
          video: {
            facingMode: 'environment',
            width: { ideal: 1280, min: 640 },
            height: { ideal: 720, min: 480 }
          }
        });
        if (videoRef.current) {
          videoRef.current.srcObject = stream;
          videoRef.current.setAttribute('playsinline', 'true');
          await videoRef.current.play();

          const barcodeDetector = new (window as any).BarcodeDetector({
            formats: ['ean_13', 'ean_8', 'code_128', 'code_39', 'upc_a', 'upc_e']
          });

          let scanLoopInterval: NodeJS.Timeout;
          const scanLoop = async () => {
            if (!videoRef.current || videoRef.current.paused || videoRef.current.ended || !isInitializedRef.current) {
              if (scanLoopInterval) clearInterval(scanLoopInterval);
              return;
            }
            try {
              const barcodes = await barcodeDetector.detect(videoRef.current);
              if (barcodes.length > 0) {
                const barcode = barcodes[0];
                const result: BarcodeResult = { code: barcode.rawValue, format: barcode.format };
                setState(prev => ({ ...prev, lastResult: result }));
                onResult(result);
                stopScanning(); // Stop after successful scan
              }
            } catch (err) {  }
          };
          scanLoopInterval = setInterval(scanLoop, 300);
        }
      } else {
        // --- ZXing Fallback Logic ---
        const codeReader = getCodeReader();
        const videoElement = videoRef.current;
        if (!videoElement) throw new Error('Video element not found for ZXing scanner.');

        codeReader.decodeFromVideoDevice(undefined, videoElement, (result, error, controls) => {
          if (result) {
            const barcodeData: BarcodeResult = { code: result.getText(), format: result.getBarcodeFormat().toString() };
            setState(prev => ({ ...prev, lastResult: barcodeData }));
            onResult(barcodeData);
            controls.stop();
            zxingControlsRef.current = null;
            isInitializedRef.current = false;
          }
          if (error && !error.message.includes('No MultiFormat Readers were able to decode')) {
            setState(prev => ({ ...prev, error: `Camera-Error: ${error.message}` }));
            stopScanning();
          }
        })
        .then(controls => { zxingControlsRef.current = controls; })
        .catch(err => {
          setState(prev => ({ ...prev, isScanning: false, error: err.message || 'Fehler beim Starten des Scanners mit ZXing.' }));
          stopScanning();
        });
      }
    } catch (err: any) {
      setState(prev => ({ ...prev, isScanning: false, error: err.message || 'Kamera-Zugriff oder Wiedergabe fehlgeschlagen' }));
      stopScanning();
    }
  }, [stopScanning, getCodeReader]);

  useEffect(() => { return () => stopScanning(); }, [stopScanning]);

  return { ...state, videoRef, startScanning, stopScanning };
}

The useBarcodeScanner hook is used in a modal component (BarcodeScanner.tsx) that renders a element for the camera feed.

import React, { useEffect, useState } from 'react';
import { X, Camera, Loader2, CheckCircle, AlertCircle } from 'lucide-react';
import { useBarcodeScanner } from '../hooks/useBarcodeScanner';
import { useOpenFoodFacts } from '../hooks/useOpenFoodFacts';

interface BarcodeScannerProps {
  onProductFound: (productData: any) => void;
  onClose: () => void;
}

const BarcodeScanner: React.FC<BarcodeScannerProps> = ({ onProductFound, onClose }) => {
  const { isScanning, error: scanError, videoRef, startScanning, stopScanning } = useBarcodeScanner();
  const { loading: fetchingProduct, error: fetchError, fetchProduct, clearError } = useOpenFoodFacts();
  const [scanResult, setScanResult] = useState<string | null>(null);
  const [productFound, setProductFound] = useState(false);

  useEffect(() => {
    if (!scanResult && !isScanning) {
      startScanning(async (result) => {
        setScanResult(result.code);
        const productData = await fetchProduct(result.code);
        if (productData) {
          setProductFound(true);
          setTimeout(() => onProductFound(productData), 1500);
        }
      });
    }
    return () => stopScanning();
  }, [startScanning, stopScanning, fetchProduct, onProductFound, scanResult, isScanning]);

 ......

export default BarcodeScanner;

This issue of inconsistent and inaccurate barcode scanning on iOS web apps, particularly compared to Android, seems to be a common pain point for many developers.

Are there specific getUserMedia constraints or video processing techniques (e.g., specific resolutions, frame rates, or image manipulation before passing to the detector) that are known to improve barcode scanning accuracy on iOS web apps?

Given my current BarcodeDetector / zxing/library fallback approach, are there any known optimizations or best practices I might be missing for iOS? (I’m aware of html5-qrcode and its BarcodeDetector integration, but I’m trying to understand the root cause and potential improvements at a lower level if possible, or if html5-qrcode truly offers a significant advantage beyond just wrapping).

Any insights, code examples, or explanations of the technical limitations specific to iOS would be greatly appreciated. Thank you!

How to get rid of double quote missing error in aria snapshot?

I’m using the method toMatchAriaSnapshot. I have dynanic text so I’m generating my snapshot text like that:

await expect(page.locator('#contract')).toMatchAriaSnapshot(`
     - dialog:
       - document:
         - heading "Add" [level=5]
         - button "Close"

         - text: "` + (error["login"] ?? "") + messages["login"] + ` *"
         - textbox
`);

But, sometimes it will require the quote, and some other time, it will not. It will result in this:

-     - text: "Identifiant *"
+     - text: Identifiant *

the error I'm getting

Sometimes, when error["login"] contains something, I will get message like this: Missing information. Error code : A-002 Identifiant * which requires the double quotes.

When not using quotes, and when I have the full message, here is the result:

Error: expect.toMatchAriaSnapshot: Nested mappings are not allowed in compact mappings at line 13, column 13:

    - text: Missing information. Error code : A-002 Identifiant *
            ^

How can I make playwright ignore the error of missing quote, or too many quotes ?

Tic-Tac-Toe Project: Restart Button Not Resetting Game State [closed]

I’m building a Tic-Tac-Toe game as part of The Odin Project JavaScript curriculum. My implementation works fine in the console version, but I’m facing an issue in the browser version.

function GameBoard() {
  let rows = 3;
  let columns = 3;

  const board = [];

  for(let i = 0; i < rows; i++) {
    board[i] = [];
    for(let j = 0; j < columns; j++) {
      board[i].push(Cell());
    }
  }

  const getBoard = () => board;

  const markCell = (row, col, token) => {
    if(board[row][col].getValue() != 0) return -1; 
    board[row][col].setValue(token);
    return 0;
  };

  const printBoard = () => {
    const boardWithCellValues = board.map((row) => row.map((cell) => cell.getValue()));
    console.log(boardWithCellValues);
  };

  return {
    getBoard,
    markCell,
    printBoard,
  };
}

function Cell() {
  let value = '';

  const setValue = (token) => {
    value = token;
  };

  const getValue = () => value;

  return {
    setValue,
    getValue,
  };
}

function GameController(
  playerOne="Player 1", 
  playerTwo="Player 2"
) {
  let turnsPlayed = 0;
  const players = [
    {
      name: playerOne,
      token: 'X'
    },
    {
      name: playerTwo,
      token: 'O'
    }
  ];
  const board = GameBoard();
  let activePlayer = players[0];

  const switchPlayerTurn = () => {
    activePlayer = activePlayer === players[0] ? players[1] : players[0];
  };

  const getActivePlayer = () => activePlayer;

  const printNewRound = () => {
    board.printBoard();
    console.log(`${getActivePlayer().name}'s turn ...`);
  }

  const checkWin = () => {
    // logic for tic tac toe game
    let hasWon = false;
    let playerToken = getActivePlayer().token;
    const boardArray = board.getBoard();

    let a = boardArray[0][0].getValue();
    let b = boardArray[1][1].getValue();
    let c = boardArray[2][2].getValue();
    if(a === b && b === c && a === playerToken) {
      hasWon = true;
    } else {
      a = boardArray[0][2].getValue();
      b = boardArray[1][1].getValue();
      c = boardArray[2][0].getValue();
      if(a === b && b === c && a === playerToken) {
        hasWon = true;
      } else {
        for(let i = 0; i < boardArray.length; i++) {
          a = boardArray[i][0].getValue();
          b = boardArray[i][1].getValue();
          c = boardArray[i][2].getValue();
          if(a === b && b === c && a === playerToken) {
            hasWon = true;
            break;
          }
    
          a = boardArray[0][i].getValue();
          b = boardArray[1][i].getValue();
          c = boardArray[2][i].getValue();
          if(a === b && b === c && a === playerToken) {
            hasWon = true;
            break;
          }
        }
      }
    }

    return hasWon;
  }

  const checkDraw = () => {
    return turnsPlayed == 9;
  }

  const playRound = (r, c) => {
    console.log(
      `Marking ${getActivePlayer().name}'s token in Cell (${r}, ${c}).`
    );
    const exit_status = board.markCell(r,c, getActivePlayer().token);
    // Do nothing if an occupied cell is marked by player
    if(exit_status == -1) return; 
    turnsPlayed++;
    if(checkWin()) {
      board.printBoard();
      console.log(`${getActivePlayer().name} has won the game.`);
    } else if (checkDraw()) {
      board.printBoard();
      console.log(`It is a Draw. Play again ...`)
    } else {
      switchPlayerTurn();
      printNewRound();
    }
  }

  // Intial Render
  printNewRound();

  return {
    playRound,
    getActivePlayer,
    checkWin,
    checkDraw,
    getBoard: board.getBoard
  };
}


function ScreenController() {
  const game = GameController();
  const playerTurnDiv = document.querySelector('.turn');
  const boardDiv = document.querySelector('.board');

  const updateScreen = () => {
    boardDiv.textContent = '';

    const board = game.getBoard();
    const activePlayer = game.getActivePlayer();

    playerTurnDiv.textContent = `${activePlayer.name}'s Turn`;

    board.forEach((row, row_idx) => {
      row.forEach((cell, col_idx) => {
        const cellButton = document.createElement('button');
        cellButton.classList.add('cell');
        cellButton.textContent = cell.getValue();
        cellButton.dataset.row_idx = row_idx;
        cellButton.dataset.col_idx = col_idx;
        boardDiv.appendChild(cellButton);
      })
    })
  }

  const displayGameOver = () => {
    const msg = document.querySelector('.game-over-message');
    if(game.checkWin()) {
      msg.textContent = `${game.getActivePlayer().name} has won the game.`
    } else {
      msg.textContent = "It is a tie."
    }
    gameOverDialog.showModal();
  }


  function clickHandlerBoard(e) {
    const row = e.target.dataset.row_idx;
    const col = e.target.dataset.col_idx; 

    if(!row) return;

    game.playRound(row, col);
    updateScreen();
    if(game.checkWin() || game.checkDraw())
      displayGameOver();
  }

  boardDiv.addEventListener("click", clickHandlerBoard);

  // Initial Render
  updateScreen();
}

ScreenController();

const gameOverDialog = document.querySelector("#game-over-screen");
const restartBtn = document.querySelector('.restartBtn');

restartBtn.addEventListener('click', () => {
  gameOverDialog.close();
  ScreenController();
});
/*
Josh's Custom CSS Reset
https://www.joshwcomeau.com/css/custom-css-reset/
*/

*, *::before, *::after {
box-sizing: border-box;
}

* {
margin: 0;
}

@media (prefers-reduced-motion: no-preference) {
html {
    interpolate-size: allow-keywords;
}
}

body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}

img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}

input, button, textarea, select {
font: inherit;
}

p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}

p {
text-wrap: pretty;
}
h1, h2, h3, h4, h5, h6 {
text-wrap: balance;
}

#root, #__next {
isolation: isolate;
}

/* CSS Starts Here */

.main-container {
    height: 100vh;
    display: flex;
    flex-direction: column;
    font-family: monospace, sans-serif;
}

.header {
    padding: 32px;
    text-align: center;
}

.header h1 {
    font-size: 48px;
}

.footer {
    text-align: center;
    padding: 16px;
}

.container {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 36px 0;
    gap: 1.5em;
}

.container .turn {
    font-size: 24px;
}

.board {
    display: grid;
    height: 500px;
    width: 500px;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(3, 1fr);
    border: 1px solid black;
    gap: 2px;
    padding: 1px;
    background-color: rgb(255, 17, 0);
}
  
.cell {
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 5rem;
    border: 1px solid grey;
    background: lightyellow;
    cursor: pointer;
}

dialog {
    font-size: 2em;
    position: fixed;
    margin: auto;
    width: 45ch;
    /* padding: 1em; */
    border: 3px solid greenyellow;
    border-radius: 8px;
    box-shadow: 0 10px 25px rgba(0,0,0,0.2);
}

dialog::backdrop {
    background: rgba(0, 0, 0, 0.5);
}

.game-over-message {
    margin-bottom: 1.5em;
    text-align: center;
}

.game-over-toolbar {
    display: flex;
    justify-content: center;
}

.restartBtn {
    padding: 0.5em;
    border-radius: 32px;
    border: none;
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
    background-color: rgb(173, 255, 47);
    display: flex;
    align-items: center;
    justify-content: center;
}

.restartBtn:hover {
    box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
    cursor: pointer;
}
<div class="main-container">
        <div class="header">
            <h1>Tic Tac Toe</h1>
        </div>
        <div class="container">
            <div class="turn"></div>
            <div class="board"></div>
        </div>
        <div class="footer">&copy; GOTW 2025</div>
    </div>
    <dialog id="game-start-screen">
        <label for="playerOneName">Enter Player 1's name:</label>
        <input type="text" id="playerOneName" autofocus>
    </dialog>
    <dialog id="game-over-screen">
        <div class="game-over-message"></div>
        <div class="game-over-toolbar">
            <button class="restartBtn">&#x21bb; Restart</button>
        </div>
    </dialog>
</body>

Problem:
After a game is won, I display a “Game Over” dialog. I’ve added a Restart button inside this dialog, which is meant to reset the game state and allow a fresh game. However, after clicking restart, although the dialog closes, the game remains stuck — it behaves as if the previous state is still active, and clicking on a cell immediately brings back the “Game Over” dialog.

Here’s the relevant part of the code:

restartBtn.addEventListener('click', () => {   
     gameOverDialog.close();   
     ScreenController(); }); 

Question:
How can I properly reset both the UI and internal game state when restarting the game via the Restart button? Is there a better way to reinitialize the game components?

Any suggestions or patterns for managing game state reset cleanly would be appreciated!

What I expected:

  1. The board should be cleared.
  2. All game state (turns, board data, game over flag) should reset.
  3. The user should be able to play a new game from scratch.

What I’ve tried:

  1. Calling ScreenController() again in the restart handler.
  2. Confirmed that the console version resets just fine.