Techniques to embed Yew components into an existing web application?

I have a web app built in ASP.NET MVC, and I’d like to start migrating it to a Yew frontend. All the tutorials on the web seem to be about building a SPA application from scratch, but my app has many different parts and I can’t replace all of them with a single SPA.

The yew::Renderer has a with_root(Element) method. As a proof of concept, I was able to add a widget to a webpage without replacing the entire page with my Rust app, like so:

use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    html! {
        <p>{"Hello from Rust!"}</p>
    }
}

fn main() {
    let document = web_sys::window().unwrap().document().unwrap();
    let element = document.get_element_by_id("app_root").unwrap();
    yew::Renderer::<App>::new().with_root(element).render();
}
<html>
  <body>
    <h1>Before Yew widget...</h1>
    <div id="app_root"></div>
    <h1>After Yew widget...</h1>
  </body>
</html>

With Trunk, this builds to a single index.html file with a WASM and JS file next to it. However, now I’d like to embed this widget into my ASP.NET app, and Trunk doesn’t seem to let me build multiple different HTML files (which I could then embed as separate pages, or reference in cshtml Razor pages).

So far, I think the simplest approach is:

  1. Disable file name hashing in Trunk, so that the WASM bundle has a predictable name;
  2. Include those files at the top of every one of my MVC pages;
  3. In the Rust code, export all the widgets as functions like fn foo_widget(el: &web_sys::Element), and each of those functions would set up the yew::Renderer pointing at the given element;
  4. In the cshtml, when I’d like to use one of the Rust widgets, I would create an element for it to populate, and then pass that to the exported function. If the widget needs static data, it’s passed as values, and if it needs dynamic data or to communicate back with the rest of the app it receives JS functions to call for the needed behavior.

Would this work correctly, and are there any official recommendations for doing such a thing?

The react android capacitor app is interrupting the file upload to the http server

I made a native android application from react in javascript using a capacitor, but it fails to upload files from the mobile application to the server, meaning it starts downloading and immediately stops, but without errors, while everything works in the web version of the application.
in the web markup code of the main page, I use the file loader from the ant design library
I tried to upload via the Http Capacitor in the web client code, and I granted different permissions in the manifest and for webview in the MainActivity.java didn’t help anything

{/* Кнопка добавления файлов */}
      <Upload
        action={${process.env.REACT_APP_API_URL}/api/Photo?event_id=${event.id}}
        listType="picture-card"
        showUploadList={false}
        multiple={true}
        onChange={handleUploadChange(event.id)} // Обработчик изменения состояния загрузки
      >
        <button style={{ border: 0, background: "none" }} type="button">
          <PlusOutlined />
          <div style={{ marginTop: 8 }}>Добавить файлы</div>
        </button>
      </Upload>
here is the code capacitor.config.ts:
import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.memory.com',
  appName: 'My TimeLines',
  webDir: 'build',

  server: {
    androidScheme: 'http',
  },
  android: {
    allowMixedContent: true,
    webContentsDebuggingEnabled: true,
  },
  plugins: {
    CapacitorHttp: {
      enabled: true,
    },
    CapacitorCookies: {
      enabled: true,
    },
  },
};

export default config;
here is the AndroidManifest code:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:largeHeap="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:usesCleartextTraffic="true"
        android:requestLegacyExternalStorage="true">

        <activity
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
            android:name=".MainActivity"
            android:label="@string/title_activity_main"
            android:theme="@style/AppTheme.NoActionBarLaunch"
            android:launchMode="singleTask"
            android:exported="true">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

        </activity>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"></meta-data>
        </provider>
    </application>

    <!-- Permissions -->
</manifest>

Why can’t I use as a component inside in React Router?

Good evening,

I am currently creating a routes folder to better organize my project as it grows.

I created a file named GameRoutes.jsx, but when I import and use it in my App.jsx, I cannot use it as a component (). Instead, I have to call it as a function ({GameRoutes()}).

My setup:

  • “react”: “^18.3.1”,
  • “react-router-dom”: “^7.1.4”,

My question:
• Why can’t I use inside like a normal React component?
• Why do I need to call it as {GameRoutes()} instead?

I appreciate any help!

src/App.jsx

// Import routes
import GameRoutes from '@/routes/GameRoutes';

<div className="min-h-screen min-w-full flex flex-col bg-slate-200">
  <Header />
  <Routes>
    {/* PUBLIC */}
    <Route path="/" element={<Home />} />
    <Route path="*" element={<NotFound />} />

    {GameRoutes()} {/* Works, but why not <GameRoutes /> ? */}

  </Routes>
  <Footer />
</div>

src/routes/GameRoutes.jsx

import { Route } from 'react-router-dom';
import Games from '@/games/_Games/Games';
import TicTacToe from '@/games/TicTacToe/TicTacToe';
import TicReact from '@/games/TicTacToe/TicReact/TicReact';
import TicMe from '@/games/TicTacToe/TicMe/TicMe';

const GameRoutes = () => {
  return (
    <>
      <Route path="/games" element={<Games />} />
      <Route path="/tic-tac-toe" element={<TicTacToe />} />
      <Route path="/tic-react" element={<TicReact />} />
      <Route path="/tic-me" element={<TicMe />} />
    </>
  );
};

export default GameRoutes;

Thanks in advance for your help!

new URL(url}.pathname deletes ../

Hi i was trying the URL API and it gave me these results:

  1. List item
let a = new URL('http://localhost:8081/../../')
console.log(a.pathname)
output: /
  1. List item
let a = new URL('http://localhost:8081/././/...//hhah')
console.log(a.pathname)
output: //...//hhah
  • can someone please explain me this behaviour?
  • is there a way to make pass the sequence /../?

Thanks in advance and have a good day

How do I prevent children within a parent from overlapping each other as the screen width reduces?

  1. I have a header which contains 2 divs.

div 1 (.hero-content) contains some typing text (.homepagebannertext), some static text (.home-hero-p) and a CTA button (.cta-button).

div 2 contains an image.

When the screen width is above 606px, div1 and div2 sit inline, side-by-side, with the children of div 1 stacked in a column.

When the screen width < 606px i have have javascript set up so that the hero-image moves inside div 1 and positions itself between .home-hero-p and .cta-button, and ive set the display of .hero-content to switch to grid using a media query.

My problem is that the children within the .hero-content div are all sat overlapping each other. i want them arranged in a neat column.

