Deploying React with Ruby on Rails 7 on Render

I have an old Ruby on Rails project from 6 years ago that was deployed on Heroku. I haven’t touched it in so long that Heroku deleted my account and now I’m looking to deploy it again on Render. I had to update to Ruby 3.2.2, Rails 7 and React 18. I deployed my app to Render and it successfully builds and deploys but when I go to the webpage I don’t see any errors in the console. I just see text “React is not working” which comes from my views/static_pages/root.html.erb file:

  <script type="text/javascript">
    window.currentUser = <%= render(
      partial: "api/users/user", 
      locals: { user: current_user },
      formats: [:json],
      handlers: [:jbuilder]
    ).html_safe %></script>
<% end %>
<div id="root">React is not working</div>

I open the sources tab in chrome devtools and see this layout:

.
|-index.js
|-assets
  |-application.js
  |-application.css
  |-page_icon.png

I open the application.css and all of my css is there. and it gets the page_icon.png perfectly fine. But when I open the application.js I don’t see any of the code from the bundle.js file in there. All I see is actioncable code from a cable.js from what I’m assuming is coming from app/assets/javascripts/cable.js, I’m assuming it’s this because I see this in the file:

console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js u2013 please update your reference before Rails 8"),

in my app when i run npm install it installs the packages and puts the bundle.js in the app/assets/javascripts/ directory and in my app/assets/javascripts/application.js I have this:

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require rails-ujs
//= require activestorage
//= require jquery
//= require jquery_ujs
//= require_tree .

and in views/layouts/application.html.erb file I have this:

    <%= stylesheet_link_tag    'application', media: 'all' %>
    <%= javascript_include_tag 'application' %>

I read somewhere that Rails 7 is moved away from sprockets so I tried installing this gem:

gem 'sprockets-rails'

but this didn’t seem to do anything.

This webapp works fine locally but for some reason it just doesn’t work when I deploy it on Render. I can share other code snippets or the Github repo if it helps. Any inputs would be greatly appreciated.

Slideshow with JS modal that opens with both click on image or button

I have an image Slideshow Gallery with a modal built into it using JavaScript.

When the user clicks on the image displayed in the slide the modal opens on that particular image. I also would like to add a button below the slideshow that opens the modal for the displayed image but I don’t know how to link it so that it opens on the current slide image.

I tried multiple scripts trying to incorporate the click on button variable into the i loop, and end up just interfering with the scripts ability to open modal thru the image.

Here is the complete slideshow: https://codepen.io/canQuackattack/pen/ogNbOme

let slideIndex = 1;
showSlides(slideIndex);

function plusSlides(n) {
  showSlides(slideIndex += n);
}

function currentSlide(n) {
  showSlides(slideIndex = n);
}

function showSlides(n) {
  let i;
  let slides = document.getElementsByClassName("mySlides");
  let dots = document.getElementsByClassName("demo");
  let captionText = document.getElementById("slide_caption");
  if (n > slides.length) {
    slideIndex = 1
  }
  if (n < 1) {
    slideIndex = slides.length
  }
  for (i = 0; i < slides.length; i++) {
    slides[i].style.display = "none";
  }
  for (i = 0; i < dots.length; i++) {
    dots[i].className = dots[i].className.replace(" active", "");
  }
  slides[slideIndex - 1].style.display = "block";
  dots[slideIndex - 1].className += " active";
  captionText.innerHTML = dots[slideIndex - 1].alt;
}

// Get the modal
var modal = document.getElementById("myModal");

var btn = document.getElementById("myBtn"); // Get the button that opens the modal

// Create reference to the modal for all images using a class!
var images = document.getElementsByClassName('myImages');

// Get each image and insert it inside the modal - use its "alt" text as a caption
var modalImg = document.getElementById("img01");
var captionText = document.getElementById("caption");

// Go through all of the images with our custom class
for (var i = 0; i < images.length; i++) {
  var img = images[i];
  // and attach our click listener for this image.
  img.onclick = function(evt) { // click on <img> to run function 
    modal.style.display = "block"; // function will display modal using class 
    modalImg.src = this.src; // var modalImg gets <img> src  
    captionText.innerHTML = this.alt; // var captionText gets <img> alt  
  }
}

// When the user clicks the button, open the modal 
btn.onclick = function(evt) {
  modal.style.display = "block";
}

// Get the <span> element that closes the modal
var span = document.getElementsByClassName("close")[0];

// When the user clicks on <span> (x), close the modal
span.onclick = function() {
  modal.style.display = "none";
}

// When the user clicks anywhere outside of the modal, close it
window.onclick = function(event) {
  if (event.target == modal) {
    modal.style.display = "none";
  }
}
/* Position the image container (needed to position the left and right arrows) */
.container {
  position: relative;
}

/* Hide the images by default */
.mySlides {
  display: none;
}

/* Add a pointer when hovering over the thumbnail images */
.cursor {
  cursor: pointer;
}

/* Next & previous buttons */
.prev,
.next {
  cursor: pointer;
  position: absolute;
  top: 40%;
  width: auto;
  padding: 16px;
  margin-top: -50px;
  color: white;
  font-weight: bold;
  font-size: 20px;
  border-radius: 0 3px 3px 0;
  user-select: none;
  -webkit-user-select: none;
}

/* Position the "next button" to the right */
.next {
  right: 0;
  border-radius: 3px 0 0 3px;
}

/* On hover, add a black background color with a little bit see-through */
.prev:hover,
.next:hover {
  background-color: rgba(0, 0, 0, 0.8);
}

/* Number text (1/3 etc) */
.numbertext {
  color: #f2f2f2;
  font-size: 12px;
  padding: 8px 12px;
  position: absolute;
  top: 0;
}

/* Container for image text */
.caption-container {
  text-align: center;
  background-color: #222;
  padding: 2px 16px;
  color: white;
}

.row:after {
  content: "";
  display: table;
  clear: both;
}

/* columns side by side */
.column {
  float: left;
  width: 16.66%;
}

/* Add a transparency effect for thumnbail images */
.demo {
  opacity: 0.6;
}

.active,
.demo:hover {
  opacity: 1;
}

/********** MODAL **********/

