Node.js and Express: Incomplete server information displayed in Discord bot dashboard

I’m working on a Discord bot dashboard using Node.js and Express. I’ve run into an issue with displaying server information on the /guilds path. Sometimes, not all server information is rendered properly, and the information only displays after refreshing the page with F5. I’ve tried using Promise.All, but the issue persists. I’m wondering if this issue is caused by Discord API limitations or if there’s something else at play. Does anyone have any insights or suggestions?

    const user = req.session.user;
    const guilds = req.session.guilds;
    const botGuilds = [];

    if (!user) {
        return res.redirect('/auth/discord');
    }

    await client.guilds.fetch();

    const botGuildIds = client.guilds.cache.map((guild) => guild.id);

    for (const guild of guilds) {
        if (botGuildIds.includes(guild.id)) {
            const botGuild = await client.guilds.fetch(guild.id);
            botGuilds.push(botGuild);
        }
    }

    res.render('guilds', { user: user, guilds: botGuilds });
});```

I've already tried using try-catch blocks, Promise.All and various console.log statements to arrive at the error somehow.

How to prevent websockets from reconnecting on page switch?

How can I prevent websockets from disconnecting and connecting again when I switch a page in NextJS 13 beta(app folder). Will even pay for the solution, telegram: @itszhu

I switch pages using next/link, pages don’t seem to refresh with this option neither.

My app/dashboard/page.js is a server component.

I’m wrapping the page.js with:

import LayoutApp from '@/server/LayoutApp';
import DashboardPage from './DashboardPage';

export default function Dashboard() {
  return (
    <>
      <LayoutApp render={(session) => <DashboardPage session={session} />} />
    </>
  );
}

And this is my LayoutApp.js:

import HeaderBE from '@/layout/be/HeaderBE';
import { LayoutWithTopbar } from '@/layout/be/LayoutWithTopbar';
import meAuth from '@/server/auth';
import { WrapWebsocket } from '@/hoc/withWebsocket';

export default async function LayoutApp({ render }, req) {
  const session = await meAuth(); // server component

  return (
    <main className="h-full w-full flex">
      <WrapWebsocket session={session}>
        <HeaderBE session={session} boss={boss} />
        <LayoutWithTopbar session={session}>{render(session)}</LayoutWithTopbar>
      </WrapWebsocket>
    </main>
  );
}

And this is my websocket context:

'use client';

import React, {
  createContext,
  useEffect,
  useState,
  useCallback,
  useRef,
} from 'react';
import useWebSocket, { ReadyState } from 'react-use-websocket';

export const WebSocketContext = createContext(null);

export const WrapWebsocket = ({ children, session }) => {
  const url = `ws://api.site.test:${process.env.PORT || 3000}`;
  const [isConnected, setIsConnected] = useState(false);
  const pingInterval = useRef(null);

  const handleOpen = () => {
    setIsConnected(true);
    console.log('Connected');
  };

  const handleClose = () => {
    setIsConnected(false);
    console.log('close ws');
  };

  const messageHandlers = useRef([]);

  const addMessageHandler = (handler) => {
    messageHandlers.current.push(handler);
  };

  const removeMessageHandler = (handler) => {
    messageHandlers.current = messageHandlers.current.filter(
      (h) => h !== handler
    );
  };

  const handleMessage = useCallback(
    (event) => {
      console.log('Received message:', event.data); // Add this line
      messageHandlers.current.forEach((handler) => handler(event));
    },
    [messageHandlers]
  );

  const { sendMessage, lastMessage } = useWebSocket(url, {
    onOpen: handleOpen,
    onClose: handleClose,
    share: true,
    retryOnError: true,
    shouldReconnect: (closeEvent) => true,
    reconnectAttempts: 25,
    reconnectInterval: 15000,
  });

  const startPingInterval = () => {
    return setInterval(() => {
      sendMessage(JSON.stringify({ type: 'ping' }));
    }, 30000); // Send ping every 30 seconds
  };

  useEffect(() => {
    if (isConnected) {
      try {
        const credentials = {
          wsId: session.user.sessionId,
        };
        sendMessage(JSON.stringify(credentials));
      } catch (error) {
        console.log(error);
      }
      pingInterval.current = startPingInterval();

      return () => {
        clearInterval(pingInterval.current);
      };
    }
  }, [isConnected, sendMessage]);

  useEffect(() => {
    if (lastMessage) {
      handleMessage(lastMessage);
    }
  }, [lastMessage, handleMessage]);

  useEffect(() => {
    const handleVisibilityChange = () => {
      if (isConnected) {
        clearInterval(pingInterval.current);
        pingInterval.current = startPingInterval();
      }
    }; //document.visibilityState === 'visible' &&

    document.addEventListener('visibilitychange', handleVisibilityChange);

    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, [isConnected, pingInterval]);

  return (
    <WebSocketContext.Provider
      value={{ isConnected, addMessageHandler, removeMessageHandler }}
    >
      {children}
    </WebSocketContext.Provider>
  );
};

I tried putting WrapWebsocket around the main layout file which crashed the app.
I tried putting it around the layout file that wraps the app folder, which also didn’t work and it kept reconnecting everytime I switched pages.

Is there a JS event to see if user has scrolled to end of slider?

