Building an AI webapp(HomeDec) with Hanko, NextJS, Prisma, Supabase and Replicate
Tags: hanko, ai, webdev, authentication
Published: 2023-10-30 15:29:50

This is a tutorial on creating an AI webapp(I like to call it HomeDec :)) to enhance room designs.
The app would have the following functionalities:

1. It would let the user login using passkey or password. It has been done using [Hanko](hanko.io).
2. Then display the dashboard page.
3. The dashboard contains a form to upload images to render the designs and display the previously rendered designs.
4. And then options for profile management and logout, implemented using Hanko.

## Let's Begin...
First let's begin with the stack used for the app.

### NextJS
I've got really comfortable with this framework and like the support from Vercel. Do check [this out](https://docs.hanko.io/quickstarts/fullstack/next)
NextJS is a React framework for fullstack application built by Vercel:

> Used by some of the world's largest companies, Next.js enables you to create full-stack Web applications by extending the latest React features, and integrating powerful Rust-based JavaScript tooling for the fastest builds.

### TailwindCSS
Tailwind CSS helps you minimize the use of CSS stylesheets. You can use Tailwind classes to style various components

### MaterialUI
It's a React component library, developed by Google. It would help you build your UI faster.

### Prisma
The application doesn't require a complicated DB. So, Prisma is perfect for that.

### Supabase
Supabase has been used for PostgreSQL hosting which could be directly accessed by Figma. Further, Supabase storage has been used to store the images.

### Replicate
Replicate allows you to run machine learning models in the cloud.

###Vercel
Vercel lets you deploy webpages easily.

## Build Start...
Hanko has already provided a[ NextJS starter pack](https://github.com/teamhanko/hanko-nextjs-starter). We'll start with that to save us time and reduce the chance of getting any error.

Open your terminal and run the following:

```
git clone https://github.com/teamhanko/hanko-nextjs-starter.git
```
This repository comes with a `pnpm-lock.yaml` file. So we are going to install the required dependencies using `pnpm`. Check [this](https://pnpm.io/).

Then run the following commands on your terminal

```
cd hanko-nextjs-starter
pnpm install
pnpm run dev
```
This would start the project on `localhost://3000`
You should get a screen like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gjgr8y9wsa2uqirxe0w6.png)

On clicking Login you will be directed to a login page. However, it would display an error. This is because we haven't set up Hanko url yet.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p7b6w2fr6i64wxs5eebp.png)


## Authentication By Hanko
It's necessary to setup Hanko to be able to use the application

### Hanko Cloud Setup
Visit [Hanko Cloud](https://cloud.hanko.io/login) and create an account
First create a new organization

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8h8afd2bt555se91va88.png)
And then create a new project. In the app URL add your development URL(could be [http://localhost:3000](http://localhost:3000))


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6xu176ejx3guuwm8qgm7.png)

You'll get your dashboard. You will also get your API URL to be used in your project.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b7sr23isabguia316fhc.png)

### Understanding the structure of NextJS Starter Pack

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ka4rmpd8g3qnhfu4grpi.png)
The starter pack is the usual NextJS project structure. It has got app directory, component directory and public directory. Also, we need to change the .env.example to .env.local. And then paste the Hanko API url obtained from the dashboard

We'll mainly need to understand the `components/` and `app/` in order to customise it for our application.

**app directory**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yw5l4x2igs75me3zni6b.png)

The app directory contains the `page.jsx` and `dashboard` and `login` directories. The `page.jsx` is the homepage of the application. The login page contains the Hanko login component:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hpu62vdgsaozgt8gas7x.png)

On successful login, the web app redirects to dashboard page. The dashboard page is rendered by the `page.tsx` file in the `dashboard/` directory. Currently, it contains just the HankoProfile component and Logout component.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/np28xukmcu2hlch56rfk.png)

Checkout this link to the [hanko-nextjs-starter app](https://hanko-nextjs-starter-six.vercel.app/dashboard).

**component directory**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a8r11jyr1rad9mj1amx4.png)
This is an overview of the component directory. As we go on customizing the UI, we'll understand it even better.

