How to compile a single .svelte file from a SvelteKit app as a web component?

I have a SvelteKit 2/Svelte 5 app with out-of-the-box configuration – what you get by running npx sv create my-app. I want to build my app normally with adapter-node, but I also want to build a single .svelte file as a web component that I can reuse in other projects.

Currently my setup is this:

// $lib/widget/MyComponent.svelte

<svelte:options
    customElement={{
        tag: 'my-component',
    }}
/>

<script lang="ts">
    let counter = $state(0)
</script>

<button onclick={() => (counter += 1)}>
    Clicked {counter} times
</button>
// svelte.config.js

import { mdsvex } from 'mdsvex'
import adapter from '@sveltejs/adapter-node'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'

const config = {
    preprocess: [vitePreprocess(), mdsvex()],
    kit: {
        adapter: adapter(),
    },
    extensions: ['.svelte', '.svx'],
}

export default config
// vite.widget.config.ts

import { svelte } from '@sveltejs/vite-plugin-svelte'

export default {
    build: {
        lib: {
            entry: 'src/lib/widget/MyComponent.svelte',
            name: 'MyComponent',
            fileName: (format) => `my-component.${format}.js`,
            formats: ['es'],
        },
        rollupOptions: {
            external: ['svelte'],
            output: {
                globals: {
                    svelte: 'svelte',
                },
            },
        },
        sourcemap: 'inline',
    },
    plugins: [
        svelte({
            compilerOptions: {
                customElement: true,
            },
        }),
    ],
}
vite build --config vite.widget.config.ts
<!-- root/my-component.html -->
<my-component></my-component>
<script src="./dist/my-component.es.js" type="module"></script>

It works, but I’m not sure if this is the correct way to do it.

I’m getting a warning in my .svelte file that I haven’t specified customElement: true, but that’s because I have my compiler options in vite.widget.config.ts.

I tried looking at the svelte/compiler docs, but I couldn’t understand how I’m supposed to use that module.

Any tips are welcome!

Angular guard flickers page then loads actual page

export const authorizedUserGuard: CanActivateFn = (route, state) => {
    console.log('guard call', route, state)
    const tokenService = inject(TokenStorageService);
    const router = inject(Router);
    let user = tokenService.getUser();
    let token = tokenService.getToken();
    if(!token || !user){
        console.log('user not logged in');
        tokenService.removeData();
        router.navigate(['/home', 'login']);
        return false;
    }
 
    return true;
    
};

Angular first loads home/login then immediately it loads actual route. I am using angular SSR.
Note: token and user both variable has value and I am not getting any logs in console.

Uncaught TypeError: $(…).hexGridWidget is not a function – on second call

I’m moving my website to a new one and a personalized code stopped working and can’t figure out why.
The old page is working perfectly: https://www.waal.co/pages/grip-builder
but the new one shows up an error (Uncaught TypeError: $(…).hexGridWidget is not a function) on the second time the function rebuild is called (when you change the dropdowns).
Sorry for the mess in the code, I’m no developer 🙂 & thanks in advance.

https://i07hdzzlg4e8jppd-80712532305.shopifypreview.com/pages/grip-builder

Code bellow:

<html>
<h3>1. Enter your |surfboard| details</h3>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>


        <div class="container">
    <div>
        <form class="cpanel" onsubmit="return false;">
                        <div class="boardetails">
                <span class="control-label">Kind of board</span><br>
                <select name="boards" id="boards">
                    <option value="short" selected>Shorboard</option>
                    <option value="retro">Retro</option>
                    <option value="minimal">Malibu</option>
                    <option value="long">Longboard</option>
                </select>
            </div>
            <div class="boardetails" style="display: flex;">
                <div class="controls" style="margin-right: 10px;">
                    <span class="control-label2">Lenght (ft):</span> <br>
                    <select name="col" id="columns" style="max-width: 70px;">
                        <option value="5">5</option>
                        <option value="6" selected>6</option>
                        <option value="7" >7</option>
                        <option value="8">8</option>
                        <option value="9">9</option>
                        <option value="10">10</option>
                    </select>
                </div>
                <div class="controls">
                    <span class="control-label2">& (inches):</span> <br>
                    <select name="col2" id="columns2" style="max-width: 70px;">
                        <option value="0">0</option>
                        <option value="1">1</option>
                        <option value="2" selected>2</option>
                        <option value="3">3</option>
                        <option value="4">4</option>
                        <option value="5">5</option>
                        <option value="6">6</option>
                        <option value="7">7</option>
                        <option value="8">8</option>
                        <option value="9">9</option>
                        <option value="10">10</option>
                        <option value="11">11</option>
                    </select>
                </div>
            </div>
            <div class="boardetails">
                <div class="controls">
                    <span class="control-label">Board width</span><br>
                    <select name="rows" id="rows">
                        <option value="17">17</option>
                        <option value="18">18</option>
                        <option value="19" selected>19</option>
                        <option value="20">20</option>
                        <option value="21">21</option>
                        <option value="22">22</option>
                        <option value="23">23</option>
                        <option value="24">24</option>
                        <option value="25">25</option>
                        <option value="26">26</option>
                        <option value="27">27</option>
                        <option value="28">28</option>
                        <option value="29">29</option>
                        <option value="30">30</option>
                        <option value="31">31</option>
                        <option value="32">32</option>
                    </select>
                </div>
            </div>

            <div class="boardetails2">
                <span class="control-label">Tail pad</span><br>
                <select name="tail" id="tail">
                    <option value="tail" selected>Yes</option>
                    <option value="no">No</option>
                </select>
            </div>
        </form>
    </div>
<h3>2. Highlight the |hexagons|</h3>
<div class="row">
        <div style="text-align:center">
            <div id="container_max">
                <div id="container" class="fluid" align="left">
                    <img id="board_image" src="" class="fluid boardimage">
                </div>
            </div>
        </div>
        
        <div class="boardetails">
            <div class="controls resultstext">
                <span class="empty"></span>
                <p style="margin-top:20px; color:#fff !important;">Click on board hexagons to select/deselect</p>
                <h3 style="color:#fff !important; margin-right:20px !important;">Your setup has:
                    <span id="logger" style="color:#88fd42;">0</span><span class="logg"
                            style="color:#fff !important;  margin-left:15px !important;">hexagons</span>
                </h3><br>
                <span id="note" style="color:#fff !important;"></span>
                <div class="sqs-block button-block sqs-block-button" data-block-type="53"
                     id="block-yui_3_17_2_5_1484323594954_8494"
                     style="padding-left:0; padding-right:0; padding-top:0 !important; padding:0; margin-top:30px !important;">

                    <div class="sqs-block-content">
                        <div class="buttona" onclick="goshop2()" style="margin-top:30px;"><a
                                class="button">ORDER NOW</a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script type="text/javascript">

  console.log("olalla");