/* The Modal (background) */
.modal {
  display: none;
  /* Hidden by default */
  position: fixed;
  /* Stay in place */
  z-index: 1;
  /* Sit on top */
  padding-top: 100px;
  /* Location of the box */
  left: 0;
  top: 0;
  width: 100%;
  /* Full width */
  height: 100%;
  /* Full height */
  overflow: auto;
  /* Enable scroll if needed */
  background-color: rgba(219, 219, 219, 0.9);
}

/* Modal Content (image) */
.modal-content {
  margin: auto;
  display: block;
  width: 40%;
  max-width: 40%;
}

/* Caption of Modal Image */
#caption {
  margin: auto;
  display: block;
  width: 80%;
  max-width: 700px;
  text-align: center;
  color: #000;
  ;
  padding: 10px 0;
  height: 150px;
}

/* Add Animation */
.modal-content,
#caption {
  -webkit-animation-name: zoom;
  -webkit-animation-duration: 0.6s;
  animation-name: zoom;
  animation-duration: 0.6s;
}

@-webkit-keyframes zoom {
  from {
    -webkit-transform: scale(0)
  }

  to {
    -webkit-transform: scale(1)
  }
}

@keyframes zoom {
  from {
    transform: scale(0)
  }

  to {
    transform: scale(1)
  }
}

/* The Close Button */
.close {
  position: fixed;
  /* prev absolute */
  top: 25px;
  /* prev 15 */
  right: 75px;
  /* prev 35 */
  color: #646363;
  font-size: 40px;
  font-weight: bold;
  transition: 0.3s;
}

.close:hover,
.close:focus {
  color: #bbb;
  text-decoration: none;
  cursor: pointer;
}

/* 100% Image Width on Smaller Screens */
@media only screen and (max-width: 700px) {
  .modal-content {
    width: 100%;
  }
}
<body>

  <h2 style="text-align:center">Slideshow Gallery</h2>

  <div class="container">
    <div class="mySlides">
      <div class="numbertext">1 / 3</div>
      <img class="myImages" src="https://www.w3schools.com/howto/img_woods_wide.jpg" style="width:100%">
    </div>

    <div class="mySlides">
      <div class="numbertext">2 / 3</div>
      <img class="myImages" src="https://www.w3schools.com/howto/img_5terre_wide.jpg" style="width:100%">
    </div>

    <div class="mySlides">
      <div class="numbertext">3 / 3</div>
      <img class="myImages" src="https://www.w3schools.com/howto/img_mountains_wide.jpg" style="width:100%">
    </div>


    <a class="prev" onclick="plusSlides(-1)">❮</a>
    <a class="next" onclick="plusSlides(1)">❯</a>

    <div class="caption-container">
      <p id="slide_caption"></p>
    </div>

    <div class="row">
      <div class="column">
        <img class="demo cursor" src="https://www.w3schools.com/howto/img_woods_wide.jpg" style="width:100%" onclick="currentSlide(1)" alt="The Woods">
      </div>
      <div class="column">
        <img class="demo cursor" src="https://www.w3schools.com/howto/img_5terre_wide.jpg" style="width:100%" onclick="currentSlide(2)" alt="Cinque Terre">
      </div>
      <div class="column">
        <img class="demo cursor" src="https://www.w3schools.com/howto/img_mountains_wide.jpg" style="width:100%" onclick="currentSlide(3)" alt="Mountains and fjords">
      </div>

    </div>
  </div>
  <div>
    <button id="myBtn">Open Modal</button>
  </div>

  <!-- The Modal -->
  <div id="myModal" class="modal">
    <span class="close">&times;</span>
    <img id="img01" class="modal-content">
    <div id="caption"></div>
  </div>

immer and JSON.parse even with same output behaves differently on Vue

const jsonPOJO = JSON.parse(JSON.stringify(model.value));
const producePOJO = produce(model.value, (draft) => ...);
console.log(jsonPOJO, producePOJO);

The output here would be just plain object.

image

when assigning these via:

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const model = computed({
  get: () => readonly(props.modelValue),
  set: (value) => emit('update:modelValue', value)
})

// has proxy
model.value = jsonPOJO

// has no proxy
model.value = producePOJO

image

Open the devtools console when visiting the following reproduction.

Reproduction:

https://play.vuejs.org/#eNqVVk1v4zYQ/SuELpZRR7I32T24zqIfyKGLNllsFttD1IMqjRwmEimIlJMg8H/vG1JSpMD5KAzYJOfNzJvhzNCPwa91He1aCtbBxmSNrK0wZNv6c6JkVevGikfRULEQd6nNrsVeFI2uxAwaswHxu67q7jyKecMGIeZPppWxQpGxlH+jQpyytfAqUUI88pfoZOt+K0RrqBlthZCQrvrt3i/cD77+mbOXOBZ/6q2wGuRJyAJG78QuLVtsjKgbff8gdCOUttEAphTxlLSjcqSnVQmkwkl95GWDmktAOESyEOFcnH72PDlKXVJU6m04c0BqZmA2lQy6kaN2tWTyb0Eif/R+ZMT5+5/wSLKD/cJHI6uKcplaWgvbtLTgs5yo9lvggN3EvlpQJ9hYquoSCtgJsXH1sDuqdE7laRIMruE0CUQM0CYeaQSLwBpwLeQ2ujFaoRQdjSTIYEiW1FzUViKWJBjKIgnSstR3X9zZQNLpXFN2e+D8xtzzWRJ8bQgR7ygJBplNmy1ZLz67PKd7rAchwmhLoF8RfiPkuWWOHvZbq3LQHuEc2z9cu0i1/W7O7i0p0wfl04p6dvgkQPtwDl8K/YnucXTS9cEeWfTteFSl9bM8esHUCF8yGDLda2trs47jLFdQxKXJXYPqsLGqq9jhflkto1W0in8iUyXByGXf7a+PjzR3fTUeHj+PAFr9pVuFGlmgZKu6xepFLHo5bzMa5I4eI/pRA0BtMGZyKqSir7wLr2auFn9w5c+46TyUKmkH5Bk2jGzrnCv/mUKv4o6h0/MMXT5xG+tuHPTBho5H9GRm7u4WyQHStaCDM4XwgM+Fn17clM57oopWZVwKwoN/yPTL5cV5b2qYQjxr7y7+vaGMQ2NIVKeNodAtjW1QfrJ4CJ0vPwvmblgMas/mAqw4FHN4NugmTEB5MOEoC58s7wNWBiliOhwQLovv9kBM3a0zmW45DgCjOG/Swj7NY8wrPngxFNRvx/FwQB0TxNR7PhhSL+wiStRQyeHoccB787dubk13/99lRbq1HWJ6m6v5QqyWy2XnDZoXiuC0IWGhxg9Vld6SMC1OpJ0ZYawsS//CvcP+B9j/MLF/rl128T6m6oEdvW6lv6JjGDqeGLp0RPBUvodMb+YEZk4mZiBhLttUupM34vkI/Y8TfR5IKZIDDTSo4KW9RsLS6nBkXblMzX5yHTE67fl6Ad7JT97pWy9hLndugT87j93s2Lt/LpvYi6YP4f4/FXI6ZQ==

