Build a Cloud-based project management app with Next.js
Efficient project management holds immense importance in today’s business landscape for achieving successful outcomes. Project Management Application empowers users with comprehensive oversight. This article will show you how to build such a NextJS app by taking advantage of various AWS services for a full cloud-based implementation.
The following is the architecture of the project management solution we will build.
The application uses the following AWS services:
-
AWS S3: For storing and retrieving assets like images and files.
-
AWS DynamoDB: As the
NoSQL
database for efficient data storage and retrieval. -
AWS Lambda: To create serverless functions for data manipulation and business logic implementation.
-
AWS API Gateway: For managing
APIs
and enabling seamless communication between frontend and backend components. -
AWS Amplify for web application hosting.
These AWS services provide scalable storage, serverless computing, efficient API
management, and a powerful frontend framework, resulting in a robust web development experience.
To fully grasp the concepts presented in this tutorial, the following are required:
- An AWS account – Sign up.
- AWS Identity and Access Management user with console access
- Basic understanding of JavaScript
- A GitHub account
Purpose of the project management application.
The Project Management Application aims to streamline and enhance project management processes, providing users with an efficient tool to track and maintain projects from start to finish, with a good user experience. By leveraging AWS services like S3
, Lambda
, DynamoDB
, API Gateway
, and Next.js
, the application offers a scalable solution that caters to the diverse needs of project managers and teams.
Project management involves systematically planning, organizing, and executing projects to achieve specific goals within defined constraints. Key points regarding the benefits of project management include:
- Provides clear direction, structure, and efficient execution of projects.
- Fosters effective communication and collaboration among stakeholders.
- Enables proactive risk management and issue resolution.
- Optimizes resource allocation, improving productivity and cost-effectiveness.
- Ensures successful project outcomes, delivering high-quality results within set parameters.
Our project management application offers the following key features and functionalities:
- Create: Users can create new projects, tasks, and milestones within the application.
- Read: Users can view and access project details, task statuses, and progress.
- Update: Users can update project information, task assignments, and milestone dates.
- Delete: Users can delete projects, tasks, and milestones when necessary.
- Comprehensive project tracking and management capabilities.
- Integration with AWS services like
DynamoDB
,S3
,Lambda
, andAPI Gateway
. - User-friendly interface for easy navigation and usage.
Role and workflow of the AWS tools we’ll use
This tutorial shows you how to create a serverless API
that performs CRUD
operations on AWS services such as DynamoDB
, S3
, Lambda functions
, and API Gateway
. The workflow for building this API
involves the following steps:
API Gateway:
Configure an HTTP API
in API Gateway
to serve as an entry for client requests to every API endpoint. Define every API route and method (GET
, POST
, PUT
, DELETE
) for the desired CRUD
operations.
Lambda Function:
Create AWS Lambda functions
that implement the business logic for each CRUD
operation. For example, a Lambda function
can handle a POST
request to create an item, a GET
request to retrieve an item, a PUT
request to update an item, and a DELETE
request to remove an item.
DynamoDB:
Utilize DynamoDB
as the NoSQL
database for storing and retrieving data. The Lambda functions interact with DynamoDB
to perform the necessary CRUD
operations. For instance, when creating an item, the Lambda function
writes the data to a DynamoDB
table, and when retrieving an item, the Lambda function
fetches the data from the table.
S3:
Storage for assets like images or files. S3
provides a scalable and durable object storage service for storing and retrieving these assets.
Following this tutorial, you will build a serverless API that effectively performs CRUD
operations using DynamoDB
, S3
, Lambda functions
, and API Gateway
.
Steps to set up the development environment for the application.
Here are the steps to set up the development environment for the application:
-
Create a
DynamoDB
Table: you begin by configuring aDynamoDB
table (Project and Client Table) with the necessary attributes and primary key. -
Set Up AWS
S3
for Assets Storage: Create anS3
bucket to store and manage assets like images or files. -
Create a
Lambda Function:
Develop a function (Project and Client Lambda functions) to handleCRUD
operations on theDynamoDB
table. -
Create an
HTTP API:
Use theAPI
Gateway Console to create anHTTP API
for Project and Client with endpoints forCRUD
operations. -
Create Routes: Define routes in the
API Gateway
for Project and Client to match your application’s functionality. -
Create an Integration: Connect the
API Gateway
for Project and Client to theLambda function
through an integration. -
Attach Your Integration to Routes: Associate the integration with the appropriate routes for Project and Client in the
API Gateway
. -
Test Your
API
: Use testing tools like theAPI Gateway
or Postman to validate theAPI’s
functionality.
In the following sections, we’ll review all the needed steps to build and deploy this app.
Session Replay for Developers
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.
Create a DynamoDB table
To store data for your API
, you will utilize a DynamoDB
table. Each item in the table will have a unique ID, which will serve as the partition key.
To create the Project DynamoDB table, follow these steps:
- Go to https://console.aws.amazon.com/dynamodb/ to access the DynamoDB console.
- Select the option to create a table.
- Enter
http-project-management-items
as the table name. - Set id as the partition key.
- Proceed to create the table.
Next, create the Client’s DynamoDB
table by following the same steps above but change the table name to http-client-management-items
.
By following these steps, you will create a DynamoDB table
to store data for your API
, ensuring efficient and effective data management.
Create S3 Bucket
Amazon Simple Storage Service (Amazon S3
) is a highly scalable and cost-effective storage service provided by AWS. It offers low-latency access to store a virtually unlimited number of objects.
To create S3 Bucket, follow these steps:
- Go to https://console.aws.amazon.com/S3/ to access the
S3
console. - Select the option to create a bucket
- Enter
http-project-management-items-bucket
as the bucket name. - Choose the AWS region closest to you or where you would like your data to reside. In this case, it is [US-east-1]
- In Block Public Access settings for this bucket category, Uncheck the BLOCK ALL PUBLIC ACCESS -In the Bucket Versioning category, choose Disabled.
- Click on the Create Bucket.
Upon successfully creating the bucket, you will receive a confirmation message at the top of the page.
Set S3 bucket permissions
- Open the Amazon
S3
console. - Select the bucket you want to set permissions for.
- Edit the
Bucket Policy
tab and paste the code below:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadWriteAccess",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:PutObject",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::http-project-management-items-bucket",
"arn:aws:s3:::http-project-management-items-bucket/*"
]
}
]
}
- Click on the Save changes
Create Lambda functions for the back end
You will create Lambda functions
for the API
backend to handle CRUD
operations for the Project and Client details DynamoDB
. The functions use API Gateway
events to interact with DynamoDB
, determining the appropriate actions.
To create a Lambda function for the Project details, follow these steps:
-
Sign in to the Lambda console at https://console.aws.amazon.com/lambda.
-
Choose Create function.
-
Enter
http-project-management-lambda-function
as the function name. -
Under Permissions, select Change default execution role.
-
Choose Create a new role from AWS policy templates.
-
Enter
http-crud-tutorial-role
as the role name. -
Choose Simple microservice permissions,
-
Choose Create function.
-
Open the console’s code editor and replace the contents of
index.mjs
with the provided code. -
Choose Deploy to update your function.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
PutCommand,
UpdateCommand,
GetCommand,
DeleteCommand,
ScanCommand,
} from "@aws-sdk/lib-dynamodb";
const dynamoDBClient = new DynamoDBClient({});
const client = new DynamoDBClient({});
const dynamoDBDocumentClient = DynamoDBDocumentClient.from(dynamoDBClient);
const dynamo = DynamoDBDocumentClient.from(client);
const tableName = "http-project-management-items";
export const handler = async (event, context) => {
let body;
let statusCode = 200;
const headers = {
"Content-Type": "application/json",
};
try {
switch (event.routeKey) {
case "PUT /items":
if (!event.body) {
throw new Error("Request body is missing.");
}
const requestJSON = JSON.parse(event.body);
await dynamoDBDocumentClient.send(
new PutCommand({
TableName: tableName,
Item: {
id: requestJSON.id,
projectName: requestJSON.projectName,
projectStatus: requestJSON.projectStatus,
},
})
);
body = "Item added successfully";
break;
case "PUT /items/{id}":
if (!event.body) {
throw new Error("Request body is missing.");
}
const id = event.pathParameters.id;
const updateRequestJSON = JSON.parse(event.body);
await dynamoDBDocumentClient.send(
new UpdateCommand({
TableName: tableName,
Key: { id },
UpdateExpression:
"SET projectName = :projectName, projectStatus = :projectStatus",
ExpressionAttributeValues: {
":projectName": updateRequestJSON.projectName,
":id": updateRequestJSON.id,
":projectStatus": updateRequestJSON.projectStatus,
},
})
);
body = "Item updated successfully";
break;
case "GET /items":
body = await dynamo.send(new ScanCommand({ TableName: tableName }));
body = body.Items;
break;
case "GET /items/{id}":
body = await dynamo.send(
new GetCommand({
TableName: tableName,
Key: {
id: event.pathParameters.id,
},
})
);
body = body.Item;
break;
case "DELETE /items/{id}":
await dynamo.send(
new DeleteCommand({
TableName: tableName,
Key: {
id: event.pathParameters.id,
},
})
);
body = `Deleted item ${event.pathParameters.id}`;
break;
default:
throw new Error(`Unsupported route: "${event.routeKey}"`);
}
} catch (err) {
statusCode = 400;
body = err.message;
} finally {
if (typeof body !== "string") {
body = JSON.stringify(body);
}
}
return {
statusCode,
body,
headers,
};
};
Next, create a Lambda function
for the Client’s details by following the same steps above but change the function name to http-client-management-lambda-function
and use the Client’s table name you created above. Also, write the CRUD
logic for PUT /clients
, GET /clients
, PUT /clients/{id}
, and DELETE /clients/{id}
.
Create an HTTP API
To create an HTTP API
, follow these steps:
- Sign in to the
API Gateway
console at https://console.aws.amazon.com/apigateway. - Choose Create
API
and select Build forHTTP API
. - Enter
http-project-management-api
as theAPI
name. - Proceed to the next step without configuring routes.
- Review the automatically created stage by
API Gateway
and proceed to the next step. - Choose
Create
to create theHTTP API
.
Next, create an HTTP API
for the Client details by following the same steps above but change the API name to http-client-management-api
Create routes
To create routes for your API
, follow these steps:
- Sign in to the
API Gateway
console at https://console.aws.amazon.com/apigateway. - Select your
API
from the available options. - Navigate to the Routes section.
- Choose Create to add a new route.
- For the method, select
GET.
- Enter
/items/{id}
as the path. The{id}
is a path parameterAPI
Gateway extracts from the request path. - Click Create to create the route.
- Repeat steps 4-7 for the routes:
GET /items,
DELETE /items/{id},
andPUT /items.
Next, Follow the same steps to create routes for the Client’s API.
Create integrations for your API
To create integrations for your API
routes, follow these steps:
- Sign in to the
API Gateway
console at https://console.aws.amazon.com/apigateway. - Select your
API
from the available options. - Go to the
Integrations
section. - Choose Manage integrations and Create to create a new integration.
- Skip the step to attach the integration to a route for now. You will complete that in a later step.
- For the integration type, select
Lambda function.
- Enter
http-project-management-lambda-function
as the Lambda function. Click Create to create the integration.
Next, Follow the same steps to create Integrations for the Client’s API
, but remember to use the correct function name you created earlier.
Attach your integration to routes
To attach integrations to your API
routes, follow these steps:
- Sign in to the
API Gateway
console at https://console.aws.amazon.com/apigateway. - Select your
API
from the available options. - Go to the Integrations section.
- Choose a specific route you want to attach an integration to.
- Under Choose an existing integration, select
http-project-management-lambda-function.
- Click Attach integration to link the integration to the route.
- Repeat steps 4-6 for all your `API routes.
- Verify that all routes indicate an attached
AWS Lambda
integration.
Next, Follow the same steps to attach Integrations for the Client’s API
, but remember to use the correct function name you created earlier.
Deal with Cross-Origin Resource Sharing (CORS)
CORS allows resources from different domains to be loaded by browsers.
To configure CORS, follow these steps:
- Sign in to the
API Gateway
console at https://console.aws.amazon.com/apigateway. - Select your
API
from the available options. - Go to the
CORS
section. - Specify the following parameters in a
CORS
configuration:
Access-Control-Allow-Origin: *, http://localhost:3000
Access-Control-Allow-Headers: Authorization, *
Access-Control-Allow-Methods: GET, POST, DELETE, PUT, *
Access-Control-Expose-Headers: Date, x-api-id
Access-Control-Max-Age: 300
- Click Save to configure your
CORS
.
Test your API
To ensure the functionality of your API
, you can use the following steps:
-
Sign in to the API Gateway console at https://console.aws.amazon.com/apigateway.
-
Select your
API
from the available options. -
Note your API’s invoke URL, which can be found under
Invoke URL
on the Details page. -
Copy the entire invoke URL, such as https://abcdef123.execute-api.us-east-2.amazonaws.com.
To create or update an item: Execute the following command to create or update an item. The command includes a request body that contains the item's ID
, project name
, client name
, gender
, client image
, and project status
.
curl --location --request PUT 'https://xxxxx.execute-api.us-east-1.amazonaws.com/items' \
--header 'Content-Type: application/json' \
--data '{
"id": "1",
"projectName": "Project one",
"clientName": "Dami",
"gender": "male",
"ClientImage": "base_64_image",
"projectStatus": "Pending"
}'
To get all items:
Use the following command to list all items.
curl --location 'https://xxxxx.execute-api.us-east-1.amazonaws.com/items' \
--data ''
To delete an item Use the following command to list all items.
curl --location --request DELETE 'https://xxxxx.execute-api.us-east-1.amazonaws.com/items/1' \
--data ''
Building the Application Frontend
To build the Application Frontend, we’ll use the NextJS framework; follow these steps:
- Navigate to the desired directory in your terminal.
- Run the following command to create a NextJS project:
npx create-next-app project-mgt-app && cd project-mgt-app
The command creates a project called project-mgt-app
and navigates into the project directory.
Installing dependencies
React-icon is a powerful library that enables you to effortlessly incorporate icons from various icon libraries into your React application. To use it, run the command below in your terminal.
npm i react-icons
Utility Directory
Create a folder named ’ util ’ in the src/app directory
. This directory will contain the functions to add client Projects and Client detail.
Adding and editing client functions
In the src/app/util
directory, create the file src/app/util/Add&editClientfunctions.js
and add the following code:
export const handleClientSubmit = async ({
event,
setFormData,
formData,
setAdd,
attachment,
router,
}) => {
event.preventDefault();
// ...
fetch("/addclient", requestOptions)
.then((response) => response.text())
.then((result) => {
setAdd(false);
// ...
})
.catch((error) => console.log("error", error));
};
export const handleClientEdit = async ({
event,
setFormData,
formData,
setAdd,
attachment,
router,
searchParams,
}) => {
event.preventDefault();
// ...
fetch(`/editclient/${searchParams.get("cid")}`, requestOptions)
.then((response) => response.text())
.then((result) => {
setAdd(false);
// ...
})
.catch((error) => console.log("error", error));
};
This code above consists of two functions, handleClientSubmit
and handleClientEdit
. Client submission is handled by handleClientSubmit, and client editing is handled by handleClientEdit
.
Adding and editing Project functions
In the src/app/util
directory, create the file src/app/util/Add&editProjectfunction.js
and add the following code:
export const handleProjectSubmit = async ({
event,
formData,
setFormData,
setAdd,
router,
}) => {
event.preventDefault();
// ...
fetch("/additem", requestOptions)
.then((response) => response.text())
.then((result) => {
setAdd(false);
// ...
})
.catch((error) => console.log("error", error));
};
export const handleProjectEdit = async ({
event,
formData,
setFormData,
router,
setEdit,
}) => {
event.preventDefault();
// ...
fetch(`/editproject`, requestOptions)
.then((response) => response.text())
.then((result) => {
// ...
})
.catch((error) => console.log("error", error));
};
This code above consists of two functions, handleProjectSubmit
and handleProjectEdit
. Client submission is handled by handleProjectSubmit
, and client editing is handled by handleProjectEdit
.
Components Directory
Create a folder named ’ components ’ in the src/app
directory. The directory will contain all the reusable components for the projects.
Form Component
In the src/app/Forms
directory, create the file src/app/Forms/ClientForm.js
and add the following code:
{/* ... */}
<form
className="max-w-lg mx-auto"
onSubmit={searchParams.get("q") ? handleEdit : handleSubmit}
>
<div className="grid grid-cols-1 gap-6 mt-4 sm:grid-cols-2">
<div>
<label
htmlFor="emailAddress"
className="text-gray-700 dark:text-gray-200"
>
Client Name
</label>
<input
required
id="clientName"
type="text"
name="clientName"
value={formData.clientName}
onChange={handleChange}
className="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200
rounded-md dark:bg-gray-800 "
/>
</div>
<div>
<label
htmlFor="clientimage"
className="text-gray-700 dark:text-gray-200"
>
Client Logo
</label>
<input
required
id="clientimage"
type="file"
accept="image/*"
name="clientImage"
onChange={(e) => {
convertToBase64(e.target.files[0]);
}}
className="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200"
/>
</div>
<div className="mb-4">
<label
htmlFor="gender"
className="block mb-2 text-gray-700 dark:text-gray-200"
>
Client Gender:
</label>
<div className="flex flex-wrap items-center">
<label
htmlFor="male"
className="text-gray-700 dark:text-gray-200 mr-4 mb-2 sm:mb-0 sm:flex-grow"
>
<input
type="checkbox"
id="male"
name="gender"
value="male"
checked={formData.gender.includes("male")}
onChange={handleChange}
className="mr-2 text-blue-500"
/>{" "}
Male
</label>
<label
htmlFor="female"
className="text-gray-700 dark:text-gray-200 sm:flex-grow"
>
<input
type="checkbox"
id="female"
name="gender"
value="female"
checked={formData.gender.includes("female")}
onChange={handleChange}
className="mr-2 text-blue-500"
/>{" "}
Female
</label>
</div>
</div>
<div>
<label
htmlFor="clientId"
className="text-gray-700 dark:text-gray-200"
>
Client ID
</label>
<input
required
id="clientId"
type="number"
name="clientId"
value={
searchParams.get("q")
? Number(searchParams.get("cid"))
: formData.clientId
}
onChange={handleChange}
className="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200
rounded-md dark:bg-gray-800 "
/>
</div>
</div>
<div className="flex justify-end mt-6">
<button
type="submit"
className="flex items-center justify-center px-4 py-2 bg-blue-200 text-blue-800
rounded-md hover:bg-blue-300 focus:outline-none focus:bg-blue-300"
>
{searchParams.get("q")
? `${Edit ? "loading..." : "Update"}`
: `${Add ? "loading..." : "Save"}`}
</button>
</div>
</form>
{/* ... */}
The code above is a component (ClientForm
) that renders a form for adding or editing client information. It includes input fields for client name, logo, gender, and client ID and handles form submission using handleSubmit
or handleEdit
functions based on query parameters. It also displays a back button and loading state during the submit action.
Next, In the src/app/Forms
directory, create the file src/app/Forms/ProjectForm.js
and add the following code:
{/* /... */}
<form
className="max-w-lg mx-auto"
onSubmit={localStorage.getItem("id") ? handleEdit : handleSubmit}
>
<div className="grid grid-cols-1 gap-6 mt-4 sm:grid-cols-2">
<div>
<label
htmlFor="username"
className="text-gray-700 dark:text-gray-200"
>
Project Name
</label>
<input
required
id="username"
type="text"
name="projectName"
value={formData?.projectName}
onChange={handleChange}
className="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 "
/>
</div>
<div className="mb-4">
<label
htmlFor="projectStatus"
className="block mb-2 text-gray-700 dark:text-gray-200"
>
Project Status:
</label>
<div className="flex flex-wrap items-center">
<label
htmlFor="pending"
className="text-gray-700 dark:text-gray-200 mr-4 mb-2 sm:mb-0 sm:flex-grow"
>
<input
type="checkbox"
id="pending"
name="projectStatus"
value="pending"
checked={formData.projectStatus.includes("pending")}
onChange={handleChange}
className="mr-2 text-blue-500"
/>
Pending
</label>
<label
htmlFor="active"
className="text-gray-700 dark:text-gray-200 mr-4 mb-2 sm:mb-0 sm:flex-grow"
>
<input
type="checkbox"
id="active"
name="projectStatus"
value="active"
checked={formData.projectStatus.includes("active")}
onChange={handleChange}
className="mr-2 text-blue-500"
/>
Active
</label>
<label
htmlFor="completed"
className="text-gray-700 dark:text-gray-200 sm:flex-grow"
>
<input
type="checkbox"
id="completed"
name="projectStatus"
value="completed"
checked={formData.projectStatus.includes("completed")}
onChange={handleChange}
className="mr-2 text-gray-700 dark:text-gray-200"
/>
Completed
</label>
</div>
</div>
<div>
<label
htmlFor="projectid"
className="text-gray-700 dark:text-gray-200"
>
Project ID
</label>
<input
required
id="projectid"
type="number"
name="projectId"
value={
localStorage.getItem("id") ? Number(id_) : formData.projectId
}
onChange={handleChange}
className="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 "
/>
</div>
</div>
<div className="flex justify-end mt-6">
<button
type="submit"
className="flex items-center justify-center px-4 py-2 bg-blue-200 text-blue-800 rounded-md hover:bg-blue-300 focus:outline-none focus:bg-blue-300"
>
{localStorage.getItem("id")
? `${Edit ? "loading..." : "Update"}`
: `${Add ? "loading..." : "Save"}`}
</button>
</div>
</form>
{/* /... */}
The code above is a component (ProjectForm
) that renders a form for adding or editing project information. It includes input fields for project name, status, and ID and handles form submission using handleSubmit
or handleEdit
functions based on the presence of a value in local storage.
Table Component
In the src/app
directory, create the file src/app/components/Tables/TableBody.js
and add the following code:
function TableBody({ data, handleDelete }) {
return (
<tbody className="bg-white divide-y divide-gray-200 dark:divide-gray-700 dark:bg-gray-900">
{data?.map((item, id) => {
return (
<tr className="hover:bg-[#97D8C4] hover:bg-blue-50 " key={id}>
<td className="px-4 py-4 text-sm text-gray-500 dark:text-gray-300 whitespace-nowrap">
{Number(item?.id)}
</td>
<td className="px-4 py-4 text-sm text-gray-500 dark:text-gray-300 whitespace-nowrap">
{item?.projectName}
</td>
<td className="px-4 py-4 text-sm font-medium text-gray-700 whitespace-nowrap">
<div className="flex items-center gap-x-2">
<div>
<h2 className="font-medium text-gray-800 dark:text-white ">
{item?.clientName}
</h2>
<p className="text-sm font-normal text-gray-600 dark:text-gray-400">
@{item?.clientName?.replace(/\s/g, "")?.toLowerCase()}
</p>
</div>
</div>
</td>
<td className="px-4 py-4 text-sm font-medium text-gray-700 whitespace-nowrap">
<div
className={`inline-flex items-center px-3 py-1 rounded-full gap-x-2 ${
item.projectStatus === "Completed" ||
item.projectStatus === "completed"
? "bg-emerald-100/60 dark:bg-gray-800"
: item.projectStatus === "Active" ||
item.projectStatus === "active"
? "dark:bg-gray-800 bg-blue-100/60"
: "dark:bg-gray-800 bg-pink-100/60"
}`}
>
<span
className={`h-1.5 w-1.5 rounded-full ${
item.projectStatus === "Completed" ||
item.projectStatus === "completed"
? "bg-emerald-500"
: item.projectStatus === "Active" ||
item.projectStatus === "active"
? "bg-blue-500 "
: " bg-pink-500 "
} `}
></span>
<h2
className={`text-sm font-normal ${
item.projectStatus === "Completed" ||
item.projectStatus === "completed"
? "text-emerald-500"
: item.projectStatus === "Active" ||
item.projectStatus === "active"
? "text-blue-500"
: "text-pink-500"
}`}
>
{item?.projectStatus?.charAt(0)?.toUpperCase() +
item?.projectStatus?.slice(1)}
</h2>
</div>
</td>
<td className="px-4 py-4 text-sm text-gray-500 dark:text-gray-300 whitespace-nowrap">
{item?.gender?.charAt(0)?.toUpperCase() + item?.gender?.slice(1)}
</td>
<td className="px-4 py-4 text-sm font-medium text-gray-700 whitespace-nowrap">
<img
className="object-cover w-10 h-10 rounded-full"
src={`https://http-project-management-items-bucket.s3.amazonaws.com/${item?.ClientImage}`}
alt=""
/>
</td>
<td className="px-4 py-4 text-sm whitespace-nowrap">
<div className="flex items-center gap-x-6">
<Link
href={{
pathname: "/newclient",
query: { q: `${item?.id}`, cid: `${item?.client_id}` },
}}
className="text-gray-500 transition-colors duration-200 dark:hover:text-yellow-500 dark:text-gray-300 hover:text-yellow-500 focus:outline-none"
>
<LuEdit className="text-blue-700" />
</Link>
<button
onClick={() => handleDelete(item)}
className="text-gray-500 transition-colors duration-200 dark:hover:text-red-500 dark:text-gray-300 hover:text-red-500 focus:outline-none"
>
<RiDeleteBin5Line className="text-blue-700" />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
);
}
export default TableBody;
The code above is a TableBody
component that renders a table’s body section. It receives data and a handleDelete function as props and maps over the data array to generate table rows with specific information. It includes editing and deleting buttons for each row.
Next, In the src/app
directory, create the file src/app/components/Tables/TableComponent.js
and add the following code:
import React from "react";
import TableBody from "./TableBody";
function TableComponent({ data, handleDelete }) {
return (
<div className="flex flex-col mt-6">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th
scope="col"
className="py-3.5 px-4 text-sm font-normal text-left rtl:text-right text-gray-500 "
>
<div className="flex items-center gap-x-3">
<span>ID</span>
</div>
</th>
<th
scope="col"
className="px-4 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-500 "
>
Project Name
</th>
<th
scope="col"
className="py-3.5 px-4 text-sm font-normal text-left rtl:text-right text-gray-500 "
>
<div className="flex items-center gap-x-3">
<span>Client Name</span>
</div>
</th>
<th
scope="col"
className="px-6 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-500 "
>
<button className="flex items-center gap-x-2">
<span>Project Status</span>
</button>
</th>
<th
scope="col"
className="px-4 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-500 "
>
Gender
</th>
<th
scope="col"
className="py-3.5 px-4 text-sm font-normal text-left rtl:text-right text-gray-500 "
>
<div className="flex items-center gap-x-3">
<span>Client Logo</span>
</div>
</th>
<th scope="col" className="relative py-3.5 px-4">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<TableBody data={data} handleDelete={handleDelete} />
</table>
</div>
);
}
export default TableComponent;
The code above is a TableComponent
that renders a table with a header and body section. It receives data and a handleDelete function as props. The table header includes columns for ID, Project Name, Client Name, Project Status, Gender, Client Logo, and an empty column for editing. The body section is rendered using the TableBody
component, passing the data and handleDelete props.
Next, In the src/app directory
, create the file src/app/components/Tables/Index.jsx
and add the following code:
"use client";
import { RiAddLine } from "react-icons/ri";
import { useRouter } from "next/navigation";
import TableComponent from "./TableComponent";
function Table({ data }) {
const router = useRouter();
const handleClick = (e) => {
e.preventDefault();
router.push("/newclient");
};
const handleDelete = async (data) => {
try {
const requestOptions = {
method: "DELETE",
redirect: "follow",
};
const deleteProjectPromise = fetch(
`/deleteproject/${data?.id}`,
requestOptions
);
const deleteClientPromise = fetch(
`/deleteclient/${data?.client_id}`,
requestOptions
);
await Promise.all([deleteProjectPromise, deleteClientPromise]);
window.location.reload();
} catch (error) {
console.log("error", error);
}
};
return (
<div>
<section className="container px-4 mx-auto ">
<div className="flex justify-between items-center ">
<div className="flex items-center gap-x-3">
<h2 className="text-lg font-medium text-gray-800 dark:text-white">
Total projects
</h2>
<span className="px-3 py-1 text-xs text-blue-600 bg-blue-100 rounded-full dark:bg-gray-800 dark:text-blue-400">
{data?.length}
</span>
</div>
<div>
<button
onClick={handleClick}
className="flex items-center justify-center px-4 py-2 bg-blue-200 text-blue-800
rounded-md hover:bg-blue-300 focus:outline-none focus:bg-blue-300"
>
<RiAddLine className="mr-2" />
New Project
</button>
</div>
</div>
<TableComponent handleDelete={handleDelete} data={data} />
</section>
</div>
);
}
export default Table;
The Table
component renders a table section with project data. It provides functionality to add new projects and delete existing ones. It also causes the TableComponent
component to pass the handleDelete
function and the data array as props.
Add a project
In the src/app
directory, create a folder named newproject
. The folder will contain the page to add a new project.
In the src/app/addproject
directory, create the file src/app/addproject/page.js
and add the following code:
"use client";
import React, { useState } from "react";
import { useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import ProjectForm from "../../components/Forms/ProjectForm";
import {
handleProjectSubmit,
handleProjectEdit,
} from "../../util/Add&editProjectfunction";
function Index() {
const router = useRouter();
const searchParams = useSearchParams();
const [attachment, setattachment] = useState("");
const [add, setAdd] = useState(false);
const [edit, setEdit] = useState(false);
const [file, setfile] = useState("");
const [formData, setFormData] = useState({
projectName: "",
projectStatus: [],
projectId: 0,
});
const handleChange = (event) => {
const { name, value, type, checked } = event.target;
if (type === "checkbox") {
const updatedValues = [...formData[name]];
if (checked) {
updatedValues.push(value);
} else {
const index = updatedValues.indexOf(value);
if (index > -1) {
updatedValues.splice(index, 1);
}
}
setFormData((prevData) => ({ ...prevData, [name]: updatedValues }));
} else if (type === "file") {
const file = event.target.files[0];
setFormData((prevData) => ({ ...prevData, [name]: file }));
} else {
setFormData((prevData) => ({ ...prevData, [name]: value }));
}
};
const convert_to_base64 = async (file) => {
setfile(file);
const file_reader = new FileReader();
file_reader.readAsDataURL(file);
file_reader.onload = () => {
setattachment(file_reader?.result?.split(",")[1]);
};
};
return (
<>
<div className="text-center mb-8 bg-[#DBEAFE] py-4">
<h1 className="text-3xl font-bold text-blue-700 mb-4">
Project Management System
</h1>
</div>
<ProjectForm
id_={localStorage.getItem("id")}
searchParams={searchParams}
handleEdit={(event) =>
handleProjectEdit({
event,
setEdit,
setFormData,
formData,
setAdd,
attachment,
router,
})
}
handleChange={handleChange}
handleSubmit={(event) =>
handleProjectSubmit({
event,
setFormData,
formData,
setAdd,
attachment,
router,
})
}
Add={add}
formData={formData}
convertToBase64={convert_to_base64}
Edit={edit}
Router={() => router.back()}
/>
</>
);
}
export default Index;
The Index
component represents the main page of a Project Management System. It includes a project form for adding and editing projects and functionality for handling form submissions and file attachments.
Add client page
In the src/app/newclient directory
, create the file src/app/newclient/page.js
and add the following code:
"use client";
import React, { useState } from "react";
import { useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import ClientForm from "../../components/Forms/ClientForm";
import {
handleClientSubmit,
handleClientEdit,
} from "../../util/Add&editClientfunctions";
function NewClient() {
const router = useRouter();
const searchParams = useSearchParams();
const [attachment, setattachment] = useState("");
const [add, setAdd] = useState(false);
const [file, setfile] = useState("");
const [formData, setFormData] = useState({
clientName: "",
gender: [],
clientId: 0,
clientImage: null,
});
const handleChange = (event) => {
const { name, value, type, checked } = event.target;
if (type === "checkbox") {
const updatedValues = [...formData[name]];
if (checked) {
updatedValues.push(value);
} else {
const index = updatedValues.indexOf(value);
if (index > -1) {
updatedValues.splice(index, 1);
}
}
setFormData((prevData) => ({ ...prevData, [name]: updatedValues }));
} else if (type === "file") {
const file = event.target.files[0];
setFormData((prevData) => ({ ...prevData, [name]: file }));
} else {
setFormData((prevData) => ({ ...prevData, [name]: value }));
}
};
const convert_to_base64 = async (file) => {
setfile(file);
const file_reader = new FileReader();
file_reader.readAsDataURL(file);
file_reader.onload = () => {
setattachment(file_reader?.result?.split(",")[1]);
};
};
return (
<>
<div className="text-center mb-8 bg-[#DBEAFE] py-4">
<h1 className="text-3xl font-bold text-blue-700 mb-4">
Project Management System
</h1>
</div>
<ClientForm
searchParams={searchParams}
handleEdit={() =>
handleClientEdit({
event,
setFormData,
formData,
setAdd,
attachment,
router,
searchParams,
})
}
Edit={add}
handleChange={handleChange}
handleSubmit={(event) =>
handleClientSubmit({
event,
setFormData,
formData,
setAdd,
attachment,
router,
})
}
Add={add}
formData={formData}
convertToBase64={convert_to_base64}
Router={() => router.back()}
/>
</>
);
}
export default NewClient;
URL Rewrites for API Integration
In the next.config.js
file, add the following code:
module.exports = () => {
const rewrites = () => {
return [
{
source: "/additem",
destination:
"https://xxxxx.execute-api.us-east-1.amazonaws.com/items",
},
{
source: "/getproject",
destination:
"https://xxxxx.execute-api.us-east-1.amazonaws.com/items",
},
{
source: "/editproject",
destination:
"https://xxxxx.execute-api.us-east-1.amazonaws.com/items",
},
{
source: "/deleteproject/:id",
destination:
"https://xxxxx.execute-api.us-east-1.amazonaws.com/items/:id",
},
{
source: "/deleteclient/:id",
destination:
"https://xxxxx.execute-api.us-east-1.amazonaws.com/clients/:id",
},
{
source: "/addclient",
destination:
"https://xxxxx.execute-api.us-east-1.amazonaws.com/clients",
},
{
source: "/editclient/:id",
destination:
"https://xxxxx.execute-api.us-east-1.amazonaws.com/clients/:id",
},
{
source: "/getclient",
destination:
"https://xxxxx.execute-api.us-east-1.amazonaws.com/clients",
},
];
};
return {
rewrites,
};
};
Note: Be sure to use your actual endpoint in the destination.
The provided code above snippet showcases a solution for handling Cross-Origin Resource Sharing (CORS) in a Next.js application. CORS is a security mechanism that restricts cross-origin HTTP requests. By configuring URL rewrites, the code enables communication with an external API by rewriting the request URLs and addressing the CORS issue.
Home Page
In the src/app/page.js
file, add the following code:
"use client";
import React, { useState, useEffect } from "react";
import Table from "./components/Tables/Index";
export default function page() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
localStorage.clear();
const fetchData = async () => {
try {
const response1 = await fetch("/getproject", { method: "GET" });
const response2 = await fetch("/getclient", { method: "GET" });
if (!response1.ok || !response2.ok) {
throw new Error("Failed to fetch data");
}
const data1 = await response1.json();
const data2 = await response2.json();
const combinedData = data1.map((item1, index) => {
const item2 = data2[index];
return {
projectName: item1?.projectName,
projectStatus: item1?.projectStatus,
id: item1?.id,
clientName: item2?.clientName,
ClientImage: item2?.ClientImage,
gender: item2?.gender,
client_id: item2?.id,
};
});
setData(combinedData);
setLoading(false);
} catch (error) {
console.error(error);
setLoading(false);
}
};
fetchData();
}, []);
return (
<>
<div className="text-center mb-8 bg-[#DBEAFE] py-4">
<h1 className="text-3xl font-bold text-blue-700 mb-4">
Project Management System
</h1>
</div>
<main className="flex min-h-screen flex-col items-center justify-between p-24">
{loading ? (
<p>Loading...</p>
) : (
<div>
<Table data={data} />
</div>
)}
</main>
</>
);
}
Testing and Deployment
Follow the steps below to test your Project Management Application:
Navigate into your project-mgt-app
directory and run:
npm run dev
Your Project Management Application is now running at localhost:8000.
To view your homepage, visit localhost:8000 in your browser. You should see the homepage.
You can click on the New Client Button to add a new project.
Also, you can click on the edit and delete icons to make changes to your project.
Deployment
To deploy your application, you will use AWS Amplify
Hosting.
AWS Amplify Hosting - the ultimate solution for fast, secure, and scalable web app deployment. Whether you’re building a static or server-side rendered app, a mobile app landing page, or a web app, Amplify Hosting’s fully managed Continuous Integration and Continuous Deployment (CI/CD)
service has everything you need.
You can quickly deploy web content with support for modern web frameworks like React
, Angular
, Vue
, Next.js
, Gatsby
, Hugo
, Jekyll
, and more.
Follow the steps below to deploy your Project Management Application:
- Push your React app to a
Git
provider - In the AWS Amplify Hosting, click the Host web app button
- Choose your Git provider (in this case, Github) and click on the
Connect branch
button. - Authorize and install AWS Amplify to access your repositories.
- Click the
Next
button to redirect you to a page where you can configure your build settings. - Click on Next. On the
Review
page, select Save and deploy
Your app will be created, and you will be taken to the app’s page in the Amplify Console.
After the deployment phase is marked complete, you can now view your app.
Conclusion
Serverless applications have gained popularity due to their appealing benefits:
- No server management is required.
- Automatic scalability and high availability.
- Cost optimization by paying only for utilized resources and usage duration.
In this tutorial, you learned how to use AWS's API Gateway
and Lambda functions
to build a REST API
that performs CRUD
operations on an AWS DynamoDB
database and utilizes AWS S3
Storage. Furthermore, you discovered how to host the API
on AWS Amplify
Hosting.
If you find AWS fascinating, there are other captivating projects you can explore, such as:
- Implementing user authentication with
Amazon Cognito
. - Simplifying the management and deployment of serverless applications using frameworks like Serverless or services like
AWS CloudFormation
.