document.addEventListener("DOMContentLoaded", function() {
  const heroImage = document.querySelector(".hero-image");
  const heroContent = document.querySelector(".hero-content");
  const hero = document.querySelector(".hero");

  if (!heroImage || !heroContent || !hero) return; // Prevents errors

  // Store the original parent and next sibling
  const originalParent = heroImage.parentElement;
  const originalNextSibling = heroImage.nextElementSibling;

  function moveHeroImage() {
    if (window.innerWidth <= 606) {
      // Move hero-image to the correct position inside hero-content (as the 3 rd child)
      if (!heroContent.contains(heroImage)) {
        const homeHeroP = document.querySelector(".home-hero-p");
        if (homeHeroP) {
          homeHeroP.insertAdjacentElement("afterend", heroImage);
        }
      }
    } else {
      // Move hero-image back to its original position above 606px
      if (originalParent && originalNextSibling) {
        originalParent.insertBefore(heroImage, originalNextSibling);
      } else if (originalParent) {
        originalParent.appendChild(heroImage); // Fallback if nextSibling is null
      }
    }
  }

  // Run on load and resize
  moveHeroImage();
  window.addEventListener("resize", moveHeroImage);
});


const text = "Hello there, you big world.";
const typingEffect = document.querySelector(".typing-effect");
let index = 0;

function typeText() {
  if (index < text.length) {
    // Check if we need to insert a line break
    if (window.innerWidth < 1498 && index === 15) { // "Hello world, yo" is 15 characters long
      typingEffect.innerHTML += "<br>";
    }
    typingEffect.innerHTML += text[index];
    index++;
    setTimeout(typeText, 100); // Typing speed
  }
}

typeText(); // Start typing animation

// Listen for window resize to update break dynamically
window.addEventListener("resize", function() {
  // Reset and restart typing animation on resize
  typingEffect.innerHTML = "";
  index = 0;
  typeText();
});
.hero {
  background-color: #000000;
  /* Black Background */
  color: #FFFFFF;
  /* White Text */
  max-height: 90vh;
  /* Full Viewport Height */
  display: flex;
  align-items: center;
  justify-content: flex-start;
  /* Keep content aligned to the left */
  padding-left: 15%;
  /* Adjust padding to nudge content to the right */
}

/* Slightly Right-Aligned Content Area */
.hero-content {
  max-width: 100%;
  /* Constrain the width of the text */
  text-align: left;
  /* Align text to the left */
  margin-top: 20px;
}

/* Headline Styling */
.hero-content h1 {
  font-size: 3rem;
  max-width: 100%;
  white-space: nowrap;
  margin-bottom: 25px;
  /* Adjust spacing below the headline */
  color: #FDE07A;
  /* Yellow Accent */
}

/* Subheadline Styling */
.hero-content p {
  font-size: 1.2rem;
  margin-bottom: 20px;
  /* Match the spacing of h1 */
  color: #FFFFFF;
}

/* Call-to-Action Button */
.cta-button {
  display: inline-block;
  background-color: #FDE07A;
  /* Yellow Button */
  color: #4A4A4A;
  /* Brownish Text */
  padding: 10px 25px;
  /* Slightly wider for balance */
  text-decoration: none;
  border-radius: 25px;
  /* Increased to create a pill-like shape */
  border-color: transparent;
  font-weight: bold;
  transition: background-color 0.3s ease;
  margin-top: 20px;
  /* Add spacing above the button */
}


/* Hero Section */
.hero {
  background-color: #000000;
  /* Black Background */
  color: #FFFFFF;
  /* White Text */
  height: 90vh;
  /* Full Viewport Height */
  display: flex;
  /* Flexbox layout */
  align-items: center;
  /* Center content vertically */
  justify-content: flex-start;
  /* Align content to the left */
  position: relative;
  padding-left: 15%;
  /* Adjust spacing for left-aligned text */
}

/* Hero Content Block */
.hero-content {
  max-width: 40%;
  /* Constrain the width of the text */
  text-align: left;
  z-index: 2;
  /* Ensure content stays above background elements */
}

/* Hero Image */
.hero-image {
  position: relative;
  left: 11.5%;
  bottom: 2%;
  z-index: 1;
  /* Place the image behind the text */


}

.hero-image img {
  width: 500px;
  /* Maintain image aspect ratio */
  max-height: 500px;
  /* Adjust image height */
  display: block;
  /* Prevent extra spacing below the image */
}

/* Typing Effect */
.typing-effect {
  display: inline;
  color: #FDE07A;
  /* Yellow Accent */
  font-size: 3rem;
  /* Keep the original size */
  font-weight: bold;
  white-space: nowrap;
  overflow: hidden;
  border-right: 2px solid transparent;
  /* Used for alignment */
}

@media screen and (max-width: 606px) {

  .hero-content {
    display: grid !important;
    grid-template-columns: 1fr;
    /* Forces all children into a single column */
    grid-auto-rows: auto;
    /* Ensures each child gets its own space */
    gap: 2vh;
    /* Adds spacing between children */
    width: 100%;
    height: auto;
    /* Ensures container grows dynamically */
  }

  .typing-effect {
    font-size: 23vw;
    /* Scale text dynamically */
    line-height: 1.1;
    max-width: 100vw;
    width: auto;
    white-space: normal;
    /* Ensures proper line breaks */


  }


  .cursor2 {
    font-size: 23vw;
    width: 20px;
    margin-left: 10px;
  }

  .home-cta {
    width: 50%;
  }


}
<header class="hero">
  <div class="hero-content">
    <h1 class="homepagebannertext">
      <span class="typing-effect"></span>
      <span class="cursor2"></span>
    </h1>

    <p class="home-hero-p">Hello world.</p>
    <a href="signuppage.html" class="cta-button home-cta">Start Earning</a>

  </div>
  <div class="hero-image">
    <picture>
      <img src="images/Builder-no-bg3.png" alt="Builder">
    </picture>
  </div>
</header>

Update Order Status in Google Sheets via API & Validate with WooCommerce API

We have a plugin called FlexOrder, whose main functionality is to sync WooCommerce orders with Google Sheets and access them there. One of its key features is updating the order status by selecting the dropdown option in the order status column of Google Sheets, and the updated status is then reflected in the corresponding order status in WooCommerce.

I am performing automation testing for our plugin using Playwright and JavaScript. However, I am facing an issue while developing an automation script for a test case.

Scenario:
Using the Google Sheets API, I will update the dropdown option in the order status column, and then I will validate the order status using the WooCommerce API to ensure that the order status is correctly updated in WooCommerce.

Where I am facing the issue:
In the first test case: The order status in Google Sheets is being successfully updated.
In the second test case: When I try to validate the order using the WooCommerce API, the status is not updating in WooCommerce.
Manually checking, the feature works correctly, but it is failing in automation testing.

Need Help:
How can I resolve this issue?
*My code is provided in the attached Doc file.text

Thanks,
Rakibul Islam
-Junior Software QA Engineer, WPPOOL First Code Image || Second Code Image || Third Code Image || Four Code Image || Five Code Image || TestCase || Error Image ||

Modal State Performance in React and MUI

Is there a performance difference between this two sets of code below?
Another thing, I notice that when I use the 2nd option is that when I close the modal, there’s some bit of flash of undefined text in the modal

Which is the better way to do or are there better ways to do it?

1st

{notesModalState.open && (
  <NotesModal
    isOpen={notesModalState.open}
    onClose={() => handleNotesModal({ open: false})}
  />
)}

2nd