// HEXGRID JS
/*global $, document*/
    var linhas = 0;
    var colunas = 0;
    var firstTime = 1;
$.fn.hexGridWidget = function (id, radius, columns, rows, columnWidth, rowHeight, cssClass) {
    'use strict';
    let createSVG = function (tag) {
        return $(document.createElementNS('http://www.w3.org/2000/svg', tag || 'svg'));
    };
    return $(this).each(function () {
        let element = $(this);
        
        const   hexClick = function () {
                    let hex = $(this);
                    element.trigger($.Event('hexclick', hex.data()));
                };

    const height = radius * Math.sqrt(3) / 2;

        const svgParent = createSVG('svg')
            .attr('tabindex', 1)
            .attr('id', id)
            .appendTo(element)
            .css({
              position: 'absolute',
                    width: columnWidth * columns + columnWidth / 3,
                    height: rowHeight * rows
                });

    const oddKey = 1 - rows % 2;

        for (let row = 0; row < rows; row++) {
            linhas = linhas + 1;
            for (let column = 0; column < columns; column++) {
                if (colunas < column) {
                    colunas = colunas + 1;
                }
                
                
              if (column % 2 != oddKey && row + 1 == rows) continue;
                let center = {
                  x: Math.round(column * columnWidth + columnWidth / 1.5),
                  y: Math.round((row + 0.5 * (1 + (column + oddKey) % 2)) * rowHeight)
                };
                let hexid = column+'-'+row;

                createSVG('polygon').attr({
                    points: [
                        {x: -1 * radius / 2, y: -1 * height},
                        {x: radius / 2, y: -1 * height},
                        {x: radius, y: 0},
                        {x: radius / 2, y: height},
                        {x: -1 * radius / 2, y: height},
                        {x: -1 * radius, y: 0}
                    ]
                    .map((it) => (it.x + center.x) + ',' + (it.y + center.y))
                    .join(' '),
                    'class':cssClass,
                    'id':hexid,
                    tabindex:1
                })
                .appendTo(svgParent).data({center:center, row:row, column:column}).on('click', hexClick).attr({'hex-row': row, 'hex-column': column});
            }
        }

    });
};

// CALCULATOR
var hexFinal = 0;
    function calculateHex() {
        var result = document.getElementById('logger');
        /*var radios = document.getElementsByName('group2');

        for (var i = 0, length = radios.length; i < length; i++) {
            if (radios[i].checked) {
                // do whatever you want with the checked radio
                var tipoSurf = radios[i].value;
                // only one radio can be logically checked, don't check the rest
                break;
            }
        }
        var myBox1 = document.getElementById('columns').value;
        var myBox2 = document.getElementById('columns2').value;
        var myBox3 = document.getElementById('rows').value;
        var result = document.getElementById('logger');
        var cp1 = parseFloat(myBox1/2);
        var cp2 = parseFloat(myBox2/2)*0.08;
        var lp = parseFloat(myBox3/2)*0.08;
        switch(tipoSurf) {
            case "1":
                var cp = cp1+cp2;
                var myResult = parseInt((3.14*cp*lp)*144);
                break;
            case "2":
                var cp = (cp1+cp2)-0.5;
                var myResult = parseInt((3.14*cp*lp)*144);
                break;
            case "3":
            var cp = (cp1+cp2)-1.5;
            var myResult = parseInt((3.14*cp*lp)*144);
                break;
            default:
                var cp = cp1+cp2;
                var myResult = parseInt((3.14*cp*lp)*144);
        }

        var hexTemp = myResult/40;
        if (myBox1 >= 7){
          hexFinal = 35 + hexTemp + 0.75*hexTemp;
        } else {
          hexFinal = 5 + hexTemp + 0.25*hexTemp;
        }*/

        

        //Center hexagon 
        var centerRow = Math.floor(linhas/2)-1;
        var centerColumn =  Math.floor(colunas/2)-1;
        var firstrow = 0;
        var umterco = Math.floor(colunas/3);
        if (tailpad == 'no') {
            var firstCol = 2;
        } else {
            var firstCol = Math.floor(colunas/3);
        }
        //ar firstCol = Math.floor(colunas/3);
        var lastCol = Math.floor(colunas - umterco);
        var linhaacesa = 1;
        const pares = 1 - linhas % 2;
        let helio = 0;
        let r = 1;
        let c = firstCol;
        const hexA = [];
        const hexB = [];




        //for (let hx = 0; hx <= hexToBuy; hx++){
            for (let rowA = 1; rowA < linhas-1; rowA++) {
                 for (let columnA = firstCol; columnA < lastCol; columnA++) {
                    if (columnA % 2 != pares && rowA + 1 == linhas-1) continue;
                    hexB.push(columnA+'-'+rowA);
                    
                }


            }

            /*if(c < lastCol) {
                c += 1 ;
                hexB.push(c+'-'+r);
                 console.log('hexagons>'+c+'-'+r);
            } else {
                c = firstCol;
                r += 1 ;
            }*/

        //}
       hexToBuy = hexB.length;
       hexFinal = hexB.length;
        result.innerHTML = parseInt(hexB.length);
        for (let ah = 0; ah < hexB.length; ah++){
            var hexId = hexB[ah];
            var elemento = document.getElementById(hexId);
             elemento.style.opacity = ".6";
             let isMainPresent = elemento.classList.contains("clicked");

                        if (isMainPresent) {
                            //ja tinha
                        } else {
                            elemento.classList.toggle('clicked');
                        } 
        }
        /*for (let i = 0; i < hexToBuy; i++) {
            

            if (i > firstCol && i > lastCol) continue;
             console.log(i+'-'+lastCol);
            /*for (let r = 1; r < linhas-1; r++) {
                    
                    var cagalhao = r;
                for (let p = firstCol; p < lastCol+2; p++){
                    var putinha = p;
                    var hexId = p+'-'+cagalhao;
                    var elemento = document.getElementById(hexId);
                    console.log(lastCol)
                    elemento.style.opacity = ".6";

                    let isMainPresent = elemento.classList.contains("clicked");

                        if (isMainPresent) {
                            //ja tinha
                        } else {
                            elemento.classList.toggle('clicked');
                        } 
                    
                    

                }
            }
            
        }*/
        
        

    }
    function acendeHex(){

    }
<!-- main initialisation -->

// Main constants

const padSize = 10.3; // Diameter, cm
const padGap = 1; // cm

