How to centrize the one element only in the div?

<div style="justify-content:center;border:1px solid;width:80px;display:flex;align-items: baseline">
<div style="font-size:2rem">12</div>
<div style="font-size:1rem">pt</div>
<div>

it shows the centered 12pt,but what I want to do is centerize 12 not including pt.

I can use layout:fix and set the pt to the slightly left.

However 12 could be changed like 123 1234 so pt position can not be fixed.

How can I centerize only the number?

PostgreSQL handle positional parameters

I’m using MySQL for years with Nodejs and I’m interested about switching to PostgreSQL. However something really scares me : positional parameters.

I remember when I switched from positional to named parameters in MySQL, this has been a real relief.

How do people handle positional parameter in PostgreSQL ? What when an errors tells you “Parameter $36 is incorrect”? Or when number of parameters doesn’t match row count? I remember really having hard times troubleshooting this kind of errors back in the day and Y don’t want to revive this…
We also usually push array of objects or directly object to backend. Having to convert them to array every time with Object.values() feels weird

Any advise or is there something I’m missing? I can’t believe PostgreSQL is so popular and people keep struggling with this.

Leaflet map: Custom popup layer always appears under Leaflet buttons, despite z-index attempts

I’m working on a web app using Leaflet and built a custom destination tile/popup outside of Leaflet’s rendering system, to keep the layout more flexible. The tile appears correctly, but it always gets overlaid by Leaflet’s default controls (e.g., zoom buttons, filters, etc.).

What I’ve tried

  • z-index on almost every element
  • Adjusting position (absolute, relative, fixed, etc.)
  • Nesting the custom element differently
  • Adding/removing pointer-events
  • Changing render order in HTML
  • Renaming/restructuring CSS classes

I’ve also consulted CSS pros with 10+ years of experience and multiple LLMs (Claude 3, Gemini 2.5, Grok, OpenAI o1/o3), but no working solution so far.

Feedback from CSS expert

“To make this work, the custom popup would need to be placed inside the leaflet-pane leaflet-map-pane container — but that isn’t possible in our setup.”

My current theory

Since the popup isn’t part of Leaflet’s internal layer structure (i.e., not added via L.popup or similar), it’s being rendered in a separate DOM context. As a result, even with higher z-index values, the Leaflet controls still appear on top.

Expected behavior

  • The custom popup (destination tile) should appear above all Leaflet controls.

Actual behavior

  • The popup always renders below Leaflet buttons/controls, no matter what z-index I apply.

Reproducible Example

Question

Is there a clean way to ensure an external popup (not managed by Leaflet itself) can appear above Leaflet controls, without moving it into Leaflet’s pane system?

Any CSS trick, layering hack, or workaround appreciated. JS-based solutions are also fine if necessary.

Nextjs 15 optional catch all segment

enter image description here

okay so i have a file named page.tsx in here, im using nextjs 15 latest, im following a tutorial which is also for nextjs 15, and mine has been working similarly as his, but unlike him, when i wrap the […slug] inside a square bracket([]), all of his route, including the base http://localhost:3000/docs became available, while any routes that i have started with docs result in error 404?

Cant use react-photo-view to display photo

I am not able to display my photos using the react-photo-view library.

My code is as follow and its not working. If i remove the photo viewer and PhotoProvider, my image will be displayed. Is the library not compatible with NextJS?:

import parse from 'html-react-parser';
import Image from 'next/image';
import { PhotoProvider, PhotoView } from 'react-photo-view';

import 'react-photo-view/dist/react-photo-view.css';

import { ImageProps } from './types';

export default async function ImageProps(props: ImageProps) {
  const { title, content, image, category } = props;

  return (
    <>
      <div className="summary text-[14px] my-[10px]">
        <PhotoProvider>
          <PhotoView src={image.id}>
            <Image
              src={image.id}
              className="w-full h-auto object-contain mb-4"
              alt="Slide Image"
            />
          </PhotoView>
        </PhotoProvider>
      </div>
    </>
  );
}

SharePoint 2019 – List View Command Bar with BaseDialog and DropDown List

Hi experts: When I customize a List View Extension – Command Bar on SharePoint 2019 On-Premises and click on my custom Command Bar, my BaseDialog – Panel appears. I have successfully retrieved the options from a Choice field in the list and displayed them in the DropDown List. Unfortunately, when I select a text, I am unable to retrieve the selected text.

I have also attached a link to an image related to the issue. As follows:

enter image description here

Here is my entire code. I have spent a long time but still cannot find the issue. I wonder if anyone with experience can provide some directions or a solution to help me resolve this problem.

I really appreciate everyone’s help. Thanks again!

A. Explanation:

(1) At the start of the program execution, the onOpen() function is called to retrieve the options from the backend Choice field in the specific list and then insert the extracted options into the DropDown List.


componentDidMount() {
    console.log('componentDidMount has been called');
    if (this.props.onOpen) {
      this.props.onOpen();
    }
  }

public onOpen(): void {
    console.log('onOpen() ==> I am here.');
    this.loadChoiceFieldOptions(); // Load machine options
  }

private async loadChoiceFieldOptions(): Promise<void> {
    console.log('loadChoiceFieldOptions() ==> I am here.');
    try {
      const listId = '5dadf73c-117d-44ea-a755-23cb4786ea95'; // 換成你的清單 GUID
      const fieldInternalName = '_x6a5f__x53f0__x7de8__x865f_'; // <-- 換成你的欄位 Internal Name
  
      const field = await sp.web.lists.getById(listId)
        .fields.getByInternalNameOrTitle(fieldInternalName)
        .get();
      const choices: string[] = (field as any).Choices; // ← 這樣就不報錯了
  
      this.machineOptions = choices.map(choice => ({
        key: choice,
        text: choice
      }));
  
      this.render(); // Re-render to update the machine options
    } catch (error) {
      console.error('載入機台選項錯誤:', error);
      this.machineOptions = [];
      this.render(); // Re-render to update the message
    }
  }


(2) In the render function, I generate a DropDown List.


