Add left/right navigation arrows to WooCommerce product gallery

I’m using WooCommerce with the following theme:
https://demo2.wpopal.com/bikys/shop/cube-nature-pro/

On the product detail page, the default product gallery only allows switching product images by clicking the thumbnail images below the main image.

I want to enhance the user experience by adding left and right arrow buttons to the sides of the main images, so that visitors can navigate through the gallery more easily.

I’ve implemented the feature using the code below:

    <style>
.woo-gallery-nav {position: absolute;top: 50%;z-index: 10;transform: translateY(-50%);background: rgba(0,0,0,0.5);color: #ffdb00;font-size: 30px;width: 30px;height: 30px;line-height: 30px;text-align: center;cursor: pointer;user-select: none;border-radius: 15px;transition: all 0.3s ease;}
.woo-gallery-nav:hover {background: rgba(0,0,0,0.8);}
.woo-gallery-nav.prev {left: 0px;}
.woo-gallery-nav.next {right: 0px;}
.flex-control-thumbs li {cursor: pointer;transition: opacity 0.3s ease;}
.flex-control-thumbs li.flex-active {opacity: 0.7;border: 2px solid #333;}
.woocommerce-product-gallery__image {transition: opacity 0.5s ease;}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
  function initializeGalleryNavigation() {
    const galleryWrapper = document.querySelector(".woocommerce-product-gallery__wrapper");
    const slides = Array.from(galleryWrapper.querySelectorAll('.woocommerce-product-gallery__image'));
    const thumbs = Array.from(document.querySelectorAll(".flex-control-thumbs li"));
    if (!slides.length || !thumbs.length) {
      setTimeout(initializeGalleryNavigation, 100);
      return;
    }
    let currentIndex = slides.findIndex(slide => slide.classList.contains('flex-active-slide'));
    if (currentIndex === -1) currentIndex = 0;
    const prevBtn = document.createElement("div");
    prevBtn.className = "woo-gallery-nav prev";
    prevBtn.innerHTML = "‹";
    const nextBtn = document.createElement("div");
    nextBtn.className = "woo-gallery-nav next";
    nextBtn.innerHTML = "›";
    galleryWrapper.parentNode.appendChild(prevBtn);
    galleryWrapper.parentNode.appendChild(nextBtn);
    function updateGallery(index) {
      if (index < 0) index = slides.length - 1;
      if (index >= slides.length) index = 0;
      thumbs[index].querySelector('img').click();
      currentIndex = index;
    }
    prevBtn.addEventListener("click", () => updateGallery(currentIndex - 1));
    nextBtn.addEventListener("click", () => updateGallery(currentIndex + 1));
    document.addEventListener("keydown", (e) => {
      if (e.key === "ArrowLeft") updateGallery(currentIndex - 1);
      if (e.key === "ArrowRight") updateGallery(currentIndex + 1);
    });
    thumbs.forEach((thumb, i) => {
      thumb.addEventListener("click", () => {
        currentIndex = i;
      });
    });
  }
  initializeGalleryNavigation();
});
</script>

and it’s somewhat functional. However, I’ve encountered a small bug on mobile devices:
When using the arrow buttons to switch images, the logic sometimes conflicts with the default thumbnail click behavior. For example, clicking the arrows may occasionally cause the thumbnails to stop working, or clicking a thumbnail may cause the arrow buttons to stop functioning correctly.

Is this an issue with how I’m handling the event bindings or gallery state? Do I need to further optimize my JavaScript to handle mobile interactions more gracefully?

Thanks in advance!

Undo the last visual change made by the mouse one by one by clicking on DIV

The goals here is when you click on the UNDO div, the last visual change made by the mouse should be undone one by one.

There are 2 div’s you can click on that make changes:

  1. kopje: This is a used as a collapse menu.

  2. taak: if you click on this, the DIV will be removed.

The undo div should undo any changes.

Example:

I expand a DIV kopje by clicking on it. After that I delete a task by clicking on it.

If I click on the undo div the task should come back on the same place where it was. If I then click undo again the div kopje should close.

// Find all headers
const kopjes = document.querySelectorAll('.kopje');

