Is it possible to have a FullCalendar dayGridYear to span across the year break

For example, if I have the following:

visibleRange: {
        start: '2024-12-23',
        end: '2025-01-19'
        },   
validRange: {
        start: '2024-12-23',
        end: '2025-01-19'
        }

Then the dayGridYear only shows Dec 23 – Jan 5. For other times of the year, this kind of range of dates is not an issue, it appears to be a problem because it is spanning across the year.

I did a search, and couldn’t find this documented as an issue anywhere.

How to add additionalProperties in nested schema in typebox ajv which has common schema import?

I tried to add the { additionalProperties : false} to one of my typebox schema

schema.ts


export const numericString = (options: { default: string }) =>
  Type.String({
    ...options,
    pattern: '^[0-9]+$'
  });

export const commonQueryParamsWithPagination = Type.Object({
  sort: Type.Optional(Type.String()),
  page: numericString({ default: '1' }),
  size: numericString({ default: '100' })
});

export const getUserRequestQuerySchema = Type.Intersect(
  [
    Type.Object({
      status: Type.String()
    }),
    commonQueryParamsWithPagination
  ]
);

export const getUserRequestSchema = Type.Object({
  query: getAllOrganizationRequestQuerySchema,
  body: Type.Any(),
  headers : Type.Any(),
  params: Type.Any()
} ,   { additionalProperties: false });


This works if any other property is passed to the the top level,

In case we need to add this property to the query schema along with request schema

Option 1:

  [
    Type.Object({
      status: Type.String()
    }),
    commonQueryParamsWithPagination
  ],
  { additionalProperties: false }
);

This approach make the all the property as additional property and is not allowed at all

In order make it work we need to make the query schema without Type.Intersect and commonQueryParamsWithPagination


export const getUserRequestQuerySchema = Type.Object({
      status: Type.String(),
      sort: Type.Optional(Type.String()),
      page: numericString({ default: '1' }),
       size: numericString({ default: '100' })
    }),

As there can be multiple conditions where I have reuse the existing schema so how would I make it work for { additionalProperties : false } for neseted schema whihc need to reuse the existing schema ?

Masonry using ajax resize

I m working on a responsive site using masonry to show pictures . The grid container is fill with Pictures ( items ) called from an ajax function : item fill from ajax this working well untill i resize for exemple for cell phone . i have an error from masonry.js output console : masonry.pkgd.min.js:9 Uncaught RangeError: Invalid array length
at Array.push
the line refer to