I’m working on a uni project where I have to make a movie ticket website. A boiled down version of what I have now for the homepage can be seen below. Part of the exercise is to incorporate ajax/fetch into the website, I want to do this the following way:

  1. the user sees 10 movies on the homepage
  2. the user scrolls to the end of this array of movies
  3. ajax or fetch requests 5 more movies from the server
  4. the site now shows 15 movies and the user can scroll on

I’m having trouble with step 2; I need some kind of “reached-end” event triggered from the scrollable flexbox. I did some searching and couldn’t find anything like this. Does anyone perhaps know how this can be done? Any help at all is appreciated!!

class Movie {
  constructor(title, image, description, duration, release, age, schedule, seats) {
    this.title = title;
    this.image = image;
    this.description = description;
    this.duration = duration;
    this.release = release;
    this.age = age;
    this.schedule = schedule; //dictionary met alle tijden per dag.
    this.seats = seats;
  }
}

const blankMovie = new Movie(
  "Blank",
  "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Grey_background.jpg/636px-Grey_background.jpg?20140113173544",
  "blank",
  "0 minutes",
  100
);

var allMovies = [];

for (let i = 0; i < 10; i++) {
  allMovies.push(blankMovie)
}

function populatewith(movies) {
  const homePage = document.getElementById("movie-slider");

  for (let i = 0; i < allMovies.length; i++) {
    const movieDiv = document.createElement("div");
    movieDiv.className = "movie-banner";

    const movieTitle = document.createElement("span");
    movieTitle.className = "movie-banner__title";
    movieTitle.textContent = allMovies[i].title;

    const movieImage = document.createElement("img");
    movieImage.className = "movie-banner__image";
    movieImage.src = allMovies[i].image;

    const movieDur = document.createElement("span");
    movieDur.className = "movie-banner__desc";
    movieDur.textContent = allMovies[i].duration;

    homePage.appendChild(movieDiv);
    movieDiv.appendChild(movieTitle);
    movieDiv.appendChild(movieImage);
    movieDiv.appendChild(movieDur);
  }
}

populatewith(allMovies);
body {
  margin: 0;
  font-family: 'Teko', sans-serif;
}

.float-right {
  float: right;
}

.content {
  transition: margin-top 0.4s;
}

.content {
  background: fixed;
  background-image: url("/images/homepage_background.jpg");
  background-position: center;
  background-size: cover;
  min-height: 100vh;
}

#movie-slider {
  display: flex;
  flex-direction: row;
  align-items: end;
  overflow: auto;
  gap: 10px;
  padding: 10vh 10%;
}

.movie-banner {
  margin-top: 50px;
  margin-bottom: 150px;
  flex-basis: 250px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
}

.movie-banner__image {
  object-fit: cover;
  height: 400px;
}

.movie-banner__title {
  color: wheat;
  text-align: center;
  font-family: 'Bebas Neue', cursive;
  font-size: 1.5em;
  padding: 10px
}