// Add a click event to each header
kopjes.forEach(kopje => {
  kopje.addEventListener('click', function() {
    // Find the task divs that come directly after the header
    let taken = [];
    let nextElement = kopje.nextElementSibling;
    while (nextElement && nextElement.classList.contains('taak')) {
      taken.push(nextElement);
      nextElement = nextElement.nextElementSibling;
    }

    // Check if the task divs are already visible
    const isVisible = taken.some(taak => taak.style.display === "block");

    if (isVisible) {
      // If they are visible, hide them
      taken.forEach(taak => {
        taak.style.display = "none";
      });
    } else {
      // If they are hidden, show them
      taken.forEach(taak => {
        taak.style.display = "block";
      });
    }
  });
});

// Remove task divs with delay and color change
document.querySelectorAll('.taak').forEach(taak => {
  taak.addEventListener('click', function() {
    // Temporarily change the background color of the task
    this.style.backgroundColor = "#128771"; // Red temporarily

    // Wait a few seconds and then remove the task
    setTimeout(() => {
      this.remove(); // Remove the task div
    }, 100); // Remove after 500ms
  });
});

// Function for reset (optional)
document.querySelector('.knop_reset').addEventListener('click', () => {
  document.querySelectorAll('.taak').forEach(taak => {
    taak.style.display = "none"; // Hide all tasks
  });
});
body {
  background-color: #000000;
  margin: 0px;
}

.kopje {
  background-color: #128771;
  color: #FFFFFF;
  cursor: pointer;
  padding: 15px 8px 15px 10px;
  font-size: 20px;
  font-family: Arial;
}

.taak {
  display: none;
  /* Standaard niet zichtbaar */
  color: #FFFFFF;
  padding: 15px 8px 15px 10px;
  font-size: 20px;
  font-family: Arial;
  cursor: pointer;

}

.knop_undo {
  background-color: #FFFFFF;
  padding: 20px 20px;
  font-size: 16px;
  cursor: pointer;
  position: fixed;
  z-index: 1000;
  left: 150px;
  bottom: 10px;
}

.knop_reset {
  background-color: #FFFFFF;
  padding: 20px 20px;
  font-size: 16px;
  cursor: pointer;
  position: fixed;
  z-index: 1000;
  left: 10px;
  bottom: 10px;
}
<div class="kopje">KOPJE 1</div>
<div class="taak">taak 1</div>
<div class="taak">taak 2</div>
<div class="taak">taak 3</div>

<div class="kopje">KOPJE 2</div>
<div class="taak">taak 1</div>
<div class="taak">taak 2</div>
<div class="taak">taak 3</div>

<div class="kopje">KOPJE 3</div>
<div class="taak">taak 1</div>
<div class="taak">taak 2</div>
<div class="taak">taak 3</div>

<div class="knop_undo">Undo</div>
<div class="knop_reset">Reset</div>

I want to create a callable navigation bar so that when I need to update it, it will automatically update the navigation bar for all pages

I currently have a site where I want to create a navigation bar where when I add a page to the nav bar, it will automatically add it to all pages that I have currently uploaded to my site.

I currently have a script for a “Back to Top” button as well as a script for a responsive navigation bar. I’m now looking to add a script that will load the nav bar from another HTML page that contains my actual nav bar. But for some reason, even after adding the script, the nav bar isn’t loading for some reason.

Below is my main index.html page that contains all the scripts:

<!doctype html>
<html lang="en-US">
<head>
    <title>Title</title>
    <meta name = "viewport" content = "width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
    <meta charset="utf-8">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
    <script src="https://kit.fontawesome.com/0a48c3a8e0.js" crossorigin="anonymous"></script>
    <script>
        $(document).ready(function() {
            $("#nav-placeholder").load("nav.html")
        })
    </script>
    <link href="styles/main.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <button onclick="topFunction()" id="myBtn" title="Go to top">Back to Top</button>
    <script src = "scripts/topnav.js"></script>
    <script src = "scripts/topbutton.js"></script>
    <div id = "nav-placeholder"></div> 
    <header>
        <nav class = "topnav" id = "myTopnav">
            <h1 class = "logo">General Gaming Guides</h1>
            <a href = "javascript:void(0);" class = "icon" onclick = "myFunction()">
                <i class = "fa fa-bars"></i>
            </a>
        </nav>
    </header>
    <div class = "middle_text">
        Text here.
    </div>
