Add Products to Database Using NextJS Server Actions (Part 2) : Building an E-Commerce Web App from Scratch with Next.js, Tailwind CSS, DaisyUI, Prisma, and MongoDB

Welcome back to the second part of our blog series, "Building an E-Commerce Web App from Scratch with NextJS, Tailwind CSS, DaisyUI, Prisma, and MongoDB." In this installment, we will take a deep dive into creating a fundamental feature of any successful online store – an "Add Product" form page.

As we progress on this exciting journey, we'll be leveraging Next.js's server actions to implement a seamless process for adding new products to our database. By the end of this post, you'll have a fully functional form that empowers you to effortlessly expand your product catalog, ensuring your E-Commerce Web App continues to grow and thrive.


We understand the importance of a user-friendly and intuitive interface, and that's exactly what we'll be striving for. By combining the power of Tailwind CSS and DaisyUI, we'll create a visually appealing form that not only looks great but also enhances the overall user experience.

So, let's get ready to equip your E-Commerce Web App with the ability to showcase your latest products with ease. Are you excited? We certainly are! Let's dive into Part 2 - "Add Product Form Page" and continue transforming your vision into a reality!

Tutorial

Please note that this is in continuation to The Foundational Setup (Part 1)  of this series. I would recommend going through Part 1 to understand the setup of this NextJS application and the MongoDB database. Thankyou!

Once we’ve got all the necessary setup done for the database and the application. We will run the development server with the following command in the project terminal:

npm run dev

This will open the dev server at https://localhost:3000 and display the following standard page:


Creating the Add Product Page

To create a new page, we will use the inherent app router and create a folder called add-product within the app directory within which we will create a page.tsx file where we will write the following code:

export default function AddProduct() {
    return (
        <div>
            <h1>
                Add Product
            </h1>
        </div>
    )
}

Then after saving this, we can view this page in our browser by going to localhost:3000/add-product which will simply display the heading of the page for now.

In the previous article, Part 1, we had setup the prettier.config.js file which seems to be giving some issues while formatting code because with the latest updates in Tailwind we can delete the prettier config file and simply use the pettier plugin as a dev dependency which will format code automatically. Otherwise, earlier we were getting errors of invalid configuration file when trying to auto format on save.

Add styling to Overall Layout

In order to add styling to the complete website, we will add some styles to the layout.tsx in the app directory in the following way:

import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "MegaCart",
  description:
    "A simple e-commerce app built with NextJS, MongoDB, Prisma, and TailwindCSS",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <main className="p-4 max-w-7xl m-auto min-w-[300px]">{children}</main>
      </body>
    </html>
  );
}

The above code is a NexttJS component named RootLayout, which serves as the layout for the entire application. Let's break down what each part of the code does:

  1. import "./globals.css";: This line imports a CSS file named "globals.css". This file likely contains global CSS styles that will be applied to the entire application.
  2. import type { Metadata } from "next";: This line imports the Metadata type from the "next" package. Metadata is used to define metadata for a Next.js page, including attributes like title and description.
  3. import { Inter } from "next/font/google";: This line imports the Inter font from Google Fonts using the next/font/google package. The Inter font is assigned to a variable named inter.
  4. const inter = Inter({ subsets: ["latin"] });: This line initializes the inter font by calling the Inter function and specifying that only the "latin" subset should be used.
  5. export const metadata: Metadata = { ... }: This exports a constant named metadata, which is of type Metadata. It contains metadata for the page, including the page title and description.
  6. export default function RootLayout({ children }: { children: React.ReactNode; }) { ... }: This exports the default function component RootLayout, which takes a prop named children of type React.ReactNode. It wraps the entire content in an HTML structure, setting the language to "en" and applying the inter font class to the <body> element. The main content is wrapped in a <main> element with some Tailwind CSS classes for styling.

In summary, this code sets up a basic Next.js layout with global styles, a custom font (Inter), and metadata for the page title and description. It will be used as a template for rendering the content of different pages in the application.

Create a basic Add Product Form