<Dropdown
          label="機台編號"
          options={[defaultOption, ...this.filterMachineOptions()]}
          onSelect={(
            event: React.FormEvent<HTMLDivElement>, 
            option?: IDropdownOption
          ) => {
            console.log('Dropdown onChange fired!', option);
          }}
          selectedKey={this.state.machineId || 'placeholder'}
        />


(3) However, in the RdasSearchPanel class, there is another render process (I am not sure if this is incorrect)


export default class RdasSearchPanel extends BaseDialog {
  private machineOptions: IDropdownOption[] = [];

  public render(): void {
    ReactDOM.render(
      <RdasSearchPanelContent 
        onDismiss={() => this.close()} 
        onOpen={() => this.onOpen()} 
        machineOptions={this.machineOptions}
      />, 
      this.domElement
    );
  }


B. Here is my entire code.

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { BaseDialog } from '@microsoft/sp-dialog';
import { Panel, PanelType, TextField, PrimaryButton, MessageBar, MessageBarType, Dropdown, IDropdownOption } from 'office-ui-fabric-react';
import { sp } from '@pnp/sp/presets/all';

export interface IRdasSearchPanelState {
  projectCode: string;
  lotNumber: string;
  machineId: string;
  items: any[];
  message: string;
  machineOptions: IDropdownOption[];
  searchQuery: string;
}

class RdasSearchPanelContent extends React.Component<any, IRdasSearchPanelState> {
  constructor(props: any) {
    super(props);
    this.state = {
      projectCode: '',
      lotNumber: '',
      machineId: '',
      items: [],
      message: '',
      machineOptions: props.machineOptions || [],
      searchQuery: ''
    };
  }

  componentDidMount() {
    console.log('componentDidMount has been called');
    if (this.props.onOpen) {
      this.props.onOpen();
    }
  }

  componentDidUpdate(prevProps: any) {
    if (prevProps.machineOptions !== this.props.machineOptions) {
      this.setState({ machineOptions: this.props.machineOptions });
    }
  }

  handleInputChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
    const target = event.target as HTMLInputElement;
    const name = target.name;
    const value = target.value;
    this.setState({ [name]: value } as unknown as Pick<IRdasSearchPanelState, keyof IRdasSearchPanelState>);
  };

  handleDropdownChange = (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
    console.log('handleDropdownChange ==> I am here.');
    if (option && option.key !== 'placeholder') {
      this.setState({ machineId: option.key as string });
    }
  };

  filterMachineOptions = (): IDropdownOption[] => {
    return this.state.machineOptions.filter(option => option.text.toLowerCase().includes(this.state.searchQuery.toLowerCase()));
  };

  searchListItems = async (): Promise<void> => {
    try {
      const { projectCode, lotNumber, machineId } = this.state;
      const baseUrl = window.location.origin;
      const url = `${baseUrl}/Lists/List4/AllItems.aspx?useFiltersInViewXml=1&FilterField1=LinkTitle&FilterValue1=${projectCode}&FilterField2=_x6279__x865f_&FilterValue2=${lotNumber}&FilterField3=_x6a5f__x53f0__x7de8__x865f_&FilterValue3=${machineId}`;
      window.location.href = url;
    } catch (error) {
      console.error('Error searching list items: ', error);
      this.setState({
        message: 'Error searching list items'
      });
    }
  };

  render() {
    const defaultOption: IDropdownOption = { key: 'placeholder', text: '請選擇機台' };

    return (
      <Panel
        isLightDismiss={false}
        isOpen={true}
        type={PanelType.medium}
        onDismissed={this.props.onDismiss}
      >
        <h2>Hello there from my custom Panel</h2>

        <TextField 
          label="提案序號" 
          name="projectCode"
          onKeyUp={this.handleInputChange} 
        />

        <TextField 
          label="批號" 
          name="lotNumber"
          onKeyUp={this.handleInputChange} 
        />

        <TextField 
          label="Search Machine"
          onKeyUp={(event) => this.setState({ searchQuery: (event.target as HTMLInputElement).value })}
        />

        <Dropdown
          label="機台編號"
          options={[defaultOption, ...this.filterMachineOptions()]}
          onSelect={(
            event: React.FormEvent<HTMLDivElement>, 
            option?: IDropdownOption
          ) => {
            console.log('Dropdown onChange fired!', option);
          }}
          selectedKey={this.state.machineId || 'placeholder'}
        />

        <PrimaryButton 
          text='Search' 
          onClick={this.searchListItems} 
        />
        {this.state.message && 
          <MessageBar 
            messageBarType={MessageBarType.info}
            isMultiline={false}
          >
            {this.state.message}
          </MessageBar>
        }
        {this.state.items.length > 0 && 
          <ul>
            {this.state.items.map(item => (
              <li key={item.Id}>{item.Title}</li>
            ))}
          </ul>
        }

        <div>
          <p>Project Code: {this.state.projectCode}</p>
          <p>Lot Number: {this.state.lotNumber}</p>
          <p>Machine ID: {this.state.machineId}</p>
        </div>
      </Panel>
    );
  }
}

export default class RdasSearchPanel extends BaseDialog {
  private machineOptions: IDropdownOption[] = [];

  public render(): void {
    ReactDOM.render(
      <RdasSearchPanelContent 
        onDismiss={() => this.close()} 
        onOpen={() => this.onOpen()} 
        machineOptions={this.machineOptions}
      />, 
      this.domElement
    );
  }

  public onOpen(): void {
    console.log('onOpen() ==> I am here.');
    this.loadChoiceFieldOptions(); // Load machine options
  }

  private async loadChoiceFieldOptions(): Promise<void> {
    console.log('loadChoiceFieldOptions() ==> I am here.');
    try {
      const listId = '5dadf73c-117d-44ea-a755-23cb4786ea95'; // 換成你的清單 GUID
      const fieldInternalName = '_x6a5f__x53f0__x7de8__x865f_'; // <-- 換成你的欄位 Internal Name
  
      const field = await sp.web.lists.getById(listId)
        .fields.getByInternalNameOrTitle(fieldInternalName)
        .get();
      const choices: string[] = (field as any).Choices; // ← 這樣就不報錯了
  
      this.machineOptions = choices.map(choice => ({
        key: choice,
        text: choice
      }));
  
      this.render(); // Re-render to update the machine options
    } catch (error) {
      console.error('載入機台選項錯誤:', error);
      this.machineOptions = [];
      this.render(); // Re-render to update the message
    }
  }
}