</body>
</html>

And my navigation bar page called nav.html code is here:

<div>
    <ul class = "mainnav">
        <li><a href = "index.html">Home</a></li>
            <li><a href = "games.html">Games</a>
                <ul class = "gamesnav">
                    <li><a href = "lostark.html">Lost Ark</a>
                        <ul class = "subnav">
                            <li><a href = "news.html">News</a></li>
                            <li><a href = "patch_notes.html">Patch Notes</a></li>
                            <li><a href = "beginner_guide.html">Beginner's Guide</a>
                                <ul class = "subnav2">
                                    <li><a href = "character_info.html">Understanding Game Design</a></li>
                                    <li><a href = "road_to_50.html">Road to Level 50</a></li>
                                </ul>
                            </li>
                            <li><a href = "dailies.html">Dailies</a></li>
                            <li><a href = "weeklies.html">Weeklies</a></li>
                            <li><a href = "current-events.html">Current Events</a></li>
                            <li><a href = "abyssaldungeons.html">Abyssal Dungeons</a></li>
                            <li><a href = "legionraids.html">Legion Raids</a>
                                <ul class = "subnav2">
                                    <li><a href = "valtan-gate-1-guide.html">Normal Mode</a></li>
                                    <li><a href="valtan-hell-gate-1.html">Inferno/Extreme Mode</a></li>
                                </ul>
                            </li>
                            <li><a href = "kazerosraids.html">Kazeros Raids</a></li>
                            <li><a href = "adventure_islands.html">Adventure Islands</a></li>
                        </ul>
                    </li>
                </ul>
            </li>
        <li><a href = "about.html">About</a></li>
        <li><a href = "support.html">Support</a></li>
    </ul>
</div>

Any help to get it to show would be appreciated!

Is it possible for pipeThrough and TransformStream to produce a byte stream?

The following code does not work. It produces the error Failed to execute 'getReader' on 'ReadableStream': Cannot use a BYOB reader with a non-byte stream Even though byteStream started out life as a byte stream, piping it through transformedStream turned it into a non-byte stream. I can’t find anywhere how to create a TransformStream that inputs and outputs byte streams.

const byteStream = new ReadableStream({
    type: "bytes",
    pull(controller) {
        controller.enqueue(new Uint8Array([1, 2, 3, 4]))
        controller.close()
    }
})

const identityTransform = new TransformStream({
    transform(chunk, controller) {
        controller.enqueue(chunk)
    }
})

const transformedStream = byteStream.pipeThrough(identityTransform)

const reader = transformedStream.getReader({ mode: "byob" })

Is there a better way to make my browser action show a popup when left clicked but do something else when shift-clicked?

I’m writing my first firefox browser extension. I want to have a typical pop-up appear when my browser action is clicked, but I also want users to be able to Shift+click or Ctrl+click on the browser action to quickly execute certain actions.

Because the browserAction.onClicked() event doesn’t fire if the browser action has a popup (default or otherwise, per the Mozilla documentation), the only way I’ve figured out how to achieve this functionality is the following code (in my background.js).

My method doesn’t seem like it should be the intended method of accomplishing this. Is there a better way to do this?

I have written the following code:

// Show the popup if the browser action is clicked on with no other key pressed
// Do something else if shift or control is held when the browser action is clicked
function browserActionClickHandler(tab, data){
    // If no other key was held, or more than one key was held, enable the popup, open it, then disable it so the onClicked event will fire on future clicks
    if(data.modifiers.length == 0 || data.modifiers.length > 1){
        browser.browserAction.setPopup({ popup: "popup.html"});
        browser.browserAction.openPopup();
        browser.browserAction.setPopup({ popup: null});
    }else if(data.modifiers.includes("Shift")){
        // Do something
    }else if(data.modifiers.includes("Ctrl")){
        // Do something else
    }
}

