Halo seru juga ni [closed]

Halo seru juga nih

<!DOCTYPE html>

<html>

<head>

  <style>

    .marquee {

      width: 100%;

      overflow: hidden;

      white-space: nowrap;

      box-sizing: border-box;

    }



    .marquee span {

      display: inline-block;

      padding-left: 100%;

      animation: marquee 10s linear infinite;

    }



    @keyframes marquee {

      0%   { transform: translateX(0%); }

      100% { transform: translateX(-100%); }

    }

  </style>

</head>

<body>

  <div class="marquee">

    <span>Ini adalah contoh tulisan yang bergerak dari kanan ke kiri!</span>

  </div>

</body>

</html>



<!DOCTYPE html>

<html>

<head>

  <style>

    .marquee {

      width: 100%;

      overflow: hidden;

      white-space: nowrap;

      box-sizing: border-box;

    }



    .marquee span {

      display: inline-block;

      padding-left: 100%;

      animation: marquee 10s linear infinite;

    }



    @keyframes marquee {

      0%   { transform: translateX(0%); }

      100% { transform: translateX(-100%); }

    }

  </style>

</head>

<body>

  <div class="marquee">

    <span>Ini adalah contoh tulisan yang bergerak dari kanan ke kiri!</span>

  </div>

</body>

</html>

FAILED: ReferenceError: html2pdf is not defined with Blazor

I use html2pdf.bundle.min.js in my Blazor project. I imported it in my main html of the project, before the main.js.
In the network tab in the browser, I see that the html2pdf.bundle.min.js is sent with status code 200. But when I want to call the html2pdf function inside another javascript function, i get ‘FAILED: ReferenceError: html2pdf is not defined’ in my browsers console.
Any idea why? The code already exists in an older version of the same project, and there it still works.
Here the function where I call the html2pdf. This function gets called successfully, but then it throws an exception because it cannot find html2pdf:

function printDiv(divName) {
  let element = document.getElementById(divName);
  let clone = document.getElementById("clone");
  try {
    let opt = {
      margin: 0,
      filename: 'myfile.pdf',
      image: { type: 'jpeg', quality: 0.98 },
      html2canvas: { scale: 1, clone: clone, removeContainer: true },
      jsPDF: { unit: 'mm', format: 'a4', clone: clone, orientation: 'landscape' }
    };
    html2pdf().set(opt).from(element).toContainer().save();
  } catch  (e) {
    console.error("FAILED:", e);
  }
}

And the App.razor where I add my scripts and stylesheets:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="@Assets["app.css"]" />
    <link rel="stylesheet" href="@Assets["/css/styles.css"]" />
    
    <ImportMap @rendermode="InteractiveServer" />
    <link rel="icon" type="image/png" href="/Assets/logo.png" />
    <HeadOutlet @rendermode="InteractiveServer" />
</head>

<body style="height:100dvh;width:100dvw">
    <Routes @rendermode="InteractiveServer" />

    <script src="_framework/blazor.web.js"></script>
    <script src="_content/MudBlazor/MudBlazor.min.js"></script>
    <script src="_content/MudBlazor.Markdown/MudBlazor.Markdown.min.js"></script>
    <script src="_content/Radzen.Blazor/Radzen.Blazor.js?v=@(typeof(Radzen.Colors).Assembly.GetName().Version)"></script>

    <script src="js/html2pdf.bundle.min.js"></script>
    <script src="js/main.js?v=1.0.0.9"></script>
</body>

</html>

How to distinguish between navigation and refresh page

I have single page app which has following URL pattern https://hostname/{organizationIdentifier}/#{pagePath}.

The app has implement some kind of “cache” based on sessionStorage.

When user switch organization context by navigating to different org (different organizationIdentifier in the URL) I want to trigger cache cleanup.

For this purpose I have implemented window.addEventLister("beforeunload", cleanCache).
I have tried also navigation.addEventListener("navigate", navigateAction) but with no success.

But the cleanCache is triggered also on page refresh.
How can I detect that the unload action will be followed with load action with same URL? How can I trigger cleanCache only on leaving app context or changing organization context?

Uncaught (in promise) SecurityError: Failed to execute ‘toBlob’ on ‘HTMLCanvasElement’: Tainted canvases may not be exported in strapi

Strapi Version: 5.11.3

Node Version: 22.0.0

Database: Sqlite

This issue occurs when I want to save the crop image results in the strapi media library but nothing happens.

I checked and there was an error in the console:
Uncaught (in promise) SecurityError: Failed to execute ‘toBlob’ on ‘HTMLCanvasElement’: Tainted canvases may not be exported

I upload the image to S3 and serve it on CDN.

Is there any additional configuration to solve this?

this is my configuration in middleware strapi:

module.exports = ({env}) => [
  'strapi::logger',
  'strapi::errors',
  'strapi::cors',
  'strapi::poweredBy',
  'strapi::query',
  'strapi::body',
  'strapi::session',
  'strapi::favicon',
  'strapi::public',
  {
   name:'strapi::security',
   config: {
    contentSecurityPolicy: {
      useDefaults: true,
      directives: {
        "connect-src": ["'self'", "https:"],
        "img-src": [
          "'self'",
          "data:",
          "blob:",
          "dl.airtable.com",
          https://${env("aws_bucket")}.s3.${env(/
            "AWS_REGION"
          )}.amazonaws.com/,
          env("CDN_URL"),
        ],
        "media-src": [
          "'self'",
          "data:",
          "blob:",
          "dl.airtable.com",
          https://${env("aws_bucket")}.s3.${env(/
            "AWS_REGION"
          )}.amazonaws.com/,
          env("CDN_URL"),
        ],
        "frame-src": [
            env("CLIENT_URL") 
          ],
        upgradeInsecureRequests: null,
        
      },
    },
  },
  },
];

And I also read with a somewhat similar issue that I should add crossorigin=”anonymous” but I’m confused where should I add that? And i have made allowOrigins and allowheaders to be * in both S3 and CDN

React Native [runtime not ready] Error: Non-js exception

Disclaimer

I am a novice at React Native and any information on what to do would be extremely helpful.

Context

My RN app was working fine and all of a sudden when I built the application and ran it on the simulator, I was hit by this error upon launching. DevTools does not log anything useful except for what is displayed on the screen. I have tried to debug this for an entire day but to no avail.

Screenshot of simulator

Cause of Error

Unknown. I reset my code to a known working commit but the error persisted. I do not know why it occurred or how to reproduce it.

Environment Info:

react-native-cli: 2.0.1

react-native: 0.79.2

System: macOS 15.4.1

Simulator: iPhone 16 Pro iOS 18.4

What I tried and didn’t work

  • I tried to rollback to a known working git commit
  • I cleaned my project and reinstalled the modules and dependencies
  • I erased all data and settings on my simulator
  • I rebooted my system

how can i get value from autocompleted field using javascript

I have an issue. I want to retrieve the value of an autocompleted input field without focusing on the element.

Here is my code:

document.addEventListener("DOMContentLoaded", function () {
    const emailField = document.getElementById("email");

    setTimeout(function () {
        console.log(emailField.value);
    }, 1000);
});
<input type="text" class="input-field" name="email" id="email" placeholder="Email" required>

I tried the code above, but it doesn’t work. I also tried focusing or clicking the element by dispatching an event on the input field, but each time I get an empty string (“”).

How can I retrieve the value?

How to add an audio to a DOM button?

I am trying to enhance my Js knowledge by building a simple music player in HTML, JS and Tailwind CSS.

I am trying to add the DOM buttons to all of the audios. I expect the buttons to play the audio if you click on it. Although, it does not play an audio and it does not show any errors. I don’t understand what is wrong with my code.

My Js code:


const allSongs = [
    {
    id: 0,
    title: "Scratching The Surface",
    artist: "Quincy Larson",
    duration: "4:25",
    src:  //here is a link
  },
  {
    id: 1,
    title: "Can't Stay Down",
    artist: "Quincy Larson",
    duration: "4:15",
    src:  //here is a link
  },
  {
    id: 2,
    title: "Still Learning",
    artist: "Quincy Larson",
    duration: "3:51",
    src:  //here is a link
  },
  {
    id: 3,
    title: "Cruising for a Musing",
    artist: "Quincy Larson",
    duration: "3:34",
    src: //here is a link
  },
  {
    id: 4,
    title: "Never Not Favored",
    artist: "Quincy Larson",
    duration: "3:35",
    src: //here is a link
  },
  {
    id: 5,
    title: "From the Ground Up",
    artist: "Quincy Larson",
    duration: "3:12",
    src: //here is a link
  },
  {
    id: 6,
    title: "Walking on Air",
    artist: "Quincy Larson",
    duration: "3:25",
    src: //here is a link
  },
  {
    id: 7,
    title: "Can't Stop Me. Can't Even Slow Me Down.",
    artist: "Quincy Larson",
    duration: "3:52",
    src: //here is a link
  },
  {
    id: 8,
    title: "The Surest Way Out is Through",
    artist: "Quincy Larson",
    duration: "3:10",
    src: //here is a link
  },
  {
    id: 9,
    title: "Chasing That Feeling",
    artist: "Quincy Larson",
    duration: "2:43",
    src: //here is a link
  },
];

const songs = allSongs.sort((a, b) => a.title.localeCompare(b.title));
const currentSongTime = 0;
const currentSong = null;
const song = songs.find((song, id) => song.id === id);
const audio = new Audio();