const rowHeight = (padSize + padGap) * Math.sqrt(3) / 2; // cm
const columnWidth = (padSize + padGap) * 0.75; // cm

const padCssClass = 'hexfield';

let tailpad = 'yes';
// Formula que converte as polegadas da prancha em cm


// Formula que calcula quantos hexes cabiam na prancha, quer horizontal como vertical > isto dá o radios

// a prancha é o container ID

//entender a proporção de radius / pixels / cms

// radius is the length (in pixels) of the side of a single hexagon (effectively the radius of the circle in which the hex field will be inscribed.

// a border dos hexagonos também tem de ser variavel dos pixels / prancha

//logger diz o numero de hexagonos >>> DONE
function tipodePrancha(tipo, tail, w, h, m){

            document.getElementById('board_image').setAttribute("style","margin-top: " + m + "px;");
            
            switch (tipo + "|" + tail) {
                case 'minimal|no':
                document.getElementById('board_image').setAttribute("src","https://cdn.shopify.com/s/files/1/0807/1253/2305/files/malibu.png?v=1709121370");
                tailpad = 'no';
                break;
            case 'minimal|tail':
                  document.getElementById('board_image').setAttribute("src","https://cdn.shopify.com/s/files/1/0807/1253/2305/files/malitail.png?v=1709121372");
                  tailpad = 'yes';
                
                break;
                case 'short|no':
                document.getElementById('board_image').setAttribute("src","https://cdn.shopify.com/s/files/1/0807/1253/2305/files/short.png?v=1705075374");
                tailpad = 'no';
                break;
                case 'short|tail':
                document.getElementById('board_image').setAttribute("src","https://cdn.shopify.com/s/files/1/0807/1253/2305/files/shorttail.png?v=1705075374");
                tailpad = 'yes';

                break;
                case 'retro|no':
                document.getElementById('board_image').setAttribute("src","https://cdn.shopify.com/s/files/1/0807/1253/2305/files/retro.png?v=1705075374");
                tailpad = 'no';
                break;
                case 'retro|tail':
                document.getElementById('board_image').setAttribute("src","https://cdn.shopify.com/s/files/1/0807/1253/2305/files/retrotail.png?v=1705075375");
                tailpad = 'yes';
                break;
                case 'long|no':
                document.getElementById('board_image').setAttribute("src","https://cdn.shopify.com/s/files/1/0807/1253/2305/files/long.png?v=1705075374");
                tailpad = 'no';
                break;
            case 'long|tail':
                document.getElementById('board_image').setAttribute("src","https://cdn.shopify.com/s/files/1/0807/1253/2305/files/longtail.png?v=1705075374");
                tailpad = 'yes';
                break;
                default:
                document.getElementById('board_image').setAttribute("src","https://cdn.shopify.com/s/files/1/0807/1253/2305/files/short.png?v=1705075374");
            }


                document.getElementById('board_image').setAttribute("width",w + "px");
                document.getElementById('board_image').setAttribute("height",h + "px");

            
            console.log(w+"-----"+h);
    // code block

  }


let rebuild = function () {

    linhas = 0;
    colunas = 0;
    const boardWidth = parseInt($('#rows').val()) * 2.54; // cm
    const boardLength = parseInt($('#columns').val()) * 2.54 * 12 + parseInt($('#columns2').val()) * 2.54; // cm

    const padsColumns = Math.ceil(boardLength / columnWidth); // count
    let padsRows = Math.ceil(0.5 + boardWidth / rowHeight); // count

    const viewWidth = padsColumns * columnWidth + columnWidth / 3 // cm
    const viewHeight = padsRows * rowHeight // cm
    const boardMarginTop = (viewHeight - boardWidth) / 2; // cm
    var clientWidth = document.getElementById('container').clientWidth;
    
    const pxScale = viewWidth/$('#container').width(); // ratio

    const pxViewWidth = viewWidth / pxScale; // px
    const pxViewHeight = viewHeight / pxScale; // px

    console.log("--------"+pxViewHeight);
    //$('#container').height(pxViewHeight);
    if (pxViewHeight === 0) {
    console.log("E ZERO");
     const pxViewHeight = 195;
        }

    const pxBoardLength = boardLength / pxScale; // px
    const pxBoardWidth = boardWidth / pxScale; // px
    const pxBoardMarginTop = boardMarginTop / pxScale; // px

    const pxRadius = padSize / (2 * pxScale); // px
    const pxRowHeight = rowHeight / pxScale; // px
    const pxColumnWidth = columnWidth / pxScale; // px

    const tipo = $('#boards').find(":selected").val();
    const tail = $('#tail').find(":selected").val();

    tipodePrancha(tipo, tail, pxBoardLength, pxBoardWidth, pxBoardMarginTop);

    //document.getElementById('hexMesh')?.remove();
$('#hexMesh').remove();
    $('#logger').text(0);
    $('#container').hexGridWidget("hexMesh", pxRadius, padsColumns, padsRows, pxColumnWidth, pxRowHeight, padCssClass)
      .on('hexclick', function (e) {
        //console.log("clic"+this);
        //this.style.opacity = ".2";
        //console.log('clicked [' + e.column + ',' + e.row + '] hex with center at [' + e.center.x + ',' + e.center.y + '] px');
      });

  let i=0;
  let isDown = false;   // Tracks status of mouse button
  calculateHex();

  /*$(document).mousedown(function() {
    isDown = true;      // When mouse goes down, set isDown to true
  })
  .mouseup(function() {
    isDown = false;    // When mouse goes up, set isDown to false
  });

  $('#container .hexfield').mouseover(function(){
    if(isDown) {        // Only change css if mouse is down
       this.classList.toggle('clicked');
    if (this.classList == 'hexfield clicked') {
        i++;
    } else {
        i--;
    }    
    $('#logger').text(i);
    const divNote = document.getElementById('note');
    if (i > 10 && i < 35) {
        divNote.innerHTML = 'Please note: A standard shortboard pack will be less expensive. <a href="https://www.waal.co/products/shortboard-pack">Click here to buy</a>';
    } else if(i > 65 && i < 100) {
        divNote.innerHTML = 'Please note: A standard longboard pack will be less expensive. <a href="https://www.waal.co/products/longboard-surf-grip-pack">Click here to buy</a>';
    } else {
        divNote.innerHTML = "";
    }
    }
  });*/
  let totalH = Math.floor(hexFinal+i);
  $('#container .hexfield').click(function () {
    this.classList.toggle('clicked');
    if (this.classList == 'hexfield clicked') {
        i++;
        this.style.opacity = ".6";
        totalH++;
    } else {
        i--;
        this.style.opacity = ".2";
        totalH--;
        //console.log(totalH);
    }

    $('#logger').text(Math.floor(hexFinal+i));
    const divNote = document.getElementById('note');
    if (totalH > 10 && totalH < 35) {
        divNote.innerHTML = 'Please note: A standard shortboard pack will be less expensive. <span style="color:#fff;"><a href="https://www.waal.co/products/shortboard-pack">Click here to buy</a></span>';
    } else if(totalH > 65 && totalH < 100) {
        divNote.innerHTML = 'Please note: A standard longboard pack will be less expensive.  <span style="color:#fff;"><a href="https://www.waal.co/products/longboard-surf-grip-pack">Click here to buy</a></span>';
    } else {
        divNote.innerHTML = "";
    }
  });
};