browser.browserAction.onClicked(browserActionClickHandler);

This code produces the result I want, but it doesn’t seem right to me that enabling, manually opening, and then disabling the popup is the only way to produce this functionality. Because I don’t know a lot about writing browser extensions, I wonder if there is a better way to do this.

Why does replaceChildren() work eratically?

For the Javascript program below:

fnInit() appendChild() is working fine and the 3 images are displayed. But for fnRoll() replaceChildren() most of the time less than 3 images are displayed. Whats wrong and how do I fix it?

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script>
const imgArray = [];

let image1 = new Image();
image1.src = "image1.png";
imgArray[0] = image1;

let image2 = new Image();
image2.src = "image2.png";
imgArray[1] = image2;

let image3 = new Image();
image3.src = "image3.png";
imgArray[2] = image3;

function fnInit() {
  document.getElementById("slot1").appendChild(imgArray[0]);
  document.getElementById("slot2").appendChild(imgArray[1]);
  document.getElementById("slot3").appendChild(imgArray[2]);
}

function fnRoll() {
  rnd1 = Math.trunc(Math.random() * 3);
  rnd2 = Math.trunc(Math.random() * 3);
  rnd3 = Math.trunc(Math.random() * 3);
  document.getElementById("slot1").replaceChildren(imgArray[rnd1]);
  document.getElementById("slot2").replaceChildren(imgArray[rnd2]);
  document.getElementById("slot3").replaceChildren(imgArray[rnd3]);
}
</script>
</head>
<body onLoad="fnInit();">
<span id="slot1"></span>
<span id="slot2"></span>
<span id="slot3"></span>
<br>
<button id="roll" type="button" onclick="fnRoll();">Roll</button>
</body>
</html>

How to make a sub component close when another element sub component is open react table 6

//Below is the code where React Table subcomponent prop is used along with React Modal

//if i open one subcomponent and roll to another, i am seeing row data of first sub component.

//Please help.