<NotesModal
  isOpen={notesModalState.open}
  onClose={() => handleNotesModal({ open: false})}
/>

NotesModal

const NotesModal = ({
  isOpen = false,
  onClose = () => {},
  onSuccess = () => {},
}: NotesModalProps) => {
 const notesData = data //retrieve from something

const [note, setNote] = useState<string>(notesData?.notes || "");

  // useEffect(() => {
  //   setNote(notesData || "");
  // }, [notesData]);

  const saveNote = async () => {
    // call API
  };

  const handleClose = () => {
    onClose();
  };

  const recipientName = notesData?.fullName

  return (
    <Dialog
      open={isOpen}
      maxWidth="sm"
      fullWidth
    >
      <>
        <DialogTitle>
          <Box display="flex" justifyContent="flex-end" alignItems="center">
            <Box display="flex" justifyContent="flex-end">
              <IconButton onClick={handleClose}>
                <FontAwesomeIcon
                  icon={faTimes}
                  className="text-secondary"
                  style={{ width: "1.7rem", height: "1.7rem" }}
                />
              </IconButton>
            </Box>
          </Box>
        </DialogTitle>
        <DialogContent>
          <Box gap={3} display="flex" flexDirection="column">
            <Box>
              <Typography variant="h4">Note for {recipientName}</Typography>
            </Box>
              <TextField
                multiline
                rows={4}
                value={note}
                onChange={(e) => setNote(e.target.value)}
                fullWidth
              />
          </Box>
        </DialogContent>
        <Stack
          direction={{ sm: "row", xs: "column-reverse" }}
          justifyContent="flex-end"
          spacing={2}
          padding={3}
        >
            <>
              <Button
                color="primary"
                onClick={handleClose}
                variant="outlined"
                disabled={isLoading}
              >
                Cancel
              </Button>
              <Button
                variant="contained"
                onClick={saveNote}
                disabled={isLoading}
              >
                Submit
              </Button>
            </>
        </Stack>
      </>
    </Dialog>
  );
};

Getting Syntax error “Unexpected token ‘with’ “

I’m trying to learn how to use npm to import and use different libraries in javascript. I was following a tutorial on how to install and import a simple library for testing, it goes by the name of “superheroes”. This is the code I am executing:

import superheroes from “superheroes”;

var superName = superheroes.random();

console.log(superName);

The problem is, whenever I run this code, I receive an error “Unexpected token ‘with’ “. I even initialized everything using npm and changed the type to “module” so that it can run via EJS.
enter image description here

Would be a big help if I can find a solution for this.
Thanks

I tried importing a different library “sillyName” just to test things out and it was working fine. My question is, why am I receiving an error importing the superheroes library? I have also linked the package.json file so that you guys can see if there is anything I have overlooked.

enter image description here

Backend and frontend do not appear to exchange data using slow connections

I have a backend server written in Rust using actix_web. I also have a frontend that is written in JavaScript using React and Next.JS.
The frontend has to make requests to the backend that may take several minutes or longer to complete. Due to this not working properly (js .fetch requests do not show up as completed in the network tab nor do the attached .then and .catch handlers trigger), I decided to use tasks that are running in a separate thread and then set a global state depending on the outcome of the task.
The frontend has to check every few seconds (3000ms) if the task completed successfully. It appears that the request to check for the status update still takes some time and is not completed, as neither the network tab nor console.log statements show the task being finished.

Here is the relevant code:

relevant main.rs backend code

#[allow(non_snake_case)]
#[derive(Clone)]
/// Current state of the application used to keep track of the logged in users, DoS/Brute force
/// attack requests and sharing a instance of the System struct.
struct AppState {
    login_requests: Arc<
        Mutex<
            HashMap<
                String, /* IP Adress of caller */
                (
                    u128, /* Unix Timestamp of last request */
                    u64,  /* Number of requests since last reset */
                ),
            >,
        >,
    >,
    login_token: Arc<Mutex<String>>,
    system: Arc<Mutex<System>>,
    username: Arc<Mutex<String>>,
    net_down: Arc<Mutex<f64>>,
    net_up: Arc<Mutex<f64>>,
    net_interface: Arc<Mutex<String>>,
    cpu_usage: Arc<Mutex<f32>>,
    net_connected_interfaces: Arc<Mutex<i32>>,
    update_jobs: Arc<RwLock<HashMap<Uuid, BackgroundTaskState>>>
}

impl AppState {
    /// Initiate a new AppState
    fn new() -> Self {
        let random_string: Vec<u8> = (0..128).map(|_| rand::random::<u8>()).collect();
        AppState {
            login_requests: Arc::new(Mutex::new(HashMap::new())),
            login_token: Arc::new(Mutex::new(
                String::from_utf8_lossy(&random_string).to_string(),
            )),
            system: Arc::new(Mutex::new(System::new())),
            username: Arc::new(Mutex::new(String::new())),
            net_up: Arc::new(Mutex::new(0_f64)),
            net_down: Arc::new(Mutex::new(0_f64)),
            net_interface: Arc::new(Mutex::new(String::new())),
            net_connected_interfaces: Arc::new(Mutex::new(0_i32)),
            cpu_usage: Arc::new(Mutex::new(0_f32)),
            update_jobs: Arc::new(RwLock::new(HashMap::new()))
        }
    }
...
// Left out as it would make this code block very long
}

#[derive(Debug)]
#[derive(Clone)]
enum BackgroundTaskState {
    Success,
    Fail,
    SuccessOutput(String),
    FailOutput(String),
    Pending
}

// Route that is used to check if a task aka. job has already finished
#[get("/api/fetchJobStatus/{jobId}")]
async fn fetch_job_status(
    session: Session,
    state: Data<AppState>,
    path: web::Path<String>
) -> HttpResponse {
    if !is_admin_state(&session, state.clone()) {
        return HttpResponse::Forbidden().body("This resource is blocked.");
    }

    let requested_id = path.into_inner().to_string();
    let jobs = state.update_jobs.read().unwrap().clone();
    let background_state = jobs.get(&uuid::Uuid::parse_str(&requested_id).unwrap());
    
    dbg!(&jobs);

    fn clear_task(state: Data<AppState>, task: String) {
        let mut jobs = state.update_jobs.write().unwrap().clone();
        jobs.remove(&uuid::Uuid::from_str(&task).unwrap());
        *state.update_jobs.write().unwrap() = jobs;
        ()
    }

    match background_state {
        Some(bs) => {
            match bs {
                BackgroundTaskState::Success => {
                    clear_task(state, requested_id);
                    HttpResponse::Ok().finish()
                },
                BackgroundTaskState::Fail => {
                    clear_task(state, requested_id);
                    HttpResponse::UnprocessableEntity().finish()},
                BackgroundTaskState::SuccessOutput(s) =>{
                    
                    clear_task(state, requested_id);
                    HttpResponse::Ok().body(s.clone())},
                BackgroundTaskState::FailOutput(f) => {
                    clear_task(state, requested_id);
                    HttpResponse::UnprocessableEntity().body(f.clone())},
                BackgroundTaskState::Pending => HttpResponse::Accepted().finish()
            }
        },
        None => HttpResponse::InternalServerError().body("Failed to fetch background task state")
    }
}