const renderSongs = () => {

  songs.forEach(data => {
    playlist.innerHTML +=`
    <li class="flex flex-row items-center gap-2 border-b border-b-yellow-600 border-b-2 border-dashed px-3 py-4">
    <button id="playlistBtn" class="flex flex-row items-center gap-3">
    <div class="flex-1 flex-wrap">${data.title}</div>
    <div class="text-[13px] flex-none mr-2">${data.artist}</div>
    <div>${data.duration}</div>
    </button>
    <button id="removeBtn">
        <svg width="30" height="25" fill="white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>
    </button>
    </li>
    `;
  });
 
  playlist.appendChild(li);
  playlist.appendChild(playlistBtn);
  playlist.appendChild(removeBtn);

  // by clicking on a song, it will play a clicked song
  playlistBtn.addEventListener("click", () => {
    audio.src = song.src;
    audio.play();
  })

}
renderSongs();

Backend to Frontend malfunction

My function “findDate()”always gives me false when called from backend. It is supposed to search for a date not in the date format but as plain text and will return true or false if it finds it or not.

I am using Google Spreadsheets paired with apps scripts. The format used for the dates is “dd/MM/yyyy”.
The html part uses bootstrap5 (for the datepicker).

Html header:

<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous">
  </script>

Html body:

<div id="datepick" class="solid mx-auto" style="margin-bottom: 10px">
  <div class="d-flex justify-content-center">
    <input id="datep" class="form-control" type="date" value="" style="width: 45%">
  </div>
  <div class="d-flex justify-content-center">
    <button class="btn btn-outline-primary" id="btn_date" style="margin: 5px;">choose date</button>
  </div>
</div>

Backend functions:

function findDate(date) {
  const data = SpreadsheetApp.openById('1FMCp-T3fuW9NHUFfvgoeBE9l5dXxWWG5UOqldF0OTDE')
      .getActiveSheet().getDataRange().getValues();
  var parts = date.split('-');
  var year = parseInt(parts[0], 10);
  var month = parseInt(parts[1], 10);
  if (month < 10) {month = "0"+month} 
  var day = parseInt(parts[2], 10);
  console.log("input=>" + date +"; day "+day+", month "+month+ ", year "+year)
  var refDate = day +"/"+ month +"/" + year
  for (var i = 1; i < data.length; i++) {
    var date_gs = data[i][0];
    console.log("Date being compared : " + date_gs);
    if (date_gs == refDate) {
      console.log('found')
      return true
    }
  }
  return false
}

function testFD() {
  var e = findDate("2025-05-23")
  console.log(e)
}
function doGet() {
  return HtmlService.createHtmlOutputFromFile('html')
}

The function works out perfectly fine when i’m testing it mannually (putting the input my frontend uses to call the backend, found on the console log). But it does not work when the backend gets called from the frontend.

So with the same input tried from within my web app and from apps script itself is different, I will always get false when using back to front and I can get true using an inside manual callout of the getDate.

Javascript:

<script>
  let dateInput;
  var btn_datepicker = document.getElementById('btn_date')
  var datepick = document.getElementById('datep')
  function getD(dateInput) {  
    console.log("findDate  backend call with arg : " + dateInput)
    var e = google.script.run.findDate(dateInput);
    console.log(e)  
  }
  btn_datepicker.addEventListener('click', function(e) {
    dateInput = datepick.value
    getD(dateInput)
  })
</script>

link to a copy of my gs:

https://docs.google.com/spreadsheets/d/1FMCp-T3fuW9NHUFfvgoeBE9l5dXxWWG5UOqldF0OTDE/edit?usp=sharing

How to prevent BlockNote editor from losing focus when clicking on an adjacent SVG, without breaking SVG text selection or events?

I’m using BlockNote for the left-side rich text editor in my React app. On the right, I render a dynamic mind map using SVG (e.g., from markmap-lib).

When I click on the (on the right side), the BlockNote editor on the left loses focus, and the blinking cursor disappears.

I want to preserve the editor focus, so that I can keep typing right after interacting with the SVG.

But if I prevent mousedown on the SVG (e.g., e.preventDefault() or e.stopPropagation()), it breaks SVG interactions, such as: selecting inside the SVG, responding to clicks/double-clicks on nodes

I want to:

Keep BlockNote editor focused even after clicking on the SVG

Allow normal SVG interaction: click, double-click, and text selection

demo:
https://stackblitz.com/edit/vitejs-vite-h237idks?file=src%2FApp.tsx

Undetectable JavaScript Error Object Override: Bypassing Standard Detection Mechanisms

I’m attempting to create an undetectable method of overriding the Error object in JavaScript without triggering standard detection mechanisms. My goal is to modify the Error prototype or constructor in a way that cannot be easily identified by typical inspection techniques.

I’ve explored multiple strategies for overriding the Error object:

  • Using Proxy
  • Arrow Function
  • Long Function(Error=function(){})
  • Short Function(Error={Error(){}}.Error)

I’ve developed a comprehensive test function to validate the stealth of the override:

function testError(){
    if(Object.hasOwn(Error,"caller") || Object.hasOwn(Error,"arguments") || Object.hasOwn(Error,"prepareStackTrace"))
        return false;
    try {
        new Error()
    } catch (error) {
        return false;
    }
    try {
        Error()
    } catch (error) {
        return false;
    }
    try {
        Object.create(Error).toString()
    } catch (error) {
        if(error.stack.includes("Object.toString"))
           return false
    }
    return true
}