How to stop a hover in moving the image options? [closed]

I have a project and I have a trouble to fix this, my problem here is that my image options is moving up and down whenever I point my cursor to my image options it affects my descriptions. What should I do to fix this? I’m just starting out, so I appreciate your understanding.

I just want to fix image option to move up and down whenever I point the other images and to not affect the descriptions.
Here is the output

Here is my code.

:root {
  --text-dark: #000000;
  --text-light: #727274;
  --extra-light: #f3f4f6;
  --max-width: 1200px;
  --header-font: "Montserrat", sans-serif;
  --text-font: "Poppins", sans-serif;
}

body {
  margin: 0;
  padding: 0;
  background-image: url('backg5.png');
  background-size: cover;
  background-position: center 10px;
  background-attachment: fixed;
  color: white;
  overflow-x: hidden;
  padding-top: 80px; 
}

header {
  display: flex;
  position: fixed;
  justify-content: center;
  align-items: center;
  top: 0;
  width: 100%;
  z-index: 1000;
  transition: top 0.3s ease-in-out;
  color: white;
  background: hsl(0, 100%, 100%);
  box-shadow: -4px 0 6px rgba(0, 0, 0, 0.1), 4px 0 6px rgba(0, 0, 0, 0.1);
  padding: 10px 15px;
}

.logo {
  display: flex;
  justify-content: center;
  align-items: center;
  flex: 1;
}

.logo img {
  max-width: 120px;
  margin-top: 5px;
}

.product-container {
  position: absolute;
  display: inline-block;
  align-items: center;
  justify-content: flex-start;
  transform: translateY(-50%);
  left: 40px;
  top: 50%;
}

.product-icon {
  font-size: 30px;
  color: green;
}

.dropdown {
  position: relative;
  display: inline-block;
  margin-left: auto;
}

.dropdown-content {
  display: none;
  position: absolute;
  margin-left: auto;
  background-color: #f9f9f9;
  width: 1260px;
  top: 71%;
  padding: 0;
  left: 0;
  z-index: 1000;
  transition: opacity 0.3s ease, visibility 0.3s ease;
  border-radius: 8px;
  max-height: 160px;
  overflow-y: auto;
}

.dropdown:hover .dropdown-content {
  display: block;
  padding-top: 3px;
  background-color: #f9f9f9;
  box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
  opacity: 1;
  visibility: visible;
}

.dropdown-content:hover {
  display: block;
  opacity: 1;
  visibility: visible;
}

.dropdown:hover .dropdown-content {
  background-color: whitesmoke;
}

.container {
  display: flex;
  overflow-x: auto;
  margin: 10px;
  scroll-padding: 0px;
  gap: 10px;
  max-height: 100vh;
  overflow-y: hidden;
}

.container::-webkit-scrollbar {
  display: block;
}

.container::-webkit-scrollbar-track {
  background-color: #AAB99A;
  border-radius: 10px;
}

#Container {
  display: flex;
  scroll-snap-type: x mandatory;
}

.item {
  flex: 0 0 auto;
}

.category {
  width: 100%;
  max-width: 300px;
  height: auto;
  border-radius: 5px;
}

.product-category {
  width: 100px;
  height: 100px;
  object-fit: contain;
  gap: 2px;
  border-radius: 10px;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  margin-bottom: 10px;
}

.product-category:hover {
  transform: scale(1.1);
  padding: 5px;
  z-index: 10;
}

.page-body {
  font-family: var(--header-font);
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 90vh;
  padding: 5px;
}

.page-container {
  background-color: #fff;
  display: flex;
  flex-direction: column;
  border-radius: 10px;
  box-shadow: 0 5px 15px rgba(0,0,0,0.1);
  padding: 30px;
  max-width: 1000px;
  width: 100%;
  margin-bottom: 0;
  padding-bottom: 0;
  gap: 10px;
}

@media (min-width: 768px) {
  .page-container {
    flex-direction: row;
  }
}

.image-gallery {
  flex: 0.9;
  text-align: left;
}

.image-gallery img {
  max-width: 100%;
  height: auto;
}

.image-wrapper {
  max-width: 450px;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.thumbnail-wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
  padding: 5px 0;
  width: 100%;
}

.thumbnail-row {
  display: flex;
  gap: 4px; 
  overflow-x: auto;
  height: 80px;
  scroll-behavior: smooth;
  pointer-events: auto;
  scrollbar-width: none;
  scroll-snap-type: x mandatory;
  padding-top: 8px;
  padding-left: 0;
  padding-right: 0;
  align-items: center;
}

.thumbnail-row::-webkit-scrollbar {
  display: none;
}

.thumbnail-row img {
  width: 75px;
  height: 75px;
  border: 2px solid transparent;
  transition: border 0.2s ease, transform 0.2s ease;
  cursor: pointer;
  pointer-events: auto;
  scroll-snap-align: start;
  box-sizing: border-box;
  object-fit: cover;
  flex-shrink: 0;
  margin-left: 0;
  margin-right: 0;
  position: relative;
  z-index: 1;
  display: block;
}

.thumbnail-row img:hover {
  border-color: #5F8B4C;
  z-index: 1;
}

/* Selected state */
.thumbnail-row img.selected {
  border-color: #5F8B4C;
}

.arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  border: none;
  font-size: 30px;
  padding: 8px;
  cursor: pointer;
  z-index: 10;
  color: white;
  background-color: rgba(87, 83, 83, 0.428);
 
}

.arrow img {
  width: 25px;
  height: 25px;
  pointer-events: none;
}

.arrow.left {
  position: absolute;
  left: -0px;
}

.arrow.right {
  position: absolute;
  right: -0px;
}

.product-details {
  flex: 1;
  padding-top: 10px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: flex-start;
  text-align: left;
  gap: 10px;
  margin-top: 0;
  padding-left: 0;
}

@media (min-width: 768px) {
  .product-details {
    padding-left: 0px;
    padding-top: 0;
    flex-direction: column;
  }
}

.product-details h2 {
  color: var(--text-dark);
  font-family: var(--header-font);
  text-align: left;
}

.price {
  color: #5F8B4C;
  font-weight: bold;
  font-family: var(--header-font);
  font-size: large;
  text-align: left;
}

.description {
  margin: 15px 0;
  color: #666;
  font-family: var(--text-font);
  font-size: 16px;
  text-align: left;
}

.color-name {
  color: var(--text-dark);
  font-weight: bold;
  text-align: left;
}

.colors {
  margin: 10px 0;
  flex-wrap: wrap;
  gap: 10px;
  overflow: hidden;
  text-align: left;
}

.color-option {
  display: inline-block;
  width: 30px;
  height: 30px;
  border-radius: 50%;

  margin-right: 10px;
  cursor: pointer;
  transition: transform 0.3s ease;
  transform-origin: center center;
  border: 2px solid #EFF3EA;
  flex-shrink: 0; 
}

.color-option:hover {
  outline: 2px solid #EFF3EA;
  outline-offset: 0px;
  transform: scale(1.2);
}


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

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>GOOJODOQ</title>

 
  <link rel="stylesheet" href="productin2.css" />

  <!-- External Scripts -->
  <script src="https://unpkg.com/scrollreveal"></script>
  <script src="main.js" defer></script>

 
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link href="https://fonts.googleapis.com/css2?family=Antonio:wght@700&family=Russo+One&display=swap" rel="stylesheet" />
  <link href="https://fonts.googleapis.com/css2?family=Teko:wght@400;700&family=Oswald:wght@400;700&family=Rajdhani:wght@400;700&display=swap" rel="stylesheet" />
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&family=Montserrat:wght@400;700&family=Raleway:wght@400;700&display=swap" rel="stylesheet" />

  <!-- Font Awesome -->
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet" />
</head>

<body>


  <header>
    <div class="logo">
      <img src="goojodoq-logo.png" alt="Goojodoq Logo" />
    </div>
    
    <div class="product-container">
      <!-- Product Dropdown -->
      <div class="dropdown">
        <a href="#" id="product-link">
          <i class="fas fa-shopping-cart product-icon"></i>
        </a>
        <div class="dropdown-content">
          <div class="container" id="Container">
            <!-- Product Items -->
            <div class="item"><img src="fan.png" alt="Fan" class="product-category" /></div>
            <div class="item"><img src="mouse.png" alt="Mouse" class="product-category" /></div>
            <div class="item"><img src="stylus_pen.png" alt="Stylus Pen" class="product-category" /></div>
            <div class="item"><img src="USB.png" alt="USB" class="product-category" /></div>
            <div class="item"><img src="keyboard.png" alt="Keyboard" class="product-category" /></div>
            <div class="item"><img src="speaker.png" alt="Speaker" class="product-category" /></div>
            <div class="item"><img src="fan.png" alt="Fan" class="product-category" /></div>
            <div class="item"><img src="mouse.png" alt="Mouse" class="product-category" /></div>
            <div class="item"><img src="stylus_pen.png" alt="Stylus Pen" class="product-category" /></div>
            <div class="item"><img src="USB.png" alt="USB" class="product-category" /></div>
            <div class="item"><img src="keyboard.png" alt="Keyboard" class="product-category" /></div>
            <div class="item"><img src="speaker.png" alt="Speaker" class="product-category" /></div>
          </div>
        </div>
      </div>
    </div>
  </header>

  
  <main>
    <div class="page-body">
      <div class="page-container">
        <!-- Image Gallery -->
        <div class="image-gallery">
          <div class="image-wrapper">
            <img id="mainImage" src="/pictures/1.jpg" alt="Headphones" />
            <div class="thumbnail-wrapper">
              <button class="arrow left" onclick="scrollThumbnails(-1)">&#8249;</button>
              <div class="thumbnail-row" id="thumbnailRow">
                <img src="/pictures/1.jpg" onclick="changeImage(this)" />
                <img src="/pictures/2.jpg" onclick="changeImage(this)" />
                <img src="/pictures/3.jpg" onclick="changeImage(this)" />
                <img src="/pictures/4.jpg" onclick="changeImage(this)" />
                <img src="/pictures/5.jpg" onclick="changeImage(this)" />
                <img src="/pictures/7.jpg" onclick="changeImage(this)" />
                
              </div>
              <button class="arrow right" onclick="scrollThumbnails(1)">&#8250;</button>
            </div>
          </div>
        </div>

        <!-- Product Details Section -->
        <div class="product-details">
          <h2>Beats Solo3 Wireless</h2>
          <div class="price">$999.99</div>
          <div class="description">
            Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
          </div>
          <div class="colors">
            <div class="color-name">Available Colors</div><br>
            <div class="color-option" style="background-color: white;"></div>
            <div class="color-option" style="background-color: pink;"></div>
            <div class="color-option" style="background-color: purple;"></div>
          </div>
        </div>

      </div>
    </div>
  </main>

</body>
</html>
document.addEventListener("DOMContentLoaded", function () {
    let lastScrollTop = 0;
    const header = document.querySelector("header");
  
    window.addEventListener("scroll", function () {
      const currentScroll = window.pageYOffset || document.documentElement.scrollTop;
      header.style.top = currentScroll > lastScrollTop ? "-100px" : "0";
      lastScrollTop = currentScroll <= 0 ? 0 : currentScroll;
    });
  
    const container = document.getElementById("Container");
    if (container) {
      container.addEventListener("wheel", function (event) {
        event.preventDefault();
        this.scrollLeft += event.deltaY;
      });
    }
  
    function changeImage(imgElement) {
        const mainImage = document.getElementById("mainImage");
        if (mainImage) {
          mainImage.src = imgElement.src;
        }
      
        document.querySelectorAll(".thumbnail-row img").forEach(img => {
          img.classList.remove("selected");
        });
      
        imgElement.classList.add("selected");
      }
  
    function changeQty(change) {
      const qtyElement = document.getElementById("quantity");
      if (qtyElement) {
        let qty = parseInt(qtyElement.textContent) || 1;
        qty = Math.max(qty + change, 1);
        qtyElement.textContent = qty;
      }
    }
  
    const scrollContainer = document.getElementById("thumbnailRow");
    const leftArrow = document.querySelector(".arrow.left");
    const rightArrow = document.querySelector(".arrow.right");
  
    function scrollThumbnails(direction) {
      const scrollAmount = 100;
      if (scrollContainer) {
        scrollContainer.scrollBy({
          left: direction * scrollAmount,
          behavior: "smooth"
        });
      }
    }
  
    if (leftArrow && rightArrow && scrollContainer) {
      leftArrow.addEventListener("click", () => scrollThumbnails(-1));
      rightArrow.addEventListener("click", () => scrollThumbnails(1));
    }
  
    const mainImage = document.getElementById("mainImage");
    const thumbnails = document.querySelectorAll(".thumbnail-row img");
  
    thumbnails.forEach(thumbnail => {
      thumbnail.addEventListener("mouseover", () => {
        // Set as main image
        if (mainImage) {
          mainImage.src = thumbnail.src;
        }
  
        thumbnails.forEach(img => img.classList.remove("selected"));
  
        // Add selected to hovered
        thumbnail.classList.add("selected");
      });
    });

    window.img = function (src) {
      const mainImage = document.getElementById("mainImage");
      if (mainImage) {
        mainImage.src = src;
      }
    };
  
    if (typeof ScrollReveal !== "undefined") {
      ScrollReveal().reveal(".page-body", {
        origin: "bottom",
        distance: "50px",
        duration: 1000,
        delay: 100,
        reset: true
      });
    } else {
      console.warn("ScrollReveal is not defined.");
    }
  
    window.changeImage = changeImage;
    window.changeQty = changeQty;
  });

How to stop a hover in moving the image options using html/css and javascript

I have a project and I have a trouble to fix this, my problem here is that my image options is moving up and down whenever I point my cursor to my image options it affects my descriptions. What should I do to fix this? I’m just starting out, so I appreciate your understanding.

I just want to fix image option to move up and down whenever I point the other images and to not affect the descriptions.
Here is the output

Here is my code.

:root {
  --text-dark: #000000;
  --text-light: #727274;
  --extra-light: #f3f4f6;
  --max-width: 1200px;
  --header-font: "Montserrat", sans-serif;
  --text-font: "Poppins", sans-serif;
}

body {
  margin: 0;
  padding: 0;
  background-image: url('backg5.png');
  background-size: cover;
  background-position: center 10px;
  background-attachment: fixed;
  color: white;
  overflow-x: hidden;
  padding-top: 80px; 
}

header {
  display: flex;
  position: fixed;
  justify-content: center;
  align-items: center;
  top: 0;
  width: 100%;
  z-index: 1000;
  transition: top 0.3s ease-in-out;
  color: white;
  background: hsl(0, 100%, 100%);
  box-shadow: -4px 0 6px rgba(0, 0, 0, 0.1), 4px 0 6px rgba(0, 0, 0, 0.1);
  padding: 10px 15px;
}

.logo {
  display: flex;
  justify-content: center;
  align-items: center;
  flex: 1;
}

.logo img {
  max-width: 120px;
  margin-top: 5px;
}

.product-container {
  position: absolute;
  display: inline-block;
  align-items: center;
  justify-content: flex-start;
  transform: translateY(-50%);
  left: 40px;
  top: 50%;
}

.product-icon {
  font-size: 30px;
  color: green;
}

.dropdown {
  position: relative;
  display: inline-block;
  margin-left: auto;
}

.dropdown-content {
  display: none;
  position: absolute;
  margin-left: auto;
  background-color: #f9f9f9;
  width: 1260px;
  top: 71%;
  padding: 0;
  left: 0;
  z-index: 1000;
  transition: opacity 0.3s ease, visibility 0.3s ease;
  border-radius: 8px;
  max-height: 160px;
  overflow-y: auto;
}

.dropdown:hover .dropdown-content {
  display: block;
  padding-top: 3px;
  background-color: #f9f9f9;
  box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
  opacity: 1;
  visibility: visible;
}

.dropdown-content:hover {
  display: block;
  opacity: 1;
  visibility: visible;
}

.dropdown:hover .dropdown-content {
  background-color: whitesmoke;
}

.container {
  display: flex;
  overflow-x: auto;
  margin: 10px;
  scroll-padding: 0px;
  gap: 10px;
  max-height: 100vh;
  overflow-y: hidden;
}

.container::-webkit-scrollbar {
  display: block;
}

.container::-webkit-scrollbar-track {
  background-color: #AAB99A;
  border-radius: 10px;
}

#Container {
  display: flex;
  scroll-snap-type: x mandatory;
}

.item {
  flex: 0 0 auto;
}

.category {
  width: 100%;
  max-width: 300px;
  height: auto;
  border-radius: 5px;
}

.product-category {
  width: 100px;
  height: 100px;
  object-fit: contain;
  gap: 2px;
  border-radius: 10px;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  margin-bottom: 10px;
}

.product-category:hover {
  transform: scale(1.1);
  padding: 5px;
  z-index: 10;
}

.page-body {
  font-family: var(--header-font);
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 90vh;
  padding: 5px;
}

.page-container {
  background-color: #fff;
  display: flex;
  flex-direction: column;
  border-radius: 10px;
  box-shadow: 0 5px 15px rgba(0,0,0,0.1);
  padding: 30px;
  max-width: 1000px;
  width: 100%;
  margin-bottom: 0;
  padding-bottom: 0;
  gap: 10px;
}

@media (min-width: 768px) {
  .page-container {
    flex-direction: row;
  }
}

.image-gallery {
  flex: 0.9;
  text-align: left;
}

.image-gallery img {
  max-width: 100%;
  height: auto;
}

.image-wrapper {
  max-width: 450px;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.thumbnail-wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
  padding: 5px 0;
  width: 100%;
}

.thumbnail-row {
  display: flex;
  gap: 4px; 
  overflow-x: auto;
  height: 80px;
  scroll-behavior: smooth;
  pointer-events: auto;
  scrollbar-width: none;
  scroll-snap-type: x mandatory;
  padding-top: 8px;
  padding-left: 0;
  padding-right: 0;
  align-items: center;
}

.thumbnail-row::-webkit-scrollbar {
  display: none;
}

.thumbnail-row img {
  width: 75px;
  height: 75px;
  border: 2px solid transparent;
  transition: border 0.2s ease, transform 0.2s ease;
  cursor: pointer;
  pointer-events: auto;
  scroll-snap-align: start;
  box-sizing: border-box;
  object-fit: cover;
  flex-shrink: 0;
  margin-left: 0;
  margin-right: 0;
  position: relative;
  z-index: 1;
  display: block;
}

.thumbnail-row img:hover {
  border-color: #5F8B4C;
  z-index: 1;
}

/* Selected state */
.thumbnail-row img.selected {
  border-color: #5F8B4C;
}

.arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  border: none;
  font-size: 30px;
  padding: 8px;
  cursor: pointer;
  z-index: 10;
  color: white;
  background-color: rgba(87, 83, 83, 0.428);
 
}

.arrow img {
  width: 25px;
  height: 25px;
  pointer-events: none;
}

.arrow.left {
  position: absolute;
  left: -0px;
}

.arrow.right {
  position: absolute;
  right: -0px;
}

.product-details {
  flex: 1;
  padding-top: 10px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: flex-start;
  text-align: left;
  gap: 10px;
  margin-top: 0;
  padding-left: 0;
}

@media (min-width: 768px) {
  .product-details {
    padding-left: 0px;
    padding-top: 0;
    flex-direction: column;
  }
}

.product-details h2 {
  color: var(--text-dark);
  font-family: var(--header-font);
  text-align: left;
}

.price {
  color: #5F8B4C;
  font-weight: bold;
  font-family: var(--header-font);
  font-size: large;
  text-align: left;
}

.description {
  margin: 15px 0;
  color: #666;
  font-family: var(--text-font);
  font-size: 16px;
  text-align: left;
}

.color-name {
  color: var(--text-dark);
  font-weight: bold;
  text-align: left;
}

.colors {
  margin: 10px 0;
  flex-wrap: wrap;
  gap: 10px;
  overflow: hidden;
  text-align: left;
}

.color-option {
  display: inline-block;
  width: 30px;
  height: 30px;
  border-radius: 50%;

  margin-right: 10px;
  cursor: pointer;
  transition: transform 0.3s ease;
  transform-origin: center center;
  border: 2px solid #EFF3EA;
  flex-shrink: 0; 
}

.color-option:hover {
  outline: 2px solid #EFF3EA;
  outline-offset: 0px;
  transform: scale(1.2);
}


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

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>GOOJODOQ</title>

 
  <link rel="stylesheet" href="productin2.css" />

  <!-- External Scripts -->
  <script src="https://unpkg.com/scrollreveal"></script>
  <script src="main.js" defer></script>

 
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link href="https://fonts.googleapis.com/css2?family=Antonio:wght@700&family=Russo+One&display=swap" rel="stylesheet" />
  <link href="https://fonts.googleapis.com/css2?family=Teko:wght@400;700&family=Oswald:wght@400;700&family=Rajdhani:wght@400;700&display=swap" rel="stylesheet" />
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&family=Montserrat:wght@400;700&family=Raleway:wght@400;700&display=swap" rel="stylesheet" />

  <!-- Font Awesome -->
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet" />
</head>

<body>


  <header>
    <div class="logo">
      <img src="goojodoq-logo.png" alt="Goojodoq Logo" />
    </div>
    
    <div class="product-container">
      <!-- Product Dropdown -->
      <div class="dropdown">
        <a href="#" id="product-link">
          <i class="fas fa-shopping-cart product-icon"></i>
        </a>
        <div class="dropdown-content">
          <div class="container" id="Container">
            <!-- Product Items -->
            <div class="item"><img src="fan.png" alt="Fan" class="product-category" /></div>
            <div class="item"><img src="mouse.png" alt="Mouse" class="product-category" /></div>
            <div class="item"><img src="stylus_pen.png" alt="Stylus Pen" class="product-category" /></div>
            <div class="item"><img src="USB.png" alt="USB" class="product-category" /></div>
            <div class="item"><img src="keyboard.png" alt="Keyboard" class="product-category" /></div>
            <div class="item"><img src="speaker.png" alt="Speaker" class="product-category" /></div>
            <div class="item"><img src="fan.png" alt="Fan" class="product-category" /></div>
            <div class="item"><img src="mouse.png" alt="Mouse" class="product-category" /></div>
            <div class="item"><img src="stylus_pen.png" alt="Stylus Pen" class="product-category" /></div>
            <div class="item"><img src="USB.png" alt="USB" class="product-category" /></div>
            <div class="item"><img src="keyboard.png" alt="Keyboard" class="product-category" /></div>
            <div class="item"><img src="speaker.png" alt="Speaker" class="product-category" /></div>
          </div>
        </div>
      </div>
    </div>
  </header>

  
  <main>
    <div class="page-body">
      <div class="page-container">
        <!-- Image Gallery -->
        <div class="image-gallery">
          <div class="image-wrapper">
            <img id="mainImage" src="/pictures/1.jpg" alt="Headphones" />
            <div class="thumbnail-wrapper">
              <button class="arrow left" onclick="scrollThumbnails(-1)">&#8249;</button>
              <div class="thumbnail-row" id="thumbnailRow">
                <img src="/pictures/1.jpg" onclick="changeImage(this)" />
                <img src="/pictures/2.jpg" onclick="changeImage(this)" />
                <img src="/pictures/3.jpg" onclick="changeImage(this)" />
                <img src="/pictures/4.jpg" onclick="changeImage(this)" />
                <img src="/pictures/5.jpg" onclick="changeImage(this)" />
                <img src="/pictures/7.jpg" onclick="changeImage(this)" />
                
              </div>
              <button class="arrow right" onclick="scrollThumbnails(1)">&#8250;</button>
            </div>
          </div>
        </div>

        <!-- Product Details Section -->
        <div class="product-details">
          <h2>Beats Solo3 Wireless</h2>
          <div class="price">$999.99</div>
          <div class="description">
            Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
          </div>
          <div class="colors">
            <div class="color-name">Available Colors</div><br>
            <div class="color-option" style="background-color: white;"></div>
            <div class="color-option" style="background-color: pink;"></div>
            <div class="color-option" style="background-color: purple;"></div>
          </div>
        </div>

      </div>
    </div>
  </main>