<button className=”label” onClick={()=>{this.submitCell(this.state)}}>submit to database
submit if any of the records are edited
<ReactTable className=”react-table”
data={ this.state.schemaData } columns={ this.state.columns } showPagination={true}
showPaginationBottom= {true}
defaultPageSize={5}
page={this.state.page}
pageSizeOptions={[3,5,7,10,15]}
onPageChange={page => this.setState({page:page})}
collapseOnDataChange={true}
SubComponent={(row) => {
return (

View Record Data
<Modal
style={{
overlay: {
backgroundColor: ‘transparent’, // Make the background transparent
}
}}
className=”modalTest”
isOpen={this.state.isPopUpOpen}
onRequestClose={this.closePopUp}
contentLabel=”Example Modal”
>
Close

{Object.keys(row.original).map((key)=>{return {key} {key!==”is_enabled”?(row.original[key]!==null?row.original[key]:”NULL”):(row.original[key]===false?0:1)}})}

  </Modal>         
           
      </div>

How to add tab icon in react application

i have a react application where i need to display png image as shown below
enter image description here

HTML is as below

8×8 Integration
my folder structure is as shown below
enter image description here

but none of the images are coming in the img tab in browser network

tried all the solution only if put https://[domain]/v2/images/logo-4b47ba934d5d44393f7141168a23f5d8.svg

what should i do get the image in browser tab please help

basic script not working but no errors, console.log not working [duplicate]

this is a very basic question. I’m trying to get firebase to work with my javascript and I’m using a very basic script from a tutorial. I’m a beginner and I don’t know if there are other ways to insert debugging statements, but my console.log() calls are not working. I also tried putting in a debugger call to no effect. I read a lot of the other questions about console.log not working but they were much more complicated than my situation (and, honestly, I couldn’t make heads or tails of them). Basically, I get nothing, as though it’s not reading that code at all. Can anyone help me debug this? Code looks as follows. Thank you for any help!

<script type="module" src="https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js">
      const firebase = require("firebase/app");

      // Your web app's Firebase configuration
      // For Firebase JS SDK v7.20.0 and later, measurementId is optional
      const firebaseConfig = {
            apiKey: "AIzaSyBADV4O4Oyc2xaHcLHwZficznzpoU13Gm0",
            authDomain: "my-first-project-1de2f.firebaseapp.com",
            projectId: "my-first-project-1de2f",
            storageBucket: "my-first-project-1de2f.firebasestorage.app",
            messagingSenderId: "741734388415",
            appId: "1:741734388415:web:6e4075ceaad15606816c0a",
            measurementId: "G-H0X2KKG0XP"
      };

      // Initialize Firebase
      const app = initializeApp(firebaseConfig);
      const analytics = getAnalytics(app);
      debugger:
      console.log('txt');
      
      // getting the text value from the database
      var bigOne = document.getElementById('bigOne');
      console.log(bigOne);
      var dbRef = firebase.database().ref().child('Text');
      console.log(dbRef);
      dbRef.on('value', snap => bigOne.innerText = snap.val());

      }
</script>

P.S. I know it was reading the code because when I’ve made errors in the code, I got errors in the console.

The limited role of “script src Property” in javascript

I need to ask a question about the script source: (<script type="text/javascript" src="A.js" defer></script>), for example, the A.js script contains many global functions that are created and not executed but called in a new script creating in many HTML pages (a.html, b.html, c.html). Is A.js important as a script source or is it the same with writing the same A.js in all these html pages. It’s just: I think JS starts working when the function is called and maybe does do nothing when it is created. So I think A.js should contain many global functions with its calling. Is it true, A.js with many functions without calling them in A.js is nothing ?

The A.js file :

`function a1() {......}
function a2() {......}
function a3() {........}
.............
function a1000() {........}`

The a.html, b.html, c.html files :

`<html>
<head></head>
<body>
  .........
<script type = "text/javascript" src = "A.js" defer></script>
<script>
 //calling the function that creating in A.js
</script>
</body>
</html>`

one of roles of script src is : If you want to run the same JavaScript on several pages in a web site, you should create an external JavaScript file, instead of writing the same script over and over again. Save the script file with A.js extension, and then refer to it using the src attribute in the tag. And that improve the performance of the page when move between pages.

But that role is it true when i creating a many functions without caliing in A.js ??. the functions is called in a.html, b.html, c.html. i bleive that nothing improving in the performance because the A.js is executing in a.html, b.html, c.html .

updating an out-of-scope variable in a recursive function

I am unable to wrap my head around how to write a recursive walkDir() function that updates an outside variable via a callback. Here is my attempt so far wherein I am trying to track the number of .js files and the size of the biggest .js file in stats{}

note: The actual code will have several different callbacks depending on some property of the file.

import fs from 'node:fs/promises';
import path from 'path';

async function walkDir(dir, cb) {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    
    for (let i = 0; i < entries.length; i++) {
        const entry = entries[i];
        const newdir = path.join(dir, entry.name);
        
        if (entry.isDirectory()) {
            await walkDir(newdir, cb);
        }
        else {
            cb(entry, file);
        }
    }
}

function cb(entry, file) {
    const stats = { numOfJs: 0, sizeOfBiggest: 0 };

    return async function(entry, file, stats) {
        if (entry) {
            if (path.extname(entry.name).substring(1) === 'js') {
                stats.numOfJs++;
                const entryStats = await fs.stat(file);
        
                if (entryStats.size > stats.sizeOfBiggest) {
                    stats.sizeOfBiggest = entryStats.size;
                }
            }
        }
        else {
            return stats
        }
    }
}

const { numOfJs, sizeOfBiggest } = walkDir('.', cb);
console.log(numOfJs, sizeOfBiggest);
// undefined undefined

How to automatically activate scroll inside a sticky div when it reaches the viewport top?

Description

I have a page with multiple sections (A,B,C,D,E,F). Section D:

  • Has a fixed height with overflow-y: auto (contains scrollable content)
  • Uses position sticky with top: 0 (sticks to the top of the viewport when reached)

Problem

When Section D becomes stuck at the viewport top:

  • Users must first move their cursor inside the section in order to activate the scroll inside of it
  • The scroll continues inside root window and not section D

Goal

Automatically “activate” the section’s scroll when it sticks to the top:

  • Wheel and scroll events should target D’s scrollbar immediately
  • No cursor movement inside D should be required
  • Should work when scrolling both up and down

Attempted Solutions (failed)

Forward the scroll from window to Section D

I tried intercepting the window’s scroll events and manually forwarding them to Section D when sticky:

window.addEventListener('wheel', (e) => {
  if (isSectionDStuck()) {  // Pseudocode for stickiness check
    e.preventDefault();
    sectionD.scrollBy({ top: e.deltaY, behavior: 'smooth' });
  }
});
  • Only works for mouse/trackpad wheel events.
  • Ignores keyboard scrolls (Spacebar/Arrow Keys), making accessibility worse.
  • Canceling the event (preventDefault()) in order to prevent scrolling at the window level can break expected page behavior.

Auto-Focusing the Section D

I attempted programmatically focusing Section D when it becomes stuck to the top:

window.addEventListener('scroll', (e) => {
  if (isSectionDStuck()) {  // Pseudocode for stickiness check
    sectionD.focus({ preventScroll: true }) // Try to avoid jumping
  }
});
  • focus() does not activate scrolling inside Section D.
  • Even with tabindex=”0″, Section D will never respond to scrolling unless the user moves the pointer inside the section

PD: I’m not using JQuery, only React. I’m not giving the code on isSectionDStuck() because that does not matter, what I need to know is how to activate the scroll on Section D programatically.

Thank you so much for the help! If you have in mind a completely different approach for what I’m trying to achieve feel free to post your solution.

I’m trying to read when one of my users bumps the server with discous or disboard

client.on("messageCreate", async (message) => {
    const guildId = message.guild.id;
    const userId = message.interactionMetadata.user.id;
    const messageType = message.interactionMetadata.type;
    const bumpPoint = 1;
    const member = await message.guild.members.fetch(userId);
    const nickname = member.nickname;
    //console.log(messageType);
    //console.log(nickname);
    const bumpExists = await userBumpDatabase.findOne({
                guildId: guildId,
                username: nickname
                });
    if (messageType === 2) { 
        if (!bumpExists) {
            const newUser = new userBumpDatabase({
                guildId: guildId,
                username: nickname,
                points: bumpPoint,
            });
            await newUser.save();
            return message.reply({ content: `Added 1 point to ${nickname}`});
        } else {
            bumpExists.points += bumpPoint;
            bumpExists.save()
            return message.reply({ content: `Added 1 point to ${nickname}. They now have ${bumpExists.points} `});
        }
    }
});

I’m trying to reward users for bumping my server with discodus and disboard.
If I comment out the messageType if loop and uncomment the 2 console.log commands the code executes perfectly and the data is logged correctly to the console.

Am I just overlooking something? using discord.js version 14.19.3

User redirect to frontend after failing to login with google authentication

Im building a backend with nestjs and mongodb with regular authentication and google authentication. The problem is when user signs up with regular authentication and after tries to login with google it passes. What is the best practice to prevent that?
I tried throw an error if user is in db and user authprovider is ‘local’ but it doesnt seem to work.

Im posting my code in here. Hope you can help me.

auth.service.ts

BadRequestException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { compare, hash } from 'bcryptjs';
import { Response } from 'express';
import { User } from 'src/users/schema/user.schema';
import { TokenPayload } from './token-payload.interface';
import { SignupDto } from './dtos/signup.dto';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { UsersService } from 'src/users/users.service';
import { ResetToken } from './schemas/reset-token.schema';
const { nanoid } = require('nanoid');
import { MailService } from './services/mail.service';

@Injectable()
export class AuthService {
constructor(
 @InjectModel(User.name) private readonly userModel: Model<User>,
 @InjectModel(ResetToken.name) private resetTokenModel: Model<ResetToken>,
 private readonly usersService: UsersService,
 private readonly configService: ConfigService,
 private readonly jwtService: JwtService,
 private mailService: MailService,
) {}


async login(user: User, response: Response, redirect = false) {
 //calculate expiration date for access token
 const expiresAcessToken = new Date();
 expiresAcessToken.setMilliseconds(
   expiresAcessToken.getTime() +
     parseInt(
       this.configService.getOrThrow('JWT_ACCESS_TOKEN_EXPIRATION_MS'),
     ),
 );

 //calculate expiration date for refresh token
 const expiresRefreshToken = new Date();
 expiresRefreshToken.setMilliseconds(
   expiresRefreshToken.getTime() +
     parseInt(
       this.configService.getOrThrow('JWT_REFRESH_TOKEN_EXPIRATION_MS'),
     ),
 );

 const tokenPayload: TokenPayload = {
   userId: user._id.toHexString(),
 };

 //create access token
 const accessToken = this.jwtService.sign(tokenPayload, {
   secret: this.configService.getOrThrow('JWT_ACCESS_TOKEN_SECRET'),
   expiresIn: `${this.configService.getOrThrow('JWT_ACCESS_TOKEN_EXPIRATION_MS')}ms`,
 });

 //create refresh token
 const refreshToken = this.jwtService.sign(tokenPayload, {
   secret: this.configService.getOrThrow('JWT_REFRESH_TOKEN_SECRET'),
   expiresIn: `${this.configService.getOrThrow('JWT_REFRESH_TOKEN_EXPIRATION_MS')}ms`,
 });

 await this.usersService.updateUser(
   {
     _id: user._id,
   },
   {
     $set: { refreshToken: await hash(refreshToken, 10) },
   },
 );

 response.cookie('access_token', accessToken, {
   httpOnly: true,
   secure: this.configService.getOrThrow('NODE_ENV') === 'production',
   expires: expiresAcessToken,
 });

 response.cookie('refresh_token', refreshToken, {
   httpOnly: true,
   secure: this.configService.getOrThrow('NODE_ENV') === 'production',
   expires: expiresRefreshToken,
 });

 if (redirect)
   response.redirect(this.configService.getOrThrow('FRONTEND_URL'));
 else
   response.status(200).json({
     _id: user._id,
     name: user.name,
     email: user.email,
   });
}

async verifyUser(email: string, password: string) {
   const user = await this.usersService.getUser({ email });

   if (!user) {
     throw new UnauthorizedException('Invalid credentials');
   }

   if (user.authProvider !== 'local') {
     console.log('User is not local');
     throw new UnauthorizedException('No account associated with this email for password login. Please use Google login.');
   }

   const authenticated = await compare(password, user.password);
   if (!authenticated) {
     throw new UnauthorizedException('Invalid credentials');
   }
   return user;
}
}

auth.controller.ts

import {
  Body,
  Controller,
  Get,
  Post,
  Put,
  Res,
  UseGuards,
} from '@nestjs/common';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { CurrentUser } from './current-user.decorator';
import { User } from 'src/users/schema/user.schema';
import { Response } from 'express';
import { AuthService } from './auth.service';
import { JwtRefreshAuthGuard } from './guards/jwt-refresh-auth.guard';
import { GoogleAuthGuard } from './guards/google-auth.guard';
import { SignupDto } from './dtos/signup.dto';
import { ChangePasswordDto } from './dtos/change-password.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { ForgotPasswordDto } from './dtos/forgot-password.dto,';
import { ResetPasswordDto } from './dtos/reset-password.dto';
import { ConfigService } from '@nestjs/config';

@Controller('auth')
export class AuthController {
  constructor(
    private readonly authService: AuthService,
    private readonly configService: ConfigService,
  ) {}

  @Post('signup')
  async signup(@Body() signupData: SignupDto) {
    return this.authService.signup(signupData);
  }

  @Post('signin')
  @UseGuards(LocalAuthGuard)
  async login(
    @CurrentUser() user: User,
    @Res({ passthrough: true }) response: Response,
  ) {
    await this.authService.login(user, response);
  }


  @Get('google')
  @UseGuards(GoogleAuthGuard)
  loginGoogle() {}

  @Get('google/callback')
  @UseGuards(GoogleAuthGuard)
  async googleCallback(
    @CurrentUser() user: User,
    @Res({ passthrough: true }) response: Response,
  ) {
    await this.authService.login(user, response, true);
  }

}

user.service.ts

import { InjectModel } from '@nestjs/mongoose';
import { User } from './schema/user.schema';
import { FilterQuery, Model, UpdateQuery } from 'mongoose';
import { hash } from 'bcryptjs';
import { SignupDto } from 'src/auth/dtos/signup.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel(User.name)
    private readonly userModel: Model<User>,
  ) {}

  async createUser(data: SignupDto) {
    if(data.authProvider === 'local'){
        data.password = await hash(data.password, 10);
    }
    const createdUser = new this.userModel(data);
    await createdUser.save();
    return createdUser;
  }

  async getUser(query: FilterQuery<User>) {
    const user = await this.userModel.findOne(query);
    if (!user) {
      throw new NotFoundException('User not found');
    }
    return user;
  }

  async updateUser(query: FilterQuery<User>, data: UpdateQuery<User>) {
    return this.userModel.findOneAndUpdate(query, data);
  }

  async getOrCreateUser(data: SignupDto) {
    const user = await this.userModel.findOne({ email: data.email });
    if (user && data.password === '') {
      return user;
    }
    return this.createUser(data);
  }
}