What techniques can successfully bypass these detection mechanisms and pass the provided testError() function?

Current challenges:

  • Most standard override techniques are easily detectable
  • Need a method that doesn’t trigger standard inspection checks

leaf task not updated after parent task update

I am trying to build a function to distribute amounts across a WBS. So if the amount is assigned to an element all the parents and children should be updated accordingly.
If I try to update an element (ie. 1.2) and then try to update 1.2.1.3 the update works fine for 1.2 but nothing is updated on the UI after changing 1.2.1.3. I can see that the function updateTaskAmount(task) is correctly called for all the relevant items but nothing changes visually. Where do I go wrong?
Please note that while debugging I am also getting rid of jquery so you will find a mix of the two in the code

        const tasks = [{"id":1,"id_gantt":1,"baseline_id":1,"text":"progetto template","start_date":"2025-01-01T08:00:00.000Z","duration":210,"parent":0,"sortorder":2,"progetto":1,"type":"project","amount":"0.00","version":1},{"id":2,"id_gantt":2,"baseline_id":1,"text":"Preparation","start_date":"2025-01-01T08:00:00.000Z","duration":8,"parent":1,"sortorder":3,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":3,"id_gantt":10,"baseline_id":1,"text":"setup project","start_date":"2025-01-01T08:00:00.000Z","duration":6,"parent":2,"sortorder":4,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":4,"id_gantt":9,"baseline_id":1,"text":"conduct kickoff","start_date":"2025-01-08T08:00:00.000Z","duration":1,"parent":2,"sortorder":5,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":5,"id_gantt":8,"baseline_id":1,"text":"quality gate","start_date":"2025-01-09T17:00:00.000Z","duration":0,"parent":2,"sortorder":6,"progetto":1,"type":"milestone","amount":"0.00","version":1},{"id":6,"id_gantt":3,"baseline_id":1,"text":"Blueprint","start_date":"2025-01-10T08:00:00.000Z","duration":41,"parent":1,"sortorder":7,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":7,"id_gantt":13,"baseline_id":1,"text":"Workshops","start_date":"2025-01-10T08:00:00.000Z","duration":41,"parent":3,"sortorder":8,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":8,"id_gantt":12,"baseline_id":1,"text":"Stream 1","start_date":"2025-01-10T08:00:00.000Z","duration":13,"parent":13,"sortorder":9,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":9,"id_gantt":11,"baseline_id":1,"text":"Stream 2","start_date":"2025-01-24T08:00:00.000Z","duration":13,"parent":13,"sortorder":10,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":10,"id_gantt":25,"baseline_id":1,"text":"Stream 3","start_date":"2025-02-07T08:00:00.000Z","duration":13,"parent":13,"sortorder":11,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":11,"id_gantt":4,"baseline_id":1,"text":"Realization","start_date":"2025-02-21T08:00:00.000Z","duration":70,"parent":1,"sortorder":12,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":12,"id_gantt":16,"baseline_id":1,"text":"Configuration","start_date":"2025-02-21T08:00:00.000Z","duration":55,"parent":4,"sortorder":13,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":13,"id_gantt":23,"baseline_id":1,"text":"Configure","start_date":"2025-02-21T08:00:00.000Z","duration":41,"parent":16,"sortorder":14,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":14,"id_gantt":22,"baseline_id":1,"text":"Unit test","start_date":"2025-04-04T07:00:00.000Z","duration":13,"parent":16,"sortorder":15,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":15,"id_gantt":15,"baseline_id":1,"text":"Development","start_date":"2025-02-21T08:00:00.000Z","duration":69,"parent":4,"sortorder":16,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":16,"id_gantt":14,"baseline_id":1,"text":"Develop & Unit Test","start_date":"2025-02-21T08:00:00.000Z","duration":69,"parent":15,"sortorder":17,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":17,"id_gantt":24,"baseline_id":1,"text":"Quality gate","start_date":"2025-05-02T16:00:00.000Z","duration":0,"parent":4,"sortorder":18,"progetto":1,"type":"milestone","amount":"0.00","version":1},{"id":18,"id_gantt":5,"baseline_id":1,"text":"Test & Training","start_date":"2025-05-05T07:00:00.000Z","duration":42,"parent":1,"sortorder":19,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":19,"id_gantt":19,"baseline_id":1,"text":"Training","start_date":"2025-05-05T07:00:00.000Z","duration":25,"parent":5,"sortorder":20,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":20,"id_gantt":18,"baseline_id":1,"text":"Test","start_date":"2025-05-19T07:00:00.000Z","duration":25,"parent":5,"sortorder":21,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":21,"id_gantt":17,"baseline_id":1,"text":"Quality gate","start_date":"2025-06-16T07:00:00.000Z","duration":0,"parent":5,"sortorder":22,"progetto":1,"type":"milestone","amount":"0.00","version":1},{"id":22,"id_gantt":6,"baseline_id":1,"text":"Go live","start_date":"2025-06-17T07:00:00.000Z","duration":43,"parent":1,"sortorder":23,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":23,"id_gantt":21,"baseline_id":1,"text":"Go no go decision","start_date":"2025-06-17T07:00:00.000Z","duration":1,"parent":6,"sortorder":24,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":24,"id_gantt":20,"baseline_id":1,"text":"Hypercare","start_date":"2025-06-18T07:00:00.000Z","duration":41,"parent":6,"sortorder":25,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":25,"id_gantt":26,"baseline_id":1,"text":"Quality gate","start_date":"2025-07-30T16:00:00.000Z","duration":0,"parent":6,"sortorder":26,"progetto":1,"type":"milestone","amount":"0.00","version":1}];

        // Add user_amount property initialized to null
        tasks.forEach(task => task.user_amount = null);

        // Build a map for quick parent lookup
        const taskMap = new Map();
        tasks.forEach(task => taskMap.set(task.id_gantt, { ...task, children: [] }));

        function buildWBS(tasks) {
        const roots = [];
        tasks.forEach(task => {
            const t = taskMap.get(task.id_gantt);
            if (task.parent !== 0) {
            const parent = taskMap.get(task.parent);
            if (parent) parent.children.push(t);
            } else {
            roots.push(t);
            }
        });
        return roots;
        }
        // Initialize the dropdown
        function initializeDropdown(data) {
            // Helper function to assign wbsNumber recursively
            function assignWBSNumbers(items, parentId = 0, prefix = '') {
                let index = 1;
                items
                    .filter(item => item.parent === parentId)
                    .sort((a, b) => a.sortorder - b.sortorder)
                    .forEach(item => {
                        const wbsNumber = prefix ? prefix + '.' + index : '' + index;
                        item.wbsNumber = wbsNumber;
                        // Recursively assign to children
                        assignWBSNumbers(items, item.id_gantt, wbsNumber);
                        index++;
                    });
            }
            assignWBSNumbers(data);
            const select = document.getElementById("element-select");
            let firstOption = document.createElement("option");
            firstOption.value = "";
            firstOption.textContent = "Choose a WBS element to update";
            select.appendChild(firstOption);
            data.forEach(item => {
                const option = document.createElement("option");
                option.value = item.id_gantt;
                option.textContent = item.wbsNumber + ' ' + item.text;
                select.appendChild(option);
            });
        }   
        function getWeight(task) {
            if (task.parent === 0) return 100;
            const parent = taskMap.get(task.parent);
            if (!parent) return 0;
            const parentDuration = parent.duration === 0 ? 1 : parent.duration;
            const taskDuration = task.duration === 0 ? 1 : task.duration;
            return ((taskDuration / parentDuration) * 100).toFixed(2);
        }

        function renderWBS(tasks, container, wbsPrefix = '', level = 1, parentId = null) {
            const numberFormat = new Intl.NumberFormat('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
            tasks.sort((a, b) => a.sortorder - b.sortorder);
            tasks.forEach((task, index) => {
                const wbsCode = wbsPrefix ? `${wbsPrefix}.${index + 1}` : `${index + 1}`;
                const hasChildren = task.children.length > 0;
                const weight = getWeight(task);

                const row = document.createElement('tr');
                row.setAttribute('data-id', task.id_gantt);
                row.setAttribute('data-level', level);
                if (parentId !== null) {
                    row.setAttribute('data-parent', parentId);
                    row.classList.add('hidden-row');
                }

                const toggleIcon = hasChildren ? `<span class="toggle-btn" data-toggle-id="${task.id}">▶</span>` : '';
                const startDate = new Date(task.start_date).toLocaleDateString();

                // Format weight: dot as thousands, comma as decimal
                const formattedWeight = numberFormat.format(weight) + '%';
                // Format amount: dot as thousands, comma as decimal, prepend € with space
                const formattedAmount = '€ ' + numberFormat.format(parseFloat(task.amount));
                // Display duration: if 0, show 1 instead
                const displayDuration = task.duration === 0 ? 1 : task.duration;

                row.innerHTML = `
                    <td>${wbsCode}</td>
                    <td>
                        <span class="indent" style="--level: ${level};">
                        ${toggleIcon}${task.text}
                        </span>
                    </td>
                    <td>${startDate}</td>
                    <td>${displayDuration}</td>
                    <td>${formattedWeight}</td>
                    <td><span class="computed-amount" data-id="${task.id_gantt}">${formattedAmount}</span></td>
                `;

                container.appendChild(row);

                if (hasChildren) {
                    renderWBS(task.children, container, wbsCode, level + 1, task.id);
                }
            });
        }

        function toggleRowChildren(toggleId, expand) {
        const rows = document.querySelectorAll(`tr[data-parent='${toggleId}']`);
        rows.forEach(row => {
            if (expand) {
            row.classList.remove('hidden-row');
            } else {
            row.classList.add('hidden-row');
            }

            // Collapse children recursively
            if (!expand) {
            const childId = row.getAttribute('data-id');
            toggleRowChildren(childId, false);
            const icon = row.querySelector(`[data-toggle-id='${childId}']`);
            if (icon) icon.textContent = '▶';
            }
        });
        }

        function addToggleHandlers() {
        document.querySelectorAll('[data-toggle-id]').forEach(icon => {
            icon.addEventListener('click', () => {
            const id = icon.getAttribute('data-toggle-id');
            const isExpanded = icon.textContent === '▼';
            toggleRowChildren(id, !isExpanded);
            icon.textContent = isExpanded ? '▶' : '▼';
            });
        });
        }

        // Expand/collapse WBS to a certain level
        let currentLevel = 1;
        function setWBSLevel(level) {
            const rows = document.querySelectorAll('#wbsTableBody tr');
            let maxLevel = 1;
            rows.forEach(row => {
                const rowLevel = parseInt(row.getAttribute('data-level'), 10);
                if (!isNaN(rowLevel) && rowLevel > maxLevel) maxLevel = rowLevel;
            });
            // Clamp level to range [1, maxLevel]
            if (level < 1) level = 1;
            if (level > maxLevel) level = maxLevel;
            currentLevel = level;
            rows.forEach(row => {
                const rowLevel = parseInt(row.getAttribute('data-level'), 10);
                if (!isNaN(rowLevel)) {
                    if (rowLevel <= level) {
                        row.classList.remove('hidden-row');
                        // set toggle icon to expanded for rows with children at this level
                        const icon = row.querySelector('[data-toggle-id]');
                        if (icon) icon.textContent = (rowLevel < level) ? '▼' : '▶';
                    } else {
                        row.classList.add('hidden-row');
                    }
                }
            });
            // For all rows at the current level, set their toggle icon to collapsed (▶)
            rows.forEach(row => {
                const rowLevel = parseInt(row.getAttribute('data-level'), 10);
                if (!isNaN(rowLevel) && rowLevel === level) {
                    const icon = row.querySelector('[data-toggle-id]');
                    if (icon) icon.textContent = '▶';
                }
            });
        }

        // Update amounts based on user input and propagate changes
        function updateAmounts() {
            const numberFormat = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
            // Reset all amounts to 0 before recalculating
            taskMap.forEach(task => {
                task.amount = 0;
            });

            // Helper function to update parent amounts recursively
            function updateParentAmounts(task) {
                if (task.parent === 0) return;
                const parent = taskMap.get(task.parent);
                if (!parent) return;
                let sumAmounts = 0;
                parent.children.forEach(child => {
                    sumAmounts += parseFloat(child.amount);
                });
                parent.amount = sumAmounts.toFixed(2);
                updateParentAmounts(parent);
            }

            // Recursive function to update amounts for a task and its children
            function updateTaskAmount(task) {
                //console.log(`Updating task: ${task.text} (ID: ${task.id_gantt})`);
                if (task.children.length > 0) {
                    if (task.user_amount !== null && task.user_amount !== '') {
                        // Clear user_amount for all children before distributing
                        task.children.forEach(child => { child.user_amount = null; });
                        // Distribute user_amount among children by weight
                        let totalWeight = 0;
                        task.children.forEach(child => {
                            totalWeight += parseFloat(getWeight(child));
                        });
                        task.children.forEach(child => {
                            const weight = parseFloat(getWeight(child));
                            const childUserAmount = (weight / totalWeight) * parseFloat(task.user_amount);
                            child.user_amount = childUserAmount.toFixed(2);
                            updateTaskAmount(child);
                        });
                        // After children updated, sum their amounts for this task
                        let sumAmounts = 0;
                        task.children.forEach(child => {
                            sumAmounts += parseFloat(child.amount);
                        });
                        task.amount = sumAmounts.toFixed(2);
                    } else {
                        // If no user_amount for this task, sum children amounts
                        let sumAmounts = 0;
                        task.children.forEach(child => {
                            updateTaskAmount(child);
                            sumAmounts += parseFloat(child.amount);
                        });
                        task.amount = sumAmounts.toFixed(2);
                    }
                } else {
                    // Leaf task
                    if (task.user_amount !== null && task.user_amount !== '') {
                        task.amount = parseFloat(task.user_amount).toFixed(2);
                    } else {
                        task.amount = '0.00';
                    }
                }
                // Ensure amount is stored as string with two decimals
                task.amount = parseFloat(task.amount).toFixed(2);
                updateParentAmounts(task);
            }

            // Always update all root tasks (full hierarchy)
            const roots = [];
            taskMap.forEach(task => {
                if (task.parent === 0) roots.push(task);
            });
            roots.forEach(root => updateTaskAmount(root));

            // After all calculations, update DOM for the full hierarchy
            taskMap.forEach(task => {
                const amountSpan = document.querySelector(`.computed-amount[data-id='${task.id_gantt}']`);
                if (amountSpan) {
                    // Format amount: dot as thousands, comma as decimal, prepend € with space
                    amountSpan.textContent = '€ ' + numberFormat.format(parseFloat(task.amount));
                }
                const inputField = document.querySelector(`.user-amount-input[data-id='${task.id_gantt}']`);
                if (inputField) {
                    // Update input field only if different from current user_amount to avoid infinite loop
                    const val = inputField.value === '' ? null : parseFloat(inputField.value);
                    if (val !== task.user_amount) {
                        inputField.value = task.user_amount !== null ? task.user_amount : '';
                    }
                }
            });
            // Ensure original tasks array is updated from taskMap
            tasks.forEach(task => {
                const updated = taskMap.get(task.id_gantt);
                if (updated) {
                    task.amount = updated.amount;
                }
            });
        }

        // Init
        const tree = buildWBS(tasks);
        initializeDropdown(tasks);
        const tbody = document.getElementById('wbsTableBody');
        renderWBS(tree, tbody);
        addToggleHandlers();

        // WBS expand/collapse level buttons
        document.getElementById('expandLevel').addEventListener('click', function() {
            const rows = document.querySelectorAll('#wbsTableBody tr');
            let maxLevel = 1;
            rows.forEach(row => {
                const rowLevel = parseInt(row.getAttribute('data-level'), 10);
                if (!isNaN(rowLevel) && rowLevel > maxLevel) maxLevel = rowLevel;
            });
            if (currentLevel < maxLevel) {
                setWBSLevel(currentLevel + 1);
            }
        });
        document.getElementById('collapseLevel').addEventListener('click', function() {
            if (currentLevel > 1) {
                setWBSLevel(currentLevel - 1);
            }
        });
        //$('#amount-input').number(true,2,',','.');
        $('#element-select').change(function (e) {
            e.preventDefault();
            let idToSearch = $(this).val();
            if (idToSearch !== '') {
                const task = taskMap.get(Number(idToSearch));
                let amountValue = '';
                if (task) {
                    // Prefer user_amount if set, otherwise fallback to calculated amount
                    //amountValue = task.user_amount !== null && task.user_amount !== undefined ? task.user_amount : task.amount;
                    amountValue = parseInt(task.amount) == 0 ? '' : task.amount;
                }
                $('#amount-input').val(amountValue);
            } else {
                $('#amount-input').val('');
            }
        });
        $('#updateAmount').click(function(e){
            e.preventDefault();
            let selectedId = $('#element-select').val();
            let inputAmount = $('#amount-input').val();
            if(selectedId != ''){
                const task = taskMap.get(Number(selectedId));
                if (task) {
                    task.user_amount = inputAmount;
                    updateAmounts();
                    // Retrieve the selected task again after updateAmounts()
                    const updatedTask = taskMap.get(Number(selectedId));
                    if (updatedTask) {
                        const userAmount = updatedTask.user_amount !== null && updatedTask.user_amount !== '' ? parseFloat(updatedTask.user_amount) : null;
                        const calculatedAmount = parseFloat(updatedTask.amount);
                        if (userAmount !== null) {
                            const diff = Math.abs(userAmount - calculatedAmount);
                            if (task.children.length > 0 && diff > 0) {
                                const itNumberFormat = new Intl.NumberFormat('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
                                const formattedUserAmount = itNumberFormat.format(userAmount);
                                const formattedCalculatedAmount = itNumberFormat.format(calculatedAmount);
                                const formattedDiff = itNumberFormat.format(diff);
                                if (typeof $(document).Toasts === "function") {
                                    $(document).Toasts('create', { 
                                        autohide: true, 
                                        delay: 4000, 
                                        title: 'Rounding problem', 
                                        class: 'bg-warning', 
                                        body: `Task: <b>${updatedTask.text}</b><br>
                                            Importo originale: <b>€ ${formattedUserAmount}</b><br>
                                            Importo risultante: <b>€ ${formattedCalculatedAmount}</b><br>
                                            Differenza: <b>€ ${formattedDiff}</b>` 
                                    });

                                } else {
                                    alert(
                                        "Piccola differenza di arrotondamenton" +
                                        "Task: " + updatedTask.text + "n" +
                                        "Importo originale: € " + formattedUserAmount + "n" +
                                        "Importo risultante: € " + formattedCalculatedAmount + "n" +
                                        "Differenza: € " + formattedDiff
                                    );
                                }
                            }
                        }
                    }
                }
            }else{
                if (typeof $(document).Toasts === "function") {
                    $(document).Toasts('create', { autohide: true, delay: 750, title: 'Baseline', class: 'bg-warning', body: 'Please select a task to update' });
                } else {
                    alert("Please select a task to update");
                }
            }
        });
        $('#back').click(function(e){
            e.preventDefault();
            location.replace('gantt.html');
        })
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<div class="content-wrapper">
                <div class="content">
                    <div class="container-fluid" id="target">
                        <div class="row text-center" style="margin-bottom: 10px;">
                            <!--<div class="col-md-2 d-none d-sm-block"></div>-->
                            <div class="col-12">
                                <h1>WBS Amount Allocation</h1>
                                <div class="row">
                                    <div class="col-4">
                                        <select id="element-select" class="custom-select form-control-border "></select>
                                    </div>
                                    <div class="col-4">
                                        <div class="input-group">
                                            <input type="text" class="form-control" id="amount-input">
                                            <span class="input-group-append">
                                              <button type="button" class="btn btn-secondary btn-flat" id="updateAmount">Update Amount</button>
                                            </span>
                                          </div>
                                    </div>
                                    <div class="col-2">
                                        <button type="button" class="btn btn-secondary btn-flat" id="expandLevel"><i class="fas fa-search-plus"></i> Zoom In</button>
                                        <button type="button" class="btn btn-secondary btn-flat" id="collapseLevel"><i class="fas fa-search-minus"></i> Zoom Out</button>
                                    </div>
                                    <div class="col-2">
                                        <button type="button" class="btn btn-secondary btn-flat" id="back"><i class="fas fa-long-arrow-alt-left"></i> Back</button>
                                        <button type="button" class="btn btn-secondary btn-flat" id="saveEvBaseline"><i class="fas fa-save"></i> Save</button>
                                        <button type="button" class="btn btn-secondary btn-flat" id="resetEvBaseline"><i class="fas fa-redo-alt"></i> Reset</button>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div class="row">
                            <div class="col-12">
                                <div id="output">
                                    <table class="table table-bordered table-striped table-hover align-middle">
                                        <thead class="table-dark">
                                          <tr>
                                            <th>WBS Code</th>
                                            <th>Text</th>
                                            <th>Start Date</th>
                                            <th>Duration</th>
                                            <th>Weight (%)</th>
                                            <th>Amount</th>
                                          </tr>
                                        </thead>
                                        <tbody id="wbsTableBody"></tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

Localized routes issue with Laravel Inertia SSR apps

I want to have a seemingly simple outcome:

  • website in two languages with URL structure like /en/blog/article1 and /de/blog/seite1
  • content of the page is visible to search engines for SEO purposes

It’s not much, but it turns out to be incredibly difficult to achieve.

Problem

  1. I’m getting urls like this in SSR (Server-side rendering) output: /blog and /blog/article1
  2. When react starts working in the browser, those URLs are corrected into /de/blog and /de/blog/setie1. So to users and to me during development it looks like everything is fine
  3. However, since google sees /blog and /blog/article1, it tries to visit those
  4. /blog is redirected to /de/blog which is problematic but works. The real issue is with /blog/article1 which is redirected to default locale /de/blog/article1 and throws 404, since in German the correct URL is /de/blog/seite1 (article1 post doesn’t exist there).

Stack

Stack is Laravel 11 + Inertia React with SSR enabled. This setup works without issues until you start with localization.

For localization I’ve installed https://github.com/mcamara/laravel-localization.

Hacky fix

I found that incorrect URLs come from this route function definition in ssr.jsx:

import { Ziggy } from '@/ziggy';
global.route = (name, params, absolute, config = Ziggy) 
    => route(name, params, absolute, config);

After days of trying to sort it out, I managed to pull localized URLs into SSR by pulling them from the props where they are put from HandleInertiaRequest middleware:

$ziggy = new Ziggy($group = null, $request->url());
return [
    // Add in Ziggy routes for SSR
    'ziggy' => $ziggy->toArray(),
    ...
]

And in ssr.jsx:

const ziggyConfig = { ...props.initialPage.props.ziggy };
global.route = (name, params, absolute) 
    => route(name, params, absolute, ziggyConfig);

This made URLs localized, but only worked properly on the home page, because all URLs suddenly became relative. Even setting absolute to true did not help. URLs became e.g. /en/contact/en/blog if I was looking at the blog link while being on the /en/contact page.

So to make URLs absolute I introduced this hack – I’m overwriting the “current URL” in ziggy to / to make URLs absolute:

const ziggyConfig = { ...props.initialPage.props.ziggy };
ziggyConfig.url = ziggyConfig.url.split('/').slice(0, 3).join('/');
global.route = (name, params) => route(name, params, true, ziggyConfig);

Which is obviously a dangerous hack and will probably have side effects. However, this currently works and all URLs seem to be correct.

Questions

The main question is how do I make this correctly without this hack?

My other problem is that I can’t really trust the SSR output anymore. What I see on the page locally during development and what I see on production is NOT the same that search engines see. This is really dangerous. How can one test this SSR setup? How can I trust that the website is working if I can’t even see how search engines see it? Any advice here would be super helpful.

I have considered not using react for “public” pages, but would like to avoid this if possible, since that would lead to having to duplicate layout/components.