Further, guys at Hanko have already setup Tailwind for us and have reduced a lot of our work.

## Building the UI
As mentioned before we are going to use MaterialUI to speed up our development. We'll also use Materical Icons.
Run the following command:

```
npm install @mui/icons-material @mui/material @emotion/styled @emotion/react
```

Lets begin with `app/page.tsx`.

```
"use client";
import Button from "@mui/material/Button";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
export default function Login() {
  return (
    <div className="flex flex-col min-h-screen justify-center items-center bg-black">
      <div>
        <div className="sm:text-9xl text-6xl text-white">HOMEDEC</div>
        <div className="sm:text-5xl text-white">
          AI HOME DECORATOR FOR ALL YOUR NEEDS
        </div>
      </div>
      <Button
        variant="contained"
        href="/login  "
        className="text-4xl flex mt-20"
      >
        GET STARTED
        <ArrowForwardIosIcon />
      </Button>
    </div>
  );
}

```
This should turn the homepage into something like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mgzzuccfnahoabkd8nhg.png)
The UI is pretty basic. But, the purpose of this project was to understand how Hanko and Supabase work.

Next, we'll work on the login page. But, before that lets check the HankoAuth component.

```
"use client";
import { useEffect, useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import { register, Hanko } from "@teamhanko/hanko-elements";

const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_KEY || "";

export default function HankoAuth() {
  const router = useRouter();

  const [hanko, setHanko] = useState<Hanko>();

  useEffect(() => {
    import("@teamhanko/hanko-elements").then(({ Hanko }) =>
      setHanko(new Hanko(hankoApi))
    );
  }, []);

  const redirectAfterLogin = useCallback(() => {
    // successfully logged in, redirect to a page in your application
    router.replace("/dashboard");
  }, [router]);

  useEffect(
    () =>
      hanko?.onAuthFlowCompleted(() => {
        redirectAfterLogin();
      }),
    [hanko, redirectAfterLogin]
  );

  useEffect(() => {
    register(hankoApi).catch((error) => {
      // handle error
    });
  }, []);

  return <hanko-auth />;
}
```
The HankoAuth component is perfectly ready for us to use in our project. So, lets focus on customizing the `/login/page.tsx` page:

```
"use client";
import dynamic from "next/dynamic";
const HankoAuth = dynamic(
  () => import("@/components/hanko-components/HankoAuth"),
  { ssr: false }
);

export default function Login() {

  return (
    <div className="flex min-h-screen justify-center items-center bg-black">
      <div className="bg-white sm:p-5 rounded-2xl shadow-md">
        <HankoAuth />
      </div>
    </div>
  );
}

```
We'll also customize the styling a bit for the login component. Add the following to the app/globals.css file:

```
:root {
  --border-radius: 20px;
  --brand-color: #1976d2;
  --brand-color-shade-1: #042c55;
  --brand-color-shade-2: #88bff7;
}

```
You should get this page on clicking the **GET STARTED** button.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tymqtxk5ig9d3brha3e4.png)
On successful login, you should be directed to `/dashboard` page. We'll first create components for the dashboard page.
The first component will be components/Form.tsx

