Tutorials
Build Freelancer Platform
Project Form Creation

Create project form in Next.js

In this tutorial, we will build a form to save data to ROQ using Next.js. We will use the ROQ SDK to save data to the database.

Let's build a simple SaaS works marketplace for freelancers. The application will have two user roles: Business Owner and Freelancer. The Business Owner will be able to create projects, and the Freelancer will be able to see the available projects. This tutorial will only cover the Business Owner role, we will cover the Freelancer role in the next tutorial.

Project setup

Create an application

To create a new application you need to have a ROQ Console account. If you don't have one, please create an account at ROQ Console (opens in a new tab) and read this guide to learn how to create an application.

If you want to follow in detail in this tutorial, these are the application configurations we used in the ROQ Console:

Create registration form variants

We need to create a registration form variant for the Business Owner and Freelancer roles. So when the user register or login will be redirected to the correct page based on the role.

Go to the ROQ Console (opens in a new tab) and click the Authentication menu. Then click the Design and then create a new registration form variant using the + Create Form button.

registration form variants

Create one each role for Business Owner and Freelancer. The registration form variants key should be business-owner and freelancer and these will be used in the code later.

Create user roles

The user roles we use are: Business Owner and Freelancer.

user roles

Please note that each roles will have different registration form variants. This make sure that the user will be able to register or signin with the correct role. For example, the Business Owner will have the business-owner registration form variant and the Freelancer will have the freelancer registration form variant.

Configure project schema

To change the data model, you need to acces the schema editor. Go to the ROQ Console (opens in a new tab) and click the Schema Editor menu.

project schema

Make sure to publish any changes, it will take a while and ROQ will notify you via email when the changes are published.

Configure access management

Next, we need to configure the access management for users and make sure to publish any changes.

user access management

Next.js quickstart

For further development, we should create another clean Next.js application outside the ROQ Console.

We will use Next.js with App Router in this tutorial. Please, read this Next.js quickstart to initialize the Next.js application with signup, login, and logout features.

Code changes

There are a few changes to the login page's src/app/page.tsx. We need to replace the role for sign-in and sign-up process with the role we created in the project setup, which is business-owner, and change the redirect page postLoginRedirect to /projects:

src/app/page.tsx
import { roqBrowserClient } from "@/lib/roq/roq-client";
 
export default function Home() {
 
  const handleSignUp = async () => {
    await roqBrowserClient.signUp('business-owner', { postLoginRedirect: 'http://locallhost:3000/projects' });
  };
 
  const handleSignIn = async () => {
    await roqBrowserClient.signIn('business-owner', { postLoginRedirect: 'http://localhost:3000/projects' });
  };
 
  return (
    {/* the code here is the same as before */}
  );
}

Building project form

Now that we have a basic understanding of how to save data to ROQ let's build a form to save data to ROQ.

Create a project form component

Create a project-form.tsx file in the same folder as src/app/page.tsx and add the following code:

src/app/project-form.tsx
import React from 'react';
import { ProjectFormProps } from '@/interface/project-data-types';
import { v4 as uuidv4 } from 'uuid';
 
export default function ProjectForm({ projectData, onSubmit, status }: ProjectFormProps) {
 
    const handleSubmit = (e) => {
        e.preventDefault();
        const formData = {
            id: uuidv4(),
            name: e.target.name.value,
            description: e.target.description.value,
            status: e.target.status.value,
            deadline: new Date(e.target.deadline.value),
          };
      
        onSubmit(formData);
    };
 
    return (
        <div className="flex flex-col justify-center items-center h-screen bg-zinc-950 px-4">
            <div className="w-full max-w-lg">
                <h1 className="text-white text-xl font-bold my-4">Create Project</h1>
                <form onSubmit={handleSubmit} className="flex flex-col gap-4 bg-zinc-800 p-6 rounded shadow-lg w-full max-w-lg">
                    <div className="flex flex-col gap-2">
                        <label htmlFor="name" className="text-gray-400">Name</label>
                        <input type="text" id="name" defaultValue={projectData.name} className="bg-zinc-950 text-white border border-gray-600 rounded p-2" />
                    </div>
 
                    <div className="flex flex-col gap-2">
                        <label htmlFor="description" className="text-gray-400">Description</label>
                        <textarea id="description" defaultValue={projectData.description} className="bg-zinc-950 text-white border border-gray-600 rounded p-2" />
                    </div>
 
                    <div className="flex flex-col gap-2">
                        <label htmlFor="status" className="text-gray-400">Status</label>
                        <select id="status" defaultValue={projectData.status} className="bg-zinc-950 text-white border border-gray-600 rounded p-2">
                            <option value="open">Open</option>
                            <option value="inprogress">In Progress</option>
                            <option value="active">Active</option>
                            <option value="inactive">Inactive</option>
                            <option value="completed">Completed</option>
                            <option value="cancelled">Cancelled</option>
                            <option value="onhold">On Hold</option>
                            <option value="pending">Pending</option>
                        </select>
                    </div>
 
                    <div className="flex flex-col gap-2">
                        <label htmlFor="deadline" className="text-gray-400">Deadline</label>
                        <input type="date" id="deadline" defaultValue={projectData.deadline ? projectData.deadline.split('T')[0] : ''} className="bg-zinc-950 text-white border border-gray-600 rounded p-2" />
                    </div>
                    
                    {status && (
                        <div className={`p-3 rounded ${status.type === 'success' ? 'bg-green-500' : 'bg-red-500'} text-white`}>
                            {status.message}
                        </div>
                    )}
 
                    <button type="submit" className="px-4 py-2 bg-indigo-500 text-white rounded shadow">Save</button>
                </form>
            </div>
        </div>
    );
}

This component basically renders a form with the project data and a submit button. The onSubmit prop is a function that will be called when the form is submitted. The project form only creates a new project with a company relation but without a relation such as a task entity.

We need also install uuid npm package to generate a unique ID for the freelance project. Run the following command to install the package:

npm i --save uuid 
npm i --save-dev @types/uuid

Create a project page

To use the ProjectForm component, we need to create a page that renders the form. Create a src/app/projects/page.tsx file and add the following code:

src/app/projects/page.tsx
import { roqBrowserClient } from "@/lib/roq/roq-client";
import { useSession } from '@/lib/roq/roq-hooks';
import { useState } from 'react';
import ProjectForm from '../project-form';
import { ItemType, StatusMessageType } from '@/interface/project-data-types';
 
export default function Projects() {
  
  const { session } = useSession();
  const [currentProject, setCurrentProject] = useState([]);
  const [status, setStatus] = useState<StatusMessageType | null>(null);
  
  const company_id = session?.user.tenantId || '';
  const roles = session?.user.roles || [];
 
  const logoutHandler = () => {
    roqBrowserClient.signOut();
  };
 
  const handleFormSubmit = async (projectData: ItemType) => {
    try {
      projectData.company_id = company_id;
      console.log(projectData)
      const project = await roqBrowserClient.project.create({
        data: projectData
      });
 
      console.log(projectData);
      setStatus({ message: 'Project saved successfully!', type: 'success' });
 
      setTimeout(() => {
        setStatus(null);
      }, 3000);
 
    } catch (error) {
      console.error('Error submitting project:', error);
      setStatus({ message: 'Error saving project.', type: 'error' });
    }
  };
 
  return (
    <div className="flex flex-col w-full h-screen bg-zinc-950">
    <nav className="flex justify-between items-center p-4 bg-zinc-800 shadow">
      <h1 className="text-white text-xl font-bold">Freelancer Inc.</h1>
      <div className="flex items-center gap-4">
        <div>
          <p className="text-gray-400 text-sm">
            Logged in as: <span className="text-white">{session?.user.email}</span>
          </p>
          <p className="text-gray-400 text-sm">
            Role: <span className="text-white">{roles[0]}</span>
          </p>
        </div>
        <button onClick={logoutHandler} className="px-3 py-1 bg-indigo-500 rounded shadow text-white font-bold">
          Log out
        </button>
      </div>
    </nav>
    <ProjectForm projectData={currentProject} onSubmit={handleFormSubmit} status={status}/>
  </div>
  
  );
}

This page renders the ProjectForm component and passes the handleFormSubmit function as the onSubmit prop. The handleFormSubmit function will be called when the form is submitted, saving the project data to ROQ.

Add interface for project data

This project uses TypeScript, so we need to create an interface for project data types. Create a project-data-types.ts file in src/app/interface folder and add the following code:

src/app/inerface/project-data-types.ts
interface ItemType {
    id: string;
    name: string;
    description: string | null;
    deadline: Date | null;
    status: string;
    company_id: string;
    created_at?: Date;
    updated_at?: Date;
}
 
type StatusMessageType = {
    message: string;
    type: 'success' | 'error';
};
 
type ProjectFormProps = {
    projectData: ItemType;
    onSubmit: (formData: ItemType) => void;
    status?: StatusMessageType;
};
 
export type { ItemType, StatusMessageType, ProjectFormProps };

Then we can import the interface in project-form.tsx and src/app/projects/page.tsx files :

src/pages/project-form.tsx
import { ProjectFormProps, ItemType, StatusMessageType } from '@/interface/project-data-types';

What have we built?

If you follow this tutorial, you should have a project form that looks like this:

project page

For now, we can only check the response data from the browser console. The response data from the browser console:

{
    "id": "7edd49af-4f44-4887-b80b-48e279999614",
    "name": "AI Chatbot",
    "description": "Creating AI chatbot for customer service",
    "deadline": "2024-01-01T00:00:00.000Z",
    "status": "open",
    "company_id": "22d80a45-0cf6-445d-a5db-f1091c6cb5d6",
    "created_at": "2023-11-11T14:28:12.654Z",
    "updated_at": "2023-11-11T14:28:12.654Z"
}

We will include a project list for freelancers in the next section.