function(t) {
"use strict";
function e(t, e) {
    var n = t.create("masonry");
    return n.prototype._resetLayout = function() {
        this.getSize(),
        this._getMeasurement("columnWidth", "outerWidth"),
        this._getMeasurement("gutter", "outerWidth"),
        this.measureColumns();
        var t = this.cols;
        for (this.colYs = []; t--; )
            this.colYs.push(0);
        console.log(this.colYs);
        this.maxY = 0
    }   

excalty there ==> this.colYs.push(0);
My function to call item and resize

    function sendFormData() {
const postData = new FormData();

// Add all selected conditions to FormData
for (const [key, value] of Object.entries(selectedConditions)) {
    console.log("Adding to FormData:", key, value); // Debugging log
    if (Array.isArray(value)) {
        value.forEach(val => postData.append(key, val)); // If it's an array (checkboxes)
    } else {
        postData.append(key, value); // If it's a single value (radio or text input)
    }
}

fetch(BASEURL + '/search_ajax.php', {
    method: 'POST',
    body: postData
})
.then(response => response.text()) // Read response as text
.then(text => {
    console.log('Raw Response:', text);
    const grid = document.getElementById('grid');
    grid.innerHTML = text;

    new AnimOnScroll(grid, {
        minDuration: 0.4, 
        maxDuration: 0.7,
        viewportFactor: 0.2 
    });

    // Ensure imagesLoaded only triggers after all images are loaded
    imagesLoaded(grid, function() {
        masonryInstance.reloadItems();
        masonryInstance.layout();
    });
})
.catch(error => {
    console.error('Error:', error);
});

}

let resizeTimeout;

window.addEventListener(‘resize’, function() {
// Clear the existing timeout to avoid multiple layout recalculations
clearTimeout(resizeTimeout);`

// Set a new timeout to delay the layout recalculation
resizeTimeout = setTimeout(function() {
    if (masonryInstance) {
        masonryInstance.layout(); 
    }
}, 100); // Adjust the timeout duration as needed (in milliseconds)

});

here the masonry ini `window.addEventListener(“load”, function() {
const grid = document.getElementById(“grid”);
masonryInstance = new Masonry(grid, {
itemSelector: “.grid-item”,
gutter: 20,
transitionDuration: “2ms”,
isFitWidth: true,

    });
    console.log("Masonry initialized using plain JavaScript");
});  

i inserted a console to watch this.colYs.push(0);
console.log(this.colYs) , the array is fine when you called item (3) [0, 0, 0] 0 : 281.375 1 : 334.5 2 : 182.703 length : 3 you have the number of column etc… but when during resizing i have a length absolutely random and which corresponds to nothing ( 56 colum , 65 column… ) . i tried to get information from the website . if someone can help me . thank you

ClerkMiddleware Not Functioning as Expected

With the deprecation of authMiddleware, I’m looking for guidance on how to update my code. My previous implementation was structured like this:The code
What would be the recommended approach to achieve the same functionality with the updated tools or methods provided by Clerk? Any examples or best practices would be greatly appreciated!

I tried going through the documentation. But being a beginner I couldn’t get much out of it. Then I tried some alternatives using the help of chatGPT. Those too were unsuccessful.

Don’t understand async await execution order

async function funcTwo() {
  return new Promise((r) => r());
}

async function funcOne() {
  console.log("A");

  (async () => {
    await funcTwo();
    console.log("B");
  })();
  console.log("C");
}

await funcOne();

console.log("Done");

According to my knowledge the output should be as follows:

A
C
B 
Done

My reasoning is:

1.funcOne runs

2.A is printed

3.async function runs and promise is resolved immediately thus console.log("B") is moved to the mircotask queue

4.C is printed

5.funcOne is resolved and console.log("Done"); is moved to the mircotask queue.

6.Tasks are fetched from the queue and B and Done are printed in that order. (console.log("B") is added to the queue before console.log("done"))

However the output is:

A
C
Done
B

B and Done are switched

Can someone explain what I got wrong?

Toggle card created in Elementor with a button inside the card

I normally do not use WordPress or Elementor but I have a project that requires it. The project requires card elements that toggle between two states, a “front” side with an image and a “back” side with text. This was very simple to set up in Elementor and works well as a hover effect on any device that has a mouse.

For reference, a testing environment that has this effect using Elementor:
https://project-001.com

The problem is when a user is on a mobile device. The cards will flip to the “back” side when they are tapped, but do NOT flip to the front side when they are tapped again. However, they will flip back when the user taps anywhere outside of the flipped card.

I’ve tried to implement buttons that can close the cards when clicked. At first, I seemed to have some success because I could get buttons outside the card to flip the card back to the “front” side. However, I could not get the button inside the card to flip the card. That’s when I realized the button outside the card was only working because it’s outside the card – it was functionally no different than tapping empty space outside the card.

My next thought was to attach an onClick event to the button (as a testing method – I’ll add an event listener later instead of onClick once this is resolved). I started with a basic alert() and was able to confirm that the button on the “back” of the card would fire. Next I added a few variations of scripts to toggle the card’s class and assigned some “transform: rotateY(180deg)” rules to the appended class.

This concept worked fine for a test card on a non-Elementor platform. For reference, see https://brukenet.com/test/index.php for a card flip that works fine by touch (this one doesn’t even use a button, tapping anywhere on the back of the card works).

However, what works outside of Elementor does not seem to work inside the Elementor site. I’ve tried to look at the events that are firing, and I’ve looked at the hooks available to Elementor (c.f. https://developers.elementor.com/docs/hooks/js/ ) but quite frankly I’ve had a hard time because there’s so much going on that I can’t separate the wheat from the chaff, so to speak.

Anyone have a good suggestion for how to get a button (or the entire card for that matter) to toggle back to the “front” in Elementor in response to a tap event?

Thanks.

JavaScript popup calling different images onclick

I have created an image popup which uses javaScript to call different images onClick.

HTML Code snippet for the same as below.

<img onmouseover="imgload();" src="11.jpg">
<img onmouseover="imgbarb();" src="13.jpg">

I use the below javaScript to load the onmouseover function

JavaScript code for 11.jpg

function imgload() {
  var imgContainers   = document.querySelectorAll('.img-container');
  var tooltipElements = document.querySelectorAll('.tooltip');
  var popup           = document.getElementById('imagePopup1');
  var popupOverlay    = document.getElementById('imagePopupOverlay1');
  var popupImage      = document.getElementById('popupImage1');
  var closePopupBtn   = document.getElementById('closeImagePopupBtn1');
  // Show tooltip on hover
  imgContainers.forEach((container, index) => {
    var tooltip = container.querySelector('.tooltip');
    var image   = container.querySelector('img');
    container.addEventListener('mouseenter', function() {
      tooltip.style.display = 'block';
    });
    container.addEventListener('mouseleave', function() {
      tooltip.style.display = 'none';
    });
    // Open full-size image popup when image is clicked
    image.addEventListener('click', function() {
      var imageSrc               = "11-new.jpg";
      popupImage.src             = imageSrc;
      popup.style.display        = 'block';
      popupOverlay.style.display = 'block';
    });
  });
  // Close the image popup when clicking the close button
  closePopupBtn.addEventListener('click', function() {
    popup.style.display        = 'none';
    popupOverlay.style.display = 'none';
  });
  // Close the image popup when clicking the overlay
  popupOverlay.addEventListener('click', function() {
    popup.style.display        = 'none';
    popupOverlay.style.display = 'none';
  });
}

JavaScript code for 13.jpg

function imgbarb() {
  var imgContainers   = document.querySelectorAll('.img-container');
  var tooltipElements = document.querySelectorAll('.tooltip');
  var popup           = document.getElementById('imagePopup1');
  var popupOverlay    = document.getElementById('imagePopupOverlay1');
  var popupImage      = document.getElementById('popupImage1');
  var closePopupBtn   = document.getElementById('closeImagePopupBtn1');
  // Show tooltip on hover
  imgContainers.forEach((container, index) => {
    var tooltip = container.querySelector('.tooltip');
    var image   = container.querySelector('img');
    container.addEventListener('mouseenter', function() {
      tooltip.style.display = 'block';
    });
    container.addEventListener('mouseleave', function() {
      tooltip.style.display = 'none';
    });
    // Open full-size image popup when image is clicked
    image.addEventListener('click', function() {
      var imageSrc               = "13-new.jpg";
      popupImage.src             = imageSrc;
      popup.style.display        = 'block';
      popupOverlay.style.display = 'block';
    });
  });
  // Close the image popup when clicking the close button
  closePopupBtn.addEventListener('click', function() {
    popup.style.display        = 'none';
    popupOverlay.style.display = 'none';
  });
  // Close the image popup when clicking the overlay
  popupOverlay.addEventListener('click', function() {
    popup.style.display        = 'none';
    popupOverlay.style.display = 'none';
  });
}

I have around 30+ images. So each time when I wish to create a popup image for a new image, I have to create a new function and then call onmouseover everytime.

I feel it is very cumbersome to manage as it also increases the code.
Is there a simple way to this so I do not have to create a new function every time when I add a new image for popup?

I tried if and else, arrays and other stuff, but it did not work.

How to have multiple vidstack players on one page?

I need to set up vidstack player this way

<zzVideo class="zzVideo" data-src="youtube/ssvK18GSsEA"></zzVideo>

Now I am trying to figure out how to intialize multiple player on one page. I came up with this code. I am sure that real programmers would correct me.

var players = [];

[...document.getElementsByTagName("zzVideo")].forEach((element, i) => {
//    console.log(element.dataset.src + ' - '+i);
    element.id = "zzVid"+i;
   
players[i] = VidstackPlayer.create({
         target: '#zzVid'+i,
         title: 'Video #'+1,
         src: element.dataset.src,
      //    poster: 'https://files.vidstack.io/sprite-fight/poster.webp',
         layout: new VidstackPlayerLayout({
      //      thumbnails: 'https://files.vidstack.io/sprite-fight/thumbnails.vtt',
         }),
       });
  });

Can you please help me to have better solution? Working jsfiddle is here https://jsfiddle.net/radek/o9bs7qv5/55/

Firefox rendering issue for ellipses

In Firefox browser , truncation of string is not working, when typing into the input box at first time truncation is not working, but when focusing out and typing once again it start working , seems its rendering problem which is not working at first time.

Above code will give the different result in firefox and the the chrome browser.

in chrome it will work at first time , means once user will start the typing into the input box , it will start showing the ellipses once text overflow will happen.
but in Firefox it will not work like this, at first time when user will start the typing it will not show ellipses although text exceeds the boundary of input box , but when user focus out from the input box and start once again start typing into the input box ellipses will be shown. this behavior is creating confusion.

Kindly provide the solution for the same
I am using 128.5.2 esr firfox version

input.b {
  white-space: nowrap;
  width: 50px;
  overflow: hidden;
  text-overflow: ellipsis;
  border: 1px solid #000000;
}

div.c {
  white-space: nowrap;
  width: 50px;
  overflow: hidden;
  text-overflow: "----";
  border: 1px solid #000000;
}
<h2>text-overflow: ellipsis:</h2> 
<input class="b" />

Updating the primary entry for a google contact using the people API without deleting previous primary entry

I am working on a Google Apps Script project that integrates with the Google People API. My goal is to update a field designated as primary (e.g., primary address). However, I haven’t found a way to directly change the primary field by modifying the metadata of the array’s entries.

For example, adding ‘primary’: true to the metadata for the new primary entry and removing it from other entries does not override the existing primary designation.

Current Approach:

The only method I’ve found to achieve this is as follows:

Remove all entries in the array except the one that should be primary.
Update the contact.
Add back the other entries, removing their metadata to avoid conflicts.
Here’s a snippet of the function where I apply the changes, including handling the primary address update (there are quite a few class objects that I have created that perform operations in the api but aren’t part of the API so apologies if that confuses the matter but it’s the methodology I’m concerned about since this seems like an opportunity for disaster, so I don’t want to build a backup in the event of an error while information isn’t saved unless I have to):

_applyChanges(person, changes) {
  if (!person || !Array.isArray(changes)) {
    throw new Error('Valid contact and changes array required');
  }
  
  try {
    let primaryAddressChanges = [];
    let removals = [];
    
    changes.forEach(change => {
      if (change.apiFieldInfo?.arrayField === 'addresses' && change.isPrimary) {
        primaryAddressChanges.push(change);
      }
    });

    if (person.addresses && primaryAddressChanges.length > 0 && person.addresses.length > 1) {
      let primaryAddress = {};
      let otherAddresses = [];
      
      person.addresses.forEach(address => {
        if (address.metadata?.primary) {
          primaryAddress = address;
        } else {
          otherAddresses.push(address);
        }
      });

      if (primaryAddress) {
        // Reorder and update addresses to make the desired one primary
        let finalAddressArray = [primaryAddress].concat(otherAddresses);
        let googlePerson = new GooglePerson(person);
        try {
          googlePerson.updateAddresses([primaryAddress]); // Only update the primary address
          googlePerson.update();
          googlePerson.updateAddresses(finalAddressArray); // Add back the other addresses
          person = googlePerson.person;
        } catch (error) {
          Logger.log(`Error updating primary address: ${error.message}`);
        }
      }
    }

    return person;
  } catch (error) {
    Logger.log(`Error applying changes: ${error.message}`);
    throw error;
  }
}

Key Problem:

Why doesn’t directly adding ‘primary’: true to the metadata of the new primary entry and removing it from the others work? Is there a better or more direct approach to achieve this?

Additional Information:
The updateAddresses method in the code above is a utility method to modify the addresses of a GooglePerson instance.
The process involves updating the primary address and then re-adding the other addresses after clearing their metadata.
I’d appreciate any insights or alternative approaches. Am I missing something in the API documentation or functionality?

If you want me to refine this further, let me know!

I’m trying to implement the Web Share Api functionality on my web app but when i test it on my iphone 14 on chrome, is not working

Has anyone else ran into this issue? I’m implementing the Web Share API and it works when i tested it using my desktop but when i tested it using my iphone 14 it works on safari but when i use chrome, the first couple of times i click on the share button it does not work ,some times it works and some times it does not so i need to spam click it hoping it works one of those times , what is also wierd is that when i used an online virtual machine and tested it on iphone12 it worked fine so i don’t know if it is a compatibility problem ?

handleCopyOpen = async (socialHandle) => {
      if (this.sharingInProgress) {
        console.log('Sharing is already in progress.');
        return;
      }
      this.sharingInProgress = true;
    
      try {
        const targetElement = document.querySelector('.milestone-popup');
        if (!targetElement) throw new Error('Target element not found.');
    
        // Get the Blob from the prepareScreenshot function
        const blob = await this.prepareScreenshot(targetElement);
    
        if (navigator.canShare && navigator.canShare({ files: [new File([blob], 'screenshot.png', { type: 'image/png' })] })) {
          const file = new File([blob], 'screenshot.png', { type: 'image/png' });
    
          await navigator.share({
            title: 'Achievement on Fanstories',
            text: 'Check out my latest achievement!',
            files: [file],
          });
          console.log('Shared successfully using Web Share API');
        } else {
          console.warn('Web Share API not supported. Showing fallback.');
          const url = URL.createObjectURL(blob);
          window.open(url, '_blank');
        }
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log('User canceled the share operation.');
        } else {
          console.error('Error during sharing:', error);
          alert('Failed to share. Please try again.');
        }
      } finally {
        this.sharingInProgress = false;
      }
    };
    
    prepareScreenshot = async (targetElement) => {
      // Define cloneContainer outside the try block for broader scope
      let cloneContainer;
    
      try {
        // Clone the target element to avoid modifying the original DOM
        cloneContainer = document.createElement('div');
        cloneContainer.style.cssText = `
          position: fixed;
          top: 0;
          left: 0;
          width: 100vw;
          height: 100vh;
          z-index: -1; /* Keeps the clone invisible */
          opacity: 0; /* Ensures it's not visible to users */
        `;
        document.body.appendChild(cloneContainer);
    
        const clonedElement = targetElement.cloneNode(true);
        cloneContainer.appendChild(clonedElement);
    
        // Adjust cloned content for capturing
        const footerElement = clonedElement.querySelector('.share-footer');
        const closeButtonElement = clonedElement.querySelector('.close-button');
        const confettiCanvas = clonedElement.querySelector('canvas');
    
        if (footerElement) footerElement.style.display = 'none';
        if (closeButtonElement) closeButtonElement.style.display = 'none';
        if (confettiCanvas) confettiCanvas.style.display = 'none';
    
        // Allow DOM changes to reflect
        await new Promise((resolve) => setTimeout(resolve, 100));
    
        // Capture the screenshot
        const canvas = await html2canvas(clonedElement, {
          backgroundColor: null,
          useCORS: true,
          scale: 2, // High resolution
        });
    
        // Convert canvas to Blob
        return new Promise((resolve, reject) => {
          canvas.toBlob(
            (blob) => {
              if (blob) {
                resolve(blob); // Return the Blob directly
              } else {
                reject(new Error('Failed to convert canvas to Blob.'));
      

        }
        },
        'image/png', // MIME type
        1.0 // Image quality (1.0 = best)
      );
    });
  } catch (error) {
    console.error('Error during screenshot preparation:', error);
    throw error;
  } finally {
    // Ensure cloneContainer is cleaned up
    if (cloneContainer) {
      document.body.removeChild(cloneContainer);
    }
  }
};

Vue: returning non-reactive values from a composable

Is it okay to return non-reactive values from a composable?

E.g., like so:

// useNavLink.js

import { toValue } from 'vue'
import { useRouter } from 'vue-router'

export default function useNavLink(link) {
  const router = useRouter()

  const handleLinkClick = (ev) => {
    ev.preventDefault()

    router.push(toValue(link))
  }

  return { handleLinkClick }
}
// NavLink.vue

<template>
  <a :href="link" @click="handleLinkClick">{{ label }}</a>
</template>

<script setup>
import useNavLink from '@/composables/useNavLink'

const { link, label } = defineProps(['link', 'label'])

const { handleLinkClick } = useNavLink(() => link)
</script>

The docs say the following,

The recommended convention is for composables to always return a plain, non-reactive object containing multiple refs.

but they don’t explicitly say that one shouldn’t return non-reactive values.

How to correctly enable/disable a parent section based on child selection?

I have a scenario where I am dynamically creating child buttons for a parent section, and I need to manage the state of the parent based on whether the child sections are selected or unselected. The parent should only be enabled when all child buttons are unselected. However, my current approach is not working as expected: when I unselect one child, the parent remains disabled, even though other children are still unselected.

below is code:

if (childItem) { 
const childButton = $('<div/>', {
    text: childItem.section_name,
    'id': childItem.id,
    'class': 'button-location-style child-section',
    click: function() {
        var index = ids.indexOf(childItem.id);

        if (index > -1) {
            // Unselect the child
            ids.splice(index, 1);  
            $(this).removeClass("changedBackground"); 

            // Enable the parent section when the child is unselected
            $(`#${parentId}`).removeClass('disabled'); 

        } else {
            // Select the child
            ids.push(childItem.id); 
            $(this).addClass("changedBackground"); 

            // Disable the parent section when the child is selected
            $(`#${parentId}`).addClass('disabled'); 
        }

        // Update the section ID input field with the selected IDs
        $('#SectionId').attr('value', ids.join(','));
    }
});

// Append the child button to the container
childContainer.append(childButton);

}

Problem:

  • The parent section should be enabled only when all child sections are
    unselected.
  • The parent is not behaving as expected. After selecting multiple
    children and unselecting one, the parent remains disabled even though
    all child sections should not be selected.

What I have tried:

  • I have checked the ids array when a child is selected or unselected,
    but the parent is still not enabled when all children are unselected.

How can I modify my code so that the parent section only gets enabled when all child sections are unselected, and stays disabled when any child section is selected?

PWA Payload and JSON

First time here, I have a problem with PWA push notifications.
It’s a chatting page made with PHP.
My push notifications are sent OK (on the phone and on the web). I have a push notification.

But there is a problem with the format of the received notification.

I received a notification text formatted like that :

{“title”:”Nouveau message de Maman”,”body”:”Test”,”icon”:”/icons/icone-app-72.png”,”url”:”/messagerie.php”}

I want to receive :

Nouveau message de maman

Test

Icons of the app and link on the click on the notification

It’s seems to be a parsing problem ? but no ideads where is problem and how to resolve it.

Payload seems to be the problem because when i try juste to send the function and do no pass informations with payload it’s OK.

Can anyone help me ?

Thanks a lot 🙂

$stmt = $pdo->prepare("SELECT endpoint, p256dh, auth FROM subscriptions WHERE user_id = ?");
        $stmt->execute([$destinataire_id]);
        $subscriptionData = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($subscriptionData) {
            $subscription = Subscription::create([
                'endpoint' => $subscriptionData['endpoint'],
                'keys' => [
                    'p256dh' => $subscriptionData['p256dh'],
                    'auth' => $subscriptionData['auth'],
                ],
            ]);
        
            $webPush = new WebPush([
                'VAPID' => [
                    'subject' => 'mailto:[email protected]',
                    'publicKey' => VAPID_PUBLIC_KEY,
                    'privateKey' => VAPID_PRIVATE_KEY,
                ],
            ]);
        
            // Payload spécifique pour la messagerie
            $payload = json_encode([
                'body' => 'Vous avez un nouveau message de ' . htmlspecialchars($_SESSION['nom']),
                'icon' => '/icons/icone-app-72.png',
                'url' => '/messagerie.php?utilisateur_id=' . $_SESSION['user_id'],
            ]);
        
            $webPush->queueNotification($subscription, $payload);
        
            foreach ($webPush->flush() as $report) {
                if (!$report->isSuccess()) {
                    error_log("Erreur d'envoi (messagerie) : " . $report->getReason());
                }
            }
        }

And here is my sw.js (services workers)

self.addEventListener('push', (event) => {
  console.log('Notification reçue.');

  let data = {
    body: 'Tu as une nouvelle notification.',
    icon: '/icons/icone-app-72.png',
    url: '/notifications.php', // URL par défaut pour redirection
  };

  try {
    // Extraire les données envoyées par le serveur (si présentes)
    if (event.data) {
      const payload = event.data.json();
      data = {
        body: payload.body || data.body,
        icon: payload.icon || data.icon,
        url: payload.url || data.url,
      };
    }
  } catch (error) {
    console.error('Erreur lors du traitement du payload:', error);
  }

  const options = {
    body: data.body,
    icon: data.icon,
    data: { url: data.url },
  };

  event.waitUntil(self.registration.showNotification('Notification', options));
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();

  if (event.notification.data && event.notification.data.url) {
    event.waitUntil(clients.openWindow(event.notification.data.url));
  }
});