google.strategy.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-google-oauth20';
import { UsersService } from '../../users/users.service';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
  constructor(
    configService: ConfigService,
    private readonly usersService: UsersService,
  ) {
    super({
      clientID: configService.getOrThrow('GOOGLE_AUTH_CLIENT_ID'),
      clientSecret: configService.getOrThrow('GOOGLE_AUTH_CLIENT_SECRET'),
      callbackURL: configService.getOrThrow('GOOGLE_AUTH_REDIRECT_URI'),
      scope: ['profile', 'email'],
    });
  }

  async validate(_accessToken: string, _refreshToken: string, profile: any) {
    if (!profile.emails || !profile.emails[0]?.value) {
      throw new UnauthorizedException('No email found in Google profile');
    }

    const email = profile.emails[0]?.value;

    const existingUser = await this.usersService.getUser({ email });
    if (existingUser && existingUser.authProvider === 'local') {
      throw new UnauthorizedException(
         'This email is already registered using password login.'
      );
    }

    const user = await this.usersService.getOrCreateUser({
      name: profile.displayName,
      email: profile.emails[0]?.value,
      password: '',
      authProvider: 'google',
    });

    return user;
  }
}

google-auth.guard.ts

// google-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class GoogleAuthGuard extends AuthGuard('google') {
  handleRequest(err, user, info, context, status) {
    const res = context.switchToHttp().getResponse();
    
    if (err || !user) {
      // Redirect to frontend with error message
      return res.redirect(
        `${process.env.FRONTEND_URL}/auth/signin?message=Authentication failed`
      );
    }

    return user;
  }
}