How to use a dynamic key inside a Jinja dictionary lookup?

So, here’s a problem I’ve been facing.

Consider this scenario:

data = {
         "images": {
                     108: {"file1": "img11.jpg", "file2": "img12.jpg"},
                     109: {"file1": "img21.jpg", "file2": "img12.jpg"}
                   }
       }

Now, {{this.id}} outputs the value 108 and {{data.images.108.file1}} correctly outputs the value img11.jpg.

However, I want to dynamically pass this.id instead of hardcoding 108. How can I do this in Jinja?

Any advice would be extremely appreciated.

I tried {{data.images.this.id.file1}}, {{data.images.[this.id].file1}} and {{data.images.get(this.id).file1}} but none worked.

Can’t get the value of a div

I’m trying to read the value of a div in javascript (see variable testo) but I can’t do it and it seems impossible to me since I can read all the other properties like the id and the style.

Here is the Html+Flask code:

<div id="TestoWrapper" style="font-size: 2.25rem; margin-left: 120px; width:inherit; text-align: center;">
<div value="{{item.Collana}}" style="font-size: 2.25rem; margin-left: 0px; width:900;    text-align: center;"id="Testo" >{{item.Collana}}</div>
<div class="u-file-icon u-icon u-block-8724-13 bloccoiconapencil" id="bloccoiconapencil"><img src="{{ url_for('static', filename = 'images/274713.png') }}" alt="" id="img-icon-edit"> </div>
<div class="u-file-icon u-icon u-block-8724-14" id="bloccoiconax" ><img src="{{ url_for('static', filename = 'images/x3.png') }}" alt="" id="img-icon-delete"></div>
<input style="display:none;",type="text"  id="Edit" value="False" name="Edit" class="u-border-2 u-border-white u-input u-input-rectangle u-radius-17 u-text-body-alt-color u-input-5">
<input style="display:none;",type="text"  id="Delete" value="False" name="Delete" class="u-border-2 u-border-white u-input u-input-rectangle u-radius-17 u-text-body-alt-color u-input-5">
<input style="display:none;",type="text"  id="ID" value="{{item.ID}}" name="" class="u-border-2 u-border-white u-input u-input-rectangle u-radius-17 u-text-body-alt-color u-input-5">
</div>

Here the JavaScript function:

document.querySelectorAll('.bloccoiconapencil').forEach(occurence => {
        occurence.addEventListener('click', (e) =>
        {
          console.log("icon-edit-clicked");
          parent=e.target.parentNode.parentNode;
          let edit = parent.querySelector("#Edit").value;
          let testo=parent.querySelector("#Testo");
          if (edit=="False"){            
            console.log(testo.value);
            parent.querySelector("#Testo").innerHTML="<input type='text' id='name-677d' value='"+ testo +"'name='name' style='width:1150px;' class='u-border-2 u-border-white u-input u-input-rectangle u-none u-radius-17 u-block-8724-10' autofocus='autofocus'>";
            parent.querySelector("#img-icon-edit").src="{{url_for('static',filename='images/753318.png') }}";
            console.log(testo);
          }
          else{
            document.getElementById("form").submit();
          }
        });
      });

when I log the testo.value it gives “undefined” whereas if I just log testo I get:

<div value="Doctor Strange" style="font-size: 2.25rem; margin-left: 0px; width:900; text-align: center;" id="Testo">
<input type="text" id="name-677d" value="[object HTMLDivElement]" name="name" style="width:1150px;" class="u-border-2 u-border-white u-input u-input-rectangle u-none u-radius-17 u-block-8724-10" autofocus="autofocus">
</div>

which is correct.

Also, if I try to log testo.style or testo.id, it does log the right value, just testo.value is undefined. I’m literally getting crazy, I used the same function with #Edit on the line above and it works just fine. The only difference is that testo.value is assigned via Flask, but I used the same thing on other pages and it works

Prevent onclick and event listeners from firing while in element selection mode

<!DOCTYPE html>
<html>
<head>
    <title>Element Selector Issue</title>
</head>
<body>
    <button onclick="console.log('Button 1 clicked')">Button 1</button>
    <button id="button2">Button 2</button>
    <button id="toggleSelector">Toggle Selector Mode</button>

    <script>
        // Add event listener to second button
        document.getElementById('button2').addEventListener('click', () => {
            console.log('Button 2 clicked');
        });

        class ElementSelector {
            constructor() {
                this.selectorModeEnabled = false;
                this.selectedElement = null;
                this.selectElement = this.selectElement.bind(this);
                this.onSelectElement = (element) => {
                    console.log('Selected element:', element);
                };
            }

            enableSelectorMode() {
                this.selectorModeEnabled = true;
                document.body.style.cursor = "crosshair";
                document.body.addEventListener("click", this.selectElement);
            }

            disableSelectorMode() {
                this.selectorModeEnabled = false;
                document.body.style.cursor = "default";
                document.body.removeEventListener("click", this.selectElement);
            }

            toggleSelectorMode() {
                if (this.selectorModeEnabled) {
                    this.disableSelectorMode();
                } else {
                    this.enableSelectorMode();
                }
            }

            selectElement(event) {
                event.preventDefault();
                event.stopPropagation();
                const element = event.target;
                this.selectedElement = element;
                this.onSelectElement(element);
            }
        }

        // Initialize and set up toggle button
        const selector = new ElementSelector();
        document.getElementById('toggleSelector').addEventListener('click', () => {
            selector.toggleSelectorMode();
        });
    </script>
</body>
</html>

When I click a button like this while in selection mode:

<button onclick="console.log('Button 1 clicked')">Click</button>

