category: How toMay 23, 2023

Building a bidding system with NextJS

Creating a bidding system with Next.JS new App Router, Server actions, Postgres and Novu

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.


Novu – the open-source notification infrastructure

Just a quick background about us. Novu is the first open-source notification infrastructure. 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


Installing the project

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

1npx create-next-app@latest

And mark the following details

1✔ What is your project named? … new-proj
2✔ Would you like to use TypeScript with this project? … No / Yes
3✔ Would you like to use ESLint with this project? … No / Yes
4✔ Would you like to use Tailwind CSS with this project? … No / Yes
5✔ Would you like to use `src/` directory with this project? … No / Yes
6✔ Use App Router (recommended)? … No / Yes
7✔ Would you like to customize the default import alias? … No / Yes

Let’s go into the folder

1cd new-proj

And modify our next.config.js to look like this

1/** @type {import('next').NextConfig} */
2const nextConfig = {
3    experimental: {
4        serverActions: true
5    }
6}
7
8module.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 💥


Creating a database

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

You can start by going to Vercel Storage 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

1create table bids
2(
3    id         SERIAL PRIMARY KEY,
4    name       varchar(255),
5    owner      varchar(255),
6    total_bids int default 0 not null
7);

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.

1npm install @vercel/postgres

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:

1import './globals.css'
2import {cookies} from "next/headers";
3import Login from "@biddingnew/components/login";
4
5export default async function RootLayout({
6  children,
7}: {
8  children: React.ReactNode
9}) {
10  const login = cookies().get('login');
11
12  return (
13    <html lang="en">
14      <body>{login ? children : <Login />}</body>
15    </html>
16  )
17}

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.

1"use client";
2
3import {FC, useCallback, useState} from "react";
4
5const Login: FC<{setLogin: (value: string) => void}> = (props) => {
6    const {setLogin} = props;
7    const [username, setUsername] = useState('');
8    const submitForm = useCallback(async (e) => {
9        setLogin(username);
10        e.preventDefault();
11        return false;
12    }, [username]);
13
14    return (
15        <div className="w-full flex justify-center items-center h-screen">
16            <form className="bg-white w-[80%] shadow-md rounded px-8 pt-6 pb-8 mb-4" onSubmit={submitForm}>
17                <div className="mb-4">
18                    <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="username">
19                        Username
20                    </label>
21                    <input
22                        onChange={(event) => setUsername(event.target.value)}
23                        className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
24                        id="username"
25                        type="text"
26                        placeholder="Enter your username"
27                    />
28                </div>
29                <div className="flex items-center justify-between">
30                    <button
31                        className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
32                        type="submit"
33                    >
34                        Sign In
35                    </button>
36                </div>
37            </form>
38        </div>
39    )
40}
41
42export default Login;

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.

1import './globals.css'
2import {cookies} from "next/headers";
3import Login from "@biddingnew/components/login";
4
5export default async function RootLayout({
6  children,
7}: {
8  children: React.ReactNode
9}) {
10  const loginFunction = async (user: string) => {
11    "use server";
12
13    cookies().set('login', user);
14    return true;
15  }
16
17  const login = cookies().get('login');
18
19  return (
20    <html lang="en">
21      <body className={inter.className}>{login ? children : <Login setLogin={loginFunction} />}</body>
22    </html>
23  )
24}

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.


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:

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

Let’s also add our login cookie information

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

The entire content of the page:

1import Image from "next/image";
2import { sql } from "@vercel/postgres";
3import { cookies } from "next/headers";
4
5export default async function Home() {
6  const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;
7
8  const login = cookies().get("login");
9
10  return (
11    <div className="text-black container mx-auto p-4 border-l border-white border-r min-h-[100vh]">
12      <div className="flex">
13        <h1 className="flex-1 text-3xl font-bold mb-4 text-white">
14          Product Listing ({login?.value!})
15        </h1>
16      </div>
17      <div className="grid grid-cols-3 gap-4">
18        {rows.map((product) => (
19          <div key={product.id} className="bg-white border border-gray-300 p-4">
20            <div className="text-lg mb-2">
21              <strong>Product Name</strong>: {product.name}
22            </div>
23            <div className="text-lg mb-2">
24              <strong>Owner</strong>: {product.owner}
25            </div>
26            <div className="text-lg">
27              <strong>Current Bid</strong>: {product.total_bids}
28            </div>
29          </div>
30        ))}
31      </div>
32    </div>
33  );
34}

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