Now, within the page.tsx within add-product folder that we created before, we will create a baisc form using TailwindCSS and DaisyUI input components (Tailwind Text Input Component — Tailwind CSS Components (daisyui.com)) in the following way:

import { Metadata } from "next";

export const metadata: Metadata = {
  title: "Add Product - MegaCart",
};

export default function AddProduct() {
  return (
    <div className="max-w-lg m-auto h-screen">
      <h1 className="font-bold text-lg mb-3 ">Add Product</h1>
      <form>
        <input
          required
          name="name"
          placeholder="Product Name"
          className="mb-3 input input-bordered w-full"
        />
        <textarea
          required
          name="description"
          placeholder="Product Description"
          className="textarea-bordered textarea mb-3 w-full"
        />
        <input
          required
          name="imageUrl"
          placeholder="Product Image URL"
          className="mb-3 input input-bordered w-full"
        />
        <input
          required
          name="price"
          placeholder="Product Price"
          type="number"
          className="mb-3 input input-bordered w-full"
        />
        <button className="btn btn-primary btn-block" type="submit">
          Add Product
        </button>
      </form>
    </div>
  );
}

The above code is a Next.js component named AddProduct, which represents a form to add a new product in an e-commerce application. Let's break down what each part of the code does:

  1. import { Metadata } from "next";: This line imports the Metadata type from the "next" package. The Metadata type is used to define metadata for a Next.js page, including attributes like title.
  2. export const metadata: Metadata = { ... }: This exports a constant named metadata, which is of type Metadata. It contains metadata for the page, specifically the title, which will be displayed in the browser tab.
  3. export default function AddProduct() { ... }: This exports the default function component AddProduct. It represents the form to add a new product in the application.
  4. The component's JSX code represents the form structure using HTML elements and applies Tailwind CSS classes for styling.
  5. The form has several input fields for the product's name, description, image URL, and price. Each input field is marked as required, meaning it must be filled before submitting the form.
  6. The form also contains a <button> element with the class "btn btn-primary btn-block" that serves as a submit button to add the product.

In summary, this code sets up a Next.js component that displays a form to add a new product. It includes basic input fields for product information and a submit button. The page title is set to "Add Product - MegaCart" based on the metadata configuration.

Using Server Actions to Add Products to Database

Earlier, we had to create separate API routes for data fetching and posting in order to add products on the server side. We could also use ExpressJS to create a separate server for this. But with NextJS server actions (Data Fetching: Server Actions | Next.js (nextjs.org)) we can do so within the same server side component that we’ve just created.

First, in order to allow server actions feature to run in the app, you’ll need to modify the next.config.ts file in the following way:

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [{ hostname: "images.unsplash.com" }],
  },
  experimental: {
    serverActions: true,
  },
};

module.exports = nextConfig;

Note that we’ve also added unplash url in the config because we will be using the the image urls from the unsplash website for product images in the form.

Before moving ahead, make sure you’ve generated the prisma client earlier with the command npx prisma generate otherwise the prisma method won’t show the available model schema.

Remember that prisma mehtods can only run on server components and if trying to run them from client side, prisma won’t allow it and throw an error.

So now, we will modify and add the following code to the page.tsx in add-product folder:

import { Metadata } from "next";
import { prisma } from "@/app/lib/db/prisma";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
  title: "Add Product - MegaCart",
};

async function addProduct(formData: FormData) {
  "use server"; // tells NextJS this function should run on the server

  // get the form data from the request body and add it to the database
  const name = formData.get("name")?.toString();
  const description = formData.get("description")?.toString();
  const imageUrl = formData.get("imageUrl")?.toString();
  const price = Number(formData.get("price") || 0);

  // validate the form data before adding it to the database
  if (!name || !description || !imageUrl || !price) {
    throw Error("Missing required fields");
  }

  await prisma.product.create({
    data: {
      name,
      description,
      imageUrl,
      price,
    },
  });

  redirect("/");
}