// Examplary route that starts a task/job
#[post("/api/installPackage")]
/// Install a package on the users system.
///
/// It requires the package name along side the sudo password in the request body.
/// This only works under apt, dnf and pacman.
async fn install_package(
    session: Session,
    json: web::Json<PackageActionRequest>,
    state: Data<AppState>,
) -> HttpResponse {
    if !is_admin_state(&session, state.clone()) {
        return HttpResponse::Forbidden().body("This resource is blocked.");
    }

    let job_id = Uuid::new_v4();

    tokio::spawn(async move {
    match packages::install_package(json.packageName.to_string(), json.sudoPassword.to_string()) {
        Ok(_) => state.update_jobs.write().unwrap().insert(job_id, BackgroundTaskState::Success),
        Err(_) => state.update_jobs.write().unwrap().insert(job_id, BackgroundTaskState::Fail),
    }});

    HttpResponse::Ok().body(job_id.to_string())
}

Relevant frontend Packages.jsx code

function startTask(adress, options = {}, interval = 3000) {
  return new Promise((resolve, reject) => {
    fetch(adress, options).then((res) => {
      if (res.ok) {
        res.text().then((uuid) => {
            let ready_for_fetch = true;

          let ivd = setInterval(() => {
              if (!ready_for_fetch) return;
              ready_for_fetch = false
            fetch(fetchURLPrefix + "/api/fetchJobStatus/" + uuid).then(
              (checkRes) => {
                ready_for_fetch = true;
                if (checkRes.status === 200) {
                  clearInterval(ivd);
                  resolve(checkRes);
                } else if (checkRes.status === 422 || checkRes.status === 500) {
                  clearInterval(ivd);
                  reject(checkRes);
                }}
            );
          }, interval);
        });
      }
    });
  });
}

I have monitored network activity, checked the expected results using command line,… and nothing seems to be causing the problem. The network requests seem to fail no matter what.

CSS/JS Website working perfectly fine on desktop but not working correctly on mobile

I made a simple website where i wanted to hide and show div using js
everything works fine on desktop, even when the window is reduced
But when i try it on mobile the navigation bar start to work in really strange way and sometimes it even make another part of the website disappear

i really don’t know where i am doing wrong

here is the complete code https://codepen.io/Giacomo-Petralia/pen/wBwVVOr

i should have a functioning menu but it is not really usable in mobile because everything lose the place i wanted
i think there is some problem with the css, maybe with

display: flex;

and also with

position: sticky;

please help me understand what is wrong here

window.eval() works, but eval doesn’t show up as a property of the Window object?

I noticed that the eval() function in JavaScript is not listed as a property of the Window object, although it is still accessible by calling window.eval(). Why is this?

listKeys(document.getElementById("container"), globalThis);

function listKeys(elem, obj){
    const keys = Object.keys(obj);
    keys.sort();
    let html = `Properties on <b>${obj}</b>:<br/>`;
    let i = 0;
    keys.forEach(key=>{
        if(key === "localStorage" || key === "sessionStorage"){
            return;
        }
        html+=`<br/>${++i}. `+key+(typeof obj[key] === "function" ? `()`:``);
    });
    elem.innerHTML = html;
}
<!doctype html>
<html>
<head>
    <style>
        body{
            font-size:14px;
            background-color:beige;
            font-family:'Segoe UI','Lucida Grande',-apple-system,BlinkMacSystemFont,'Liberation Sans',sans-serif;
        }
    </style>
</head>
<body>
<div id="container"></div>
</body>
</html>

React Native Expo Background Tasks continuously

I am currently developing an app where a user connects a bluetooth Beacon. When the distance (RSSI) gets to low an alarm (playing a loud sound) is triggered.

Now I have a question regarding background tasks:

The user should be able to lock his phone (single click on the PWR Button). Then, the app should still check if the RSSI is to low and play the sound when its triggered.

Is there a way to implement this in react native with expo?
Learned React Native a few weeks ago so I am glad for any help ^^

Here is my code so far:

import React, { useState, useEffect } from "react";
import { SafeAreaView, StyleSheet, Text, TouchableOpacity, View, Image, Alert } from "react-native";
import useBLE from "./useBLE";
import Slider from "@react-native-community/slider";
import { LinearGradient } from 'expo-linear-gradient';
import CheckBox from '@react-native-community/checkbox';
import useAudioPlayer from './useAudioPlayer';
import useLocation from "./useLocation";

const audioSource = require('./img/alert.mp3');