.movie-banner__desc {
  color: wheat;
  font-size: 1.3em;
  text-align: center;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Movie Tickets</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Teko:wght@300&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="/css/index.css">
  <link rel="stylesheet" href="/css/general.css">
</head>

<body>
  <article class="content">
    <section id="movie-slider">
    </section>
  </article>
  <script src="/js/index.js" type="module"></script>
  <script src="/js/general.js" type="module"></script>
</body>

</html>

SvelteKit local form variable isn’t updating after running applyAction

I am trying to build a form that allows users to change their password on my app using SvelteKit’s newish form actions. I’ve followed the docs pretty much to the letter other than changing variable names and such to suit my project, and the weird thing is that $page.form, the global store for that form, updates to the correct values.

So in my Svelte Page i have

const handleChangePasswords: SubmitFunction = () => {
        return async ({ result }) => {
            if (result.type === 'success') {
                invalidateAll();
            } else {
                await applyAction(result);
            }
        };
    };
<form
    class="password-info-form"
    method="POST"
    action="?/changePasswords"
    use:enhance={handleChangePasswords}
>
    <span class="password-info-heading">{editMode ? 'update' : ''} password</span>
    <div class="form-group">
        <label for="first-name">Current Password</label>
        <TextField
            type="password"
            name="currentPassword"
            placeholder="Please enter your current password"
            bind:value={data.oldPassword}
            disabled={!editMode}
        />
    </div>
    <div class="form-group">
        <label for="last-name">New Password</label>
        <TextField
            type="password"
            name="newPassword"
            placeholder="Please enter your new password"
            bind:value={data.newPassword}
            disabled={!editMode}
        />
    </div>
    <div class="form-group full-width">
        <label for="email">Confirm new Password</label>
        <TextField
            type="password"
            name="confirmPassword"
            placeholder="Please enter your new password again"
            bind:value={data.confirmPassword}
            disabled={!editMode}
        />
    </div>
    {#if form?.errors}
        <p class="error">This field is required</p>
    {/if}
    {#if editMode}
        <button class="cancel-button">Cancel</button>
        <button class="save-button" disabled={submitButtonEnabled}>Save changes</button>
    {/if}
</form>

And in my actions object on the server.ts file

changePasswords: async ({ request }) => {
        const passwords: Record<string, string> = {}; // Setting up blank object to hold user profile information

        const formData: FormData = await request.formData();

        const userAuthenticationToken = formData.get('token') as string;
        if (!userAuthenticationToken) {
            return fail(400, { code: 'NO_TOKEN', incorrect: true });
        }

        formData.forEach((value, key) => {
            if (!formData.get(key)) {
                // do we need this check?
                passwords[key as keyof typeof passwords] = '';
            }
            passwords[key as keyof typeof passwords] = value as string;
        });
        // check if we have all the required fields
        if (!passwords.currentPassword || !passwords.newPassword || !passwords.confirmPassword) {
            return fail(400, { code: 'INVALID_INPUT', incorrect: true });
        }

        // check if the new password and confirm password match
        if (passwords.newPassword !== passwords.confirmPassword) {
            console.log('match?');

            return fail(400, {
                code: 'PASSWORDS_DO_NOT_MATCH',
                newPassword: true,
                confirmPassword: true,
                incorrect: true
            });
        }

        return {
            success: true
        };
    }
https://stackoverflow.com/questions/ask#

Can’t see anything I’m missing and like I say, the global store for the form is updating so applyActions must be running.

Why using renderMessage in react-native-gifted-chat removes my Avatar?

I have a react native app, with react-native-gifted-chat. I wanted to change my fontFamily in all places so I changed it in the renderComposer and renderMessage props. After adding the renderMessage prop to my GiftedChat component my Avatars dissapeared. I tried to bring it back with the renderAvatar, but it does not work…

How can I have custom fonts and custom Avatars at the same time?

<GiftedChat
    messages={messages}
    onSend={onSend}
    user={{
    _id: 1,
    avatar: USER_AVATAR,
    name: USER_ID
    }}
    
    //Custom styles
    dateFormat="YYYY/MM/DD"
    timeFormat="HH:mm"
    renderInputToolbar={props => customtInputToolbar(props)}
    renderSystemMessage={props => customSystemMessage(props)}
    renderSend={props => customSendButton(props)}
    renderComposer={props => customComposer(props)}
    renderMessage={props => customMessageBubble(props)}
    renderAvatar={(props) => <Avatar {...props} />}
/>

...my styles...

export const customMessageBubble = props => {
    const renderMessageText=(props)=>{
        return (
            <>
            <MessageText
                {...props}
                textStyle={{
                right:{
                    fontFamily:'QuicksandMedium'
                },
                left:{
                    fontFamily:'QuicksandMedium'
                }
                }}
            />
            </>
        )
    }

    return(
        <Bubble 
        {...props}
        renderMessageText={renderMessageText}
        />
    )
}

export const customComposer = props => {
    return(
        <Composer 
        {...props} 
        placeholder={'Aa'}
        textInputStyle={{
            fontFamily:'QuicksandMedium',
        }}
        />
    )
}

enter image description here

Objects passed by reference

What exactly is happening here at the line( param = [1]) when passing the data, that the output is 0? If I switch the lines inside the function I get [0,2].

const  data = [0];

function changeData(param) {
  param = [1]
  param.push(2);

}
changeData(data);

Webpack bundling emits “require” statements in the distribution file

I have a TypeScript project, and I use Webpack to bundle the code to single JavaScript file. I’m using ESM only. When I try to run the distribution file by running: node ./dist/index.js I get this error:

const external_minimist_namespaceObject = require("minimist");
                                          ^

ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and 'package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.

Indeed, I configured "type": "module" in the package.json file.
I have this script to run the Webpack: "dist": "node --loader ts-node/esm node_modules/webpack-cli/bin/cli.js -c ./webpack.config.ts", in my package.json file.

This is my webpack.config.ts file:

import path from 'node:path';
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';

import webpack from 'webpack';
import nodeExternals from 'webpack-node-externals';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import WebpackShellPluginNext from 'webpack-shell-plugin-next';

const packageJsonData = await fs.readFile('package.json', 'utf-8');
const packageJsonObject = JSON.parse(packageJsonData);
const version = packageJsonObject.version;

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const configuration: webpack.Configuration = {
    context: __dirname,
    mode: 'production',
    target: 'node',
    entry: './src/index.ts',
    // * https://stackoverflow.com/questions/48673408/should-javascript-npm-packages-be-minified
    optimization: { minimize: false },
    externals: [nodeExternals({ modulesDir: path.join(__dirname, 'node_modules') })],
    experiments: { outputModule: true },
    module: {
        rules: [
            {
                test: /.ts$/,
                use: [
                    {
                        loader: 'ts-loader',
                        options: { configFile: 'tsconfig.build.json' },
                    },
                ],
                exclude: /node_modules/,
            },
        ],
    },
    plugins: [
        new WebpackShellPluginNext({
            onBuildStart: {
                scripts: ['rimraf dist'],
                blocking: true,
            },
            safe: true,
        }),
        new webpack.DefinePlugin({
            __PACKAGE_VERSION__: JSON.stringify(version),
        }),
    ],
    resolve: {
        extensions: ['.ts'],
        plugins: [
            new TsconfigPathsPlugin({
                configFile: './tsconfig.base.json',
                extensions: ['.ts'],
            }),
        ],
    },
    output: {
        filename: 'index.js',
        path: path.resolve(__dirname, 'dist'),
        library: { type: 'module' },
        chunkFormat: 'module',
    },
};

export default configuration;

And this is my tsconfig.base.json file which is the important one:

{
    "extends": "./tsconfig.paths.json",
    "compilerOptions": {
        "target": "ES2018",
        "module": "ESNext",
        "moduleResolution": "bundler",
        "noEmit": true,
        "baseUrl": "./",
        "allowJs": false,
        "skipLibCheck": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "esModuleInterop": true,
        "resolveJsonModule": true,
        "isolatedModules": true,
        "incremental": true,
        "removeComments": true,
        "allowUnreachableCode": false,
        "allowUnusedLabels": false,
        "alwaysStrict": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitAny": true,
        "noImplicitOverride": true,
        "noImplicitReturns": true,
        "noImplicitThis": true,
        "noPropertyAccessFromIndexSignature": true,
        "noUncheckedIndexedAccess": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "typeRoots": ["./node_modules/@types", "./@types"]
    }
}

Webpack completes successfully, but I cannot run the distribution file with NodeJS. Why does the distribution file contain require statements instead of import to successfully run with ESM?

Internal Server error (500) in POST to MongoDB react

I am making a form that must POST to the Mongo database.

So I have the following code to make the change of the states in the Front:

const [project, setProject] = useState({
    project_start: null,
    project_end: null,
    project_name: "",
    usersId: [],
    partnerId: "",
    categoryId: "",
  });

  const handleChange = (name, newVal) => {
    setProject({ ...project, [name]: newVal });
  };

and the POST looks like this:

const handleSubmit = async (e) => {
    e.preventDefault();

    const res = await fetch("http://localhost:5001/client/projects", {
      method: "POST",
      body: JSON.stringify(project),
      headers: { "Content-Type": "application/json" },
    });

    const data = await res.json(project);
    console.log(data);
    console.log(project);
  };

And this would be a summary of what my form consists of:

<form onSubmit={handleSubmit}>
<Box width="50%">
                <LocalizationProvider dateAdapter={AdapterDayjs}>
                  <DatePicker
                    id="project_start"
                    name="project_start"
                    value={project.project_start}
                    onChange={handleChange.bind(null, "project_start")}
                    slotProps={{
                      textField: {
                        size: "small",
                        margin: "dense",
                      },
                    }}
                  />
                </LocalizationProvider>
                <TextField
                  id="nombreP"
                  name="project_name"
                  value={project.project_name}
                  onChange={handleChange}
                  margin="dense"
                  size="small"
                />
                <FormControl size="small" sx={{ m: 1 }}>
                  <Select
                    id="encargadoP"
                    multiple
                    name="usersId"
                    value={project.usersId}
                    onChange={(e) =>
                      handleChange(
                        "usersId",
                        typeof e.target.value === "string"
                          ? e.target.value.split(",")
                          : e.target.value
                      )
                    }
                    MenuProps={MenuProps}
                    renderValue={(selected) => (
                      <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
                        {selected.map((value) => (
                          <Chip key={value} label={value} />
                        ))}
                      </Box>
                    )}
                    sx={{ width: 205 }}
                  >
                    {data?.map(({ _id, name }) => (
                      <MenuItem key={_id} value={name}>
                        {name}
                      </MenuItem>
                    ))}
                  </Select>
                </FormControl>
              </Box>
            </Box>
            <Button
              type="submit"
              variant={"outlined"}
              size={"large"}
              sx={{
                width: 420,
                border: "1px solid white",
                m: "3rem 0 0 0",
                color: "white",
                borderRadius: "30px",
              }}
            >
              Agregar proyecto
            </Button>
          </Box>
        </form>

Here is my schema

import mongoose from "mongoose";

const ProjectSchema = new mongoose.Schema(
  {
    project_start: Date,
    project_end: Date,
    project_name: {
      type: String,
      required: true,
      min: 2,
      max: 100,
    },
    usersId: {
      type: [mongoose.Types.ObjectId],
      of: Number,
    },
    partnerId: {
      type: [mongoose.Types.ObjectId],
      of: Number,
    },
    categoryId: {
      type: [mongoose.Types.ObjectId],
      of: Number,
    },
  },
  { timestamps: true }
);

const Project = mongoose.model("Project", ProjectSchema);
export default Project;

In the Routes I have this:

import express from "express";
import {
  getAllUsers,
  getPartner,
  getCategory,
  addProject,
} from "../controllers/client.js";

const router = express.Router();

router.get("/users", getAllUsers);
router.get("/partners", getPartner);
router.get("/categories", getCategory);
router.post("/projects", addProject);

export default router;

In the controller this (in short):

import Project from "../models/Projects.js";
export const addProject = async (req, res) => {
  try {
    const {
      project_start,
      project_end,
      project_name,
      usersId,
      partnerId,
      categoryId,
    } = req.body;

    const newProject = new Project({
      project_start,
      project_end,
      project_name,
      usersId,
      partnerId,
      categoryId,
    });
    const savedProjecct = await newProject.save();
    res.status(201).json(savedProjecct);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

At the moment I get a 500 error when making the POST, I guess it is because of the schema I made with Mongoose, my questions are:

  • Is that the problem?
  • How can I take the id of the user, partner and category instead of the string (name) shown in the front end?

I am very grateful to people who can help me 🙂

Vite build.sourcemap hidden not loading in Chrome or Firefox

Migrating my react app from create-react-app to Vite and seeing some unexpected behavior with source maps. Vite docs regarding source maps are here. I was leaning toward using sourcemap: true until I saw that sourcemap: 'hidden' is the same thing except it is supposed to hide my comments … sounds like exactly what I want.

If I create a build with sourcemap: true, each JS file gets its own map file, and the JS file gets a comment like //# sourceMappingURL=mylibrary-033b4774.js.map appended at the end of it. The source maps get loaded into Chrome and Firefox as expected.

If I create a build with sourcemap: 'hidden', the only difference in output is that the //# sourceMappingURL=mylibrary-033b4774.js.map comment is NOT appended at the end of the JS file. A map file is still produced for each JS file, and it is accessible if I try to access it manually in the browser by typing its full path. However, the browsers don’t seem to like this … they don’t show the map at all.

Is this a bug in Vite or am I doing something wrong here?

Dynamic keys in typescript and javascript syntax

// Makes `function.name` return given name

function nameFunction(name: string, body: (args?: any[]) => any) {
    return {
        [name](...args: any[]) {
            return body(...args);
        }
    }[name];
}

In javascript I can do dynamic keys with such syntax:

let key = "jk_96";
let o = {
   [key]: ()=>{ console.log('Yup');} 
}

How is allowed in typescript to skip : after key name, and what [name] after closing brace of objects means?

Random element not working in play again button

This is a Higher or Lower game where the user guesses which driver has higher or lower stat depending on the difficulty. The play again button is the problem. What I want is when the user presses the play again button when the game ends it should pick 2 random drivers but not the same two drivers from the last game like my code does. I guess this is just a small problem but I’m not seeing what’s wrong.

// Define F1 driver data
const f1Drivers = [
      {
        name: 'Lewis Hamilton',
        wins: 103,
        podiums: 192,
        points: 4443.5
      },
      {
        name: 'Michael Schumacher',
        wins: 91,
        podiums: 155,
        points: 1566
      },
      {
        name: 'Sebastian Vettel',
        wins: 53,
        podiums: 122,
        points: 3098
      },
      {
        name: 'Alain Prost',
        wins: 51,
        podiums: 106,
        points: 798.5
      },
      {
        name: 'Ayrton Senna',
        wins: 41,
        podiums: 80,
        points: 614
      },
      {
        name: 'Nigel Mansell',
        wins: 31,
        podiums: 59,
        points: 482
      },
      {
        name: 'Jim Clark',
        wins: 25,
        podiums: 32,
        points: 274.5
      },
      {
        name: 'Juan Manuel Fangio',
        wins: 24,
        podiums: 35,
        points: 277.64
      },
      {
        name: 'Niki Lauda',
        wins: 25,
        podiums: 54,
        points: 420.5
      },
      {
        name: 'Jack Brabham',
        wins: 14,
        podiums: 31,
        points: 261
      },
      {
        name: 'Fernando Alonso',
        wins: 32,
        podiums: 101,
        points: 2106
      },
      {
        name: 'Max Verstappen',
        wins: 37,
        podiums: 80,
        points: 2080.5
      },
      {
        name: 'Nico Rosberg',
        wins: 23,
        podiums: 57,
        points: 1594.5
      },
      {
        name: 'Kimi Raikkonen',
        wins: 21,
        podiums: 103,
        points: 1873
      },
      {
        name: 'Mika Hakkinen',
        wins: 20,
        podiums: 51,
        points: 420
      },
      {
        name: 'Jenson Button',
        wins: 15,
        podiums: 50,
        points: 1235
      },
      {
        name: 'Jackie Stewart',
        wins: 27,
        podiums: 43,
        points: 359
      },
      {
        name: 'Damon Hill',
        wins: 22,
        podiums: 42,
        points: 360
      },
      {
        name: 'Felipe Massa',
        wins: 11,
        podiums: 41,
        points: 1167
      },
      {
        name: 'Valtteri Bottas',
        wins: 10,
        podiums: 67,
        points: 1791
      },
      {
        name: 'Mark Webber',
        wins: 9,
        podiums: 50,
        points: 1235
      },
      {
        name: 'Daniel Ricciardo',
        wins: 8,
        podiums: 32,
        points: 1311
      },
      {
        name: 'Charles Leclerc',
        wins: 5,
        podiums: 24,
        points: 874
      },
      {
        name: 'Sergio Perez',
        wins: 5,
        podiums: 28,
        points: 1255
      },
    ];
    
    // Get HTML elements
    const difficultySelect = document.getElementById('difficulty-select');
    const startGameButton = document.querySelector('button');
    const gameContainer = document.getElementById('game-container');
    const higherButton = document.getElementById('higher-button');
    const lowerButton = document.getElementById('lower-button');
    const resultContainer = document.getElementById('result-container');
    const playAgainButton = document.getElementById('play-again-button');
    const frontImage = document.getElementById('bikar');
    const easy = document.getElementById('easy_level');
    const normal = document.getElementById('normal_level');
    const hard = document.getElementById('hard_level');
    const diff = document.getElementById('diff-text');

    
    
    let currentDriverIndex;
    let previousDriverIndex;
    let currentDifficulty;
    let score;
    
    
    // Add event listener to the "Start Game" button
    startGameButton.addEventListener('click', startGame);
    
    function startGame() {
        startGameButton.style.display = 'none'
        frontImage.style.display = 'none';
        easy.style.display = 'none';
        normal.style.display = 'none';
        hard.style.display = 'none';
        difficultySelect.style.display = 'none'
        diff.style.display = 'none'


      currentDifficulty = difficultySelect.value;
      
      // Show the type of data to be guessed by the user
      const dataTypeElement = document.getElementById('data-type');
        if (currentDifficulty === 'easy') {
        dataTypeElement.textContent = 'Guess the driver with more wins';
      } else if (currentDifficulty === 'normal') {
        dataTypeElement.textContent = 'Guess the driver with more podiums';
      } else if (currentDifficulty === 'hard') {
        dataTypeElement.textContent = 'Guess the driver with more points';
      }
    
      higherButton.addEventListener('click', onHigherButtonClicked);
      lowerButton.addEventListener('click', onLowerButtonClicked);
      
      score = 0;
    
      // Hide the result container and play again button
      resultContainer.textContent = '';
      playAgainButton.style.display = 'none';
    
      // Show the first driver
      showNextDriver();
    }
    
    
    function onHigherButtonClicked() {
      checkAnswer('higher');
    }
    
    function onLowerButtonClicked() {
      checkAnswer('lower');
    }
    
    let lastDriverIndex;
    
    function showNextDriver() {
      // Clear the previous driver's data
      gameContainer.innerHTML = '';
    
      // Pick two random drivers from the list
      if (!currentDriverIndex) {
        currentDriverIndex = getRandomDriverIndex();
      }
    
      if (!previousDriverIndex) {
        previousDriverIndex = getRandomDriverIndex(currentDriverIndex);
      }
    
      // Create and append elements to display the two drivers and their data
      const currentDriverElement = document.createElement('div');
      const previousDriverElement = document.createElement('div');
      const currentDriverDataElement = document.createElement('ul');
      const previousDriverDataElement = document.createElement('ul');
      const vsElement = document.createElement('div');
    
      currentDriverElement.classList.add('driver');
      previousDriverElement.classList.add('driver');
      vsElement.textContent = "Vs";
    
      currentDriverElement.innerHTML = `
          <h2>${f1Drivers[currentDriverIndex].name}</h2>
          <img src="driver-images/${f1Drivers[currentDriverIndex].name.toLowerCase().replace(' ', '-')}.png">
        `;
      previousDriverElement.innerHTML = `
          <h2>${f1Drivers[previousDriverIndex].name}</h2>
          <img src="driver-images/${f1Drivers[previousDriverIndex].name.toLowerCase().replace(' ', '-')}.png">
        `;
    
      currentDriverElement.appendChild(currentDriverDataElement);
      previousDriverElement.appendChild(previousDriverDataElement);
      gameContainer.appendChild(currentDriverElement);
      gameContainer.appendChild(vsElement);
      gameContainer.appendChild(previousDriverElement);
    
      // Show the "Higher or Lower" buttons
      const buttonContainer = document.getElementById('button-container');
      buttonContainer.style.display = 'block';
    }
    
    function getRandomDriverIndex(excludeIndex) {
      let index;
      do {
        index = Math.floor(Math.random() * f1Drivers.length);
      } while (index === excludeIndex);
      return index;
    }
    
    
    function checkAnswer(guess) {
      const previousDriver = f1Drivers[previousDriverIndex];
      const currentDriver = f1Drivers[currentDriverIndex];
      let previousData, currentData;
      if (currentDifficulty === 'easy') {
        previousData = previousDriver.wins;
        currentData = currentDriver.wins;
      } else if (currentDifficulty === 'normal') {
        previousData = previousDriver.podiums;
        currentData = currentDriver.podiums;
      } else if (currentDifficulty === 'hard') {
        previousData = previousDriver.points;
        currentData = currentDriver.points;
      }
    
      const answerIsCorrect =
        (guess === 'higher' && currentData <= previousData) ||
        (guess === 'lower' && currentData >= previousData);
    
    
      if (answerIsCorrect) {
        score++;
        currentDriverIndex = previousDriverIndex;
        previousDriverIndex = null;
        showNextDriver();
      } else {
        endGame();
      }
    }
    
    
    function endGame() {
      // Remove event listeners from the buttons
      higherButton.removeEventListener('click', onHigherButtonClicked);
      lowerButton.removeEventListener('click', onLowerButtonClicked);
    
      let message = "";
      if (score <= 1) {
        const messages = [
          "You may need to get a new pit crew - they're clearly not feeding you the right information!",
          "That answer is a bit like a car stuck in the gravel trap - not quite what we were hoping for!",
          "Looks like you need to spend less time watching the races and more time studying the history books!",
          "Looks like you need some more practice laps before you get the hang of this."
        ];
        const randomIndex = Math.floor(Math.random() * messages.length);
        message = `${messages[randomIndex]} ${message}`;
      } else if (score <= 4) {
        const messages = [
          "Let's just say, if you were driving in the F1, you'd be lapped by now.",
          "Very Bad - You might want to stick to bumper cars!",
          "Don't worry, even the best drivers have their off days. Maybe you'll do better next time.",
          "Well, that answer was definitely not pole position material."
        ];
        const randomIndex = Math.floor(Math.random() * messages.length);
        message = `${messages[randomIndex]} ${message}`;
      } else if (score <= 10) {
        const messages = [
          "You're like a midfield driver - solid, but not quite podium material.",
          "You're doing okay, but maybe you should watch a few more races before playing again.",
          "You're not exactly setting the track on fire, but you're not stalling out either.",
          "Not bad, not bad at all! You're definitely on the right track to becoming an F1 expert."
        ];
        const randomIndex = Math.floor(Math.random() * messages.length);
        message = `${messages[randomIndex]} ${message}`;
      } else {
        const messages = [
          "I think we need to do a doping test on you because that score is unreal!",
          "Congratulations! You just set a new lap record for F1 trivia. Absolutely amazing!",
          "Wow, you're like the Lewis Hamilton of F1 trivia! Impressive!",
          "Hold on, let me check if you're not secretly connected to the FIA. Your knowledge is on another level!"
        ];
        const randomIndex = Math.floor(Math.random() * messages.length);
        message = `${messages[randomIndex]} ${message}`;
      }
    
      // Display the user's score and message
      resultContainer.textContent = `Game over! You got ${score} correct. ${message}`;
      playAgainButton.style.display = 'block';
      playAgainButton.addEventListener('click', startGame);
    }

console returns output for querySelectorAll as null even when Node exists

I am trying to use javascript to find element containing a class among bunch of elements and assign its index no to a variable, but the output of following code is always null, even when the document contains element with the class. Alert windows shows 25.

elementzParent = document.querySelectorAll(".question-counter i");
alert(elementzParent.length);
console.log(elementzParent);
for (let i = 0; i < elementzParent.length; i++) {
  elementzclass = elementzParent[i].classList;
  console.log(elementzclass);
  if (elementzclass.contains('fa-lg')) {

    var PYQNo = i + 1;
    console.log(PYQNo);
  };
};

If I change the console.log to console.info

elementzParent = document.querySelectorAll(".question-counter i");
alert(elementzParent.length);
console.info(elementzParent);
for (let i = 0; i < elementzParent.length; i++) {
  elementzclass = elementzParent[i].classList;
  console.info(elementzclass);
  if (elementzclass.contains('fa-lg')) {

    var PYQNo = i + 1;
    console.log(PYQNo);
  };
};

the output becomes 25 in alert window and

NodeList(25) [ i.fa.fa-circle.ng-tns-c105-1.fa-lg.correct, i.fa.fa-circle.ng-tns-c105-1, i.fa.fa-circle.ng-tns-c105-1, i.fa.fa-circle.ng-tns-c105-1, i.fa.fa-circle.ng-tns-c105-1, i.fa.fa-circle.ng-tns-c105-1, i.fa.fa-circle.ng-tns-c105-1, i.fa.fa-circle.ng-tns-c105-1, i.fa.fa-circle.ng-tns-c105-1, i.fa.fa-circle.ng-tns-c105-1, … ]
DOMTokenList(5) [ "fa", "fa-circle", "ng-tns-c105-1", "fa-lg", "correct" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
DOMTokenList(3) [ "fa", "fa-circle", "ng-tns-c105-1" ]
undefined

HTML is

   <div _ngcontent-oly-c105="" class="question-counter ng-tns-c105-1">   
      <span _ngcontent-oly-c105="" class="ng-tns-c105-1 ng-star-inserted" style="">
        <span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1 fa-lg correct"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><span _ngcontent-oly-c105="" class="question-progress-bar ng-tns-c105-1 ng-star-inserted">
          <i _ngcontent-oly-c105="" aria-hidden="true" class="fa fa-circle ng-tns-c105-1"></i>
        </span><!---->
      </span><!---->
      <!---->
    </div>

I trying javascript for first time by reading from web and running this in firefox console. I would like some guidance as to why my code may not be working?

I tried to use getAttribute() instead of classlist first but that didn’t work too.
Tried console.info which returns all the matching nodes, but it was of no benefit.
Also tried getElementsByClassName instead of querySelectorAll still null output.
Also tried using window.onload.
Works on codepen.io

Transform array to the following format

I have an array of objects that have 2 fields key and type, the type field is practice or theory

Result:

  1. group the array by key

  2. transform the array: in the new array the object with type practice should come in the even index and object with type theory should be in the odd index. (check the input and the output if my explanation wasn’t clear)

here’s my code but it doesn’t work as expected in the transform the array

function transformArray(arr) {
  const grouped = arr.reduce((result, item) => {
    const key = item.key;
    if (!result[key]) {
      result[key] = { key, values: [] };
    }
    result[key].values.push(item);
    return result;
  }, {});

  for (const group in grouped) {
    const values = grouped[group].values;
    const newValues = [];
    for (let i = 0; i < values.length; i++) {
      const item = values[i];
      if (item.type === 'practice' && i % 2 === 0) {
        newValues.push(item);
      } else if (item.type === 'theory' && i % 2 === 1) {
        newValues.push(item);
      }
    }
    grouped[group].values = newValues;
  }

  return Object.values(grouped);
}

example input:

const input = [
{ key: 'A', type: 'practice' },
{ key: 'A', type: 'practice' },
{ key: 'A', type: 'theory' },
{ key: 'A', type: 'theory' },

{ key: 'B', type: 'practice' },
{ key: 'B', type: 'theory' },
{ key: 'B', type: 'practice' },

{ key: 'C', type: 'practice' },
{ key: 'C', type: 'theory' },
{ key: 'C', type: 'practice' },
]

output

[
  {
    key: 'A',
    values: [
      { key: 'A', type: 'practice'},  // index = even ==> practice
      { key: 'A', type: 'theory' },    // index = odd ==> theory
      { key: 'A', type: 'practice'}, // index = even ==> practice
      { key: 'A', type: 'theory'}, // index = odd ==> theory
    ],
  },
  {
    key: 'B',
    values: [
      { key: 'B', type: 'practice' },
      { key: 'B', type: 'theory',},
      { key: 'B', type: 'practice' },
    ],
  },
  {
    key: 'C',
    values: [
      { key: 'C', type: 'practice' },
      { key: 'C', type: 'theory', },
      { key: 'C', type: 'practice'},
    ],
  },
]

how can i convert the html file to a jsx file?

function calculate() {
  var num1, num2, num3, res;

  num1 = Number(document.getElementById('length').value);
  num2 = Number(document.getElementById('width').value);
  num3 = Number(document.getElementById('height').value);
  res = num1 * num2 * num3;
  document.formcalcvolume.volume.value = (res).toFixed(2);

  document.getElementById("tbl").innerHTML = "";

  if (res > 0) {

    var table = `<tr>
                              <td data-col-title="Name" class="tabledatastyle"><b>${"GEN S"}</b></td>
                              <td data-col-title="CADR (m3/h)">${180}</td>
                              <td data-col-title="Volume">${res.toFixed(2)}</td>
                              <td data-col-title="100% ACH">${(180/res).toFixed(2)}</td>
                              <td data-col-title="50% ACH">${(90/res).toFixed(2)}</td>
                          </tr>`;
    document.getElementById('tbl').innerHTML += table;

    var table = `<tr>
                              <td data-col-title="Name" class="tabledatastyle"><b>${"GEN X"}</b></td>
                              <td data-col-title="CADR (m3/h)">${727}</td>
                              <td data-col-title="Volume">${res.toFixed(2)}</td>
                              <td data-col-title="100% ACH">${(727/res).toFixed(2)}</td>
                              <td data-col-title="50% ACH">${(727/(res*2)).toFixed(2)}</td>
                          </tr>`;
    document.getElementById('tbl').innerHTML += table;

    var table = `<tr>
                              <td data-col-title="Name" class="tabledatastyle"><b>${"GEN Y"}</b></td>
                              <td data-col-title="CADR (m3/h)">${633}</td>
                              <td data-col-title="Volume">${res.toFixed(2)}</td>
                              <td data-col-title="100% ACH">${(633/res).toFixed(2)}</td>
                              <td data-col-title="50% ACH">${(633/(res*2)).toFixed(2)}</td>
                          </tr>`;
    document.getElementById('tbl').innerHTML += table;

    var table = `<tr>
                              <td data-col-title="Name" class="tabledatastyle"><b>${"GEN Z"}</b></td>
                              <td data-col-title="CADR (m3/h)">${1156}</td>
                              <td data-col-title="Volume">${res.toFixed(2)}</td>
                              <td data-col-title="100% ACH">${(1156/res).toFixed(2)}</td>
                              <td data-col-title="50% ACH">${(1156/(res*2)).toFixed(2)}</td>
                          </tr>`;
    document.getElementById('tbl').innerHTML += table;

    clearForm();

  } else reset();
}

function clearForm() {
  document.getElementById('length').value = null;
  document.getElementById('height').value = null;
  document.getElementById('width').value = null;

}

function reset() {
  document.getElementById('length').value = null;
  document.getElementById('height').value = null;
  document.getElementById('width').value = null;
  document.getElementById('volume').value = null;

  document.getElementById("tbl").innerHTML = "";
}

function cleartable() {}
<div class="boddy">

  <div class="total">
    <div class="cadr-info">
      <form name="formcalcvolume" id="form1" class="classicForm">
        <h3 style="color:rgb(64,141,194)">Room Volume</h3>
        <p>
          <label><b>Length (m)</b><b style="color: red">*</b></label>
          <input type="text" id="length" />
        </p>
        <p>
          <label><b>Width (m)</b><b style="color: red">*</b></label>
          <input type="text" id="width" />
        </p>
        <p>
          <label><b>Height (m)</b><b style="color: red">*</b></label>
          <input type="text" id="height" />
        </p>
        <p>
          <button type="button" onclick="calculate()">Calculate</button>
        </p>
        <p>
          <button type="button" onclick="reset()">Reset</button>
        </p>
        <p>
          <label>Room Volume  (m3)</label>
          <input type="text" id="volume" />
        </p>
      </form>
    </div>
    <div class="cadr-info">
      <h3 style="color:rgb(64,141,194)"><b>Air Changes per Hour</b></h3>

      <form class="cadr-form">
        <table id="cadrtable" class="cadr-table" style="width:100%">
          <thead class="tableheading">
            <tr>
              <th scope="col" style="background-color:#6a78ed; color:white;"><b>Name</b></th>
              <th scope="col" style="background-color:#6a78ed; color:white;"><b>CADR(m3/h)</b></th>

              <th scope="col" style="background-color:#6a78ed; color:white;"><b>Room Volume</b></th>
              <th scope="col" style="background-color:#6a78ed; color:white;"><b>100% ACH</b></th>
              <th scope="col" style="background-color:#6a78ed; color:white;"><b>50% ACH</b></th>
            </tr>

          </thead>
          <tbody id="tbl" class="tablecontent"></tbody>
        </table>

      </form>
      <h4 id="emaillink" style="color:rgb(64,141,194)">For a more detailed report, contact us on <br>[email protected]</h4>
    </div>

  </div>
</div>