export default function AddProduct() {
  return (
    <div className="max-w-lg m-auto h-screen">
      <h1 className="font-bold text-lg mb-3 ">Add Product</h1>
      <form action={addProduct}>
        <input
          required
          name="name"
          placeholder="Product Name"
          className="mb-3 input input-bordered w-full"
        />
        <textarea
          required
          name="description"
          placeholder="Product Description"
          className="textarea-bordered textarea mb-3 w-full"
        />
        <input
          required
          name="imageUrl"
          placeholder="Product Image URL"
          className="mb-3 input input-bordered w-full"
        />
        <input
          required
          name="price"
          placeholder="Product Price"
          type="number"
          className="mb-3 input input-bordered w-full"
        />
        <button className="btn btn-primary btn-block" type="submit">
          Add Product
        </button>
      </form>
    </div>
  );
}

Above code is a Next.js component that represents a form to add a new product in an e-commerce application. It also includes a server-side function to handle the form submission and database interaction. Let's break down what each part of the code does:

  1. import { Metadata } from "next";: This line imports the Metadata type from the "next" package. The Metadata type is used to define metadata for a Next.js page, including attributes like title.
  2. import { prisma } from "@/lib/db/prisma";: This line imports the prisma object, which is an instance of the Prisma client used to interact with the database.
  3. import { redirect } from "next/navigation";: This line imports the redirect function from the "next/navigation" module. The redirect function is used to redirect the user to another page after the form is submitted successfully.
  4. export const metadata: Metadata = { ... }: This exports a constant named metadata, which is of type Metadata. It contains metadata for the page, specifically the title, which will be displayed in the browser tab.
  5. async function addProduct(formData: FormData) { ... }: This is an async function named addProduct, which will handle the form submission on the server-side. It takes formData as an argument, which is the data submitted through the form.
  6. The function contains code to extract the product information (name, description, image URL, and price) from the formData. It then validates the data to ensure that all required fields are provided.
  7. If the form data is valid, the function uses the Prisma client (prisma) to add the new product to the database by calling prisma.product.create({ ... }).
  8. After adding the product to the database, the function uses redirect("/") to redirect the user back to the homepage.
  9. export default function AddProduct() { ... }: This exports the default function component AddProduct. It represents the form to add a new product in the application.
  10. The component's JSX code represents the form structure using HTML elements and applies Tailwind CSS classes for styling.
  11. The form has several input fields for the product's name, description, image URL, and price. Each input field is marked as required, meaning it must be filled before submitting the form.
  12. The form's action attribute is set to the addProduct function, which means that when the form is submitted, it will call the addProduct function on the server-side.

In summary, this code sets up a Next.js component that displays a form to add a new product. When the form is submitted, the addProduct function will be called on the server-side to validate the data and add the new product to the database. If the submission is successful, the user will be redirected back to the homepage.


The above image shows how the add product form will look like.

Now, we’ll try to add a product by manually adding the following information in the respective fields and view our MongoDB collection to check if the server action is actually working like its supposed to.

{
  "name": "Polaroid Instant Camera",
  "description": "Capture and print your memories instantly with this classic Polaroid Instant Camera. It's easy to use and delivers charming, vintage-style photos.",
  "image_url": "<https://images.unsplash.com/photo-1526170375885-4d8ecf77b99f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80>",
  "price": 999
}

When we click on Add Product button, it adds the data to the MongoDB collection and redirects us to the home page.


As you can see in the image above, a new document is created within the products collection with the same data as we submitted via the form. Hurray! The form is working!

Now, we cannot see a loading indicator on the add product button but it is not possible to add the indicator directly within the server component with the server actions since loading will require a state which is a client side feature.

So, to implement this loading indicator, we will extract the button from the form, create the functionality for it in a separate client side component, and then hook it to the server action.

First, let’s create a FormSubmitButton.tsx component in a components folder within the src directory with the following code:

"use client"; // tells NextJS this function should run on the client
import React, { ComponentProps } from "react";
import { experimental_useFormStatus as useFormStatus } from "react-dom"; // this is a custom hook that we created to get the status of the form. It will return true if the form is pending and false otherwise.