</body>
</html>
document.addEventListener("DOMContentLoaded", function () {
    let lastScrollTop = 0;
    const header = document.querySelector("header");
  
    window.addEventListener("scroll", function () {
      const currentScroll = window.pageYOffset || document.documentElement.scrollTop;
      header.style.top = currentScroll > lastScrollTop ? "-100px" : "0";
      lastScrollTop = currentScroll <= 0 ? 0 : currentScroll;
    });
  
    const container = document.getElementById("Container");
    if (container) {
      container.addEventListener("wheel", function (event) {
        event.preventDefault();
        this.scrollLeft += event.deltaY;
      });
    }
  
    function changeImage(imgElement) {
        const mainImage = document.getElementById("mainImage");
        if (mainImage) {
          mainImage.src = imgElement.src;
        }
      
        document.querySelectorAll(".thumbnail-row img").forEach(img => {
          img.classList.remove("selected");
        });
      
        imgElement.classList.add("selected");
      }
  
    function changeQty(change) {
      const qtyElement = document.getElementById("quantity");
      if (qtyElement) {
        let qty = parseInt(qtyElement.textContent) || 1;
        qty = Math.max(qty + change, 1);
        qtyElement.textContent = qty;
      }
    }
  
    const scrollContainer = document.getElementById("thumbnailRow");
    const leftArrow = document.querySelector(".arrow.left");
    const rightArrow = document.querySelector(".arrow.right");
  
    function scrollThumbnails(direction) {
      const scrollAmount = 100;
      if (scrollContainer) {
        scrollContainer.scrollBy({
          left: direction * scrollAmount,
          behavior: "smooth"
        });
      }
    }
  
    if (leftArrow && rightArrow && scrollContainer) {
      leftArrow.addEventListener("click", () => scrollThumbnails(-1));
      rightArrow.addEventListener("click", () => scrollThumbnails(1));
    }
  
    const mainImage = document.getElementById("mainImage");
    const thumbnails = document.querySelectorAll(".thumbnail-row img");
  
    thumbnails.forEach(thumbnail => {
      thumbnail.addEventListener("mouseover", () => {
        // Set as main image
        if (mainImage) {
          mainImage.src = thumbnail.src;
        }
  
        thumbnails.forEach(img => img.classList.remove("selected"));
  
        // Add selected to hovered
        thumbnail.classList.add("selected");
      });
    });

    window.img = function (src) {
      const mainImage = document.getElementById("mainImage");
      if (mainImage) {
        mainImage.src = src;
      }
    };
  
    if (typeof ScrollReveal !== "undefined") {
      ScrollReveal().reveal(".page-body", {
        origin: "bottom",
        distance: "50px",
        duration: 1000,
        delay: 100,
        reset: true
      });
    } else {
      console.warn("ScrollReveal is not defined.");
    }
  
    window.changeImage = changeImage;
    window.changeQty = changeQty;
  });

Why does browser.tabs.sendMessage() not return a specific Error?

This question is about the Firefox API function:

https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/sendMessage

Why does browser.tabs.sendMessage() not seem to return a specific error that causes the rejection of the Promise as a reason? Instead, the function appears to return a generic Error and the same message that a specific Error would have.

Is this due to the implementation of sendMessage()? Does it use the generic Error type, because it must be able to return all types of Errors?


I know that https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject says that:

The static Promise.reject function returns a Promise that is rejected. For debugging purposes and selective error catching, it is useful to make reason an instanceof Error.


I would expect to know the specific Error, because if it is just an Error, then I am not sure if it is always easy to understand what causes it.

Reactive form module is getting time to render in case of edit mode

i am using angular reactive from module to my application here is my html code

  <form *ngIf="myForm" [formGroup]="myForm" (submit)="onSubmit()">
    <mat-select *ngIf="!option.isMultiple" [formControlName]="option.id">
   <mat-option [value]="option.values">
     {{ option.values }}
   </mat-option>
 </mat-select>
 </mat-form-field>
</form>

And inside the typescript i am populating the data whatever user was selected

  ngOnInit(){
      let data = items.map(e=>e.key);
      this.form.controls[this.option.id].setValue(data);// this is talking time to render
 }

if i am not setting the values inside formcontrol its working fine however if the items length is even 100 its almost stuck for 10 seconds. What is wrong here.

Drop down is able to render 1000 records immediately then why setting formValue is getting stuck i don’t see any DOM insertion its just a single statement to set form value.

Does re-rendering of child passed as a prop cause re-rendering of the whole parant?

import { CSSProperties, ReactNode, useState } from 'react';

type LayoutWithSidebarProps = {
    className?: string;
    sidebar: ReactNode;
    content: ReactNode;
    maxWidth?: string;
    isPageLayout?: boolean;
};

export function LayoutWithSidebar({
    className = '',
    sidebar,
    content,
    maxWidth,
    isPageLayout = false,
}: LayoutWithSidebarProps) {
    const [isSidebarExpanded, setSidebarExpanded] = useState<boolean>(false);

    const toggleSidebar = () => {
        setSidebarExpanded(!isSidebarExpanded);
    };

    return (
        <div
            className={`quark-layout-with-sidebar ${isPageLayout ? 'quark-layout-with-sidebar--page-layout' : ''} ${className}`}
            style={style}
        >
            <div className={'quark-layout-with-sidebar__sidebar-expand'}>
                <a onClick={toggleSidebar}>{'>>'}</a>
            </div>
            {isSidebarExpanded && (
                <div
                    className={`quark-layout-with-sidebar__overlay ${isSidebarExpanded ? 'quark-layout-with-sidebar__overlay--active' : ''}`}
                    onClick={toggleSidebar}
                />
            )}

            <div
                className={`quark-layout-with-sidebar__sidebar ${isSidebarExpanded ? 'quark-layout-with-sidebar__sidebar--expanded' : ''}`}
            >
                <div className={`quark-layout-with-sidebar__sidebar-inner`}>
                    {sidebar}
                </div>
            </div>
            <div className={'quark-layout-with-sidebar__content'}>
                <div className={`quark-layout-with-sidebar__content-inner`}>
                    {content}
                </div>
            </div>
        </div>
    );
}

I have a layout wrapper component that accepts two ReactNodes as props -> sidebar and content. The result is a styled page layout, so I dont have to do it over and over again in all my apps

But I am not sure if it is a good pattern to pass this components (sidebar and content) as a props. My biggest question is, if for example I pass a component to the content prop, and the component will re-render, will it cause the whole parent to re-render?

Are there any differences in the behaviour in react 19 or older ones?

How is the best way to write such layout wrapper components, or should they be avoided completely?

Thank you

Trying to make a simpler discord search bar using react

I’m working on this blog website of my university project and my team wanted to make a discord like search when for example searching for articles by its category-title-author ect … we wanted it to look like the discord one where you search by user and links he sent ect .. enter image description here

The best approach I’ve came up with so far is rendering some spans before the input but then I don’t get the form like this category : AI author : James
But I get category: author: AI James

Why does my register action break when deployed?

My register server action works on local host but breaks when deployed to digitalocean. I have tried running it on the server in development mode, but that doesn’t make a difference.

I’m receiving a timeout error and I have no idea why.

Error in registerUser: [Error: Connection timeout] { code: 'ETIMEDOUT', command: 'CONN' }

Here is the server action:

"use server";

import { actionClient } from "@/lib/safe-action";
import { registerSchema } from "@/schemas/Register-schema";
import { prisma } from "@/prisma";
import bcrypt from "bcryptjs";
import crypto from "crypto";
import { sendSMS } from "@/lib/sms";
import { sendEmail } from "@/lib/email";

export const registerUser = actionClient
  .schema(registerSchema)
  .action(
    async ({
      parsedInput: {
        forename,
        surname,
        email,
        mobile,
        password,
        confirmPassword,
        role,
      },
    }) => {
      try {
        const emailLower = email.toLowerCase();
        if (password !== confirmPassword) {
          throw new Error("Passwords do not match.");
        }
        const existingUser = await prisma.user.findFirst({
          where: {
            email: emailLower,
          },
        });

        if (existingUser) {
          return { error: "User already exists" };
        }
        const salt = bcrypt.genSaltSync(10);
        const pwHash = bcrypt.hashSync(password, salt);
        const expires = new Date();
        expires.setHours(expires.getHours() + 6);
        const token = crypto.randomBytes(32).toString("hex");

        await prisma.user.create({
          data: {
            forename,
            surname,
            email: emailLower,
            mobile,
            role,
            password: pwHash,
          },
        });

        await prisma.emailToken.create({
          data: {
            token,
            expires,
            user: {
              connect: {
                email: emailLower,
              },
            },
          },
        });

        const mobileToken = (
          Math.floor(Math.random() * (999999 - 100000 + 1)) + 100000
        ).toString();
        const mobileExpires = new Date();
        mobileExpires.setHours(mobileExpires.getHours() + 6);

        await prisma.mobileToken.create({
          data: {
            token: mobileToken,
            expires: mobileExpires,
            user: {
              connect: {
                mobile,
              },
            },
          },
        });
        await sendEmail(
          email,
          "[email protected]",
          "Verify your email",
          `Visit ${process.env.BASE_URL}/verify-email?token=${token} to verify your email address.`,
          `<a href="${process.env.BASE_URL}/verify-email?token=${token}">Verify your email</a>`
        );

        await sendSMS(
          mobile,
          "Tutacall",
          `Your verification code is: ${mobileToken}. Login and use it within 6 hours.`
        );

        return { success: "Verification Email and SMS sent!" };
      } catch (error) {
        console.error("Error in registerUser:", error);
        return { error: "An error occurred. Please try again." };
      }
    }
  );

I would be very grateful for some help with this.

Webworker offscreen-canvas performance issues

I have two canvases inside the body of an HTML-file. I access the first canvas normally from an main thread script and perform an animation with requestAnimationFrame. The second canvas should draw the background and gets an array of objects per frame to draw. Because this background drawing is relativly heavy and I don’t want the animation at the first canvas to lag, I transfered the second canvas via transferControllTofOffscreen to a webworker. Every frame I send the list of objects to the webworker and he draws them directly to the second canvas. But somehow the performance on the main canvas is also getting worse, if I increase the rendering tasks on the worker? It‘s so bad that even rendering everything inside the main thread appears to be faster.

I already tried to only request another frame if the worker has rendered the previous one and I am aware that I might could increase the performance by making the rendering functions more performant but I wonder why I am getting so bad results out of the webworker.

main.js
const mainCanvas = document.getElementById("main-canvas");
const ctx = mainCanvas.getContext("2d");
let offscreenCanvas = document.getElementById("offscreen-canvas");
offscreeenCanvas = offscreenCanvas.tranferControlToOffscreen();

const worker = new Worker("worker.js");
worker.postMessage({type: "init", canvas: offscreenCanvas}, [offscreenCanvas]);

let workerIsDone = true;

function drawScene(objects){

    renderForeground(ctx);

    if (workerIsDone){
        workerIsDone = false;
        worker.postMessage({type:"render", objects: objects});
    }

    requestAnimationFrame(drawScene);
}

worker.onmessage = ()=> workerIsDone = true;
//worker.js

let offscreenCanvas;
let ctx;

onmessage = (e) => {
    if (e.data.type === 'init') {
        const { canvas } = e.data;

        offscreenCanvas = canvas;
        ctx = offscreenCanvas.getContext('2d');
    }

    if (e.data.type === 'render' && ctx) {
        renderBackground(ctx, e.data.objects);

        postMessage("Worker is finished");
    }
};