Building a bidding system with NextJS 🚀
Tags: javascript, webdev, beginners, programming
Published: 2023-05-23 12:04:06

TL;DR
-----

NextJS introduced its new server actions components, and I had to test them out to see what is all the fuss about 🥳

I have built a simple app where you can register to the system, add a new product, and bid on it.

Once the bid is in, it will notify the other bidders that they have been outbid.

It will also inform the seller of a new bid in the system.

<figure class="wp-block-image size-full">![](https://manage.novu.co/wp-content/uploads/2023/05/darth-1.gif)</figure><hr />

Novu - the open-source notification infrastructure
--------------------------------------------------

Just a quick background about us. Novu is the first open-source [notification infrastructure](https://novu.co/). We basically help to manage all the product notifications. It can be **In-App** (the bell icon like you have in the Dev Community - **Websockets**), Emails, SMSs, etc.

I would be super happy if you could give us a star! It will help me to make more articles every week 🚀  
<https://github.com/novuhq/novu>

![Novu](https://res.cloudinary.com/practicaldev/image/fetch/s--nshlTtHJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yn90yvsd87tgik03c08v.gif)

<hr />

Installing the project
----------------------

We will start the project by initiating a new NextJS project:


```bash
npx create-next-app@latest
```
And mark the following details


```bash
✔ What is your project named? … new-proj
✔ Would you like to use TypeScript with this project? … No / Yes
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use Tailwind CSS with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Use App Router (recommended)? … No / Yes
✔ Would you like to customize the default import alias? … No / Yes
```
Let's go into the folder


```javascript
cd new-proj
```
And modify our **next.config.js** to look like this


```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
    experimental: {
        serverActions: true
    }
}

module.exports = nextConfig;
```
We are adding the ability to use the server actions as it's currently still in the beta stage. This will allow us to call the server directly from the client 💥

<figure class="wp-block-image size-full">![](https://manage.novu.co/wp-content/uploads/2023/05/tu-001.gif)</figure><hr />

Creating a database
-------------------

For our project, we are going to use Postgres. Feel free to host it yourself (use docker), [neon.tech](https://neon.tech), [supabase](https://supabase.com), or equivalent. For our example, we will use Vercel Postgres.

You can start by going to [Vercel Storage](https://vercel.com/dashboard/stores) area and creating a new database.

We will start by adding our bid table. It will contain the product's name, the product, the owner of the product, and the current amount of bids.

Click on the query tab and run the following query


```sql
create table bids
(
    id         SERIAL PRIMARY KEY,
    name       varchar(255),
    owner      varchar(255),
    total_bids int default 0 not null
);
```
Click on ".env.local" tab, and copy everything.

Open a new file in our project named `.env` and paste everything inside.

Then install Vercel Postgres by running.


```sql
npm install @vercel/postgres
```
<figure class="wp-block-image size-full">![](https://manage.novu.co/wp-content/uploads/2023/05/tu-002.gif)</figure><hr />

Building the Login page
-----------------------

We don't want people to have access to any page without logging in (in any path).

For that, let's work on the main layout and put our login logic there.

Edit `layout.tsx` and replace it with the following code:


```jsx
import './globals.css'
import {cookies} from "next/headers";
import Login from "@biddingnew/components/login";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const login = cookies().get('login');

  return (
    <html lang="en">
      <body>{login ? children : <Login />}</body>
    </html>
  )
}
```
Very simple react component.

We are getting the login cookie from the user.

If the cookie exists - let Next.JS render any route.

If not, show the login page.

Let's look at a few things here.

- Our component is `"async"` which is required when using the new App router directory.
- We are taking the cookie without any `"useState"`, as this is not a `"client only"` component, and the state doesn't exist.

If you are unsure about the App router's new capabilities, please watch the video at the bottom as I explain everything.

### Let's build the login component

This is a very simple login component, just the person's name, no password or email.


```jsx
"use client";

import {FC, useCallback, useState} from "react";

const Login: FC<{setLogin: (value: string) => void}> = (props) => {
    const {setLogin} = props;
    const [username, setUsername] = useState('');
    const submitForm = useCallback(async (e) => {
        setLogin(username);
        e.preventDefault();
        return false;
    }, [username]);

    return (
        <div className="w-full flex justify-center items-center h-screen">
            <form className="bg-white w-[80%] shadow-md rounded px-8 pt-6 pb-8 mb-4" onSubmit={submitForm}>
                <div className="mb-4">
                    <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="username">
                        Username
                    </label>
                    <input
                        onChange={(event) => setUsername(event.target.value)}
                        className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                        id="username"
                        type="text"
                        placeholder="Enter your username"
                    />
                </div>
                <div className="flex items-center justify-between">
                    <button
                        className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
                        type="submit"
                    >
                        Sign In
                    </button>
                </div>
            </form>
        </div>
    )
}

export default Login;
```
<figure class="wp-block-image size-full">![](https://manage.novu.co/wp-content/uploads/2023/05/image.png)</figure>Let's take a few notes here:

- We are using `"use client"`, which means this component will run over the client. As a result, you can see that we have the `"useState"` available to us. To clarify, you can use client components inside server components but not vice versa.
- We have added a requirement for a parameter called `setLogin` it means once somebody clicks on the submit function, it will trigger the login function.

Let's build the `setLogin` over the main layout page.  
This is where the magic happens 🪄✨  
We will create a function using the new Next.JS server-actions method.  
The method will be written in the client. However, it will run over the server.  
In the background, Next.JS actually sends an HTTP request.


```jsx
import './globals.css'
import {cookies} from "next/headers";
import Login from "@biddingnew/components/login";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const loginFunction = async (user: string) => {
    "use server";

    cookies().set('login', user);
    return true;
  }

  const login = cookies().get('login');

  return (
    <html lang="en">
      <body className={inter.className}>{login ? children : <Login setLogin={loginFunction} />}</body>
    </html>
  )
}
```
As you can see, there is a new function called `loginFunction`, and at the start, there is a "use server" This tells the function to run over the server. It will get the user name from the `setLogin` and set a new cookie called "login".

Once done, the function will re-render, and we will see the rest of the routes.

<hr />

Building the bidding page
-------------------------

Let's start by editing our `page.tsx` file

We will start by adding a simple code for getting all the bids from our database:


```javascript
const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;
```
Let's also add our login cookie information


```javascript
const login = cookies().get("login");
```
The entire content of the page:


```jsx
import Image from "next/image";
import { sql } from "@vercel/postgres";
import { cookies } from "next/headers";

export default async function Home() {
  const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;

  const login = cookies().get("login");

  return (
    <div className="text-black container mx-auto p-4 border-l border-white border-r min-h-[100vh]">
      <div className="flex">
        <h1 className="flex-1 text-3xl font-bold mb-4 text-white">
          Product Listing ({login?.value!})
        </h1>
      </div>
      <div className="grid grid-cols-3 gap-4">
        {rows.map((product) => (
          <div key={product.id} className="bg-white border border-gray-300 p-4">
            <div className="text-lg mb-2">
              <strong>Product Name</strong>: {product.name}
            </div>
            <div className="text-lg mb-2">
              <strong>Owner</strong>: {product.owner}
            </div>
            <div className="text-lg">
              <strong>Current Bid</strong>: {product.total_bids}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
```
<figure class="wp-block-image size-large">![](https://manage.novu.co/wp-content/uploads/2023/05/image-1-1024x193.png)</figure>Very simple component.

We get all the bids from the database, iterate and display them.

Now let's create a simple component to add new products.

Create a new component named `new.product.tsx`


```jsx
"use client";

import { FC, useCallback, useState } from "react";

export const NewProduct: FC<{ addProduct: (name: string) => void }> = (
  props
) => {
  const { addProduct } = props;
  const [name, setName] = useState("");
  const addProductFunc = useCallback(() => {
    setName("");
    addProduct(name);
  }, [name]);
  return (
    <div className="flex mb-5">
      <input
        value={name}
        placeholder="Product Name"
        name="name"
        className="w-[23.5%]"
        onChange={(e) => setName(e.target.value)}
      />
      <button
        type="button"
        onClick={addProductFunc}
        className="w-[9%] bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
      >
        New Product
      </button>
    </div>
  );
};

```
The component looks almost exactly like our login component.

Not the moment the user adds a new product, we will trigger a function that will do that for us.


```jsx
  const addProduct = async (product: string) => {
    "use server";

    const login = cookies().get("login");
    const { rows } =
      await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;
    revalidatePath("/");
  };
```
- We use SQL here directly from the client 🤯 You can also see that the SQL function takes care of any XSS or SQL injections (I haven't used any bindings).
- We insert into the database a new product. You can see that we use the "owner" by using the name saved in the cookie. We also set the bidding to 0.
- In the end, we must tell the app to revalidate the path. If not, we will not see the new product.

The final page will look like this:


```jsx
import { sql } from "@vercel/postgres";
import { cookies } from "next/headers";
import { NewProduct } from "@biddingnew/components/new.product";
import { revalidatePath } from "next/cache";

export default async function Home() {
  const addProduct = async (product: string) => {
    "use server";

    const login = cookies().get("login");
    const { rows } = await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;
    revalidatePath("/");
  };

  const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;
  const login = cookies().get("login");

  return (
    <div className="text-black container mx-auto p-4 border-l border-white border-r min-h-[100vh]">
      <div className="flex">
        <h1 className="flex-1 text-3xl font-bold mb-4 text-white">
          Product Listing ({login?.value!})
        </h1>
      </div>
      <NewProduct addProduct={addProduct} />
      <div className="grid grid-cols-3 gap-4">
        {rows.map((product) => (
          <div key={product.id} className="bg-white border border-gray-300 p-4">
            <div className="text-lg mb-2">
              <strong>Product Name</strong>: {product.name}
            </div>
            <div className="text-lg mb-2">
              <strong>Owner</strong>: {product.owner}
            </div>
            <div className="text-lg">
              <strong>Current Bid</strong>: {product.total_bids}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

```
Now let's create a new component for adding a bid to a product.

Create a new file called `bid.input.tsx` and add the following code:


```javascript
"use client";

import { FC, useCallback, useState } from "react";

export const BidInput: FC<{
  id: number;
  addBid: (id: number, num: number) => void;
}> = (props) => {
  const { id, addBid } = props;
  const [input, setInput] = useState("");

  const updateBid = useCallback(() => {
    addBid(id, +input);
    setInput("");
  }, [input]);

  return (
    <div className="flex pt-3">
      <input
        placeholder="Place bid"
        className="flex-1 border border-black p-3"
        value={input}
        onChange={(e) => setInput(e.target.value)}
      />
      <button type="button" className="bg-black text-white p-2" onClick={updateBid}>
        Add Bid
      </button>
    </div>
  );
};

```
The component is almost the same as the product component.  
The only difference is that it also gets an ID parameter of the current product to tell the server which bid to update.

Now let's add the bidding logic:


```javascript
  const addBid = async (id: number, bid: number) => {
    "use server";

    const login = cookies().get("login");
    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;

    revalidatePath("/");
  };
```
A very simple function that gets the bid id and increases the total.

The full-page code should look like this:


```javascript
import { sql } from "@vercel/postgres";
import { cookies } from "next/headers";
import { NewProduct } from "@biddingnew/components/new.product";
import { revalidatePath } from "next/cache";
import { BidInput } from "@biddingnew/components/bid.input";

export default async function Home() {
  const addBid = async (id: number, bid: number) => {
    "use server";

    const login = cookies().get("login");
    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;
    revalidatePath("/");
  };

  const addProduct = async (product: string) => {
    "use server";

    const login = cookies().get("login");
    const { rows } =
      await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;

    revalidatePath("/");
  };

  const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;

  const login = cookies().get("login");

  return (
    <div className="text-black container mx-auto p-4 border-l border-white border-r min-h-[100vh]">
      <div className="flex">
        <h1 className="flex-1 text-3xl font-bold mb-4 text-white">
          Product Listing ({login?.value!})
        </h1>
      </div>
      <NewProduct addProduct={addProduct} />
      <div className="grid grid-cols-3 gap-4">
        {rows.map((product) => (
          <div key={product.id} className="bg-white border border-gray-300 p-4">
            <div className="text-lg mb-2">
              <strong>Product Name</strong>: {product.name}
            </div>
            <div className="text-lg mb-2">
              <strong>Owner</strong>: {product.owner}
            </div>
            <div className="text-lg">
              <strong>Current Bid</strong>: {product.total_bids}
            </div>
            <div>
              <BidInput addBid={addBid} id={product.id} />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
```
<figure class="wp-block-image size-full">![](https://manage.novu.co/wp-content/uploads/2023/05/image-2.png)</figure>We have a fully functional bidding system 🤩

The only thing left is to send notifications to the users where there is a new bid.

Let's do it 🚀

<hr />

Adding notifications
--------------------

We will show a nice bell icon on the right to send notifications between users on a new bid.

Go ahead and register for [Novu](https://web.novu.co).

Once done, enter the [Settings](https://web.novu.co/settings) page, move to the API Keys tab, and copy the Application Identifier.

Let's install Novu in our project


```bash
npm install @novu/notification-center
```
Now let's create a new component called `novu.tsx` and add the notification center code.


```javascript
"use client";

import {
    NotificationBell,
    NovuProvider,
    PopoverNotificationCenter,
} from "@novu/notification-center";
import { FC } from "react";

export const NovuComponent: FC<{ user: string }> = (props) => {
    const { user } = props;
    return (
        <>
            <NovuProvider subscriberId={user} applicationIdentifier="APPLICATION_IDENTIFIER">
                <PopoverNotificationCenter onNotificationClick={() => window.location.reload()}>
                    {({ unseenCount }) => <NotificationBell unseenCount={unseenCount!} />}
                </PopoverNotificationCenter>
            </NovuProvider>
        </>
    );
};
```
The component is pretty simple. Just ensure you update the application identifier with the one you have on the Novu dashboard. You can find the full reference of the notification component over [Novu Documentation](https://docs.novu.co/notification-center/react/react-components/).

As for the `subscriberId`, it can be anything that you choose. For our case, we use the name from the cookie, so each time we send a notification, we will send it to the name from the cookie.

This is not a safe method, and you should send an [encrypted](https://docs.novu.co/notification-center/react/react-components/#hmac-encryption) id in the future. But it's ok for the example :)

Now let's add it to our code.

The full-page code should look something like this:


```jsx
import Image from "next/image";
import { sql } from "@vercel/postgres";
import { cookies } from "next/headers";
import { NovuComponent } from "@biddingnew/components/novu.component";
import { NewProduct } from "@biddingnew/components/new.product";
import { revalidatePath } from "next/cache";
import { BidInput } from "@biddingnew/components/bid.input";

export default async function Home() {
  const addBid = async (id: number, bid: number) => {
    "use server";

    const login = cookies().get("login");
    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;
    const { rows } = await sql`SELECT * FROM bids WHERE id = ${id}`;

    revalidatePath("/");
  };

  const addProduct = async (product: string) => {
    "use server";

    const login = cookies().get("login");
    const { rows } =
      await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;

    revalidatePath("/");
  };

  const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;

  const login = cookies().get("login");

  return (
    <div className="text-black container mx-auto p-4 border-l border-white border-r min-h-[100vh]">
      <div className="flex">
        <h1 className="flex-1 text-3xl font-bold mb-4 text-white">
          Product Listing ({login?.value!})
        </h1>
        <div>
          <NovuComponent user={login?.value!} />
        </div>
      </div>
      <NewProduct addProduct={addProduct} />
      <div className="grid grid-cols-3 gap-4">
        {rows.map((product) => (
          <div key={product.id} className="bg-white border border-gray-300 p-4">
            <div className="text-lg mb-2">
              <strong>Product Name</strong>: {product.name}
            </div>
            <div className="text-lg mb-2">
              <strong>Owner</strong>: {product.owner}
            </div>
            <div className="text-lg">
              <strong>Current Bid</strong>: {product.total_bids}
            </div>
            <div>
              <BidInput addBid={addBid} id={product.id} />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
```
<figure class="wp-block-image size-large">![](https://manage.novu.co/wp-content/uploads/2023/05/Screenshot-2023-05-23-at-16.18.09-1024x299.png)</figure>We can see the Novu notification bell icon, however, we are not sending any notifications yet.

So let's do it!

Every time somebody creates a new product, we will create a new topic for it.

Then on each notification to the same product, we will register subscribers to it.

Let's take an example:

2. The host creates a new product - a topic is created.
5. User 1 adds a new bid, registers himself to the topic, and sends everybody registered to the topic that there is a new bid (currently, nobody is registered).
8. User 2 adds a new bid, registers himself to the topic, and sends everybody registered to the topic that there is a new bid (User 1 gets a notification).
11. User 3 adds a new bid, registers himself to the topic, and sends everybody registered to the topic that there is a new bid (User 1 gets a notification, and User 2 gets a notification).
14. User 1 adds a new bid (to the same topic) and sends everybody registered to the topic that there is a new bid (User 2 gets a notification, and User 3 gets a notification).

<figure class="wp-block-image size-full">![](https://manage.novu.co/wp-content/uploads/2023/05/tu-003.gif)</figure>Now let's go over to the Novu dashboard and add a [new template](https://web.novu.co/templates/create)

Let's create a new template and call it "New bid in the system." let's drag a new "In-App" channel and add the following content:


```bash
{{name}} just added a bid of {{bid}}
```
<figure class="wp-block-image size-full">![](https://manage.novu.co/wp-content/uploads/2023/05/tu-005.gif)</figure>Once done, enter the [Settings](https://web.novu.co/settings) page, move to the API Keys tab, and copy the API KEY.

Let's add Novu to our project:


```bash
npm install @novu/node
```
And let's add it to the top of our file and change the API\_KEY with our API KEY from the Novu dashboard:


```javascript
import { Novu } from "@novu/node";
const novu = new Novu("API_KEY");
```
Now, when the host creates a new product, let's create a new topic, so let's modify our `addProduct` Function to look like this:


```javascript
  const addProduct = async (product: string) => {
    "use server";

    const login = cookies().get("login");
    const { rows } =
      await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;
    await novu.topics.create({
      key: `bid-${rows[0].id}`,
      name: "People inside of a bid",
    });
    revalidatePath("/");
  };
```
We have added a new `novu.topics.create` function, which creates a new topic.

The topic key must be unique.

We used the created ID of the bid to create the topic.

The name is anything that you want to understand what it is in the future.

So we have created a new topic. On a new bid, the only thing left is to register the user to the topic and notify everybody about it. Let's modify `addBid` and add the new logic:


```javascript
const addBid = async (id: number, bid: number) => {
    "use server";

    const login = cookies().get("login");
    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;

    await novu.topics.addSubscribers(`bid-${id}`, {
      subscribers: [login?.value!],
    });

    await novu.trigger("new-bid-in-the-system", {
      to: [{ type: "Topic", topicKey: `bid-${id}` }],
      payload: {
        name: login?.value!,
        bid: bid,
      },
      actor: { subscriberId: login?.value! },
    } as ITriggerPayloadOptions);
    revalidatePath("/");
  };
```
As you can see, we use `novu.topics.addSubscribers` to add the new user to the topic.

And then, we trigger the notification to the topic with `novu.trigger` to notify everybody about the new bid.

We also have the `actor` parameter, since we are already registered to the topic, we don't want to send a notification to ourselves. We can pass our identifier to the `actor` parameter to avoid that.

Only one thing is missing.

The host is clueless about what's going on.

The host is not registered to the topic and not getting any notifications.

We should send the host a notification on any bid.

So let's create a new template for that.

Now let's go over to the Novu dashboard and add a [new template](https://web.novu.co/templates/create)

Let's create a new template and call it "Host bid" Let's drag a new "In-App" channel and add the following content:


```bash
Congratulation!! {{name}} just added a bid of {{bid}}
```
Now the only thing left is to call the trigger every time to ensure the host gets the notification. Here is the new code of `addBid`


```javascript
const addBid = async (id: number, bid: number) => {
    "use server";
    // @ts-ignore
    const login = cookies().get("login");
    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;
    const { rows } = await sql`SELECT * FROM bids WHERE id = ${id}`;

    await novu.trigger("host-bid", {
      to: [
        {
          subscriberId: rows[0].owner,
        },
      ],
      payload: {
        name: login?.value!,
        bid: bid,
      },
    });

    await novu.topics.addSubscribers(`bid-${id}`, {
      subscribers: [login?.value!],
    });

    await novu.trigger("new-bid-in-the-system", {
      to: [{ type: "Topic", topicKey: `bid-${id}` }],
      payload: {
        name: login?.value!,
        bid: bid,
      },
      actor: { subscriberId: login?.value! },
    } as ITriggerPayloadOptions);
    revalidatePath("/");
  };
```
Here is the full code of the page:


```jsx
import Image from "next/image";
import { sql } from "@vercel/postgres";
import { cookies } from "next/headers";
import { NovuComponent } from "@biddingnew/components/novu.component";
import { NewProduct } from "@biddingnew/components/new.product";
import { revalidatePath } from "next/cache";
import { BidInput } from "@biddingnew/components/bid.input";
import { ITriggerPayloadOptions } from "@novu/node/build/main/lib/events/events.interface";
import { Novu } from "@novu/node";
const novu = new Novu("API_KEY");

export default async function Home() {
  const addBid = async (id: number, bid: number) => {
    "use server";
    // @ts-ignore
    const login = cookies().get("login");
    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;
    const { rows } = await sql`SELECT * FROM bids WHERE id = ${id}`;

    await novu.trigger("host-inform-bid", {
      to: [
        {
          subscriberId: rows[0].owner,
        },
      ],
      payload: {
        name: login?.value!,
        bid: bid,
      },
    });

    await novu.topics.addSubscribers(`bid-${id}`, {
      subscribers: [login?.value!],
    });

    await novu.trigger("new-bid-in-the-system", {
      to: [{ type: "Topic", topicKey: `bid-${id}` }],
      payload: {
        name: login?.value!,
        bid: bid,
      },
      actor: { subscriberId: login?.value! },
    } as ITriggerPayloadOptions);
    revalidatePath("/");
  };

  const addProduct = async (product: string) => {
    "use server";
    // @ts-ignore
    const login = cookies().get("login");
    const { rows } =
      await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;
    await novu.topics.create({
      key: `bid-${rows[0].id}`,
      name: "People inside of a bid",
    });
    revalidatePath("/");
  };

  const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;

  // @ts-ignore
  const login = cookies().get("login");

  return (
    <div className="text-black container mx-auto p-4 border-l border-white border-r min-h-[100vh]">
      <div className="flex">
        <h1 className="flex-1 text-3xl font-bold mb-4 text-white">
          Product Listing ({login?.value!})
        </h1>
        <div>
          <NovuComponent user={login?.value!} />
        </div>
      </div>
      <NewProduct addProduct={addProduct} />
      <div className="grid grid-cols-3 gap-4">
        {rows.map((product) => (
          <div key={product.id} className="bg-white border border-gray-300 p-4">
            <div className="text-lg mb-2">
              <strong>Product Name</strong>: {product.name}
            </div>
            <div className="text-lg mb-2">
              <strong>Owner</strong>: {product.owner}
            </div>
            <div className="text-lg">
              <strong>Current Bid</strong>: {product.total_bids}
            </div>
            <div>
              <BidInput addBid={addBid} id={product.id} />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

```
And you are done 🎉

<hr />

You can find the full source code of the project here:

<https://github.com/novuhq/blog/tree/main/bidding-new>

You can watch the full video of the same tutorial here:



{% embed https://www.youtube.com/embed/RXhC67ypom0 %}

<hr />

Novu Hackathon is live!
-----------------------

The [ConnectNovu](https://connect.novu.co) Hackathon is live 🤩  
This is your time to showcase your skills, meet new team members and grab awesome prizes.

If you love notifications, this Hackathon is for you.  
You can create any system that requires notifications using Novu.  
SMS, Emails, In-App, Push, anything you choose.  
We have also prepared a list of 100 topics you can choose from - just in case you don't know what to do.

<figure class="wp-block-image">![ConnectNovu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6p7o28m9rcn7o1qyo86o.png)</figure>Some fantastic prizes are waiting for you:  
Such as GitHub sponsorships of $1500, Novu’s Swag, Pluralsight subscription, and excellent Novu benefits.