const HomeScreen = () => {
  const { playSound, stopSound } = useAudioPlayer(audioSource);
  let { location } = useLocation();
  const {
    requestPermissions,
    scanForPeripherals,
    stopScan,
    setConnectedDevice,
    setDeviceSearching,
    connectedDevice,
    deviceSearching,
    rssi,
  } = useBLE();
  const [rssiAlert, setRssiAlert] = useState<boolean>(false);
  const [countdown, setCountdown] = useState<number | null>(null);
  const [startCountdown, setStartCountdown] = useState<boolean>(false);
  const [rssiTreshold, setRssiTrheshold] = useState<number>(-70);
  const [showMetaData, setShowMetaData] = useState<boolean>(false);


  useEffect(() => {
    if (rssi < rssiTreshold) {
      setStartCountdown(true);
    } else {
      setStartCountdown(false);
      setRssiAlert(false);
      setCountdown(null);
    }
  }, [rssi, rssiTreshold]);

  useEffect(() => {
    if (startCountdown) {
      setCountdown(5);
      const interval = setInterval(() => {
        setCountdown((prev) => {
          if (prev !== null && prev > 0) {
            return prev - 1;
          } else {
            clearInterval(interval);
            setRssiAlert(true);
            playSound();
            console.log(location);
            return null;
          }
        });
      }, 1000);

      return () => clearInterval(interval);
    }
  }, [startCountdown]);

  const handleScan = async () => {
    const isPermissionsEnabled = await requestPermissions();
    if (isPermissionsEnabled) {
      scanForPeripherals();
    }
  };

  const getBarColor = () => {
    if (rssi >= -50) return "green";
    if (rssi >= -70) return "yellow";
    return "red";
  };

  const resetApp = () => {
    stopScan();
    setConnectedDevice(null);
    setStartCountdown(false);
    setRssiAlert(false);
    setDeviceSearching(false);
    stopSound();
  }

  return (
    <LinearGradient
      colors={['#d9a7c7', '#fffcdc']}
      style={styles.container}
    >
      <SafeAreaView style={{ flex: 1 }}>
        <View style={styles.contentWrapper}>
          {connectedDevice ? (
            <>
              <Text style={styles.titleText}>Beacon verbunden!</Text>
              <Text style={styles.titleSubText}>Du reitest jetzt sicher!</Text>

              {rssiAlert ? <Image style={styles.unicornImage} source={require('./img/unicorn_smiling.png')} />
                : (<Image style={styles.unicornImage} source={require('./img/unicorn_smiling.png')} />)}


              {showMetaData ? (<>
                <Text style={styles.rssiText}>RSSI: {rssi}</Text>
                <View style={[styles.rssiBar, { width: `${100 + rssi}%`, backgroundColor: getBarColor() }]} />

                <Text style={styles.sliderLabel}>Schwellenwert: {rssiTreshold}</Text>
                <Slider
                  style={styles.slider}
                  minimumValue={-100}
                  maximumValue={-30}
                  step={1}
                  value={rssiTreshold}
                  onValueChange={setRssiTrheshold}
                  minimumTrackTintColor="red"
                  maximumTrackTintColor="gray"
                />

                {rssi < rssiTreshold && countdown !== null && (
                  <Text style={styles.countdownText}>Alarm in {countdown} Sekunden...</Text>
                )}
              </>) : (<></>)}

              {rssiAlert && <Text style={styles.alertText}>Ein Alarm wurde ausgelöst!</Text>}
              {location && <Text style={styles.alertText}>Deine Koordinaten: {location.coords.latitude}-{location.coords.longitude}</Text>}
            </>
          ) : (
            <>
              <Text style={styles.titleText}>Bitte verbinde einen Beacon!</Text>
              <Image style={styles.unicornImage} source={require('./img/unicorn_scanning.png')} />
              {deviceSearching ? <Text>Suche Beacon...</Text> : null}
            </>
          )}
        </View>

        <TouchableOpacity
          onPress={connectedDevice ? resetApp : handleScan}
          style={[styles.ctaButton]}
        >
          <Text style={styles.ctaButtonText}>
            {connectedDevice ? "Abbrechen" : "Beacon suchen"}
          </Text>
        </TouchableOpacity>

        <View style={styles.metaData}>
          <Text>Zeige Metadaten?</Text>
          <CheckBox
            value={showMetaData}
            onValueChange={(newValue: boolean) => setShowMetaData(newValue)}
          />
        </View>
      </SafeAreaView>
    </LinearGradient>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  connectedBackground: {
    backgroundColor: "#90EE90",
  },
  contentWrapper: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  titleText: {
    fontSize: 30,
    fontWeight: "bold",
    textAlign: "center",
    marginHorizontal: 20,
    color: "black"
  },
  titleSubText: {
    fontSize: 25,
    textAlign: "center",
    marginHorizontal: 20,
    color: "black"
  },
  pulse: {
    marginTop: 200,
  },
  unicornImage: {
    marginTop: 20,
    height: 220,
    width: 250
  },
  rssiText: {
    fontSize: 25,
    marginTop: 15,
  },
  alertText: {
    fontSize: 20,
    color: "red",
    marginTop: 10,
  },
  countdownText: {
    fontSize: 18,
    color: "orange",
    marginTop: 10,
  },
  rssiBar: {
    height: 20,
    borderRadius: 10,
    marginTop: 10,
    width: "50%",
  },
  sliderLabel: {
    fontSize: 18,
    marginTop: 20,
  },
  slider: {
    width: 200,
    height: 40,
  },
  ctaButton: {
    backgroundColor: "#FF6060",
    justifyContent: "center",
    alignItems: "center",
    height: 50,
    marginHorizontal: 20,
    marginBottom: 20,
    borderRadius: 8,
  },
  ctaButtonText: {
    fontSize: 18,
    fontWeight: "bold",
    color: "white",
  },
  metaData: {
    justifyContent: "center",
    alignItems: "center",
    flexDirection: "row"
  }
});

export default HomeScreen;
enter code here

Why transition-duration along with transform scale could not be set via js/css? (But could be set via DevTools)

The problem is setting transition-duration along with transform scaling results in transformation with zero duration (although style shows up in DevTools as expected) if being set through css, js or html style. But results correctly if being set manually through browser DevTools (changing aforesaid style in DevTools).

The MRE is just:

    <div style="transform: scale(~times~); transition-duration: ~time~">
        sandy
    </div>

Working through Django latest version.

In the meantime, opacity trasition-duration works as expected, so the trouble is about scaling.

Checked the issue in firefox, edge and chrome

On the frontend the Select2 is not populating the fields for the user to select

I have an invoiceform to collect the date for postgresql database and Im having trouble with endpoints working properly and populating the drop down list for Salesman field and Product name field.

I checked the json response and it is providing the frontend with with sets whereas with my other backend code although the json responses were valid the select2 couldn’t populate then retain the typed out name in the field.

Frontend

// Initialize Select2 components
$(document).ready(function() {
  $('#salesman').select2({
    placeholder: 'Select Salesman',
    ajax: {
      url: '/api/salesmen',
      dataType: 'json',
      delay: 250,
      processResults: data => ({
        results: data
      })
    }
  });
  initProductSelect($('.product-row').first().find('.product-name'));
});

function initProductSelect(element) {
  element.select2({
    placeholder: 'Select Product',
    ajax: {
      url: '/api/products',
      dataType: 'json',
      delay: 250,
      processResults: data => ({
        results: data
      })
    }
  }).on('select2:select', async function(e) {
    const row = $(this).closest('.product-row');
    try {
      const response = await fetch(`/api/product/${e.params.data.id}`);
      const product = await response.json();
      row.data('conversion', product.conversion_rate)
        .find('.stock-ctn, .added-ctn, .return-ctn').attr('placeholder', product.primary_unit)
        .closest('tr').find('.total-dispatch-ctn, .sold-ctn').text(`0 ${product.primary_unit}`);
    } catch (error) {
      console.error('Error loading product:', error);
    }
  });
}

function convertToPieces(ctn, pcs, row) {
  return (ctn * row.data('conversion')) + (parseInt(pcs) || 0);
}

function updateTotalDispatch(row) {
  const $row = $(row);
  const conversion = $row.data('conversion') || 1;
  const stockCtn = parseInt($row.find('.stock-ctn').val()) || 0;
  const stockPcs = parseInt($row.find('.stock-pcs').val()) || 0;
  const addedCtn = parseInt($row.find('.added-ctn').val()) || 0;
  const addedPcs = parseInt($row.find('.added-pcs').val()) || 0;
  const totalPieces = (stockCtn + addedCtn) * conversion + stockPcs + addedPcs;

  $row.find('.total-dispatch-ctn').text(`${Math.floor(totalPieces/conversion)} ${$row.find('.stock-ctn').attr('placeholder')}`);
  $row.find('.total-dispatch-pcs').text(`${totalPieces % conversion} Pcs`);
}