The console.log still fires even though I’m using both event.preventDefault() and event.stopPropagation(). I even tried event.stopImmediatePropagation() but the original click handlers still execute.
How can I completely prevent all click events from firing on elements while in selection mode?
Expected behavior: When in selection mode, clicking an element should only trigger my selection logic, not any existing click handlers.
Actual behavior: Original click handlers (both onclick attributes and addEventListener) still fire even with event.preventDefault() and stopPropagation().
Any help would be appreciated!

Pinia loading fails from CDN because devtoolsApi is not defined

When trying to load Pinia via CDN (https://unpkg.com/[email protected]/dist/pinia.iife.js) it fails when it tries to run the loading function, “var Pinia = (function (exports, vue, devtoolsApi) {… })({}, Vue, devtoolsApi);” in Chrome. Chrome devtools throws a “devtoolsApi is not defined” error. And since Pinia is not defined, my Vue code that tries to call “Pinia.createPinia()” fails. This was all working at some point and then all of a sudden it is not.

Gemini says: “Chrome (and other browsers) intentionally do not provide the devtoolsApi to JavaScript functions running within a regular web page. This is a crucial security measure.” which makes me wonder how it ever worked. I don’t really want to go backwards to find out why it was working before if I can just see how to proceed.

How can I conditionaly remove the default behavior of an anchor (a) tag added by insertAdjacentHTML

I am trying to add an anchor tag that should redirect the user to another page only if a condition is met (here if array.length<3). I have followed Iananswer of this SO question, but in my case I am inserting the a tag using a function and insertAdjacentHTML as described below. The other thing is that I need to pass a parameter (the array itself) to check its length. I could not get it to work
the first snippet is form Ian answer which works perfectly. The second is another way of doing it using a div and the third one is the one I am breaking my head overbut can’t find the proper way of writing it.

<a onclick='return handleClick()' href="https://www.example.com">click</a>

<div class="check-div" id="check-div" style="cursor:pointer;">click</div>

<div class="elem1"> I am elem1</div>
//snippet 1

let checkDiv = document.querySelector('.check-div')

let array = ['1', '2', '3']

const handleClick = () =>{
    // return false
    if(array.length <= 3){
        location.href = 'https://www.example.com'
    }else{
        alert('Maximum 3 items to be compared')
        return false
    }
}

//snippet 2

checkDiv.addEventListener('click', ()=>{
    if(array.length <= 3){
        location.href = 'https://www.example.com'
    }else{
        alert('Maximum 3 items to be compared')
    }

})

//snippet 3

function addElements(elem){
    let elem1 = document.querySelector('.elem1')
    elem1.insertAdjacentHTML('beforeend',`<a class='atag' onclick="ps_block_user(elem)" href="./compare-products/" target="_blank">cliquez pour comparer</a>`)
}

function ps_block_user(elem){
    if(elem.length>3){
        alert('Un maximum de 3 produits à comparer est possible!')
        return false
    }
}

addElements(array)

I also tried using an click event listener as below but here still we can’t prevent redirection as oppposed to the first snippet..

let atag = document.querySelector('.atag')
atag.addEventListener('click', ()=>{
    if(array.length>2){
        // return false
        alert('Un maximum de 2 produits à comparer est possible!')
        return false
    }
})

fs.writeFile adds extra brackets to json of key=>values object in node.js

I’m trying to update one JSON file in a for loop called asynchronously. Each time I update the whole file, with 1 object.

This is my very complex code after half a day of research. (I know it’s too complex, but I wanted to show what I’ve tried so far)

    async function saveJsonFile(data, fileName = 'myFile') {
      try {
        const jsonData = JSON.stringify(data);

        // clear cache
        delete require.cache[require.resolve(`./${fileName}.json`)];

        // ensure the file exists
        await fs.readFile(`${fileName}.json`, 'utf8', (err) => {
          if (err) {} 
          else {
            // clear the content of the file
            fs.writeFile(`${fileName}.json`, "", "utf8", (err) => {
              if (err) {} 
              else {

                // save the json << THIS IS WHERE I STARTED WITH
                fs.writeFile(`${fileName}.json`, jsonData, 'utf8', (err) => {
                  if (err) {
                  }
                });                
                
                // try saving again
                fs.writeFile(`${fileName}.json`, jsonData, 'utf8', (err) => {
                  if (err) {
                  }
                });
              }
            });
          }
        });
      } 
    }

Called from another async func:

async function runTrading()
{
  try {
    for (let i = 1; i <= data.length; i++) {
    ...
    lastBuyPrice[symbol] = currentPrice;
    await saveJsonFile(lastBuyPrice);         

This is what I get (different loops, tries, versions, etc.)
Notice the 2 brackets at the end of each line, or in the middle. In the log it looks OK before saving!

{"prod1":32154.22}}
or
{"prod1":32154.22,"prod2":0,"prod3":0}32}}

How to hide text field floating suggestions when field is left, but not if a suggestion is clicked?

I made a vue component (my first ever!) that aims to show suggestions for options as you type:

const AutoCompleteComponent = {
    data()  {
        return {
            open: false,
            current: 0,
            /** @type {string[]} **/
            suggestions: ["first", "ble", "hello"],
            fieldWidth: 0,
        }
    },
    mounted() {
        this.fieldWidth = this.$refs.inputField.clientWidth;
    },
    methods: {

        focus() {
            console.log("Focus activated");
            this.open = true;
        },

        blur() {
            console.log("Focus deactivated")
            // when I do this, the suggestions dissappear the frame before they are
            // clicked, causing the suggestionClick to not be called
            this.open = false;
        },
    
        //For highlighting element
        isActive(index) {
            return index === this.current;
        },
    
        //When the user changes input
        change() {
            this.loadSuggestions();
            //console.log("change()");
            if (this.open == false) {
                this.open = true;
                this.current = 0;
            }
        },
    
        //When one of the suggestion is clicked
        suggestionClick(index) {
            this.currentText = this.matches[index];
            console.log("Clicked suggestion: ", index, this.matches[index]);
            this.open = false;
        },
    },
    computed: {
    
        /**
         * Filtering the suggestion based on the input
         * @this {ReturnType<AutoCompleteComponent["data"]>}
         */
        matches() {
            console.log("computed.matches() str=", this.currentText, " suggestions=", this.suggestions);
            return this.suggestions.filter((str) => {
                const withoutAccents = str.toLowerCase();
                return withoutAccents.indexOf(this.currentText.toLowerCase()) >= 0;
            });
        },
    
        //The flag
        openSuggestion() {
            return this.currentText !== "" &&
                   this.matches.length != 0 &&
                   this.open === true;
        },

        copiedWidth() {
            return this.fieldWidth + "px";
        }
    },
    template: "#vue-auto-complete-template"
};

This is it’s HTML template:

    <template id="vue-auto-complete-template">
      <div v-bind:class="{'open':openSuggestion}" class="auto-complete-field">
        <div class="field-wrapper">
          <input class="form-control" type="text" v-model="currentText" @keydown.enter='enter' @keydown.down='down'
          @keydown.up='up' @input='change' @focus="focus" @blur="blur" ref="inputField" />
        </div>

        <div class="suggestions-wrapper">
          <ul class="field-suggestions" :style="{ width: copiedWidth }" >
            <li v-for="(suggestion, suggestion_index) in matches" v-bind:class="{'active': isActive(suggestion_index)}"
              @click="suggestionClick(suggestion_index)">
              {{ suggestion }}
            </li>
          </ul>
        </div>
      </div>
    </template>

So then I create it like this:

<AutoCompleteComponent class="some class names"></AutoCompleteComponent>

To make it appear under the field, the following CSS is applied:

.auto-complete-field {
    display:inline-block;
}
.auto-complete-field .suggestions-wrapper {
    display:block;
    position: relative;
}

.auto-complete-field.open ul {
    display:initial;
}

.auto-complete-field ul {
    list-style:none;
    padding:0;
    margin:0;

    display: none;

    position: absolute;
    top:0px;
    left: 0px;

    border-bottom: 1px solid black;
}
.auto-complete-field ul li {
    background-color: white;
    border: 1px solid black;
    border-bottom: none;
}

Now the problem is if you look onto the blur() function, it sets open to false which in turn hides the suggestions ul.field-suggestions using the active class name.

Because of the order in which events are handled, blur event on the field hides the .auto-complete-field.open ul before the click event is created, causing it to instead be invoked on whatever was under it.

Quick and dirty remedy to this would be setTimeout(()=>{this.open=false}, 100). I think for this to work, the timeout must actually be two render frames at least. It didn’t work as a microtask nor RequestAnimationFrame. I don’t want to use timeout, especially a big one, because it can cause GUI flickering with fast clicks.

I am looking for a more solid solution to this. I’d hope Vue has something for this. Plain JS solution to this is usually reimplementing blur event by listening on multiple events on Window, and checking where they occured. I’d rather avoid that.

Can I use the ‘this’ argument within the typescript Array.map() function to increment a counter?

Here’s the code I’m trying to get to work:

const playlist = {
  playlist_idea_uri: uri.toString(),
  type: 'master'
}
const insertedPlaylistIdResponse = await db
  .insertInto('playlist')
  .values(playlist)
  .onConflict((oc) => oc.doNothing())
  .returning('id')
  .executeTakeFirst()
let songs = masterPlaylist.songs.map((song) => {
  return {
    track_name: song.trackName,
    track_mb_id: song.trackMbId,
    album_artwork_ref: song.albumArtwork?.ref.toString(),
    created_at: song.createdAt
  }
})
const insertedSongIdsResponse = await db
  .insertInto('song')
  .values(songs)
  .onConflict((oc) => oc.doNothing())
  .returning('id')
  .execute()
var position = 0
if (insertedPlaylistIdResponse?.id) {
  const playlistItems = insertedSongIdsResponse.map((songIdResponse) => {
    if (this !== undefined) {
      return undefined
    } else {
      const newPosition = this + 1
      return {
        playlist_id: insertedPlaylistIdResponse.id,
        song_id: songIdResponse.id,
        position: newPosition
      }
    }
  }, position)
  await db
    .insertInto('playlist_item')
    .values(playlistItems)
    .execute()
}

So basically I want to insert an object into the playlist table, insert song objects into the song table, take the resulting ids of those inserted songs and the inserted playlist and for each song’s id map to a kind of object that I can then insert into another table called playlist_item, like this:

const playlistItems = insertedSongIdsResponse.map((songIdResponse) => {
  if (this !== undefined) {
    return undefined
  } else {
    const newPosition = this + 1
    return {
      playlist_id: insertedPlaylistIdResponse.id,
      song_id: songIdResponse.id,
      position: newPosition
    }
  }
}, position)

The playlist_item table has this schema:

export interface PlaylistItemTable {
  id: Generated<number>
  playlist_id: number
  song_id: number
  position: number
}

Basically a PlaylistItem is a song within the playlist, at a certain position (like the first song in the playlist will have position = 1, the next would have position = 2, etc.

The thing to focus on is the position variable, and whether I can use the thisArg parameter of the Array.map() function as a kind of iterator that each time it maps it will increment, based on the way I’ve used it. Is that possible, or will it not work? Currently I’m getting the error that the variable this could be undefined, but I’m not sure why because I’ve checked for that right before attempting to use it.

Any help is appreciated!

Why do my Three.js game camera controls glitch / not work

I have made a game, and everything works, except when I move the camera with my mouse, everything duplicates in my view. I see a quickly alternating version alternating between my original camera view and my new camera view.

I made this code, and I have tried to make the camera update and change it instead of duplicating it, but it doesn’t work. Here is my broken code so far:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Platformer</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script>
        // Create a scene
        var scene = new THREE.Scene();

        // Create a camera with increased FOV
        var camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000);

        // Create a renderer
        var renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // Create a large platform
        var platformGeometry = new THREE.BoxGeometry(20, 1, 20);
        var platformMaterial = new THREE.MeshBasicMaterial({ color: 0xaaaaaa });
        var platform = new THREE.Mesh(platformGeometry, platformMaterial);
        platform.position.y = -1;
        scene.add(platform);

        // Create a player
        var playerGeometry = new THREE.BoxGeometry(1, 1, 1);
        var playerMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
        var player = new THREE.Mesh(playerGeometry, playerMaterial);
        scene.add(player);

        // Create additional cubes
        var cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
        var cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
        for (var i = 0; i < 5; i++) {
            var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
            cube.position.set(Math.random() * 18 - 9, 0.5, Math.random() * 18 - 9);
            scene.add(cube);
        }

        // Create an AI that runs around randomly
        var aiGeometry = new THREE.BoxGeometry(1, 1, 1);
        var aiMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
        var ai = new THREE.Mesh(aiGeometry, aiMaterial);
        ai.position.set(0, 0.5, -5); // Initial position
        scene.add(ai);

        // AI movement variables
        var aiSpeed = 0.05;
        var aiDirection = new THREE.Vector3(Math.random() * 2 - 1, 0, Math.random() * 2 - 1).normalize();
        var aiJumpSpeed = 0.2;
        var aiVelocityY = 0;
        var aiOnGround = true;

        // Player controls
        var playerSpeed = 0.1;
        var jumpSpeed = 0.2;
        var velocityY = 0;
        var onGround = true;

        var keys = {
            w: false,
            a: false,
            s: false,
            d: false,
            space: false
        };

        window.addEventListener('keydown', function(event) {
            if (keys.hasOwnProperty(event.key)) keys[event.key] = true;
        });

        window.addEventListener('keyup', function(event) {
            if (keys.hasOwnProperty(event.key)) keys[event.key] = false;
        });

        // Mouse controls
        var mouse = {
            isDragging: false,
            previousX: 0,
            previousY: 0,
            deltaX: 0,
            deltaY: 0
        };

        document.addEventListener('mousedown', function(event) {
            mouse.isDragging = true;
            mouse.previousX = event.clientX;
            mouse.previousY = event.clientY;
        });

        document.addEventListener('mouseup', function() {
            mouse.isDragging = false;
        });

        document.addEventListener('mousemove', function(event) {
            if (mouse.isDragging) {
                mouse.deltaX = event.clientX - mouse.previousX;
                mouse.deltaY = event.clientY - mouse.previousY;

                camera.rotation.y -= mouse.deltaX * 0.002;
                camera.rotation.x -= mouse.deltaY * 0.002;

                camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x)); // Prevent flipping

                mouse.previousX = event.clientX;
                mouse.previousY = event.clientY;
            }
        });

        // Animation loop
        function animate() {
            requestAnimationFrame(animate);

            // Player movement
            if (keys.w) player.position.z -= playerSpeed;
            if (keys.a) player.position.x -= playerSpeed;
            if (keys.s) player.position.z += playerSpeed;
            if (keys.d) player.position.x += playerSpeed;

            // Prevent jumping off the edge
            player.position.x = Math.max(-9.5, Math.min(9.5, player.position.x));
            player.position.z = Math.max(-9.5, Math.min(9.5, player.position.z));

            // Player jumping
            if (keys.space && onGround) {
                velocityY = jumpSpeed;
                onGround = false;
            }

            player.position.y += velocityY;
            velocityY -= 0.01; // Gravity effect

            if (player.position.y <= 0) {
                player.position.y = 0;
                onGround = true;
            }

            // AI random movement and jumping
            ai.position.add(aiDirection.clone().multiplyScalar(aiSpeed));
            if (Math.random() < 0.05 && aiOnGround) { // Increased jump frequency
                aiVelocityY = aiJumpSpeed;
                aiOnGround = false;
            }

            ai.position.y += aiVelocityY;
            aiVelocityY -= 0.01; // Gravity effect

            if (ai.position.y <= 0) {
                ai.position.y = 0;
                aiOnGround = true;
            }

            // Avoid getting stuck with AI
            var distance = player.position.distanceTo(ai.position);
            if (distance < 1) {
                var pushDirection = new THREE.Vector
                                Vector3().subVectors(player.position, ai.position).normalize();
                player.position.add(pushDirection.multiplyScalar(0.1));
            }

            // Position camera
            camera.position.set(player.position.x, player.position.y + 1.5, player.position.z);
            camera.lookAt(player.position.x + Math.sin(camera.rotation.y), player.position.y + 1.5, player.position.z - Math.cos(camera.rotation.y));

            renderer.render(scene, camera);
        }
        animate();
    </script>