```
"use client";
import Button from "@mui/material/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import { styled } from "@mui/material/styles";
import * as React from "react";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import { useRouter } from "next/navigation"; //this for use later


const VisuallyHiddenInput = styled("input")({
  clip: "rect(0 0 0 0)",
  clipPath: "inset(50%)",
  height: 1,
  overflow: "hidden",
  position: "absolute",
  bottom: 0,
  left: 0,
  whiteSpace: "nowrap",
  width: 1,
});
const currencies = [
  {
    value: "Living Room",
    label: "Living Room",
  },
  {
    value: "Store",
    label: "Store",
  },
  {
    value: "Bedroom",
    label: "Bedroom",
  },
  {
    value: "Bathroom",
    label: "Bathroom",
  },
  {
    value: "Office",
    label: "Office",
  },
  {
    value: "Kitchen",
    label: "Kitchen",
  },

  {
    value: "Balcony",
    label: "Balcony",
  },
];

export const Form = () => {
  const [loading, setLoading] = React.useState(false);
  const [file, setFile] = React.useState<File>();
  const [name, setName] = React.useState("Nothing");
  const [uploadUrl, setUploadUrl] = React.useState("");
  const [valuee, setValuee] = React.useState("Living Room");
  const [value, setValue] = React.useState("");
  return (
    <div className=" space-y-12 ">
      <div className="text-lg font-medium">
        Upload an image and tell us what you want for your room.
      </div>
      <div>
        <div className="space-y-4">
          <Button
            component="label"
            variant="contained"
            startIcon={<CloudUploadIcon />}
          >
            <div className="font-bold text-xl">Upload file</div>
            <VisuallyHiddenInput
              type="file"
              accept=".png"
              onChange={(e) => {
                setName(e.target.files![0].name);
                setFile(e.target.files![0]);
              }}
            />
          </Button>
          <br></br>
          <div>{name} uploaded!</div>
        </div>
      </div>
      <div>
        <div className="text-white">
          <TextField
            id="outlined-basic"
            label={<div style={{ color: "#1976D2" }}>Tell us more</div>}
            variant="outlined"
            maxRows="3"
            multiline
            value={value}
            onChange={(e) => {
              setValue(e.target.value);
            }}
            sx={{ width: "80%" }}
            inputProps={{ style: { color: "white" } }}
            focused
          />
        </div>
      </div>
      <div>
        <div style={{ width: "100%" }}>
          <TextField
            id="outlined-select-currency"
            select
            label="Select Room Type"
            defaultValue="Living Room"
            value={valuee}
            helperText="Please select your currency"
            sx={{ width: "80%" }}
            focused
            SelectProps={{ style: { color: "white" } }}
            onChange={(e) => {
              setValuee(e.target.value);
            }}
          >
            {currencies.map((option) => (
              <MenuItem key={option.value} value={option.value}>
                {option.label}
              </MenuItem>
            ))}
          </TextField>
        </div>
      </div>
      <div>
        <Button
          variant="contained"
          onClick={(e) => {
            if (file != null) {
              {
                
              }
            } else {
              window.alert("You need to upload an image");
            }
          }}
        >
          {!loading ? (
            <div className="font-bold text-xl">RENDER DESIGNS</div>
          ) : (
            <div className="font-bold text-xl">PLEASE WAIT..</div>
          )}
        </Button>
      </div>
    </div>
  );
};

```
Then, we'll create the Profile component `components/Form.tsx`. Try to figure out how the profile modal would close XD.

```
"use client";
import { useEffect, useState } from "react";
import { register } from "@teamhanko/hanko-elements";
import ManageAccountsIcon from "@mui/icons-material/ManageAccounts";
const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_KEY;

export const Profile = () => {
  const [openState, setOpenState] = useState(false);

  useEffect(() => {
    register(hankoApi ?? "").catch((error) => {
      console.log(error);
    });
  }, []);

  const openProfile = () => {
    setOpenState(!openState);
  };

  return (
    <>
      <button
        type="button"
        onClick={openProfile}
        className="font-bold text-2xl"
      >
        <ManageAccountsIcon fontSize="large" />
        PROFILE
      </button>
      {openState && (
        <div className=" absolute top-14 ">
          <section className=" w-[450px] h-auto rounded-2xl bg-white p-5">
            <hanko-profile />
          </section>
        </div>
      )}
    </>
  );
};
```
Then finally the `components/Logout.tsx`:

```
"use client";
import { useState, useEffect, useCallback } from "react";
import { useRouter } from "next/navigation";
import { Hanko } from "@teamhanko/hanko-elements";
import LogoutIcon from "@mui/icons-material/Logout";

const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_KEY;

export const Logout = () => {
  const router = useRouter();
  const [hanko, setHanko] = useState<Hanko>();

  useEffect(() => {
    import("@teamhanko/hanko-elements").then(({ Hanko }) =>
      setHanko(new Hanko(hankoApi ?? ""))
    );
  }, []);

  const logout = () => {
    hanko?.user
      .logout()
      .then(() => {})
      .catch((error) => {
        console.log(error);
      });
    router.push("/");
    router.refresh();
  };
  return (
    <>
      <button type="button" onClick={logout} className="font-bold text-2xl">
        <LogoutIcon fontSize="large" />
        LOGOUT
      </button>
    </>
  );
};

```
Finally, we combine everything into pages/dashboard.tsx:

```
import { Logout } from "@/components/Logout";
import { Profile } from "@/components/Profile";
import { Form } from "@/components/Form";
import Card from "@mui/material/Card";
import CardActions from "@mui/material/CardActions";
import CardMedia from "@mui/material/CardMedia";

export default async function Todo() {
  return (
    <main className="bg-black text-white">
      <div className="w-full flex py-8 px-12 space-x-6 justify-around">
        <Profile />
        <div className="font-bold sm:text-4xl text-2xl">
          <a href="/">HOMEDEC</a>
        </div>
        <Logout />
      </div>
      {/* <div className="bg-slate-300 rounded-3xl py-6  h-[400px] w-[450px] flex flex-col text-slate-800">
        <h1 className="text-3xl text-center">My to dos</h1>
        <NewTodo />
        <ul className="px-6">
          <TodoItem todos={todos} />
        </ul>
      </div> */}
      <div className="sm:grid grid-cols-3 gap-x-4 px-16 py-20">
        <div className="col-span-1 pr-6 text-center">
          <Form />
        </div>

        <div className="col-span-2 space-y-4">
          <div className="text-3xl p-4 font-bold">YOUR DESIGNS</div>
          <div className="sm:grid grid-cols-2 space-y-4">
          </div>
        </div>
      </div>
    </main>
  );
}

```
This should render a UI like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3jtjk9s1s0d91l7673bh.png)

Now, we are done setting up the basic UI. Time to move to the next parts.

## Setting Up Prisma and Supabase
First thing, install Prisma:

```
pnpm install prisma
```
Set up prisma with the following:

```
pnpm prisma init --datasource-provider postgresql
```
This will generate a prisma directory. In that directory, customise the schema.prisma file as follows:

```
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Item {
  userId String
  id String @id @default(uuid())
  title String
  src String @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
```
Now, it's time to setup Supabase to host our database. Go to [Supabase](https://supabase.com/) and click on **Start your project**. Then sign into it and create a new organization and then a new project. Go into the Supabase project settings and head over to Database. Then copy the URI.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p42yamifeh0h0kh19cvj.png)
Paste the URI into .env file as:

```
DATABASE_URL=<Don't put quotes for this one>
```
After that run the following commands:

```
pnpm prisma migrate dev --name init
pnpm prisma db push
pnpm prisma generate
```
We'll soon be able to upload data into the database. But, we'll first need to copy the Anon Key in the API tab into our .env file:

```
NEXT_PUBLIC_SUPABASE_ANON="Put this inside quotes :)"
```
Also, create `db.ts` in the root directory. This is to prevents problems when instantiating [Prisma client](https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices).
## Setting Up Replicate
In order to generate images using AI, we'll need to setup Replicate. Head over to [Replicate](https://replicate.com/), sign up ad get API tokens. Copy the token and paste it into your env file:

```
NEXT_PUBLIC_REPLICATE_API_KEY="Your API Key"
```
## Generating Images
To be able to generate images we'll set up api routes in the app directory. Create the following in the `app/api/generate/route.ts`:

```
import { NextResponse } from "next/server";
import { headers } from "next/headers";

// Create a new ratelimiter, that allows 5 requests per 24 hours

export async function POST(request: Request) {
  // Rate Limiter Code

  const { imageUrl, theme, room } = await request.json();

  // POST request to Replicate to start the image restoration generation process
  let startResponse = await fetch("https://api.replicate.com/v1/predictions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Token " + process.env.REPLICATE_API_KEY,
    },
    body: JSON.stringify({
      version:
        "854e8727697a057c525cdb45ab037f64ecca770a1769cc52287c2e56472a247b",
      input: {
        image: imageUrl,
        prompt:
          "Make a " + room + ".The person want this in that room: " + theme,
        a_prompt:
          "best quality, extremely detailed, photo from Pinterest, interior, cinematic photo, ultra-detailed, ultra-realistic, award-winning, Room",
        n_prompt:
          "longbody, lowres, bad anatomy, bad hands, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality",
      },
    }),
  });

  let jsonStartResponse = await startResponse.json();

  let endpointUrl = jsonStartResponse.urls.get;

  // GET request to get the status of the image restoration process & return the result when it's ready
  let restoredImage: string | null = null;
  while (!restoredImage) {
    // Loop in 1s intervals until the alt text is ready
    console.log("Please wait...");
    let finalResponse = await fetch(endpointUrl, {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
        Authorization: "Token " + process.env.REPLICATE_API_KEY,
      },
    });
    let jsonFinalResponse = await finalResponse.json();

    if (jsonFinalResponse.status === "succeeded") {
      restoredImage = jsonFinalResponse.output;
    } else if (jsonFinalResponse.status === "failed") {
      break;
    } else {
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }

  return NextResponse.json(
    restoredImage ? restoredImage : "Failed to restore image"
  );
}

```
The above api will be called from the Form component, which will make a call to the Replicate API and get back a url containing AI generated image. Now, we need to make an API route to send the data to the Prisma database. We create `app/api/todo/route.ts`

```
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import * as jose from "jose";
import { prisma } from "@/db";

export async function userId() {
  const token = cookies().get("hanko")?.value;
  const payload = jose.decodeJwt(token ?? "");
  return payload.sub;
}

export async function POST(req: Request) {
  const userID = await userId();
  const { src } = await req.json();
  const title = "abc";

  if (userID) {
    if (typeof title !== "string" || title.length === 0) {
      throw new Error("That can't be a title");
    }
    await prisma.item.create({
      data: { title, src, userId: userID ?? "" },
    });

    return NextResponse.json({ message: "Created Todo" }, { status: 200 });
  } else {
    return NextResponse.json({ error: "Not Found" }, { status: 404 });
  }
}

```
We're done with the backend. Now, our job is to make calls from the Form to store generate AI images and store them into the database. Modify the `components/Form.tsx`