function updateSoldStock(row) {
  const $row = $(row);
  const conversion = $row.data('conversion') || 1;

  // Parse inputs with fallback to 0 if empty or invalid
  const stockCtn = parseInt($row.find('.stock-ctn').val()) || 0;
  const stockPcs = parseInt($row.find('.stock-pcs').val()) || 0;
  const addedCtn = parseInt($row.find('.added-ctn').val()) || 0;
  const addedPcs = parseInt($row.find('.added-pcs').val()) || 0;
  const returnCtn = parseInt($row.find('.return-ctn').val()) || 0;
  const returnPcs = parseInt($row.find('.return-pcs').val()) || 0;

  // Calculate total stock and return
  const totalStock = (stockCtn + addedCtn) * conversion + (stockPcs + addedPcs);
  const totalReturn = returnCtn * conversion + returnPcs;

  // Validate return doesn't exceed stock
  if (totalReturn > totalStock) {
    const maxReturn = totalStock;
    $row.find('.return-ctn').val(Math.floor(maxReturn / conversion));
    $row.find('.return-pcs').val(maxReturn % conversion);
    alert("Return exceeds available stock!");
    return;
  }

  // Calculate sold stock
  const soldPcs = totalStock - totalReturn;
  const soldCtn = Math.floor(soldPcs / conversion);
  const soldRemainingPcs = soldPcs % conversion;

  // Update display
  $row.find('.sold-ctn').text(`${soldCtn} ${$row.find('.stock-ctn').attr('placeholder')}`);
  $row.find('.sold-pcs').text(`${soldRemainingPcs} Pcs`);

  // Disable "Add Rate" button if all stock is allocated
  let totalAllocated = 0;
  $row.find('.rate-rows tr').each(function() {
    const rateCtn = parseInt($(this).find('.rate-ctn').val()) || 0;
    const ratePcs = parseInt($(this).find('.rate-pcs').val()) || 0;
    totalAllocated += rateCtn * conversion + ratePcs;
  });

  $row.find('.add-rate').prop('disabled', totalAllocated >= soldPcs);
}

function addRate(button) {
  const row = $(button).closest('.product-row');
  const rateRows = row.find('.rate-rows');
  const soldPcs = convertToPieces(
    parseInt(row.find('.sold-ctn').text()),
    parseInt(row.find('.sold-pcs').text()),
    row
  );

  let totalAllocated = 0;
  rateRows.find('tr').each(function() {
    totalAllocated += convertToPieces(
      parseInt($(this).find('.rate-ctn').val()),
      parseInt($(this).find('.rate-pcs').val()),
      row
    );
  });

  if (totalAllocated >= soldPcs) {
    alert("Allocated quantity exceeds sold stock!");
    return;
  }

  const newRow = $('<tr>').html(`
                <td>
                    <div class="rate-qty-inputs">
                        <input type="number" class="rate-ctn" min="0" placeholder="Unit">
                        <span>-</span>
                        <input type="number" class="rate-pcs" min="0" placeholder="Pcs">
                    </div>
                </td>
                <td><input type="number" class="rate-value" min="0" step="0.01" placeholder="रू"></td>
                <td>${rateRows.children().length === 0 ? 
                    '<input type="checkbox" class="rate-all" onchange="selectAllQuantity(this)">' : ''}
                </td>
            `);
  rateRows.append(newRow);
  updateSoldStock(row);
}

function removeRate(button) {
  const row = $(button).closest('.product-row');
  const rateRows = row.find('.rate-rows');
  const lastRateRow = rateRows.find('tr').last();

  if (lastRateRow.length > 0) {
    lastRateRow.remove(); // Remove the last rate row
    updateSoldStock(row); // Update sold stock after removal
  } else {
    alert("No rate rows to remove!");
  }
}

function selectAllQuantity(checkbox) {
  const row = $(checkbox).closest('.product-row');
  const rateRow = $(checkbox).closest('tr');
  const conversion = row.data('conversion');
  const soldCtn = parseInt(row.find('.sold-ctn').text());
  const soldPcs = parseInt(row.find('.sold-pcs').text());

  if (checkbox.checked) {
    rateRow.find('.rate-ctn').val(soldCtn).prop('disabled', true);
    rateRow.find('.rate-pcs').val(soldPcs).prop('disabled', true);
  } else {
    rateRow.find('.rate-ctn, .rate-pcs').val('').prop('disabled', false);
  }
}

function addProductRow() {
  const newRow = $('.product-row').first().clone();
  newRow.find('input').val('');
  newRow.find('.select2-product').remove();
  newRow.find('td:first').html('<select class="product-name select2-product"></select>');
  newRow.find('.serial-number').text($('.product-row').length + 1);
  newRow.find('.rate-rows').empty();
  newRow.find('.total-amount, .sold-ctn, .sold-pcs, .total-dispatch-ctn, .total-dispatch-pcs').text('0');
  $('#products > tbody').append(newRow);
  initProductSelect(newRow.find('.product-name'));
  updateSerialNumbers();
}

function removeProductRow(button) {
  const rows = $('.product-row');
  if (rows.length === 1) {
    rows.find('input').val('');
    rows.find('.rate-rows').empty();
    rows.find('.select2-product').val(null).trigger('change');
    updateSoldStock(rows);
    updateTotalDispatch(rows);
  } else {
    $(button).closest('.product-row').remove();
  }
  updateSerialNumbers();
}

function updateSerialNumbers() {
  $('.product-row .serial-number').each((i, el) => $(el).text(i + 1));
}

function calculateTotal() {
  let grandTotal = 0;
  $('.product-row').each(function() {
    let rowTotal = 0;
    $(this).find('.rate-rows tr').each(function() {
      const ctn = parseInt($(this).find('.rate-ctn').val()) || 0;
      const pcs = parseInt($(this).find('.rate-pcs').val()) || 0;
      const rate = parseFloat($(this).find('.rate-value').val()) || 0;
      const conversion = $(this).closest('.product-row').data('conversion') || 1;
      rowTotal += ((ctn * conversion) + pcs) * rate;
    });
    $(this).find('.total-amount').text(`रू ${rowTotal.toFixed(2)}`);
    grandTotal += rowTotal;
  });
  $('#grand-total').text(`रू ${grandTotal.toFixed(2)}`);
}

async function saveChallan() {
  const challanData = {
    date: $('#date').val(),
    miti: $('#challanMiti').val(),
    challan_no: $('#challanNo').val(),
    salesman_id: $('#salesman').val(),
    items: [],
    total_amount: parseFloat($('#grand-total').text().replace('रू ', ''))
  };

  $('.product-row').each(function() {
    const $row = $(this);
    const item = {
      product_id: $row.find('.product-name').val(),
      stock_van: {
        ctn: parseInt($row.find('.stock-ctn').val()) || 0,
        pcs: parseInt($row.find('.stock-pcs').val()) || 0
      },
      stock_added: {
        ctn: parseInt($row.find('.added-ctn').val()) || 0,
        pcs: parseInt($row.find('.added-pcs').val()) || 0
      },
      stock_return: {
        ctn: parseInt($row.find('.return-ctn').val()) || 0,
        pcs: parseInt($row.find('.return-pcs').val()) || 0
      },
      rates: []
    };

    $row.find('.rate-rows tr').each(function() {
      item.rates.push({
        ctn: parseInt($(this).find('.rate-ctn').val()) || 0,
        pcs: parseInt($(this).find('.rate-pcs').val()) || 0,
        rate: parseFloat($(this).find('.rate-value').val()) || 0
      });
    });

    challanData.items.push(item);
  });

  try {
    const response = await fetch('/api/challan', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(challanData)
    });
    const result = await response.json();
    alert(result.message);
    if (response.ok) cancelForm();
  } catch (error) {
    console.error('Error:', error);
    alert('Error saving challan');
  }
}