</body>
</html>

What is the correct code for the game Mastermind? [closed]

My code is not working in Codehs to make the game Mastermind. It is something I’ve been working on for a couple of weeks and is refusing to work. I just need the code to work.

I tried multiple different ways to make the code work and I expected at least one to work. I used for loops, while loops, arrays, etc but for some reason it still isn’t working.

Custom tooltips not displayed for all lines

I created a dashboard using Google Charts and wanted to annotate data by using custom tooltips. But I only manage to display my custom tooltips for the latest created line (line of plot87 in JSFiddle), but lines created before are using the default tooltip (line of plot66 in JSFiddle).

It seems like I am somehow causing a fallback to the default tooltip, but I do not see how. After logging the data seems to be the same structure for both lines, so I do not know, where I force the fallback or cancel out my custom tooltip.

Here is my code, where I tried to reproduce my problem:

const jsonData = {
    "title": "Dashboard",
    "updated_on": "2025-02-21T14:08:28.741617",
    "panels": [
        {
            "type": "timeseries",
            "table": "air_temp",
            "title": "Air Temperature 2 m above Surface",
            "unit": "u00b0C",
            "data": {
                "plot66": [
                    {
                        "time": "2025-01-01T00:00:00",
                        "value": 7.468658447265625
                    },
                    {
                        "time": "2025-01-01T03:00:00",
                        "value": 7.459381103515625
                    },
                    {
                        "time": "2025-01-01T06:00:00",
                        "value": 7.389312744140625
                    },
                    {
                        "time": "2025-01-01T09:00:00",
                        "value": 9.727935791015625
                    },
                    {
                        "time": "2025-01-01T12:00:00",
                        "value": 10.906646728515625
                    },
                    {
                        "time": "2025-01-01T15:00:00",
                        "value": 11.067535400390625
                    },
                    {
                        "time": "2025-01-01T18:00:00",
                        "value": 8.872955322265625
                    },
                    {
                        "time": "2025-01-01T21:00:00",
                        "value": 8.459625244140625
                    },
                    {
                        "time": "2025-01-02T00:00:00",
                        "value": 8.005035400390625
                    },
                    {
                        "time": "2025-01-02T03:00:00",
                        "value": 7.952545166015625
                    },
                    {
                        "time": "2025-01-02T06:00:00",
                        "value": 7.978424072265625
                    },
                    {
                        "time": "2025-01-02T09:00:00",
                        "value": 10.216461181640625
                    },
                    {
                        "time": "2025-01-02T12:00:00",
                        "value": 10.678863525390625
                    },
                    {
                        "time": "2025-01-02T15:00:00",
                        "value": 10.543609619140625
                    },
                    {
                        "time": "2025-01-02T18:00:00",
                        "value": 9.369049072265625
                    },
                    {
                        "time": "2025-01-02T21:00:00",
                        "value": 9.340240478515625
                    },
                    {
                        "time": "2025-01-03T00:00:00",
                        "value": 8.868316650390625
                    },
                    {
                        "time": "2025-01-03T03:00:00",
                        "value": 8.211090087890625
                    },
                    {
                        "time": "2025-01-03T06:00:00",
                        "value": 7.642486572265625
                    },
                    {
                        "time": "2025-01-03T09:00:00",
                        "value": 9.831451416015625
                    },
                    {
                        "time": "2025-01-03T12:00:00",
                        "value": 10.954010009765625
                    },
                    {
                        "time": "2025-01-03T15:00:00",
                        "value": 11.588531494140625
                    },
                    {
                        "time": "2025-01-03T18:00:00",
                        "value": 10.276763916015625
                    },
                    {
                        "time": "2025-01-03T21:00:00",
                        "value": 10.511627197265625
                    },
                    {
                        "time": "2025-01-04T00:00:00",
                        "value": 10.682769775390625
                    },
                    {
                        "time": "2025-01-04T03:00:00",
                        "value": 10.562896728515625
                    },
                    {
                        "time": "2025-01-04T06:00:00",
                        "value": 10.225006103515625
                    },
                    {
                        "time": "2025-01-04T09:00:00",
                        "value": 10.914947509765625
                    },
                    {
                        "time": "2025-01-04T12:00:00",
                        "value": 11.296051025390625
                    },
                    {
                        "time": "2025-01-04T15:00:00",
                        "value": 10.217437744140625
                    },
                    {
                        "time": "2025-01-04T18:00:00",
                        "value": 8.537506103515625
                    },
                    {
                        "time": "2025-01-04T21:00:00",
                        "value": 7.460845947265625
                    },
                    {
                        "time": "2025-01-05T00:00:00",
                        "value": 6.343170166015625
                    },
                    {
                        "time": "2025-01-05T03:00:00",
                        "value": 5.995269775390625
                    },
                    {
                        "time": "2025-01-05T06:00:00",
                        "value": 6.114410400390625
                    },
                    {
                        "time": "2025-01-05T09:00:00",
                        "value": 6.575592041015625
                    },
                    {
                        "time": "2025-01-05T12:00:00",
                        "value": 7.698150634765625
                    },
                    {
                        "time": "2025-01-05T15:00:00",
                        "value": 7.824859619140625
                    },
                    {
                        "time": "2025-01-05T18:00:00",
                        "value": 7.553375244140625
                    },
                    {
                        "time": "2025-01-05T21:00:00",
                        "value": 7.653472900390625
                    }
                ],
                "plot87": [
                    {
                        "time": "2025-01-01T00:00:00",
                        "value": 7.423492431640625
                    },
                    {
                        "time": "2025-01-01T03:00:00",
                        "value": 7.418609619140625
                    },
                    {
                        "time": "2025-01-01T06:00:00",
                        "value": 7.349761962890625
                    },
                    {
                        "time": "2025-01-01T09:00:00",
                        "value": 9.715728759765625
                    },
                    {
                        "time": "2025-01-01T12:00:00",
                        "value": 10.905670166015625
                    },
                    {
                        "time": "2025-01-01T15:00:00",
                        "value": 11.054107666015625
                    },
                    {
                        "time": "2025-01-01T18:00:00",
                        "value": 8.802398681640625
                    },
                    {
                        "time": "2025-01-01T21:00:00",
                        "value": 8.401763916015625
                    },
                    {
                        "time": "2025-01-02T00:00:00",
                        "value": 7.939361572265625
                    },
                    {
                        "time": "2025-01-02T03:00:00",
                        "value": 7.887359619140625
                    },
                    {
                        "time": "2025-01-02T06:00:00",
                        "value": 7.899566650390625
                    },
                    {
                        "time": "2025-01-02T09:00:00",
                        "value": 10.189361572265625
                    },
                    {
                        "time": "2025-01-02T12:00:00",
                        "value": 10.664215087890625
                    },
                    {
                        "time": "2025-01-02T15:00:00",
                        "value": 10.540191650390625
                    },
                    {
                        "time": "2025-01-02T18:00:00",
                        "value": 9.300933837890625
                    },
                    {
                        "time": "2025-01-02T21:00:00",
                        "value": 9.263092041015625
                    },
                    {
                        "time": "2025-01-03T00:00:00",
                        "value": 8.797027587890625
                    },
                    {
                        "time": "2025-01-03T03:00:00",
                        "value": 8.135162353515625
                    },
                    {
                        "time": "2025-01-03T06:00:00",
                        "value": 7.564117431640625
                    },
                    {
                        "time": "2025-01-03T09:00:00",
                        "value": 9.803619384765625
                    },
                    {
                        "time": "2025-01-03T12:00:00",
                        "value": 10.940582275390625
                    },
                    {
                        "time": "2025-01-03T15:00:00",
                        "value": 11.579498291015625
                    },
                    {
                        "time": "2025-01-03T18:00:00",
                        "value": 10.209869384765625
                    },
                    {
                        "time": "2025-01-03T21:00:00",
                        "value": 10.452056884765625
                    },
                    {
                        "time": "2025-01-04T00:00:00",
                        "value": 10.628082275390625
                    },
                    {
                        "time": "2025-01-04T03:00:00",
                        "value": 10.505523681640625
                    },
                    {
                        "time": "2025-01-04T06:00:00",
                        "value": 10.157867431640625
                    },
                    {
                        "time": "2025-01-04T09:00:00",
                        "value": 10.914459228515625
                    },
                    {
                        "time": "2025-01-04T12:00:00",
                        "value": 11.318023681640625
                    },
                    {
                        "time": "2025-01-04T15:00:00",
                        "value": 10.220611572265625
                    },
                    {
                        "time": "2025-01-04T18:00:00",
                        "value": 8.527984619140625
                    },
                    {
                        "time": "2025-01-04T21:00:00",
                        "value": 7.444732666015625
                    },
                    {
                        "time": "2025-01-05T00:00:00",
                        "value": 6.330230712890625
                    },
                    {
                        "time": "2025-01-05T03:00:00",
                        "value": 5.982574462890625
                    },
                    {
                        "time": "2025-01-05T06:00:00",
                        "value": 6.104888916015625
                    },
                    {
                        "time": "2025-01-05T09:00:00",
                        "value": 6.563629150390625
                    },
                    {
                        "time": "2025-01-05T12:00:00",
                        "value": 7.687896728515625
                    },
                    {
                        "time": "2025-01-05T15:00:00",
                        "value": 7.812896728515625
                    },
                    {
                        "time": "2025-01-05T18:00:00",
                        "value": 7.523345947265625
                    },
                    {
                        "time": "2025-01-05T21:00:00",
                        "value": 7.621490478515625
                    }
                ]
            }
        }
    ]
}

// Load Google Charts
google.charts.load('current', { packages: ['corechart', 'line'] });
google.charts.setOnLoadCallback(() => drawTimeSeriesChart(jsonData.panels));

// This part is not need for this fiddle, but in original script this is used for loading JSON data
/* async function fetchAndDrawCharts() {
  try {
    const response = await fetch('../db_data/google_dashboard/ketipis/dashboard.json');
    const text = await response.text(); // Read as text first
    // console.log("Raw JSON Response:", text); // Debug log

    const data = JSON.parse(text); // Parse manually to catch errors
    console.log("Parsed JSON:", data);

    drawTimeSeriesChart(data.panels);
  } catch (error) {
    console.error("Error loading JSON:", error);
  }
}
*/

function createChartContainer(id) {
  // Select the container where you want to add the chart div
  const container = document.querySelector(".dashboard-container");
  if (!container) {
    console.error("Dashboard container not found.");
    return;
  }

  // Create a new div element
  const chartDiv = document.createElement("div");
  chartDiv.id = id;  // Set ID
  chartDiv.className = "chart";  // Set class

  // Append the new div inside the container
  container.appendChild(chartDiv);
}

function drawTimeSeriesChart(panels) {
  if (!panels || panels.length === 0) {
    console.error("No panels found in JSON data");
    return;
  }

  panels.forEach(panel => {
    let timeSeriesData = panel.data;
    let chartData = new google.visualization.DataTable(); // Unique DataTable per chart

    // Define columns (Time, Value per sensor, Tooltip)
    chartData.addColumn({ type: 'date', label: 'Time' });  
    const plots = Object.keys(timeSeriesData);
    plots.forEach(plot => chartData.addColumn({ type: 'number', label: plot }));
    chartData.addColumn({ type: 'string', role: 'tooltip' });

    const rows = [];
    let originalData = [];

    // Store the original data for reference
    plots.forEach(plot => {
      timeSeriesData[plot].forEach(entry => {
        const dateObj = new Date(entry.time);
        const date = entry.time.split("T")[0];
        const time = entry.time.split("T")[1].slice(0,5);
        const tooltipText = `${date} ${time} n${plot}: ${entry.value.toFixed(2)} ${panel.unit}`;

        const row = [dateObj];
        let rowValues = {};
        plots.forEach(p => {
          const value = (p === plot ? entry.value : null);
          row.push(value);
          rowValues[p] = value;
        });
        row.push(tooltipText); // Ensure tooltip is attached

        rows.push(row);
        originalData.push({ dateObj, rowValues, tooltipText });
      });
    });

    chartData.addRows(rows);

    // Track visibility of each series
    let seriesVisibility = plots.reduce((obj, plot) => {
      obj[plot] = true;
      return obj;
    }, {});

    // Chart options
    let options = {
      title: panel.title,
      curveType: 'function',
      backgroundColor: '#1e1e1e',
      titleTextStyle: { color: '#fff' },
      legendTextStyle: { color: '#fff' },
      hAxis: { textStyle: { color: '#fff' }, titleTextStyle: { color: '#fff' } },
      vAxis: { textStyle: { color: '#fff' }, titleTextStyle: { color: '#fff' } },
      legend: { position: 'top' },
      tooltip: { isHtml: true },
      explorer: { axis: 'horizontal', keepInBounds: true, maxZoomIn: 0.05 },
    };

    // Create separate chart container
    createChartContainer(panel.table);
    const chart = new google.visualization.LineChart(document.getElementById(panel.table));

    // Function to toggle series visibility
    function updateChart() {
      let newData = new google.visualization.DataTable(); // Create fresh DataTable
      newData.addColumn({ type: 'date', label: 'Time' });
      plots.forEach(plot => newData.addColumn({ type: 'number', label: plot }));
      newData.addColumn({ type: 'string', role: 'tooltip' });

      let newRows = originalData.map(entry => {
        let row = [entry.dateObj];
        plots.forEach(plot => {
          row.push(seriesVisibility[plot] ? entry.rowValues[plot] : null);
        });
        row.push(entry.tooltipText);
        return row;
      });

      newData.addRows(newRows);
      chart.draw(newData, options);
    }

    // Click event for legend toggling
    google.visualization.events.addListener(chart, 'select', () => {
      const selection = chart.getSelection();
      if (selection.length > 0 && selection[0].column > 0) {
        const seriesIndex = selection[0].column - 1;
        const plotKey = plots[seriesIndex];

        // Toggle visibility
        seriesVisibility[plotKey] = !seriesVisibility[plotKey];
        updateChart();
      }
    });

    updateChart(); // Initial chart render
  });
}
body {
  background-color: #1e1e1e;
  color: #fff;
  font-family: Arial, sans-serif;
  padding: 20px;
}
.dashboard-container {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.chart {
  width: 900px;
  height: 450px;
  margin: 20px 0;
  background-color: #2e2e2e;
  padding: 10px;
  border-radius: 8px;
  box-shadow: 0 4px 8px rgba(255, 255, 255, 0.2);
}
<!DOCTYPE html>
<html>
  <head>
    <title>Dashboard</title>
    <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
  </head>
  <body>
    <h1>Sensor Dashboard</h1>
    <div class="dashboard-container"></div>
  </body>
</html>

How to wait until hcaptcha is executed

I’m trying to implement module to send hcaptcha via ajax, but load/show it only when some specific checks are completed, so after showing it I need to wait until user completes the captcha and right after it’s completed I need to submit the form without any other user interaction. Hcaptcha has an option to call a function on captcha completion

<div
    class="h-captcha"
    data-sitekey="your_site_key"
    data-callback="onSuccess" <-
></div>

but I need not to call another function. I need to continue executing this one, is there a way to track when it tries to call the function?