Adding a custom operator in react-querybuilder and json-logic

I’m trying to add a custom operator to my react-querybuilder project, but it’s just not working.

I’ve added the following custom operators to my builder:

const customOperators = [
  {
    name: 'lengthequals',
    label: 'Length =',
  },
  {
    name: 'lengthgreaterthan',
    label: 'Length >',
  },
  {
    name: 'lengthlessthan',
    label: 'Length <',
  },
];

const operators = [...defaultOperators, ...customOperators];
<QueryBuilder
    fields={fields}
    operators={operators}
    query={query}
/>

This correctly adds the opperator to the list of available opperators in the builder.
This will add a rule to my builder query that looks like this:

{
    field: "FirstName",
    operator: "lengthgreaterthan",
    value: "4",
    valueSource: "value"
}

I also add an opperation to json-logic like this:

add_operation('lengthgreaterthan', (val, compareTo) => {
  return (val?.length ?? 0) > compareTo;
});

My understanding is that this would let jsonlogic understand what the operator is trying to do. But when I try to format the query with

formatQuery(query, 'jsonlogic')

It always returns false. Well, if I add a second rule in my query builder that isn’t custom, that rule will appear in the formatted query, so it’s more like the custom rules are being ignored.

What do I need to do to make formatQuery work with my operators? There’s gotta be a step where I tell the query formatter what to do with these operators, but there’s no documentation telling me what.