1"use client";
2
3import { FC, useCallback, useState } from "react";
4
5export const NewProduct: FC<{ addProduct: (name: string) => void }> = (
6  props
7) => {
8  const { addProduct } = props;
9  const [name, setName] = useState("");
10  const addProductFunc = useCallback(() => {
11    setName("");
12    addProduct(name);
13  }, [name]);
14  return (
15    <div className="flex mb-5">
16      <input
17        value={name}
18        placeholder="Product Name"
19        name="name"
20        className="w-[23.5%]"
21        onChange={(e) => setName(e.target.value)}
22      />
23      <button
24        type="button"
25        onClick={addProductFunc}
26        className="w-[9%] bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
27      >
28        New Product
29      </button>
30    </div>
31  );
32};
33

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.

1  const addProduct = async (product: string) => {
2    "use server";
3
4    const login = cookies().get("login");
5    const { rows } =
6      await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;
7    revalidatePath("/");
8  };
  • 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:

1import { sql } from "@vercel/postgres";
2import { cookies } from "next/headers";
3import { NewProduct } from "@biddingnew/components/new.product";
4import { revalidatePath } from "next/cache";
5
6export default async function Home() {
7  const addProduct = async (product: string) => {
8    "use server";
9
10    const login = cookies().get("login");
11    const { rows } = await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;
12    revalidatePath("/");
13  };
14
15  const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;
16  const login = cookies().get("login");
17
18  return (
19    <div className="text-black container mx-auto p-4 border-l border-white border-r min-h-[100vh]">
20      <div className="flex">
21        <h1 className="flex-1 text-3xl font-bold mb-4 text-white">
22          Product Listing ({login?.value!})
23        </h1>
24      </div>
25      <NewProduct addProduct={addProduct} />
26      <div className="grid grid-cols-3 gap-4">
27        {rows.map((product) => (
28          <div key={product.id} className="bg-white border border-gray-300 p-4">
29            <div className="text-lg mb-2">
30              <strong>Product Name</strong>: {product.name}
31            </div>
32            <div className="text-lg mb-2">
33              <strong>Owner</strong>: {product.owner}
34            </div>
35            <div className="text-lg">
36              <strong>Current Bid</strong>: {product.total_bids}
37            </div>
38          </div>
39        ))}
40      </div>
41    </div>
42  );
43}
44

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:

1"use client";
2
3import { FC, useCallback, useState } from "react";
4
5export const BidInput: FC<{
6  id: number;
7  addBid: (id: number, num: number) => void;
8}> = (props) => {
9  const { id, addBid } = props;
10  const [input, setInput] = useState("");
11
12  const updateBid = useCallback(() => {
13    addBid(id, +input);
14    setInput("");
15  }, [input]);
16
17  return (
18    <div className="flex pt-3">
19      <input
20        placeholder="Place bid"
21        className="flex-1 border border-black p-3"
22        value={input}
23        onChange={(e) => setInput(e.target.value)}
24      />
25      <button type="button" className="bg-black text-white p-2" onClick={updateBid}>
26        Add Bid
27      </button>
28    </div>
29  );
30};
31

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:

1  const addBid = async (id: number, bid: number) => {
2    "use server";
3
4    const login = cookies().get("login");
5    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;
6
7    revalidatePath("/");
8  };

A very simple function that gets the bid id and increases the total.

The full-page code should look like this:

1import { sql } from "@vercel/postgres";
2import { cookies } from "next/headers";
3import { NewProduct } from "@biddingnew/components/new.product";
4import { revalidatePath } from "next/cache";
5import { BidInput } from "@biddingnew/components/bid.input";
6
7export default async function Home() {
8  const addBid = async (id: number, bid: number) => {
9    "use server";
10
11    const login = cookies().get("login");
12    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;
13    revalidatePath("/");
14  };
15
16  const addProduct = async (product: string) => {
17    "use server";
18
19    const login = cookies().get("login");
20    const { rows } =
21      await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;
22
23    revalidatePath("/");
24  };
25
26  const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;
27
28  const login = cookies().get("login");
29
30  return (
31    <div className="text-black container mx-auto p-4 border-l border-white border-r min-h-[100vh]">
32      <div className="flex">
33        <h1 className="flex-1 text-3xl font-bold mb-4 text-white">
34          Product Listing ({login?.value!})
35        </h1>
36      </div>
37      <NewProduct addProduct={addProduct} />
38      <div className="grid grid-cols-3 gap-4">
39        {rows.map((product) => (
40          <div key={product.id} className="bg-white border border-gray-300 p-4">
41            <div className="text-lg mb-2">
42              <strong>Product Name</strong>: {product.name}
43            </div>
44            <div className="text-lg mb-2">
45              <strong>Owner</strong>: {product.owner}
46            </div>
47            <div className="text-lg">
48              <strong>Current Bid</strong>: {product.total_bids}
49            </div>
50            <div>
51              <BidInput addBid={addBid} id={product.id} />
52            </div>
53          </div>
54        ))}
55      </div>
56    </div>
57  );
58}

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 🚀


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.

Once done, enter the Settings page, move to the API Keys tab, and copy the Application Identifier.