```
"use client";
import Button from "@mui/material/Button";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import { styled } from "@mui/material/styles";
import * as React from "react";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
import { useRouter } from "next/navigation";
import { createClient } from "@supabase/supabase-js";
const VisuallyHiddenInput = styled("input")({
  clip: "rect(0 0 0 0)",
  clipPath: "inset(50%)",
  height: 1,
  overflow: "hidden",
  position: "absolute",
  bottom: 0,
  left: 0,
  whiteSpace: "nowrap",
  width: 1,
});
const currencies = [
  {
    value: "Living Room",
    label: "Living Room",
  },
  {
    value: "Store",
    label: "Store",
  },
  {
    value: "Bedroom",
    label: "Bedroom",
  },
  {
    value: "Bathroom",
    label: "Bathroom",
  },
  {
    value: "Office",
    label: "Office",
  },
  {
    value: "Kitchen",
    label: "Kitchen",
  },

  {
    value: "Balcony",
    label: "Balcony",
  },
];
export const Form = () => {
  const supabase = createClient(
    "https://dgahpknmwckcozpfuyrp.supabase.co",
    process.env.NEXT_PUBLIC_SUPABASE_ANON!
  );
  const router = useRouter();
  const [loading, setLoading] = React.useState(false);
  const handleSubmit = async (
    file: File,
    name: string,
    value: string,
    valuee: string
  ) => {
    setLoading(true);
    const { data, error } = await supabase.storage
      .from("Images")
      .upload("/" + file.name, file, { upsert: true });
    const publicUrl = supabase.storage
      .from("Images")
      .getPublicUrl("/" + data!.path);
    if (data) {
      console.log(publicUrl.data.publicUrl);
    } else {
      console.log(error);
    }
    const res = await fetch("/api/generate", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        imageUrl: publicUrl.data.publicUrl,
        value,
        valuee,
      }),
    });

    let newPhoto = await res.json();
    if (res.status !== 200) {
      console.log(res);
    } else {
      console.log(newPhoto[1]);
    }
    setTimeout(() => {
    }, 1300);
    await fetch(`/api/todo`, {
      method: "POST",
      credentials: "include",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        src: newPhoto[1],
      }),
    });
    router.refresh();
    setLoading(false);
  };
  const [file, setFile] = React.useState<File>();
  const [name, setName] = React.useState("Nothing");
  const [uploadUrl, setUploadUrl] = React.useState("");
  const [valuee, setValuee] = React.useState("Living Room");
  const [value, setValue] = React.useState("");
  return (
    <div className=" space-y-12 ">
      <div className="text-lg font-medium">
        Upload an image and tell us what you want for your room.
      </div>
      <div>
        <div className="space-y-4">
          <Button
            component="label"
            variant="contained"
            startIcon={<CloudUploadIcon />}
          >
            <div className="font-bold text-xl">Upload file</div>
            <VisuallyHiddenInput
              type="file"
              accept=".png"
              onChange={(e) => {
                setName(e.target.files![0].name);
                setFile(e.target.files![0]);
              }}
            />
          </Button>
          <br></br>
          <div>{name} uploaded!</div>
        </div>
      </div>
      <div>
        <div className="text-white">
          <TextField
            id="outlined-basic"
            label={<div style={{ color: "#1976D2" }}>Tell us more</div>}
            variant="outlined"
            maxRows="3"
            multiline
            value={value}
            onChange={(e) => {
              setValue(e.target.value);
            }}
            sx={{ width: "80%" }}
            inputProps={{ style: { color: "white" } }}
            focused
          />
        </div>
      </div>
      <div>
        <div style={{ width: "100%" }}>
          <TextField
            id="outlined-select-currency"
            select
            label="Select Room Type"
            defaultValue="Living Room"
            value={valuee}
            helperText="Please select your currency"
            sx={{ width: "80%" }}
            focused
            SelectProps={{ style: { color: "white" } }}
            onChange={(e) => {
              setValuee(e.target.value);
            }}
          >
            {currencies.map((option) => (
              <MenuItem key={option.value} value={option.value}>
                {option.label}
              </MenuItem>
            ))}
          </TextField>
        </div>
      </div>
      <div>
        <Button
          variant="contained"
          onClick={(e) => {
            if (file != null) {
              {
                handleSubmit(file, name, value, valuee);
              }
            } else {
              window.alert("You need to upload an image");
            }
          }}
        >
          {!loading ? (
            <div className="font-bold text-xl">RENDER DESIGNS</div>
          ) : (
            <div className="font-bold text-xl">PLEASE WAIT..</div>
          )}
        </Button>
      </div>
    </div>
  );
};

```
We would also like to see the designs generated by the AI. We'll do it by modifying the `app/dashboard/page.tsx`.