$( "#boards" ).change(rebuild);
$( "#rows" ).change(rebuild);
$( "#columns" ).change(rebuild);
$( "#columns2" ).change(rebuild);
$( "#tail" ).change(rebuild);

rebuild();
  
let goshop2 = function () {
  let hexB = $('#logger').text();
  //console.log(hexB);
          window.location.href = '/cart/47876151935313:'+hexB+'?storefront=true';

        }



</script>
</html>

how do I make text that goes below the screen on left div (screen split in 2 divs left and right) write itself on right div instead

I’m working on a typewriter animation where text is dynamically written in the left div of a split-screen layout (left and right divs). The issue is that when the text reaches the bottom of the left div, it continues writing below the visible area.

I want to ensure that any text that overflows in the left div is instead redirected to the right div, keeping all the text in view. I do not want to use a scrollbar, meaning the overflowing text should automatically continue on the right div rather than extending beyond the screen.

The goal is to keep the animation seamless while ensuring all text remains visible. How can I achieve this behavior?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TOP SECRET</title>
    
    <!-- External Styles -->
    <link rel="stylesheet" href="main.css">
    <link rel="stylesheet" href="about.css">

    <!-- Fonts & Internal Styles -->
    <style>
        @import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');

        body {
            font-family: 'VT323', monospace;
            background-color: #000;
            color: #ff9500;
            margin: 0;
            padding: 0;
            overflow-x: hidden;
            font-size: 2rem;
            display: flex;
            flex-direction: row;
            height: 100vh;
        }

        .navbar {
            display: flex;
            justify-content: center;
            background-color: #111;
            padding: 1rem;
            position: fixed;
            width: 100%;
            top: 0;
            z-index: 1000;
            border-bottom: 2px solid #ff9500;
        }

        .navbar a {
            color: #ff9500;
            text-decoration: none;
            padding: 0.5rem 1.2rem;
            font-size: 1.5rem;
            cursor: pointer;
            transition: background-color 0.3s ease, transform 0.2s ease;
        }

        .navbar a:hover {
            background-color: #552800;
            transform: scale(1.1);
        }

        .left, .right {
            width: 50%;
            padding: 2rem;
            box-sizing: border-box;
            overflow-y: hidden; /* Prevent vertical scrolling */
        }

        .left {
            text-align: left;
        }

        .right {
            padding-top: 6rem; /* Adjust for navbar height */
        }

        .section {
            display: none;
            padding: 4rem 2rem;
            margin-top: 5rem;
        }

        .section.active {
            display: block;
        }

        h1 {
            font-size: 2.5rem;
            text-transform: uppercase;
            margin-bottom: 1rem;
        }

        .typing, .intro {
            font-size: 1.8rem;
            white-space: nowrap;
            overflow: hidden;
            letter-spacing: 1px;
            border-right: 2px solid #ff8c00; /* Cursor */
            animation: blinkCursor 0.7s step-end infinite;
        }

        .intro {
            height: 6rem;
        }

        input[type="text"] {
            width: 80%;
            padding: 0.8rem;
            font-size: 1.2rem;
            border: 2px solid #ff9500;
            background-color: #111;
            color: #ff9500;
            border-radius: 5px;
            outline: none;
            margin-bottom: 1rem;
            text-align: center;
        }

        input[type="text"]::placeholder {
            color: rgba(255, 140, 0, 0.6);
        }

        @keyframes blinkCursor {
            0% {
                opacity: 1;
            }
            50% {
                opacity: 0;
            }
            100% {
                opacity: 1;
            }
        }
    </style>
</head>
<body>
    <div class="navbar">
        <a href="home.html">Home</a>
        <a onclick="openSection('debrief')">Debrief</a>
        <a href="#" onclick="openSection('search')">Search</a>
    </div>

    <div class="left">
        <div id="intro" class="section active">
            <p id="introText" class="intro"></p>
        </div>

        <div id="debrief" class="section">
            <h1>INFO</h1>
            <div class="text"><span id="text"></span></div>
        </div>

        <div id="search" class="section">
            <h1>SEARCH</h1>
            <input type="text" id="searchInput" onkeyup="searchFunction()" placeholder="Search...">
            <ul id="searchList">
                <li onclick="openSection('spaceships')">Asteroids</li>
                <li onclick="openSection('aliens')">Aliens</li>
                <li onclick="openSection('timeTravel')">Time Travel</li>
                <li onclick="openSection('galaxies')">Galaxies</li>
                <li onclick="openSection('robots')">Robots</li>
            </ul>
        </div>

        <div id="spaceships" class="section">
            <h1>Asteroids</h1>
            <p>One of the first games ever made</p>
        </div>

        <div id="aliens" class="section">
            <h1>Aliens</h1>
            <p>Not a stereotypical little green martian...</p>
        </div>

        <div id="timeTravel" class="section">
            <h1>Time Travel</h1>
            <p>Currently impossible...</p>
        </div>

        <div id="galaxies" class="section">
            <h1>Galaxies</h1>
            <p>They're huge...</p>
        </div>

        <div id="robots" class="section">
            <h1>Robots</h1>
            <p>Metal consciousness...</p>
        </div>
    </div>

    <div class="right">
        <div id="rightText"></div>
    </div>

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            runIntroAnimation();
        });

        function runIntroAnimation() {
            const introText = document.getElementById("introText");
            const introSequence = [
                "!DOCTYPE html",
                "html lang=en",
                "head ",
                "meta charset=UTF-8",
                "Running file."
            ];

            let i = 0;
            function typeLine() {
                if (i < introSequence.length) {
                    introText.innerHTML += `<p>${introSequence[i++]}</p>`;
                    setTimeout(typeLine, 1000);
                } else {
                    setTimeout(() => {
                        document.getElementById("intro").classList.remove("active");
                        document.getElementById("debrief").classList.add("active");
                        startQuoteAnimation();
                    }, 1000);
                }
            }
            typeLine();
        }

        function startQuoteAnimation() {
            const textElement = document.getElementById("text");
            const rightTextElement = document.getElementById("rightText");
            const textArray = [
                "We are a Classified Agency funded by the U.S. government...",
                "Our sector [Sector 6] is tasked with intercepting all interstellar transmissions...",
                "If the public knew about this, chaos would ensue...",
                "It is your mission to work with us with your time here and to make sure NO ONE knows...",
                "INFO",
                "Task 1",
                "Task 2",
                "Task 3",
                "Task 4"
            ];

            let textIndex = 0, charIndex = 0;
            let isLeft = true;

            function typeText() {
                if (textIndex < textArray.length) {
                    if (charIndex < textArray[textIndex].length) {
                        if (isLeft) {
                            textElement.innerHTML += textArray[textIndex].charAt(charIndex);
                        } else {
                            rightTextElement.innerHTML += textArray[textIndex].charAt(charIndex);
                        }
                        charIndex++;
                        setTimeout(typeText, 25);
                    } else {
                        if (isLeft) {
                            textElement.innerHTML += "<br><br>";
                        } else {
                            rightTextElement.innerHTML += "<br><br>";
                        }
                        textIndex++;
                        charIndex = 0;
                        if (textElement.scrollHeight > textElement.clientHeight) {
                            isLeft = false;
                        }
                        setTimeout(typeText, 1000);
                    }
                }
            }
            typeText();
        }

        function openSection(sectionId) {
            document.querySelectorAll('.section').forEach(section => {
                section.classList.remove('active');
            });
            document.getElementById(sectionId).classList.add('active');
        }

        function searchFunction() {
            let input = document.getElementById("searchInput").value.trim().toLowerCase();
            document.querySelectorAll("#searchList li").forEach(item => {
                item.style.display = item.innerText.toLowerCase().includes(input) ? "" : "none";
            });
        }
    </script>