// generally we use interface instead of type for the props but here we need to extend the props of the button so we use type which we cannot achieve with interface.

type FormSubmitButtonProps = {
  children: React.ReactNode; // this will allow whatever we pass as children to be rendered inside the button with the same props as the button

  className?: string; // this will allow us to pass a className prop to the button from outside the component
} & ComponentProps<"button">;
// ComponentProps is a utility type that extracts the props of a component. In this case, we are extracting the props of the button component. It only works with types and not interfaces.

export default function FormSubmitButton(
  {
    children,
    className,
    ...props // this will allow us to pass any other props to the button from outside the component
  }: FormSubmitButtonProps // we are destructuring the props and passing them to the button
) {
  const { pending } = useFormStatus(); // this will give us the status of the form. If the form is pending, it will return true otherwise false.

  return (
    <button
      {...props} // this will pass all the props to the button
      type="submit"
      disabled={pending}
      className={`btn btn-primary ${className}`}
    >
      {children}
      {pending && <span className="loading loading-dots" />}
    </button>
  );
}

The above code is a React function component named FormSubmitButton, which represents a custom button used for form submission. Let's break down what each part of the code does:

  1. use client"; // tells NextJS this function should run on the client: This comment indicates that the function should run on the client-side when the component is rendered in the browser.
  2. import React, { ComponentProps } from "react";: This line imports the necessary modules for the component, including React and ComponentProps.
  3. type FormSubmitButtonProps = { ... } & ComponentProps<"button">;: This defines a TypeScript type named FormSubmitButtonProps, which extends the props of a standard HTML button (ComponentProps<"button">). The type includes the following properties:
    • children: It allows any React nodes to be passed as children to be rendered inside the button.
    • className?: string: It allows the component to accept a className prop to add custom CSS classes to the button.
    • Other button-related props: The ComponentProps<"button"> extracts all the props that are available to a standard HTML button.
  4. export default function FormSubmitButton({ ... }: FormSubmitButtonProps) { ... }: This exports the default function component FormSubmitButton. It takes the destructured FormSubmitButtonProps as its parameter.
  5. const { pending } = useFormStatus();: This line uses the useFormStatus custom hook (imported from "react-dom") to get the status of the form. The useFormStatus hook returns an object with a property named pending, which is true if the form is pending (e.g., during form submission) and false otherwise.
  6. The return statement renders a <button> element with the following properties:
    • ...props: This spreads all the props passed to the FormSubmitButton component, allowing any additional button-specific props to be added from outside the component.
    • type="submit": Sets the button type to "submit," indicating that it is a form submission button.
    • disabled={pending}: The button will be disabled if the pending value is true, i.e., when the form is being submitted.
    • className={btn btn-primary ${className}}: Combines the "btn" and "btn-primary" CSS classes with the custom className passed to the component.
  7. The button's content is defined by the children prop, which can contain any React nodes (e.g., text or other components).
  8. {pending && <span className="loading loading-dots" />}: If the pending value is true, it renders a loading indicator (a span element with CSS classes "loading loading-dots").

In summary, this code defines a custom FormSubmitButton component that renders a button with form submission functionality. It handles form submission status and displays a loading indicator while the form is being submitted. The component is flexible and accepts additional button-specific props and custom CSS classes.

For the different loading spinners, checkout, Tailwind Loading Component — Tailwind CSS Components (daisyui.com)

Now we will import this into the page.tsx under the add-product folder and replace the earlier button with the new one in the following way:

import { Metadata } from "next";
import { prisma } from "@/app/lib/db/prisma";
import { redirect } from "next/navigation";
import FormSubmitButton from "@/components/FormSubmitButton";
export const metadata: Metadata = {
  title: "Add Product - MegaCart",
};