Let’s install Novu in our project

1npm install @novu/notification-center

Now let’s create a new component called novu.tsx and add the notification center code.

1"use client";
2
3import {
4    NotificationBell,
5    NovuProvider,
6    PopoverNotificationCenter,
7} from "@novu/notification-center";
8import { FC } from "react";
9
10export const NovuComponent: FC<{ user: string }> = (props) => {
11    const { user } = props;
12    return (
13        <>
14            <NovuProvider subscriberId={user} applicationIdentifier="APPLICATION_IDENTIFIER">
15                <PopoverNotificationCenter onNotificationClick={() => window.location.reload()}>
16                    {({ unseenCount }) => <NotificationBell unseenCount={unseenCount!} />}
17                </PopoverNotificationCenter>
18            </NovuProvider>
19        </>
20    );
21};

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.

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 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:

1import Image from "next/image";
2import { sql } from "@vercel/postgres";
3import { cookies } from "next/headers";
4import { NovuComponent } from "@biddingnew/components/novu.component";
5import { NewProduct } from "@biddingnew/components/new.product";
6import { revalidatePath } from "next/cache";
7import { BidInput } from "@biddingnew/components/bid.input";
8
9export default async function Home() {
10  const addBid = async (id: number, bid: number) => {
11    "use server";
12
13    const login = cookies().get("login");
14    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;
15    const { rows } = await sql`SELECT * FROM bids WHERE id = ${id}`;
16
17    revalidatePath("/");
18  };
19
20  const addProduct = async (product: string) => {
21    "use server";
22
23    const login = cookies().get("login");
24    const { rows } =
25      await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;
26
27    revalidatePath("/");
28  };
29
30  const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;
31
32  const login = cookies().get("login");
33
34  return (
35    <div className="text-black container mx-auto p-4 border-l border-white border-r min-h-[100vh]">
36      <div className="flex">
37        <h1 className="flex-1 text-3xl font-bold mb-4 text-white">
38          Product Listing ({login?.value!})
39        </h1>
40        <div>
41          <NovuComponent user={login?.value!} />
42        </div>
43      </div>
44      <NewProduct addProduct={addProduct} />
45      <div className="grid grid-cols-3 gap-4">
46        {rows.map((product) => (
47          <div key={product.id} className="bg-white border border-gray-300 p-4">
48            <div className="text-lg mb-2">
49              <strong>Product Name</strong>: {product.name}
50            </div>
51            <div className="text-lg mb-2">
52              <strong>Owner</strong>: {product.owner}
53            </div>
54            <div className="text-lg">
55              <strong>Current Bid</strong>: {product.total_bids}
56            </div>
57            <div>
58              <BidInput addBid={addBid} id={product.id} />
59            </div>
60          </div>
61        ))}
62      </div>
63    </div>
64  );
65}

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:

  1. The host creates a new product – a topic is created.
  2. 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).
  3. 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).
  4. 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).
  5. 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).

Now let’s go over to the Novu dashboard and add a new template

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:

1{{name}} just added a bid of {{bid}}

Once done, enter the Settings page, move to the API Keys tab, and copy the API KEY.

Let’s add Novu to our project:

1npm 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:

1import { Novu } from "@novu/node";
2const 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:

1  const addProduct = async (product: string) => {
2    "use server";
3
4    const login = cookies().get("login");
5    const { rows } =
6      await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;
7    await novu.topics.create({
8      key: `bid-${rows[0].id}`,
9      name: "People inside of a bid",
10    });
11    revalidatePath("/");
12  };

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:

1const addBid = async (id: number, bid: number) => {
2    "use server";
3
4    const login = cookies().get("login");
5    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;
6
7    await novu.topics.addSubscribers(`bid-${id}`, {
8      subscribers: [login?.value!],
9    });
10
11    await novu.trigger("new-bid-in-the-system", {
12      to: [{ type: "Topic", topicKey: `bid-${id}` }],
13      payload: {
14        name: login?.value!,
15        bid: bid,
16      },
17      actor: { subscriberId: login?.value! },
18    } as ITriggerPayloadOptions);
19    revalidatePath("/");
20  };

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

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