```
import { Logout } from "@/components/Logout";
import { Profile } from "@/components/Profile";
import { prisma } from "@/db";
import { userId } from "../api/todo/route";
import { Form } from "@/components/Form";
import Card from "@mui/material/Card";
import CardActions from "@mui/material/CardActions";
import CardMedia from "@mui/material/CardMedia";

export default async function Todo() {
  const userID = await userId();

  const items = await prisma.item.findMany({
    where: {
      userId: { equals: userID },
    },
  });
  return (
    <main className="bg-black text-white">
      <div className="w-full flex py-8 px-12 space-x-6 justify-around">
        <Profile />
        <div className="font-bold sm:text-4xl text-2xl">
          <a href="/">HOMEDEC</a>
        </div>
        <Logout />
      </div>
     
      <div className="sm:grid grid-cols-3 gap-x-4 px-16 py-20">
        <div className="col-span-1 pr-6 text-center">
          <Form />
        </div>

        <div className="col-span-2 space-y-4">
          <div className="text-3xl p-4 font-bold">YOUR DESIGNS</div>
          <div className="sm:grid grid-cols-2 space-y-4">
            {items
              .slice(0)
              .reverse()
              .map((item) => {
                return (
                  <div key={item.id}>
                    <Card sx={{ maxWidth: 345 }}>
                      <CardMedia
                        component="img"
                        alt={item.src}
                        image={item.src}
                      />
                      <CardActions>
                        <a
                          href={item.src}
                          target="_blank"
                          className="text-green-600 font-semibold"
                        >
                          DOWNLOAD
                        </a>
                      </CardActions>
                    </Card>
                  </div>
                );
              })}
          </div>
        </div>
      </div>
    </main>
  );
}

```
You might wonder why we've named one api route as todo. This project was built after understanding this [Todo app](https://dev.to/hanko/building-a-nextjs-13-todo-app-with-prisma-and-passkey-authentication-by-hanko-4o8a).
## Update1:Enhancment Of The UI
After checking the response of the users, I could identify a few ways to enhance the UI.
### Adding an alert to let the user upload only an image.
If you go to the `Form.tsx` file under components directory, you'll find a Button MUI element. We are going to modify it.

```
<Button
          variant="contained"
          onClick={(e) => {
            if (file != null && file!.type.match("image.*")) {
//Handle submit is called only when the user uploads image
              {
                handleSubmit(file!, name, value, valuee);
              }
            } else {
//Else the user gets a window alert
              window.alert("You need to upload an image");
            }
          }}
        >
          {!loading ? (
            <div className="font-bold text-xl">RENDER DESIGNS</div>
          ) : (
            <div className="font-bold text-xl">PLEASE WAIT..</div>
          )}
</Button>
```
### Add a circular animation after the user clicks on Render Design
This is just to make the UI look better. We'll finally end up with the following for the Button in the `Form.tsx` file.

```
<Button
          variant="contained"
          onClick={(e) => {
            if (file != null && file!.type.match("image.*")) {
              {
                handleSubmit(file!, name, value, valuee);
              }
            } else {
              window.alert("You need to upload an image");
            }
          }}
        >
          {!loading ? (
            <div className="font-bold text-xl">RENDER DESIGNS</div>
          ) : (
            <div className="font-bold text-xl flex justify-items-center">
              PLEASE WAIT
              <CircularProgress color="secondary" />
            </div>
          )}
        </Button>
```
Remember to import the CircularProgress element from MUI

```
import CircularProgress from "@mui/material/CircularProgress";
```
### Alerting the user if there is no response from the API
Currently, the project has been hosted for free on Replicate. So, due to resource limitations there could be no response from it. Hence, we are going to keep an alert for the same.
In the `Form.tsx` file, we'll modify the handleSubmit function.

```
const handleSubmit = async (
    file: File,
    name: string,
    value: string,
    valuee: string
  ) => {
    setLoading(true);
    const { data, error } = await supabase.storage
      .from("Images")
      .upload("/" + file.name, file, { upsert: true });
    const publicUrl = supabase.storage
      .from("Images")
      .getPublicUrl("/" + data!.path);
    const res = await fetch("/api/generate", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        imageUrl: publicUrl.data.publicUrl,
        value,
        valuee,
      }),
    });

    if (res.status !== 200) {
      setLoading(false);
      window.alert(
        "Sorry, our resources are busy currently. If the issue persists, please contact the owner"
      );
    }//This is the function to alert the user
    let newPhoto = await res.json();
    setTimeout(() => {}, 1300);
    await fetch(`/api/todo`, {
      method: "POST",
      credentials: "include",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        src: newPhoto[1],
      }),
    });
    router.refresh();
    setLoading(false);
  };
```

## Congratulations!!!
The end result should look something like this one:
[https://hanko-ai-app.vercel.app/](https://hanko-ai-app.vercel.app/)
Refer to this [GitHub](https://github.com/Rajarshi-Misra/Hanko-AI-App) repo to understand the working of this project.
Do leave a feedback. And, I'd be happy to colab for further projects.
Thanks!