async function addProduct(formData: FormData) {
  "use server"; // tells NextJS this function should run on the server

  // get the form data from the request body and add it to the database
  const name = formData.get("name")?.toString();
  const description = formData.get("description")?.toString();
  const imageUrl = formData.get("imageUrl")?.toString();
  const price = Number(formData.get("price") || 0);

  // validate the form data before adding it to the database
  if (!name || !description || !imageUrl || !price) {
    throw Error("Missing required fields");
  }

  await prisma.product.create({
    data: {
      name,
      description,
      imageUrl,
      price,
    },
  });

  redirect("/");
}

export default function AddProduct() {
  return (
    <div className="max-w-lg m-auto h-screen ">
      <h1 className="font-bold text-lg mb-3 ">Add Product</h1>
      <form action={addProduct}>
        <input
          required
          name="name"
          placeholder="Product Name"
          className="mb-3 input input-bordered w-full"
        />
        <textarea
          required
          name="description"
          placeholder="Product Description"
          className="textarea-bordered textarea mb-3 w-full"
        />
        <input
          required
          name="imageUrl"
          placeholder="Product Image URL"
          className="mb-3 input input-bordered w-full"
        />
        <input
          required
          name="price"
          placeholder="Product Price"
          type="number"
          className="mb-3 input input-bordered w-full"
        />
        <FormSubmitButton className="btn-block">Add Product</FormSubmitButton>
      </form>
    </div>
  );
}

The above code is a Next.js component named AddProduct that represents a form to add a new product in an e-commerce application. It utilizes server-side functionality to add the product to the database upon form submission. Let's break down the code:

  1. import { Metadata } from "next";: This line imports the Metadata type from the "next" package. The Metadata type is used to define metadata for a Next.js page, including attributes like title.
  2. import { prisma } from "@/app/lib/db/prisma";: This line imports the prisma object, which is an instance of the Prisma client used to interact with the database. The prisma object likely contains methods for database operations related to the "Product" model.
  3. import { redirect } from "next/navigation";: This line imports the redirect function from the "next/navigation" module. The redirect function is used to redirect the user to another page after the form is submitted successfully.
  4. import FormSubmitButton from "@/components/FormSubmitButton";: This line imports the FormSubmitButton component from the "@/components" directory. The FormSubmitButton is a custom component used as the submit button for the form.
  5. export const metadata: Metadata = { ... }: This exports a constant named metadata, which is of type Metadata. It contains metadata for the page, specifically the title, which will be displayed in the browser tab.
  6. async function addProduct(formData: FormData) { ... }: This is an async function named addProduct, which handles the form submission on the server-side. It takes formData as an argument, which is the data submitted through the form.
  7. The function extracts the product information (name, description, image URL, and price) from the formData.
  8. It then validates the form data to ensure that all required fields are provided. If any of the required fields are missing, the function throws an Error with the message "Missing required fields".
  9. If the form data is valid, the function uses the Prisma client (prisma) to add the new product to the database by calling prisma.product.create({ ... }).
  10. After adding the product to the database, the function uses redirect("/") to redirect the user back to the homepage.
  11. The export default function AddProduct() { ... } block represents the main component that renders the form:
  • The component renders a <div> element containing the form elements.
  • The form has various input fields for the product's name, description, image URL, and price, each marked as required.
  • The form uses the FormSubmitButton component as its submit button.
  • The FormSubmitButton component is used with the prop className="btn-block" to style the button as a block-level element.

In summary, this code sets up a Next.js component that displays a form to add a new product. When the form is submitted, the addProduct function is called on the server-side to validate the data and add the new product to the database. The page title is set using metadata, and a custom FormSubmitButton component is used for the form submission.

Now, let’s try the form with the below product data:

{
  "name": "Vintage 35mm Film Camera",
  "description": "Rediscover the golden age of photography with this elegant Vintage 35mm Film Camera. Crafted with precision and alluring details, it offers a unique shooting experience that digital cameras can't replicate. Embrace manual control, capture stunning images with its high-quality glass lens, and create timeless memories on film.",
  "image_url": "<https://images.unsplash.com/photo-1511184059754-e4b5bbbcef75?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=580&q=80>",
  "price": 249.99
}