function cancelForm() {
  $('#salesman').val(null).trigger('change');
  $('#date, #challanMiti, #challanNo').val('');
  $('.product-row').each(function(index) {
    if (index > 0) $(this).remove();
    else {
      $(this).find('input').val('');
      $(this).find('.rate-rows').empty();
      $(this).find('.select2-product').val(null).trigger('change');
      $(this).find('.total-amount, .sold-ctn, .sold-pcs, .total-dispatch-ctn, .total-dispatch-pcs').text('0');
    }
  });
  $('#grand-total').text('रू 0.00');
  updateSerialNumbers();
}

// Event listeners for dynamic updates
$('#products').on('input', 'input', function() {
  const row = $(this).closest('.product-row');
  updateTotalDispatch(row);
  updateSoldStock(row);
});
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap');

body {
  font-family: 'Roboto', sans-serif;
  margin: 20px;
  font-size: 14px;
  color: #333;
  background-color: beige;
}

.header {
  margin-bottom: 20px;
  background-color: white;
  padding: 15px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.center-title {
  text-align: center;
  margin: 5px 0;
}

h2 {
  font-size: 18px;
  font-weight: 500;
}

h3,
h4 {
  font-size: 16px;
  font-weight: 400;
}

table {
  width: 70%;
  border-collapse: collapse;
  margin-top: 10px;
  background-color: white;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

th,
td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
  font-size: 13px;
}

th {
  background-color: #f2f2f2;
  font-weight: 500;
}

input[type="text"],
input[type="number"],
input[type="date"] {
  border: 1px solid #ccc;
  padding: 5px;
  border-radius: 4px;
  font-size: 13px;
  background-color: #f9f9f9;
}

input:focus {
  border-color: #007BFF;
  outline: none;
  background-color: white;
}

.total-row {
  font-weight: bold;
  text-align: right;
}

.buttons {
  margin-top: 20px;
  display: flex;
  gap: 10px;
}

button {
  padding: 4px 8px;
  font-size: 13px;
  border: none;
  border-radius: 4px;
  background-color: #007BFF;
  color: white;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

button:hover {
  background-color: #0056b3;
}

.total-amount {
  font-weight: bold;
  color: #007BFF;
}

.stock-inputs,
.added-inputs,
.return-inputs,
.rate-qty-inputs {
  display: flex;
  gap: 5px;

  align-items: center;
}

.rate-table {
  width: 100%;
  margin-top: 10px;
  border-collapse: collapse;
}

.rate-table th,
.rate-table td {
  border: 1px solid #ddd;
  padding: 5px;
  font-size: 12px;
}

.rate-table th {
  background-color: #f2f2f2;
}

.stacked {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.stacked span {
  display: block;
  text-align: center;
}

.select2-container {
  width: 200px !important;
  margin-bottom: 5px;
}

.select2-product {
  width: 180px !important;
}

/* Fix for button spacing */
.product-row td:last-child {
  display: flex;
  gap: 5px;
  /* Adds spacing between buttons */
}

/* Style for Add Rate and Remove Rate buttons */
.add-rate,
.remove-rate {
  margin-right: 5px;
  /* Adds spacing between buttons */
}
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" />

<div class="header">
  <h2 class="center-title"> Store</h2>
  <h3 class="center-title">Bau</h3>
  <h4 class="center-title">Daily Challan</h4>
  <div>
    <label>Salesman Name: <select id="salesman" class="select2-salesman"></select></label>
    <label>Challan Date: <input type="date" id="date"></label>
    <label>Challan Miti: <input type="text" id="challanMiti" placeholder="YYYY-MM-DD (Nepali)"></label>
    <label>Challan No: <input type="text" id="challanNo"></label>
  </div>
</div>

<table id="products">
  <thead>
    <tr>
      <th>S.No.</th>
      <th>Product Name</th>
      <th>Stock in Van (Unit/Pcs)</th>
      <th>Stock Added (Unit/Pcs)</th>
      <th>Stock Return (Unit/Pcs)</th>
      <th>Total Dispatch</th>
      <th>Stock Sold (Unit/Pcs)</th>
      <th>Rate per Unit (रू)</th>
      <th>Total Amount (रू)</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody>
    <tr class="product-row">
      <td class="serial-number">1</td>
      <td><select class="product-name select2-product"></select></td>
      <td>
        <div class="stock-inputs">
          <input type="number" class="stock-ctn" min="0" placeholder="Unit">
          <span>-</span>
          <input type="number" class="stock-pcs" min="0" placeholder="Pcs">
        </div>
      </td>
      <td>
        <div class="added-inputs">
          <input type="number" class="added-ctn" min="0" placeholder="Unit">
          <span>-</span>
          <input type="number" class="added-pcs" min="0" placeholder="Pcs">
        </div>
      </td>
      <td>
        <div class="return-inputs">
          <input type="number" class="return-ctn" min="0" placeholder="Unit">
          <span>-</span>
          <input type="number" class="return-pcs" min="0" placeholder="Pcs">
        </div>
      </td>
      <td class="total-dispatch">
        <div class="stacked">
          <span class="total-dispatch-ctn">0 Unit</span>
          <span class="total-dispatch-pcs">0 Pcs</span>
        </div>
      </td>
      <td class="sold-stock">
        <div class="stacked">
          <span class="sold-ctn">0 Unit</span>
          <span class="sold-pcs">0 Pcs</span>
        </div>
      </td>
      <td>
        <table class="rate-table">
          <thead>
            <tr>
              <th>Quantity (Unit/Pcs)</th>
              <th>Rate (रू)</th>
              <th>All</th>
            </tr>
          </thead>
          <tbody class="rate-rows"></tbody>
        </table>
        <div>
          <button class="add-rate" onclick="addRate(this)">Add Rate</button>
          <button class="remove-rate" onclick="removeRate(this)">Remove Rate</button>
        </div>
      </td>
      <td class="total-amount">रू 0.00</td>
      <td>
        <button onclick="addProductRow()">Add Product</button>
        <button onclick="removeProductRow(this)">Remove Product</button>
      </td>
    </tr>
  </tbody>
</table>

<div class="buttons">
  <button onclick="history.back()">Back to Challan CRUD</button>
  <button onclick="calculateTotal()">Calculate Total</button>
  <button onclick="saveChallan()">Save Challan</button>
  <button onclick="cancelForm()">Cancel</button>
</div>

<table>
  <tr>
    <td colspan="7" class="total-row">Grand Total:</td>
    <td id="grand-total" class="total-amount">रू 0.00</td>
  </tr>
</table>

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>

my backend:

from flask import Flask, request, jsonify, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS

app = Flask(__name__)
CORS(app)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:Twlight20@localhost/inventory_db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

# Models
class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), unique=True, nullable=False)
    primary_unit = db.Column(db.String(20), nullable=False)  # ctn, sck, etc.
    conversion_rate = db.Column(db.Integer, nullable=False)  # pieces per unit

class Salesman(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), unique=True, nullable=False)

class Challan(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    date = db.Column(db.Date, nullable=False)
    miti = db.Column(db.String(10), nullable=False)
    challan_no = db.Column(db.String(50), unique=True, nullable=False)
    salesman_id = db.Column(db.Integer, db.ForeignKey('salesman.id'), nullable=False)
    total_amount = db.Column(db.Float, nullable=False)
    items = db.relationship('ChallanItem', backref='challan', cascade="all, delete-orphan")

class ChallanItem(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    challan_id = db.Column(db.Integer, db.ForeignKey('challan.id'), nullable=False)
    product_id = db.Column(db.Integer, db.ForeignKey('product.id'), nullable=False)
    stock_van = db.Column(db.JSON, nullable=False)
    stock_added = db.Column(db.JSON, nullable=False)
    stock_return = db.Column(db.JSON, nullable=False)
    rates = db.Column(db.JSON, nullable=False)

# Routes
@app.route('/')
def index():
    return render_template('challan.html')

# Get all or search products
@app.route('/api/products', methods=['GET'])
def get_products():
    search = request.args.get('q', '').strip()
    
    if search:
        products = Product.query.filter(Product.name.ilike(f'%{search}%')).all()
    else:
        products = Product.query.all()

    if not products:
        return jsonify({'message': 'No products found'}), 404

    return jsonify([{'id': p.id, 'name': p.name, 'primary_unit': p.primary_unit, 'conversion_rate': p.conversion_rate} for p in products])

# Get a single product by ID
@app.route('/api/product/<int:product_id>', methods=['GET'])
def get_product(product_id):
    product = Product.query.get(product_id)
    
    if not product:
        return jsonify({'error': 'Product not found'}), 404

    return jsonify({
        'id': product.id,
        'name': product.name,
        'primary_unit': product.primary_unit,
        'conversion_rate': product.conversion_rate
    })

# Get all or search salesmen
@app.route('/api/salesmen', methods=['GET'])
def get_salesmen():
    search = request.args.get('q', '').strip()
    
    if search:
        salesmen = Salesman.query.filter(Salesman.name.ilike(f'%{search}%')).all()
    else:
        salesmen = Salesman.query.all()

    if not salesmen:
        return jsonify({'message': 'No salesmen found'}), 404

    return jsonify([{'id': s.id, 'name': s.name} for s in salesmen])

# Get a single salesman by ID
@app.route('/api/salesman/<int:salesman_id>', methods=['GET'])
def get_salesman(salesman_id):
    salesman = Salesman.query.get(salesman_id)
    
    if not salesman:
        return jsonify({'error': 'Salesman not found'}), 404

    return jsonify({'id': salesman.id, 'name': salesman.name})

# Save a new Challan
@app.route('/api/challan', methods=['POST'])
def save_challan():
    data = request.json

    # Validate required fields
    required_fields = ['date', 'miti', 'challan_no', 'salesman_id', 'total_amount', 'items']
    for field in required_fields:
        if field not in data:
            return jsonify({'error': f'Missing field: {field}'}), 400

    if not isinstance(data['items'], list) or len(data['items']) == 0:
        return jsonify({'error': 'Items must be a non-empty list'}), 400

    # Ensure the salesman exists
    salesman = Salesman.query.get(data['salesman_id'])
    if not salesman:
        return jsonify({'error': 'Invalid salesman ID'}), 400

    # Ensure the challan number is unique
    existing_challan = Challan.query.filter_by(challan_no=data['challan_no']).first()
    if existing_challan:
        return jsonify({'error': 'Challan number already exists'}), 400

    challan = Challan(
        date=data['date'],
        miti=data['miti'],
        challan_no=data['challan_no'],
        salesman_id=data['salesman_id'],
        total_amount=data['total_amount']
    )
   
    for item in data['items']:
        # Validate each item
        required_item_fields = ['product_id', 'stock_van', 'stock_added', 'stock_return', 'rates']
        for field in required_item_fields:
            if field not in item:
                return jsonify({'error': f'Missing item field: {field}'}), 400

        # Ensure the product exists
        product = Product.query.get(item['product_id'])
        if not product:
            return jsonify({'error': f'Invalid product ID: {item["product_id"]}'}), 400

        challan_item = ChallanItem(
            product_id=item['product_id'],
            stock_van=item['stock_van'],
            stock_added=item['stock_added'],
            stock_return=item['stock_return'],
            rates=item['rates']
        )
        challan.items.append(challan_item)

    db.session.add(challan)
    db.session.commit()
    return jsonify({'message': 'Challan saved successfully', 'challan_id': challan.id})

if __name__ == '__main__':
   app.run(host='0.0.0.0', port=5000, debug=True)

window.__TAURI__ is not available

Tauri v2 with Next.js v15

I was trying to use database SQLite in my Tauri app, I followed the official documentation and the database is working correctly. Like I can perform DB operations normally, but now I wanted to make a singleton instance for database. So I made a hook for database, but the problem is its not working as its supposed to, please have a look at my hook component.

I am running the project in Tauri environment

npm run tauri dev

The output of the console is:

Tauri detected: false

And:

window.__TAURI__ is undefined

import { useState, useEffect } from 'react';
import Database from '@tauri-apps/plugin-sql';

type DatabaseInstance = Awaited<ReturnType<typeof Database.load>>;

export const useDatabase = () => {
    const [db, setDb] = useState<DatabaseInstance | null>(null);

    useEffect(() => {
        // Check here
        console.log('Tauri detected:', !!window.__TAURI__);
        console.log(window.__TAURI__);

        if (typeof window !== 'undefined' && window.__TAURI__) {
            Database.load('sqlite:test.db')
                .then((database) => {
                    console.log('Database loaded successfully:', database);
                    setDb(database);
                })
                .catch((error) => {
                    console.error('Failed to load database:', error);
                });
        }
    }, []);

    return db;
};

Since __TAURI__ is inserted at runtime i had to make a type file for it.

import { Database } from '@tauri-apps/plugin-sql';

declare global {
  interface Window {
    __TAURI__: {
      sql: {
        load: (connectionString: string) => Promise<Database>;
      };
    };
  }
}

package.json

"@tauri-apps/api": "^2.2.0",
"@tauri-apps/cli": "^2.2.7",
"@tauri-apps/plugin-sql": "^2.2.0",
"next": "15.1.6",
"react": "^19.0.0",
"react-color": "^2.19.3",
"react-dom": "^19.0.0"