</body>
</html>

What is visible

What stuff is there but not visible

What I want to do with what is not Visible

Uncaught runtime erros in axios get request in react app with express js backend

I’ve been working on a project and i react frontend and express backend so the backend is running on port 3000 and frontend is running on port 3001 what i was trying to do is add a axios get.to a button in my signup page like this when i run the frontend and backend and i get network issue the console error was i getting is this

axios error and console error

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:3000/auth/github. (Reason: CORS header ‘Access-Control-Allow-Origin’ does not match ‘http://localhost:3000’).

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:3000/auth/github. (Reason: CORS request did not succeed). Status code: (null).

Uncaught (in promise) 
Object { stack: "AxiosError@http://localhost:3001/static/js/bundle.js:42195:18nhandleError@http://localhost:3001/static/js/bundle.js:41553:14n", message: "Network Error", name: "AxiosError", code: "ERR_NETWORK", config: {…}, request: XMLHttpRequest }

my axios get is this

const githubApiCall = () => {
  axios.get('http://localhost:3000/auth/github');
}

my tsx file link is this

<div className="text-center mt-6">
          <button onClick={githubApiCall} className="w-full flex items-center justify-center bg-white border p-3 rounded mb-4">
          <GitHubIcon className="h-5 w-5 mr-2" />
            Signup with GitHub
          </button>
        </div>

the backend route i have is this

authroute.get(
  "/github/",
  passport.authenticate("github", { scope: ["profile", "email"] }),
);
authroute.get(
  "/github/callback",
  passport.authenticate("github"),
  (req: Request, res: Response) => {
    res.redirect("http://localhost:3001");
  },
);

my express setup is this

import express, { Request, Response } from "express";
import { IndexRoute } from "./routes";
import DB from "./lib/DB";
import { authroute } from "./routes/auth";
import { json } from "body-parser";
const cookieSession = require("cookie-session");
require("dotenv").config();

const cors = require('cors');

DB;

const app = express();
const port: number = 3000;


app.use(cors());
app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept'
  );
  next();
});
app.use(
  cookieSession({
    maxAge: 24 * 60 * 60 * 1000,
    keys: [process.env.COOKIEKEY],
  }),
);

app.use(json());
app.use("/", IndexRoute);
app.use("/auth", authroute);

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
}).on('error',(e)=>{
  console.log(e);
});

Safari – Audio Management for inactive tabs

Problem

In our enterprise React application, we’re experiencing audio playback issues in Safari with the following scenario:

  1. The app contains an iframe that plays MP3 narrations using Howler.js
  2. When the tab becomes inactive due to no user activity, Safari kills the resources and stops the audio
  3. Upon returning to the tab and interacting, all functionality resumes except for audio playback
  4. Currently, the only workaround is closing the tab and opening a new one

Question

Is there a way to restore audio playback functionality when the tab becomes active again, without requiring a tab refresh?

Technical Details

  1. Framework: React Audio
  2. Library: Howler.js
  3. Browser: Safari Content
  4. MP3 files played within an iframe

Expected Behavior

Audio playback should resume normally when returning to the tab and interacting with it.

Current Behavior

Audio remains non-functional after tab becomes active again, requiring a complete tab refresh to restore functionality.

How to exchange selected value using toggle functionality?

There is a project for travel agency in which i am showing airport name and city and i want to swap/exchange selected airport and city using toogle functionality.

Here is the picture:
toogle functionality