If this was successful, you’ll see that when you click on the add product button, it becomes disabled for a moment and displays a loading indicator within the button before redirecting to the home page.

Adding a default error page

Lastly, we’ll just add a default error page to display when something goes wrong with the server requests. In NextJS, the standard way of doing this is by creating an error.tsx file in the app directory with the following basic code for the moment (we can style it in our own way later) :

"use client"; // tells NextJS this function should run on the client

export default function ErrorPage() {
  return (
    <div>Something went wrong, please refresh the page or try again later.</div>
  );
}

The above code is a Next.js function component named ErrorPage, which represents an error page that will be displayed on the client-side when something goes wrong. Let's break down what each part of the code does:

  1. use client"; // tells NextJS this function should run on the client: This comment indicates that the function should run on the client-side when the component is rendered in the browser. It means that this component is intended for client-side rendering and won't be executed on the server-side during server rendering.
  2. export default function ErrorPage() { ... }: This exports the default function component ErrorPage.
  3. The component's JSX code represents the content of the error page, which is a simple <div> element displaying the message "Something went wrong, please refresh the page or try again later."

In summary, this code sets up a Next.js component named ErrorPage that represents an error page to be displayed on the client-side when something goes wrong. It is a simple component with a single <div> element containing an error message.

And with this, we’re done with the add product page where we have added two products to the database. To populate the database, we’ll further 2 more items before moving on to displaying them on the home page. You can use the below product details or create them on your own.

{
  "name": "Canon EOS Rebel T7i DSLR Camera",
  "description": "Unleash your creativity with the Canon EOS Rebel T7i DSLR Camera. Capture stunning photos and Full HD videos with its 24.2MP sensor and advanced autofocus system. The vari-angle touchscreen LCD, built-in Wi-Fi, NFC, and Bluetooth add convenience to your photography.",
  "image_url": "<https://images.unsplash.com/photo-1502920917128-1aa500764cbd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80>",
  "price": 799.99
}
{
  "name": "Complete Camera Kit with Bag",
  "description": "Capture stunning images and videos with this Complete Camera Kit. It includes a high-quality DSLR camera, versatile lenses, and a premium camera bag for easy and secure transportation. The camera features a 24.2MP sensor, advanced autofocus, Wi-Fi, NFC, and a flip-out touchscreen for creative shooting. Perfect for both photography enthusiasts and content creators.",
  "image_url": "<https://images.unsplash.com/photo-1571689936008-083b32a9dcca?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1042&q=80>",
  "price": 1099.99
}

With this, we will have 4 products within the MongoDB collection as you can see in the dashboard:


Next up, we will display these products on our home page.

Conclusion to Part 2

Congratulations on successfully adding the "Add Product" form page to your E-Commerce Web App! In this second part of our series, we've empowered your online store with a crucial feature that allows you to effortlessly expand your product catalog.

By harnessing the power of Next.js's server actions, we've ensured that the process of adding new products to your database is seamless and efficient. The integration of Tailwind CSS and DaisyUI has not only made the form visually appealing but also contributed to an enhanced user experience, keeping your customers engaged and satisfied.

But our journey doesn't end here! In the next part of this series, we'll take a step further and unveil an exciting new feature: displaying the added products on the home page. Your E-Commerce Web App will come to life as customers can now explore and interact with your diverse range of products right from the main page.

We understand that building a successful E-Commerce platform is all about offering a seamless shopping experience, and that's exactly what we'll be focusing on in the upcoming post. By the end of it, you'll have a home page that not only showcases your products beautifully but also encourages users to make that perfect purchase.

As always, we'll guide you through each step, ensuring that you have a clear understanding of the process. Get ready to enhance your customers' shopping journey and elevate your online store to new heights.

Thank you for joining us on this incredible journey of building an E-Commerce Web App from scratch. We hope this series has been informative and enjoyable, inspiring you to take your project to new heights.

Stay tuned for the next installment, and remember, with Next.js, Tailwind CSS, DaisyUI, Prisma, and MongoDB in your toolkit, there's no limit to what you can achieve. Happy coding!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript