Links not working on cell phone (iPhone), works on desktop including when resized to a mobile format. Uses Javascript and React

I’m utilizing React-Bootstrap Offcanvas as a mobile sidebar to scroll to various portions of the page, however when I click the links on my iPhone they do not work, with some exceptions, and seems to focus on elements behind it. On desktop, when I inspect and change the dimensions everything works fine.

Using onClick event listener to trigger scrollIntoView method via document.getElementById(id).scrollIntoView();

Using chrome browser on both devices. Website is hosted through Github Pages. Website is a personal site I’m using to practice my code and share things I enjoy.

The code is a little large and is linked here: https://github.com/ShaunDM/ShaunDM.github.io

Website here: https://shaundm.github.io/#/

Relevant components:

  • src/layout/Layout.js for the offcanvas.
  • src/layout/Navbar.js for looking into whether components behind the offcanvas are being affected, specifically the navbar.
  • src/sidebar (primarily ./SidebarList.js) for the offcanvas content.
  • src/layout/Hit.js and it’s relevant variables/functions in src/layout/Layout.js if you’d like to track events on a mobile device.

Exception 1:

If I activate the Offcanvas and zoom out through pinching, the links work. This doesn’t work if I zoom in. Will often maintain functionality for a few seconds after initial zoom out without having to repeat method.

Exception 1 in action, can see highlight of elements behind click

Exception 2:

On one of my pages I have a sidebar that contains multiple lists of links. It’ll navigate the horizontal scroll, but not the document’s y axis, unless you use exception 1. Page: https://shaundm.github.io/#/portfolio

Exception 2 in action

I’ve implemented a component that will render either “Hit” or “No hit” that I use to track triggers of events on a mobile device. When I click the link it registers as being fired. When I use it to track onFocus, onFocusCapture, onTouchStart, and/or onTouchStartCapture of the elements behind the Offcanvas it doesn’t register despite the elements being highlighted afterward as if they are being focused.

Can see trigger of link click event here. The trigger of events regarding elements behind offcanvas are in the prior gifs. Also can see zoom in not working.

Be aware that not all of the pages render a sidebar, Home and Contact Me. There will be no triple bar icon on the navbar.

The current version of the code has set up to change based on if a link is clicked or not.

  • I’ve tried implementing z-index:9999 for the sidebar, and 0 for , #root, and the container I surround all my code with except the offcanvas.
  • Setting the sidebar’s position to relative instead of fixed and set relative to the above mentioned elements, while maintaining z-index above.
  • I’ve moved the offcanvas sidebar as a direct child of .
  • I’ve double checked on my desktop browser that the sidebar is not being overlapped, by the elements behind using inspect and it doesn’t seem to be the case.
  • I’ve tried eliminating custom css for the offcanvas.
  • I’ve tried surrounding the document.getElementById(id).scrollIntoView(); method with setTimeOut, which seemed to fix others with similar issues.
  • I’ve tried utilizing e.preventDefault() prior to triggering scrollIntoView().

react-select – focused option resets on React state change

I have a custom component wrapper around a react-select, but I’ve also noticed this same behavior in the react-select by itself as well. The behavior occurs when using react-select whenever the React state is updated, whether by the onChange function passed to the Select or by anything else.

When the Select component is outside of the React state, if I select an option and then reopen the Select, the selected option will be focused and keyboard navigation proceeds from the selected option. However, if the React state changes (thereby triggering a re-render), reopening the Select results in the first option being focused instead of the selected option.

I’ve tried passing both value and defaultValue to the Select, but neither seem to have had any effect on the focused option.

A basic TypeScript example using options from the react-select website:

import Select from "react-select";

type ColorOption = {
    value: string, label: string, color: string
};

export const DemoComponent = 

    const [selectedColor, setSelectedColor] = useState<ColorOption>();

    const colorOptions: ColorOption[] = [
        { value: 'ocean', label: 'Ocean', color: '#00B8D9' },
        { value: 'blue', label: 'Blue', color: '#0052CC' },
        { value: 'purple', label: 'Purple', color: '#5243AA' },
        { value: 'red', label: 'Red', color: '#FF5630' },
        { value: 'orange', label: 'Orange', color: '#FF8B00' }
    ];

    return <Select 
        options={colorOptions} 
        defaultValue={selectedColor} 
        onChange={(e) => {setSelectedColor(e)}} 
    />;
}

initial open

If I select Purple here and then reopen the Select, I would expect it to focus Purple and for the keyboard to navigate from there, which is what happens if the onChange function is removed and no re-render occurs. But instead, it focuses the first option.

open after select

Bootstrap Multiselect Unable to preventDefault inside passive event listener invocation on mobile error

I’m using bootstrap multiselect (https://github.com/davidstutz/bootstrap-multiselect – current version) in a form. On desktop it’s working fine, but on mobile or if I use the Responsive Design Mode in Firefox or the Toggle Device Emulation in Chrome dev tool, I’m getting an error:

Unable to preventDefault inside passive event listener invocation.

and the select behavior is not working as expected on mobile. I have this error with the default, but also with my custom config:

// Multiselect Config
const opts_template = {
    button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown"><span class="multiselect-selected-text"></span><i class="fa fa-caret-down" aria-hidden="true"></i></button>',
    filter: '<div class="multiselect-filter"><div class="input-group"><span class="input-group-btn"><button class="btn btn-default btn-multisearch" type="button" disabled><i class="fa fa-sm fa-search"></i></button></span><input type="search" class="multiselect-search form-control" /><span class="input-group-btn"><button class="btn btn-default multiselect-clear-filter" type="button"><i class="fa fa-sm fa-times"></i></button></span></div></div>',
};

// Multiselect Select One Mininum
function multiSelectOneMin(val) {
    if(val.multiselect){
        var $element = val.multiselect.$select;
    } else {
        var $element = $(val.element);
    }
    let $click = val.click ? val.click : '';
    if($element.val().length == 0) {
        $element.multiselect('select', $click);
    }
}

const opts_basic_one_min = {
    nonSelectedText: 'Select',
    maxHeight: 250,
    buttonWidth: '100%',
    templates: opts_template,
    onChange: function(element, checked) {
        multiSelectOneMin({ multiselect: this, element: null, values: this.$select.val(), click: element.val() });
    }
};

$('#subjectContact').multiselect(opts_basic_one_min);

Here is the live form: https://www.qumundo.com/contact/

How can I fix this error on mobile?

I’ve tried deactivating the event.preventDefault() as suggested in some posts:

https://github.com/davidstutz/bootstrap-multiselect/blob/aade5e6f6a55add71c2ba42348cfe72ea51b563b/dist/js/bootstrap-multiselect.js#L795

//event.preventDefault();

but then all the mobile attempts seem to work but not the desktop attempts.

I’ve also noticed that with deactvitating //event.preventDefault(); the multiselect .onChange() is running:

Desktop: 2 times; Mobile: 3 times

If I activate it:

Desktop: 1 time; Mobile: 2 times (including the error)

I’ve also noticed a difference in the Copy Selector and Copy Styles of an option select:

Desktop:

#form-group-subject > span > div > div.multiselect-container.dropdown-menu > button.multiselect-option.dropdown-item.active > span > label
document.querySelector("#form-group-subject > span > div > div.multiselect-container.dropdown-menu > button.multiselect-option.dropdown-item.active > span > label")

Mobile:

#form-group-subject > span > div > div > button.multiselect-option.dropdown-item.active > span > label
document.querySelector("#form-group-subject > span > div > div > button.multiselect-option.dropdown-item.active > span > label")

I’ve also tried:

//$('#subjectContact').multiselect(opts_basic_one_min).change(function(e) { e.preventDefault(); });

I’ve also tried

document.addEventListener('touchstart', function(event) {
    event.preventDefault();
    console.log('touchstart event');
}, { passive: false } );

Desktop seems to work, but on mobile I can’t open the bootstrap multiselect anymore.

jQuery v3.7.1

NextJs useEffect not executing inside hook

I have a hook that checks if the theme is dark or light:

"use client";

import { useEffect, useState } from "react";

function useDarkMode() {
  const [isDarkMode, setIsDarkMode] = useState(false);

  useEffect(() => {
    console.warn("checked");
  }, []);

  useEffect(() => {
    let observer: MutationObserver | null = null;

    const init = () => {
      const themeDiv = document.getElementById("theme");
      if (!themeDiv) {
        console.warn("#theme not found");
        return;
      }

      const update = () => setIsDarkMode(themeDiv.classList.contains("dark"));

      observer = new MutationObserver(update);
      observer.observe(themeDiv, {
        attributes: true,
        attributeFilter: ["class"],
      });

      // Initial check
      update();
    };

    // Delay to ensure DOM is ready
    requestAnimationFrame(init);

    return () => observer?.disconnect();
  }, []);

  return isDarkMode;
}

export default useDarkMode;

I use it inside my DotGrid component with is from react-bites
Link to react bites component.

Inside const DotGrid I added:

const isDarkMode = useDarkMode();
console.log("is Dark Mode", isDarkMode);

The mode is always false because useEffect won’t run. The DotGrid has use client at the top of component.

I can’t get the useEffect running, console.log do not displays. Both are use client components. What are the reason and how to fix it?

I expect console.log from useEffect to be displayed.

Javascript problem validate script date text field

I have a problem with a validate script for a contact form. It’s a html text input. What’s the right value to validate?

part of html form

<div class="uk-form-row rsform-block rsform-block-data-arrivo-3">
                <div class="formControls">
                    <input type="text" value="" size="20" placeholder="Date Arrival" name="arrival" id="arrival" class="omnidatepicker-from uk-form-large rsform-input-box" aria-required="true" />
                    <span id="arrivalError" class="error-message"></span>
                </div>
            </div>

and part of script:

// Validate Arrival
      if (arrivalInput.value.trim() === '') {
        arrivalError.textContent = 'Please insert your Arrival date.';
        arrivalInput.classList.add('invalid'); // Add a class for styling
        isValid = false;
      } else {
        arrivalDateError.textContent = '';
        arrivalInput.classList.remove('invalid');
      }

I tried to insert dd-MM-yyyy here: if (arrivalInput.value.trim() === ‘dd-MM-yyyy’) but it’s not working.

Twitch API Utilization

I’m attempting to create a twitch embed on a website that either uses the latest uploaded vod when the stream is offline, or the current livestream if the channel is online.
I’m very new to using API’s and I want to know how to figure out how I can get the information from the promise into HTML where I can check the variables.
Please let me know if I’m overthinking this as well and if theres an easier solution.

I’ve gotten the “search channels” function of the API to send a promise result to the debug log using this code

 <script>
      const request = new Request("https://api.twitch.tv/helix/search/channels?query=[Channel]", {
        headers: {
          'Client-ID': '[id]',
          'Authorization': 'Bearer [id]'
        },
      })

    fetch(request)
    .then((response) => {
    console.log(response.json())
    return response.json()
  })
    </script>

Typescript Node test cannot redifine property?

So i have this test doing in my typescript project and when i do the mock tests two test have been failing all the issues are related to TypeError [Error]: Cannot redefine property: fetchAndNormalizePullRequests i looked all the way and couldn’t find a way to fix the issue. this is the

pull-request.handler.test.ts

import { test, describe, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
import type { Request, Response } from 'express';
import { getAllPullRequests } from '../../handlers/pull-requests.handler.js';

const originalEnv = process.env;
// const pullRequestsService = await import('../../services/pull-requests.service.js');

describe('Pull Requests Handler', () => {
  let mockReq: Partial<Request>;
  let mockRes: Partial<Response>;
  let statusMock: any;
  let jsonMock: any;
  let fetchMock: any;

  beforeEach(() => {
    mock.restoreAll();

    statusMock = mock.fn(() => mockRes);
    jsonMock = mock.fn();
    
    mockRes = {
      status: statusMock,
      json: jsonMock
    };

    process.env = { ...originalEnv };
  });

  afterEach(() => {
    process.env = originalEnv;
    mock.restoreAll();
  });

  test('should return 500 when GITHUB_API_TOKEN is missing', async () => {
    delete process.env.GITHUB_API_TOKEN;
    mockReq = {
      query: { owner: 'testowner', repo: 'testrepo' }
    };

    await getAllPullRequests(mockReq as Request, mockRes as Response);

    assert.strictEqual(statusMock.mock.callCount(), 1);
    assert.deepStrictEqual(statusMock.mock.calls[0].arguments, [500]);
    assert.strictEqual(jsonMock.mock.callCount(), 1);
    assert.deepStrictEqual(jsonMock.mock.calls[0].arguments, [{ error: 'Missing environment variables.' }]);
  });

  test('should return 500 when owner is missing', async () => {
    process.env.GITHUB_API_TOKEN = 'test-token';
    mockReq = {
      query: { repo: 'testrepo' }
    };

    try{
    await getAllPullRequests(mockReq as Request, mockRes as Response);
    }
    catch(e){
      console.error(e);
    }

    assert.strictEqual(statusMock.mock.callCount(), 1);
    assert.deepStrictEqual(statusMock.mock.calls[0].arguments, [500]);
    assert.strictEqual(jsonMock.mock.callCount(), 1);
    assert.deepStrictEqual(jsonMock.mock.calls[0].arguments, [{ error: 'Missing environment variables.' }]);
  });

  test('should return 500 when repo is missing', async () => {
    process.env.GITHUB_API_TOKEN = 'test-token';
    mockReq = {
      query: { owner: 'testowner' }
    };

    await getAllPullRequests(mockReq as Request, mockRes as Response);

    assert.strictEqual(statusMock.mock.callCount(), 1);
    assert.deepStrictEqual(statusMock.mock.calls[0].arguments, [500]);
    assert.strictEqual(jsonMock.mock.callCount(), 1);
    assert.deepStrictEqual(jsonMock.mock.calls[0].arguments, [{ error: 'Missing environment variables.' }]);
  });

  test('should return 200 with pull requests when all parameters are valid', async () => {
    process.env.GITHUB_API_TOKEN = 'test-token';
    mockReq = {
      query: { owner: 'testowner', repo: 'testrepo' }
    };

    const mockPullRequests = [
      {
        prNumber: 1,
        title: 'Test PR',
        creator: 'testuser',
        creationTimestamp: '2024-01-01T00:00:00Z',
        requestedReviewers: ['reviewer1'],
        lastActionType: 'open',
        lastActionTimestamp: '2024-01-01T00:00:00Z'
      }
    ];

    const fetchMock = mock.fn(() => Promise.resolve(mockPullRequests));
    mock.method(await import('../../services/pull-requests.service.js'), 'fetchAndNormalizePullRequests', fetchMock);

    await getAllPullRequests(mockReq as Request, mockRes as Response);

    assert.strictEqual(statusMock.mock.callCount(), 1);
    assert.deepStrictEqual(statusMock.mock.calls[0].arguments, [200]);
    assert.strictEqual(jsonMock.mock.callCount(), 1);
    assert.deepStrictEqual(jsonMock.mock.calls[0].arguments, [mockPullRequests]);
  });

  test('should return 500 when service throws an error', async () => {
    process.env.GITHUB_API_TOKEN = 'test-token';
    mockReq = {
      query: { owner: 'testowner', repo: 'testrepo' }
    };

    const fetchMock = mock.fn(() => Promise.reject(new Error('Service error')));
    mock.method(await import('../../services/pull-requests.service.js'), 'fetchAndNormalizePullRequests', fetchMock);

    await getAllPullRequests(mockReq as Request, mockRes as Response);

    assert.strictEqual(statusMock.mock.callCount(), 1);
    assert.deepStrictEqual(statusMock.mock.calls[0].arguments, [500]);
    assert.strictEqual(jsonMock.mock.callCount(), 1);
    assert.deepStrictEqual(jsonMock.mock.calls[0].arguments, [{ error: 'Internal server error.' }]);
  });
});

and this is my fetchAndNormalizePullRequest function

import axios from 'axios';
import type{ NormalizedPR } from '../types/types.js';

export const fetchAndNormalizePullRequests = async (owner: string, repo: string, token: string): Promise<NormalizedPR[]> => {
  const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=all`;

  try {
    const response = await axios.get(url, {
      headers: {
        Authorization: `token ${token}`,
        'Content-Type': 'application/json',
      },
    });

    return response.data.map((pr: any) => ({
      prNumber: pr.number,
      title: pr.title,
      creator: pr.user.login,
      creationTimestamp: pr.created_at,
      requestedReviewers: pr.requested_reviewers.map((reviewer: any) => reviewer.login),
      lastActionType: pr.merged_at ? 'merged' : pr.state,
      lastActionTimestamp: pr.updated_at,
    }));
  } catch (error) {
    throw new Error('Failed to fetch pull requests from GitHub API.');
  }
};

what am i doing wrong? can’t fix the issue

iPad – how to have a numeric floating keyboard with return/enter or tab key

This is what a number floating keyboard on iPad is looking like

iPad numeric keyboard

I want to have a enter/return or tab key here. There is an input field for each row in a table and I want to enter some value in it (0-9) and move to next row/cell. If I use the full screen keyboard, it suffices the requirement but the keyboard is taking 80% of the screen. So I want to use this floating keyboard but if I want to go to next cell, I need to click to that next cell and then enter value. I want to improve user experience on it. I tried input type number, numeric and tel and it did not help.

Problem with contact form HTML & JS for submit on php after required fields [closed]

I have a problem with my contact form. I have required fields that work when I click on submit, but once everything is entered, the form is not sent to the .php file. Maybe it’s only a small problem, so I add the whole code. Sorry for the length of the codes.
Where am I going wrong? I’ve been working on this for hours 🙁 I hope someone can help me.

const form = document.getElementById('userForm');
const firstNameInput = document.getElementById('name_surname');
const emailInput = document.getElementById('mail');
const firstNameError = document.getElementById('firstNameError');
const emailError = document.getElementById('emailError');
const submitBtn = document.getElementById("invia");

form.addEventListener('submit', function(event) {
  // Prevent the form from submitting normally
  event.preventDefault();

  if (validateForm()) {
    return;
    // You can now submit the form programmatically or clear it
  }

  // do animation
  submitBtn.classList.add("is-active");
  setTimeout(function() {
    submitBtn.classList.remove("is-active");

    actual_submit();
  }, 2500);

})

function actual_submit() {
  alert('Form was submitted')
  return; // for testing purposes
  $.ajax({
    type: 'post',
    url: '../../php/kontakt.php',
    data: $('form').serialize(),
    success: function() {
      alert('form was submitted');
    }
  });
}

function validateForm() {
  let isValid = true;

  // Validate First Name
  if (firstNameInput.value.trim() === '') {
    firstNameError.textContent = 'First name is required.';
    firstNameInput.classList.add('invalid'); // Add a class for styling
    isValid = false;
  } else {
    firstNameError.textContent = '';
    firstNameInput.classList.remove('invalid');
  }

  // Validate Email
  const emailValue = emailInput.value.trim();
  const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/; // Basic email regex

  if (emailValue === '') {
    emailError.textContent = 'Email is required.';
    emailInput.classList.add('invalid');
    isValid = false;
  } else if (!emailRegex.test(emailValue)) {
    emailError.textContent = 'Please enter a valid email address.';
    emailInput.classList.add('invalid');
    isValid = false;
  } else {
    emailError.textContent = '';
    emailInput.classList.remove('invalid');
  }

  return isValid;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<form id="userForm" method="post"> <!-- Do not remove this ID, it is used to identify the page so that the pagination script can work correctly -->
  <fieldset class="formContainer uk-form uk-form-stacked" id="rsform_3_page_0">
    <div class="uk-form-row rsform-block rsform-block-nome-cognome">
      <div class="formControls">
        <input type="text" value="" size="20" placeholder="Name & Nachname" name="form[name_surname]" id="name_surname" class="uk-form-large rsform-input-box" />
        <span id="firstNameError" class="error-message"></span>
      </div>
    </div>
    <div class="uk-form-row rsform-block rsform-block-mail">
      <div class="formControls">
        <input type="email" value="" size="20" placeholder="Mailadresse" name="form[mail]" id="mail" class="uk-form-large rsform-input-box" aria-required="true" />
        <span id="emailError" class="error-message"></span>
      </div>
    </div>
    <div class="uk-form-row rsform-block rsform-block-data-arrivo-3">
      <div class="formControls">
        <input type="text" value="" size="20" placeholder="Datum Anreise" name="form[arrival]" id="arrival" class="omnidatepicker-from uk-form-large rsform-input-box" aria-required="true" />
        <span class="formValidation"><span id="component61" class="formNoError"></span></span>
      </div>
    </div>
    <div class="uk-form-row rsform-block rsform-block-data-partenza-3">
      <div class="formControls">
        <input type="text" value="" size="20" placeholder="Datum Abreise" name="form[departure]" id="departure" class="omnidatepicker-to uk-form-large rsform-input-box" aria-required="true" />

      </div>
    </div>

    <div class="uk-form-row rsform-block rsform-block-messaggio">
      <div class="formControls">
        <textarea cols="50" rows="3" placeholder="Mitteilung (Bitte teilen Sie uns alle wichtigen Details mit, damit wir Ihren Aufenthalt bestens gestalten können.)" name="form[message]" id="message" class="uk-form-large rsform-text-box"></textarea>
        <span class="formValidation"><span id="component28" class="formNoError"></span></span>
      </div>
    </div>

    <div class="uk-form-row rsform-block rsform-block-accetto-privace">
      <div class="formControls">
        <div><label for="accetto_privace0"><input type="checkbox"  name="form[privacy][]" value="1" id="privacy" class="rsform-checkbox" aria-required="true" />Akzeptiere <a href="#privacy-3" data-uk-modal>Privacy</a><div id="privacy-3" class="uk-modal"><div class="uk-modal-dialog uk-modal-dialog-lightbox uk-modal-dialog-large"><a href="/" class="uk-modal-close uk-close uk-close-alt"></a><iframe src="https://www.omnigrafitalia.it/gdpr/index.php?site=https%3A%2F%2Fwww.musacomo.com%2F#googtrans(it)" width="100%" height="600px"></iframe></div></div></label> </div>
        <span class="formValidation"><span id="component31" class="formNoError"></span></span>
      </div>
    </div>


    <div id="rsform_error_3" style="display: none;">
      <p class="formRed"><i class="uk-icon-exclamation-triangle"></i> Bitte alle Felder ausfüllen.</p>
    </div>
    <div class="uk-form-row rsform-block rsform-block-invia">
      <div class="formControls">
        <input type="submit" name="form[invia]" id="invia" class="uk-width-1-1 rsform-submit-button  uk-button uk-button-primary" value="Anfrage absenden" />
        <span class="formValidation"></span>
      </div>
    </div>

  </fieldset>

  <input type="hidden" name="form[language]" id="language" value="de" />
  <input type="hidden" name="form[hiddenlist_local]" id="hiddenlist_local" value="1" />

  <input type="hidden" name="form[nome_offerta_3]" id="nome_offerta_3" value="Angebot Relax BHB" />

</form>

How to automatically refresh access token when receiving 403 errors in Redux Toolkit async thunks?

I’m building a React application with Redux Toolkit and need to handle token expiration automatically. When my access token expires, the server returns a 403 error, and I want to automatically refresh the token and retry the original request.
I have favourites-related async thunks that need authentication. The server expects the access token in the Authorization header. My access token is stored in Redux memory (state.auth.accessToken). Here’s my current code:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

const url = import.meta.env.VITE_APP_SERVER_URL;

// Thunk to add to favourites
export const addToFavouritesThunk = createAsyncThunk(
  'favourites/add',
  async (dishId, { rejectWithValue, getState }) => {
    try {
      const { auth: { accessToken } } = getState();
      const response = await axios.post(
        `${url}favourites`,
        { dishId },
        {
          headers: {
            'Authorization': `Bearer ${accessToken}`
          }
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(
        error.response?.data?.message || 'Failed to add to favourites'
      );
    }
  }
);

// Thunk to get all favourites
export const getFavouritesThunk = createAsyncThunk(
  'favourites/getAll',
  async (_, { rejectWithValue, getState }) => {
    try {
      const { auth: { accessToken } } = getState();
      const response = await axios.get(
        `${url}favourites`,
        {
          headers: {
            'Authorization': `Bearer ${accessToken}`
          }
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(
        error.response?.data?.message || 'Failed to get favourites'
      );
    }
  }
);

// Thunk to remove from favourites
export const removeFromFavouritesThunk = createAsyncThunk(
  'favourites/remove',
  async (dishId, { rejectWithValue, getState }) => {
    try {
      const { auth: { accessToken } } = getState();
      const response = await axios.delete(
        `${url}favourites/${dishId}`,
        {
          headers: {
            'Authorization': `Bearer ${accessToken}`
          }
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(
        error.response?.data?.message || 'Failed to remove from favourites'
      );
    }
  }
);

// Refresh token thunk (simplified version)
export const refreshTokenThunk = createAsyncThunk(
  'auth/refresh',
  async (_, { rejectWithValue, getState }) => {
    try {
      const { auth: { refreshToken } } = getState();
      const response = await axios.post(
        `${url}auth/refresh`,
        { refreshToken }
      );
      return response.data; // Contains new accessToken
    } catch (error) {
      return rejectWithValue(
        error.response?.data?.message || 'Token refresh failed'
      );
    }
  }
);

const favouritesSlice = createSlice({
  name: 'favourites',
  initialState: {
    items: [],
    loading: false,
    error: null,
    lastAction: null
  },
  reducers: {
    clearFavouritesError: (state) => {
      state.error = null;
    },
    resetLastAction: (state) => {
      state.lastAction = null;
    }
  },
  extraReducers: (builder) => {
    builder
      // Add to favourites
      .addCase(addToFavouritesThunk.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(addToFavouritesThunk.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload;
        state.lastAction = { type: 'add' };
      })
      .addCase(addToFavouritesThunk.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      })
      // Get all favourites
      .addCase(getFavouritesThunk.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(getFavouritesThunk.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload.favourites || [];
      })
      .addCase(getFavouritesThunk.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      })
      // Remove from favourites
      .addCase(removeFromFavouritesThunk.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(removeFromFavouritesThunk.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload;
        state.lastAction = { type: 'remove' };
      })
      .addCase(removeFromFavouritesThunk.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  }
});

export const { clearFavouritesError, resetLastAction } = favouritesSlice.actions;
export default favouritesSlice.reducer;

The Problem
When the access token expires, my server returns a 403 error. I want to automatically:

Detect 403 errors in any thunk

Call refreshTokenThunk to get a new access token

Retry the original request with the new token

Handle concurrent requests (if multiple requests fail with 403 at the same time)

Fetching overridden events from Google Calendar

I have a Google Apps Script to automate the description of Google Calendar events. The script runs once a week and tries to fetch, and modify, the occurrence for the week to follow.

The code to find the next meeting looks like this:

  const now = new Date();
  const nowIso = now.toISOString();
  const oneWeekLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
  const oneWeekLaterIso = oneWeekLater.toISOString();
  const instances = Calendar.Events.instances(calendarId, recurringEventId, {
    timeMin: nowIso,
    timeMax: oneWeekLaterIso,
    maxResults: 1
  });
  if (!instances.items || !instances.items.length) {
    // No upcoming occurrences found; snothing to do
    Logger.log("No upcoming instances found");
    return;
  }
  const nextInstance = instances.items[0];

This normally works. However, if an event has been modified (either manually or by my own code), it is not found by this. It seems Calendar.Events.instances only lists unaltered events. How do I get around this? I want to find the next occurrence (modified or not, but not deleted) of a recurring event.

How can I add structured data (JSON-LD) for multiple products on a single blog page?

I’m trying to improve the SEO of a blog page that also highlights a few of our products. For example, here’s one of the pages I’m working with:

Famous Places to visit in Salem

The page is mostly a travel article, but in between the content, we mention some of our products (different varieties of Salem mangoes).

I know how to add JSON-LD schema for a single Product, but in this case, I’m not sure:

Should I add multiple “@type”: “Product” blocks inside one tag, or use multiple tags?

Since the page is primarily a blog post and not a product listing, is it still valid to use Product schema here, or should I only use Article schema and skip products?

What’s the best practice so Google doesn’t treat this as “spammy” structured data?

Here’s an example of what I tried with two products:

<script type="application/ld+json">
{
  "@context": "https://schema.org/",
  "@type": "Product",
  "name": "Alphonso Mango",
  "image": "https://www.salemmango.com/images/alphonso-mango.jpg",
  "description": "Premium Alphonso Mangoes from Salem",
  "offers": {
    "@type": "Offer",
    "priceCurrency": "INR",
    "price": "999",
    "availability": "https://schema.org/InStock"
  }
},
{
  "@context": "https://schema.org/",
  "@type": "Product",
  "name": "Imam Pasand Mango",
  "image": "https://www.salemmango.com/images/imam-pasand-mango.jpg",
  "description": "Rare Salem Imam Pasand Mango",
  "offers": {
    "@type": "Offer",
    "priceCurrency": "INR",
    "price": "899",
    "availability": "https://schema.org/InStock"
  }
}
</script>

But this throws validation errors in Google’s Rich Results Test.

What’s the correct way to structure multiple product schemas on the same blog page?

Using browser’s native pdfViewer from JavaScript?

Browser’s navigator.pdfViewerEnabled property indicates whether the browser supports inline display of PDF files when navigating to them.

If we think of going further – is it possible to use browser’s built-in (native) capability of processing PDF files from JavaScript code? One particular thing I need is to get page size and page count of a selected local PDF file.

In other words, is there any API exposed by a browser to use its native PDF-processing capabilities without importing any JS/npm library or implementing it from scratch?

How to intercept all HTTP requests from page

I am trying to learn how to use JavaScript to block all outgoing HTTP requests from an HTML file. The problem is when you click the link to www.example.com it doesn’t seem to catch it as a fetch event.

test.html:

<!DOCTYPE html>
<html>
<body>
<script type="text/javascript">


if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("./sw.js", { scope: "/" }).then(
    (registration) => {
      console.log("Service worker registration succeeded:", registration);
    },
    (error) => {
      console.error(`Service worker registration failed: ${error}`);
    },
  );
} else {
  console.error("Service workers are not supported.");
}

  
</script>
<div>
<a href="https://www.example.com">Visit Example</a>
</div>
</body>
</html>

sw.js:

console.log('loaded')
self.addEventListener('fetch', function (event) {
    console.log('fetch', event.request.url);
});

Looking at my console:

loaded
fetch test.html
test.html:31 Service worker registration succeeded
fetch favicon.ico
favicon.ico:1   GET favicon.ico 404 (Not Found)

So you can see above that it even blocks an attempt to load favicon.ico (host provider icon) but it doesn’t block me if I click the link – I just go to example.com.