1Congratulation!! {{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

1const addBid = async (id: number, bid: number) => {
2    "use server";
3    // @ts-ignore
4    const login = cookies().get("login");
5    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;
6    const { rows } = await sql`SELECT * FROM bids WHERE id = ${id}`;
7
8    await novu.trigger("host-bid", {
9      to: [
10        {
11          subscriberId: rows[0].owner,
12        },
13      ],
14      payload: {
15        name: login?.value!,
16        bid: bid,
17      },
18    });
19
20    await novu.topics.addSubscribers(`bid-${id}`, {
21      subscribers: [login?.value!],
22    });
23
24    await novu.trigger("new-bid-in-the-system", {
25      to: [{ type: "Topic", topicKey: `bid-${id}` }],
26      payload: {
27        name: login?.value!,
28        bid: bid,
29      },
30      actor: { subscriberId: login?.value! },
31    } as ITriggerPayloadOptions);
32    revalidatePath("/");
33  };

Here is the full code of the page:

1import Image from "next/image";
2import { sql } from "@vercel/postgres";
3import { cookies } from "next/headers";
4import { NovuComponent } from "@biddingnew/components/novu.component";
5import { NewProduct } from "@biddingnew/components/new.product";
6import { revalidatePath } from "next/cache";
7import { BidInput } from "@biddingnew/components/bid.input";
8import { ITriggerPayloadOptions } from "@novu/node/build/main/lib/events/events.interface";
9import { Novu } from "@novu/node";
10const novu = new Novu("API_KEY");
11
12export default async function Home() {
13  const addBid = async (id: number, bid: number) => {
14    "use server";
15    // @ts-ignore
16    const login = cookies().get("login");
17    await sql`UPDATE bids SET total_bids = total_bids + ${bid} WHERE id = ${id}`;
18    const { rows } = await sql`SELECT * FROM bids WHERE id = ${id}`;
19
20    await novu.trigger("host-inform-bid", {
21      to: [
22        {
23          subscriberId: rows[0].owner,
24        },
25      ],
26      payload: {
27        name: login?.value!,
28        bid: bid,
29      },
30    });
31
32    await novu.topics.addSubscribers(`bid-${id}`, {
33      subscribers: [login?.value!],
34    });
35
36    await novu.trigger("new-bid-in-the-system", {
37      to: [{ type: "Topic", topicKey: `bid-${id}` }],
38      payload: {
39        name: login?.value!,
40        bid: bid,
41      },
42      actor: { subscriberId: login?.value! },
43    } as ITriggerPayloadOptions);
44    revalidatePath("/");
45  };
46
47  const addProduct = async (product: string) => {
48    "use server";
49    // @ts-ignore
50    const login = cookies().get("login");
51    const { rows } =
52      await sql`INSERT INTO bids (name, owner, total_bids) VALUES(${product}, ${login?.value!}, 0) RETURNING id`;
53    await novu.topics.create({
54      key: `bid-${rows[0].id}`,
55      name: "People inside of a bid",
56    });
57    revalidatePath("/");
58  };
59
60  const { rows } = await sql`SELECT * FROM bids ORDER BY id DESC`;
61
62  // @ts-ignore
63  const login = cookies().get("login");
64
65  return (
66    <div className="text-black container mx-auto p-4 border-l border-white border-r min-h-[100vh]">
67      <div className="flex">
68        <h1 className="flex-1 text-3xl font-bold mb-4 text-white">
69          Product Listing ({login?.value!})
70        </h1>
71        <div>
72          <NovuComponent user={login?.value!} />
73        </div>
74      </div>
75      <NewProduct addProduct={addProduct} />
76      <div className="grid grid-cols-3 gap-4">
77        {rows.map((product) => (
78          <div key={product.id} className="bg-white border border-gray-300 p-4">
79            <div className="text-lg mb-2">
80              <strong>Product Name</strong>: {product.name}
81            </div>
82            <div className="text-lg mb-2">
83              <strong>Owner</strong>: {product.owner}
84            </div>
85            <div className="text-lg">
86              <strong>Current Bid</strong>: {product.total_bids}
87            </div>
88            <div>
89              <BidInput addBid={addBid} id={product.id} />
90            </div>
91          </div>
92        ))}
93      </div>
94    </div>
95  );
96}
97

And you are done 🎉


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:


Novu Hackathon is live!

The ConnectNovu 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.

ConnectNovu

Some fantastic prizes are waiting for you:
Such as GitHub sponsorships of $1500, Novu’s Swag, Pluralsight subscription, and excellent Novu benefits.

Related Posts

category: How to

A Proper Guide to Web and Mobile Push Notification Service

Implement push notification services successfully by following this actionable guide on choosing platforms, setting up, and personalizing notifications for better results.

Emil Pearce
Emil Pearce
category: How to

How to Add Real-Time Notifications to a React App

Learn how to integrate real-time notifications into your React app using WebSockets, Server-Sent Events, Firebase Cloud Messaging (FCM), and Novu for improved user engagement and instant updates.

Emil Pearce
Emil Pearce
category: How to

A Developer’s Guide to Choosing the Best Notification Platform

A comprehensive guide for developers on selecting notification platforms, covering different types of notifications (push, in-app, email, SMS, and chat), key features to consider, popular providers, and best practices for implementation. Learn how to evaluate notification platforms based on integration ease, cost-effectiveness, scalability, and security, with practical insights on platforms like FCM, Twilio, SendGrid, and Novu.

Emil Pearce
Emil Pearce