Here is the code:

 <div style={{position:'relative'}} className='col-lg-6'>
       
        <span className='switch_destination'><Link onClick={toogleDestination} to=''> <PiArrowsLeftRight/></Link></span>
       
        <div className='row g-0'>
            
        <div className='col-lg-6'> 
                <AirportContainer
                  Source={'From'}
                  icon={
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      width="27"
                      viewBox="0 0 640 512"
                    >
                      <path d="M381 114.9L186.1 41.8c-16.7-6.2-35.2-5.3-51.1 2.7L89.1 67.4C78 73 77.2 88.5 87.6 95.2l146.9 94.5L136 240 77.8 214.1c-8.7-3.9-18.8-3.7-27.3 .6L18.3 230.8c-9.3 4.7-11.8 16.8-5 24.7l73.1 85.3c6.1 7.1 15 11.2 24.3 11.2H248.4c5 0 9.9-1.2 14.3-3.4L535.6 212.2c46.5-23.3 82.5-63.3 100.8-112C645.9 75 627.2 48 600.2 48H542.8c-20.2 0-40.2 4.8-58.2 14L381 114.9zM0 480c0 17.7 14.3 32 32 32H608c17.7 0 32-14.3 32-32s-14.3-32-32-32H32c-17.7 0-32 14.3-32 32z" />
                    </svg>
                  }
                  onAirportSelect={handleAirportSelectFrom}
                  airportPayload={
                    searchPayload?.segments && searchPayload?.segments[0].origin
                  }
                />
              </div>

              <div className='col-lg-6'> 
              
                <AirportContainer
                  Source={'To'}
                  icon={
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      width="27"
                      viewBox="0 0 640 512"
                    >
                      <path d="M381 114.9L186.1 41.8c-16.7-6.2-35.2-5.3-51.1 2.7L89.1 67.4C78 73 77.2 88.5 87.6 95.2l146.9 94.5L136 240 77.8 214.1c-8.7-3.9-18.8-3.7-27.3 .6L18.3 230.8c-9.3 4.7-11.8 16.8-5 24.7l73.1 85.3c6.1 7.1 15 11.2 24.3 11.2H248.4c5 0 9.9-1.2 14.3-3.4L535.6 212.2c46.5-23.3 82.5-63.3 100.8-112C645.9 75 627.2 48 600.2 48H542.8c-20.2 0-40.2 4.8-58.2 14L381 114.9zM0 480c0 17.7 14.3 32 32 32H608c17.7 0 32-14.3 32-32s-14.3-32-32-32H32c-17.7 0-32 14.3-32 32z" />
                    </svg>
                  }
                  onAirportSelect={handleAirportSelectTo}
                  airportPayload={
                    searchPayload?.segments &&
                    searchPayload?.segments[0].destination
                  }
                />
              </div>
            </div>
          </div>

    </div>

Why is form.getEditor(‘CheckInToDate’) returning undefined in my DevExpress form with custom templates?

I’m using DevExpress’s Form widget to create two date editors: CheckInFromDate and CheckInToDate. I want to enable the second date box (CheckInToDate) only when the first date box (CheckInFromDate) has a valid date selected. However, calling form.getEditor(‘CheckInToDate’) inside onValueChanged of the first date box returns undefined.

Here’s a simplified version of my code:

$(function () {
  const formData = {};

  const form = $("#formContainer")
    .dxForm({
      formData: formData,
      items: [
        {
          itemType: "group",
          colCount: 2,
          items: [
            {
              dataField: "CheckInFromDate",
              label: { text: "Check-In from" },
              editorType: "dxDateBox",
              template: function (data, $itemElement) {
                const editorTemplate = $("<div>").dxDateBox({
                  dataField: "CheckInFromDate2",
                  showClearButton: true,
                  displayFormat: "dd/MM/yyyy",
                  onValueChanged: function (e) {
                    const minDate = e.value;
                    form.getEditor("CheckInToDate").option("min", minDate);
                    form
                      .getEditor("CheckInToDate")
                      .option("disabled", !minDate);
                  },
                });

                editorTemplate
                  .dxValidator({
                    validationGroup: "checkInValidator",
                    validationRules: [
                      {
                        type: "required",
                        message: "At least one valid date pair is required.",
                      },
                    ],
                  })
                  .appendTo($itemElement);

                editorTemplate.dxDateBox("instance");
              },
            },
            {
              dataField: "CheckInToDate",
              label: { text: "To" },
              template: function (data, $itemElement) {
                $("<div>")
                  .dxDateBox({
                    showClearButton: true,
                    displayFormat: "dd/MM/yyyy",
                    disabled: true,
                  })
                  .dxValidator({
                    validationGroup: "checkInValidator",
                    validationRules: [
                      {
                        type: "required",
                        message: "At least one valid date pair is required.",
                      },
                    ],
                  })
                  .appendTo($itemElement);
              },
            },
          ],
        },
      ],
    })
    .dxForm("instance");
});

I assumed that adding dataField: 'CheckInToDate' plus a custom template for the second item would make form.getEditor('CheckInToDate') return the editor instance.

I also attempted setting editorType: 'dxDateBox' for the second item, but the custom template overrides that.

My expectation is to retrieve the second date box instance via the form’s getEditor method so I can enable/disable it dynamically..

Programmatically import a CSS file by its relative file path

Background

I’m hosting legacy React components in an Angular app using a method similar to this one. There are lots of React components to bring in. They all import their own CSS with JS module imports:

import "./Admin.css";

This means every component is responsible for importing its own stylesheet, which is great. If I could just get the compiler to be happy with this like it was in the React app, that would be the best possible outcome.

The problem

The problem is this doesn’t work when the React component is hosted in Angular. Importing a CSS file this way breaks compilation:

X [ERROR] Could not resolve "./Admin.css"

src/app/components/react/Admin/Admin.tsx:3:7:
  3 │ import "./Admin.css";
    ╵        ~~~~~~~~~~~~~

Instead, I have to use CSS imports to include the React component’s stylesheet in the .scss file of the Angular host:

// Inside AdminHost.component.scss
@import '../../react/Admin/Admin';

This comes with a much bigger problem: Now that each React component isn’t responsible for its own styles, I have to dig through all of Admin.tsx’s child components and manually import all their stylesheets as above. Then their children, and so on. Even if a component is the child of several parents, I have to repeat this process every time unless I happen to remember all the stylesheets it needs. There are a lot of components in the project, so it’s a ton of tree-walking.

Attempt 1

I tried programmatically importing the CSS files with a helper function:

// sanitizer is injected by the Angular host
export function importCss(cssUrl: string, sanitizer: DomSanitizer) {
  const head = document.getElementsByTagName('head')[0];
  const style = document.createElement('link');
  style.rel = 'stylesheet';
  style.href = sanitizer.sanitize(SecurityContext.URL, cssUrl);
  head.appendChild(style);
}

// inside Admin.tsx:
importCss("path/to/Admin.css");

You may have already spotted the problem: cssUrl is a URL, not a relative file path. This means I can no longer import "./Admin.css". I have to use a URL, and that means Admin.css has to live in a static assets folder. I explored this a little before deciding it wouldn’t save me any work.

Attempt 2

My current approach is to use a convention. Whenever I bring over a React component, I use the following rules:

  1. The stylesheet of the base React component gets imported into the Angular host’s .scss.
  2. Each React component should have a .scss file of the same name in the same folder.
  3. If a component has children, it should import its immediate children’s stylesheets into its own .scss. Each level is responsible for providing its own styles and importing the next level.
  4. The React stylesheets are all .css files. Whenever I complete this process for a component and its entire descendant tree, I rename the stylesheet to .scss. This way, if some other component includes it as a child, I know I can just import that one sheet and don’t have to dig through all its descendants looking for styles.

The advantage here is it saves a lot of repeated effort. The problem is it doesn’t save enough. There’s still a lot of pain and manual tree-walking. I’m hoping there’s a better way.

Ideas

  1. In a perfect world, there’s a way to make Angular’s compiler recognize the import "./Admin.css" line and play nicely with it. All my attempts to do this have failed. (I should say I’m just assuming this is from Angular’s compiler, since it worked in the React app.)
  2. I could use a Bash script to dump all the style files in a static assets folder. That would make the idea from Attempt 1 pretty doable. The downside is they wouldn’t be as easy to find when you’re working on a component, and since some have duplicate names, the proper way to do it would be to recreate the same folder structure. That seems a little awkward, and it’s pushing the limits of my Bash skills.
  3. There might be a way to import the CSS file as a blob, turn it into a data URL, and use that in a link tag. Then Attempt 1 would work. I don’t know how to do that, though.

Unable to handle browser’s inbuilt pfx authentication (SSL_CLIENT_AUTH_CERT) certificate popup in Playwright

(This is with reference to the answer https://stackoverflow.com/a/78943350/12876441)

I am facing same issue i.e not able to handle ‘Select Certificate’ pop up and click on ok button from the pop up with Playwright Version 1.50.0 and latest Chromium version.
Here is my code for the reference

    import { test } from '@playwright/test';
    import { defineConfig } from '@playwright/test';
    const path = require('path');
    const fs = require('fs');

    export default defineConfig({
    use: {
        clientCertificates: [{
          origin: 'https://t01:8443/Ter/#/login',
          pfxPath: 'C:/o/tk/cert/t-lp-tre/t-lp-truststore.p12',
          passphrase: 'tore', 
        }],       
      },      
    });

    // Main login function
    async function login(browser, username, password) {
      console.log('Starting login process...');

        const context = await browser.newContext({
        ignoreHTTPSErrors: true, 
      });

      const page = await context.newPage();     
      
      await page.goto("https://t01:8443/Ter/#/login");

      console.log('Navigating to the login page...');     

      console.log('Filling in credentials...');
      await page.fill('input[placeholder="Username"]', username);
      await page.fill('input[placeholder="Password"]', password);
      await page.click('button[type="submit"]');  
      await page.waitForLoadState('load'); 

      console.log(`${username} logged in successfully`);
    }

    test('Run Tests with Parallel Users', async ({ browser }) => {
      const dataPath = 'testdata/testdata_2Usr.json';
      const testData = await fs.promises.readFile(dataPath, 'utf-8');
      const jsonData = JSON.parse(testData);
      
      for (const user of jsonData) {
        const { username, password } = user;
        await login(browser, username, password);
      }
    });

And after using solution provided in the link, I am still not able to handle TLS Client Certificates pop up.
I even tried with third party utilities like autoIT but still not able to click on ‘ok‘ on ‘select certificate‘ pop up.

  • Can someone please help with how to click Ok on this pop up?

(I am fairly new to Playwright and js so any suggestions are appreciated.)

  • Note: The p12 certificate I am using is a valid one. And if we click OK button manually on the pop-up then we can see actual web page.

  • Here is the reference image of pop up

Here is the reference image of pop up

Reset Filters button only updates UI after second click [duplicate]

I have buttons Apply Filters and Reset Filters. When I click Apply Filters, it applies filters in the UI immediately, but when I click Reset Filters, it only is clicked after being clicked two times. Here’s the code:

import { useState, useEffect } from 'react';
import { getBusinesses } from '../api/DBRequests';
import categories from '../constants/categories';

const BusinessListPage = () => {
  const [businesses, setBusinesses] = useState([]);
  const [filters, setFilters] = useState({
    category: '',
    state: '',
    minPrice: '',
    maxPrice: '',
    minRevenue: '',
    maxRevenue: '',
  });
  const [sortBy, setSortBy] = useState('');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    fetchBusinesses();
  }, []);

  const fetchBusinesses = async () => {
    setLoading(true);
    try {
      const adjustedSortBy =
        sortBy === 'asc' ? 'asc' : sortBy === 'desc' ? 'desc' : '';
      const businesses = await getBusinesses(adjustedSortBy, filters);
      setBusinesses(businesses);
    } catch (error) {
      console.error('Error fetching businesses:', error.message);
    } finally {
      setLoading(false);
    }
  };

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setFilters((prevFilters) => ({ ...prevFilters, [name]: value }));
  };

  const handleResetFilters = () => {
    setFilters({
      category: '',
      state: '',
      minPrice: '',
      maxPrice: '',
      minRevenue: '',
      maxRevenue: '',
    });
    setSortBy('');
    fetchBusinesses();
  };

  return (
    <div className="container-fluid mt-4">
      <div className="row">
        <div className="col-md-3">
          <div className="bg-light border p-3">
            <h5>Filters</h5>
            <div className="mb-3">
              <label htmlFor="category" className="form-label">
                Category
              </label>
              <select
                id="category"
                name="category"
                className="form-control"
                value={filters.category}
                onChange={handleInputChange}
              >
                <option value="">All Categories</option>
                {categories.map((category, index) => (
                  <option key={index} value={category}>
                    {category}
                  </option>
                ))}
              </select>
            </div>
            <button
              className="btn btn-primary btn-block"
              onClick={fetchBusinesses}
            >
              Apply Filters
            </button>
            <button
              className="btn btn-secondary btn-block mt-2"
              onClick={handleResetFilters}
            >
              Reset Filters
            </button>
          </div>
        </div>

        {/* Businesses List Section */}
        <div className="col-md-9">
          {loading && <p className="d-block mx-auto">Loading</p>}
          <div className="row">
            {businesses.length === 0 && !loading && (
              <div className="col-12">
                <p className="mt-4 text-center">No businesses found.</p>
              </div>
            )}
            {businesses.map((business) => (
              <div className="col-md-4 mb-4" key={business._id}>
                <div className="card h-100">
                  <div className="card-body">
                    <h5 className="card-title">{business.name}</h5>
                  </div>
                  <div className="card-footer">
                    <a
                      href={`/business/${business._id}`}
                      className="btn btn-primary btn-sm btn-block"
                    >
                      View Details
                    </a>
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
};

export default BusinessListPage;

Docker Failed to load resource: net::ERR_NAME_NOT_RESOLVED error

I’m setting up a multi-container environment using Docker Compose (version: ‘3.8’). My setup includes:

Percona MySQL Master & Slave for database replication
Backend service (Spring Boot)
Frontend service (React)
NGINX as a reverse proxy
However, when I try to access the frontend and make call api, I get the following error in the browser network:

Failed to load resource: net::ERR_NAME_NOT_RESOLVED
This suggests that the frontend is unable to resolve the backend API endpoint.

Docker-Compose Configuration (docker-compose.yml)
yaml

version: '3.8'

services:

  # Percona master
  percona-master:
    image: percona:8.0
    container_name: percona-master
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=replicated_db
      - MYSQL_USER=replication_user
      - MYSQL_PASSWORD=replication_password
    ports:
      - "3307:3306"
    volumes:
      - percona_master_data:/var/lib/mysql
      - ./deploy/mysql/master-init:/docker-entrypoint-initdb.d
    command:
      --server-id=1 --log-bin=mysql-bin --binlog-do-db=replicated_db

  # Percona slave
  percona-slave:
    image: percona:8.0
    container_name: percona-slave
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=replicated_db
      - MYSQL_USER=replication_user
      - MYSQL_PASSWORD=replication_password
    ports:
      - "3308:3306"
    volumes:
      - percona_slave_data:/var/lib/mysql
    depends_on:
      - percona-master
    command:
      --server-id=2 --relay-log=relay-bin --read-only=1

  # Backend service
  back:
    build:
      context: ./deploy/back
      dockerfile: Dockerfile
      args:
        GIT_ACCESS_TOKEN: ${GIT_ACCESS_TOKEN}
        GIT_REPO_HTTPS_URL: ${GIT_REPO_BACKEND_HTTPS_URL}
        GIT_BRANCH: ${GIT_BACKEND_BRANCH}
    deploy:
      replicas: 2
      endpoint_mode: dnsrr # Enable DNS round-robin for service discovery
    environment:
      - SPRING_DATASOURCE_URL=jdbc:mysql://percona-master:3306/replicated_db
      - SPRING_DATASOURCE_USERNAME=replication_user
      - SPRING_DATASOURCE_PASSWORD=replication_password
    depends_on:
      - percona-master

  # Frontend service
  front:
    build:
      context: ./deploy/front
      dockerfile: Dockerfile
      args:
        GIT_ACCESS_TOKEN: ${GIT_ACCESS_TOKEN}
        GIT_REPO_HTTPS_URL: ${GIT_REPO_FRONTEND_HTTPS_URL}
        GIT_BRANCH: ${GIT_FRONTEND_BRANCH}
        REACT_APP_API_SERVER: ${REACT_APP_API_SERVER}
    ports:
      - "80:80" 
    environment:
      - REACT_APP_API_SERVER=http://nginx/api
    depends_on:
      - nginx

  # NGINX load balancer
  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "8080:80" # Expose NGINX on localhost:8080
    volumes:
      - ./deploy/nginx/nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - back

volumes:
  percona_master_data:
  percona_slave_data:

NGINX Configuration (nginx.conf)

events {}

http {
    upstream backend {
        server back:8080; # Ensure this matches the backend's exposed port
    }

    server {
        listen 80;

        location /api/ {
            proxy_pass http://backend/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

How can I fix this problem ?

How should you snap an image to a grid via Javascript after it has been rotated with CSS?

I have a 9×6 grid with 80 px squares.

<div class = "grid" style="width: 720px; height:480px; margin: auto; background-color: white;">
<style>
    .grid {
        background-image:
            repeating-linear-gradient(#ccc 0 1px, transparent 1px 100%),
            repeating-linear-gradient(90deg, #ccc 0 1px, transparent 1px 100%);
        background-size: 80px 80px;
        outline: 10px solid black;
    }
</style>

I have an image that is 160px wide and 80px tall.

<img class="movable" src="stick.png" id="Stick" style="left: 256.5px; top: 168px;">

The user can drag the image and drop it onto the grid. It snaps into place perfectly, accounting for the position of the grid.

If the image is rotated using CSS, the snap is 40px off for both dimensions. I tried adding a parent div as a container and noticed that the origin of the image remains in the same place, no matter how it is rotated. Because of this, targ.style.left and targ.getBoundingClientRect().left are totally different values. I also tried rotating the image but moving the parent div – this results in the same behavior. Furthermore, the snap is off even more for larger images.

160px x 80px image is off by (40, -40)

80px x 240px image is off by (-80, 40)

80px x 400px image is off by (-160, 160)

function startDrag(e) {
    // determine event object
    if (!e) {
        var e = window.event;
    }

    if(e.preventDefault) e.preventDefault();

    // IE uses srcElement, others use target
    targ = e.target ? e.target : e.srcElement;

    if (targ.className != 'movable') {return};

    // calculate event X, Y coordinates
        offsetX = e.clientX;
        offsetY = e.clientY;

    // assign default values for top and left properties
    if(!targ.style.left) { targ.style.left=offsetX-(e.target.getBoundingClientRect().width/2)};
    if (!targ.style.top) { targ.style.top=offsetY-(e.target.getBoundingClientRect().height/2)};

    // calculate integer values for top and left 
    // properties
    coordX = parseInt(targ.style.left);
    coordY = parseInt(targ.style.top);
    drag = true;

    // move div element
        document.onmousemove=dragDiv;
    return false;

}

function dragDiv(e) {
    if (!drag) {return};
    if (!e) { var e= window.event};
    // var targ=e.target?e.target:e.srcElement;
    // move div element
    targ.style.left=coordX+e.clientX-offsetX+'px';
    targ.style.top=coordY+e.clientY-offsetY+'px';
    return false;
}

function stopDrag() {
    drag=false;
    let grid_offset = grid.getBoundingClientRect();
    let item_offset = targ.getBoundingClientRect();

    let xa = Math.abs(grid_offset.left - (80*Math.floor(parseInt(grid_offset.left)/80)));
    let ya = Math.abs(grid_offset.top - (80*Math.floor(parseInt(grid_offset.top)/80)));
    
    let snap = {
        x: 80*Math.round((item_offset.left - xa)/80),
        y: 80*Math.round((item_offset.top - ya)/80)
    }
    
    targ.style.left = snap.x + xa;
    targ.style.top = snap.y + ya;

}

function rotate_item (e) {
    e.preventDefault();
    if (!e.target.classList.contains("movable")) return;
    if (!e.target.style.rotate) e.target.style.rotate = "0deg";
    let deg = e.target.style.rotate.slice(0,-3);
    if (e.deltaY < 0) {
        deg = parseInt(deg) + 90;
        e.target.style.rotate = deg + "deg";
    } else {
        deg = parseInt(deg) - 90;
        e.target.style.rotate = deg + "deg";
    }
}

I suppose I could write a lengthy switch to account for all possible image sizes and orientations, or maybe add the difference between targ.style.left and targ.getBoundingClientRect().left, but I’m looking for a more elegant method and want to understand what’s going on. What am I doing wrong here? What is the “proper” way to achieve this?