April 21, 2023
Social Networking Application using React.js and Nest.js. Part-6 Add Friends.
This article will be about adding friends. The functionality will be minimal. I’ll continue chat functionality after the completion of…

By Fahad Ali
15 min read
This article will be about adding friends. The functionality will be minimal. I'll continue chat functionality after the completion of friends. It will be a lot easier.
Add Friend Backend API:
Create a friends module inside your backend folder.
nest g mo friendsnest g mo friendsAlso, create a controller and a service.
nest g co friends
nest g s friendsnest g co friends
nest g s friendsInside your friends.controller.ts file paste the following code.
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from 'src/users/auth.guard';
import { JwtPayload } from 'src/users/constants';
import { UserDecorator } from 'src/users/users.decorator';
import { FriendsService } from './friends.service';
@UseGuards(AuthGuard)
@Controller('friends')
export class FriendsController {
constructor(
private friendsService: FriendsService
) { }
@Post("new")
addNewFriend(@UserDecorator() user: JwtPayload, @Body() payload: { userId: string }) {
return this.friendsService.addFriend(user.id, payload.userId);
}
@Get("all")
getAllFriends(@UserDecorator() user: JwtPayload) {
return this.friendsService.allFriends(user.id);
}
@Get("non-friends")
getListOfNonFriends(@UserDecorator() user: JwtPayload) {
return this.friendsService.listOfNonFriends(user.id);
}
@Post("remove")
removeFriend(@UserDecorator() user: JwtPayload, @Body() payload: { userId: string }) {
return this.friendsService.removeFriend(user.id, payload.userId);
}
}
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from 'src/users/auth.guard';
import { JwtPayload } from 'src/users/constants';
import { UserDecorator } from 'src/users/users.decorator';
import { FriendsService } from './friends.service';
@UseGuards(AuthGuard)
@Controller('friends')
export class FriendsController {
constructor(
private friendsService: FriendsService
) { }
@Post("new")
addNewFriend(@UserDecorator() user: JwtPayload, @Body() payload: { userId: string }) {
return this.friendsService.addFriend(user.id, payload.userId);
}
@Get("all")
getAllFriends(@UserDecorator() user: JwtPayload) {
return this.friendsService.allFriends(user.id);
}
@Get("non-friends")
getListOfNonFriends(@UserDecorator() user: JwtPayload) {
return this.friendsService.listOfNonFriends(user.id);
}
@Post("remove")
removeFriend(@UserDecorator() user: JwtPayload, @Body() payload: { userId: string }) {
return this.friendsService.removeFriend(user.id, payload.userId);
}
}
- The
FriendsControllerclass is decorated with@Controller('friends'), which sets the base path for all of its endpoints to/friends. - The controller has several endpoints, each of which is decorated with a method decorator (
@Postor@Get) to specify the HTTP method for the route. - All endpoints are also decorated with
@UseGuards(AuthGuard), which applies an authentication guard to each endpoint to ensure that only authenticated users can access them. - The
addNewFriendandremoveFriendendpoints are bothPOSTroutes that accept a JSON payload in the request body. These endpoints call methods in theFriendsServiceclass to add or remove friends for the authenticated user. - The
getAllFriendsandgetListOfNonFriendsendpoints areGETroutes that return data to the client. These endpoints call methods in theFriendsServiceclass to retrieve a list of all the authenticated user's friends or non-friends, respectively. - The
@UserDecorator()decorator on the function parameters is a custom decorator that retrieves the authenticated user's information from the request and injects it into the function as a parameter.
AuthGuard:
Inside users module, create a file named auth.guard.ts.
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';
import { jwtConstants } from './constants';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {
}
async canActivate(
context: ExecutionContext,
): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(
token,
{
secret: jwtConstants.secret
}
);
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers['authorization']?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';
import { jwtConstants } from './constants';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {
}
async canActivate(
context: ExecutionContext,
): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(
token,
{
secret: jwtConstants.secret
}
);
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers['authorization']?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}- This code checks if an incoming request has a valid JSON Web Token (JWT) attached to it.
- The
AuthGuardclass implements theCanActivateinterface from the@nestjs/commonmodule, which contains thecanActivate()method that determines whether a request should be allowed or rejected based on certain conditions. In this case, thecanActivate()method checks for the presence of a JWT token in the HTTP headers of the incoming request and validates it using theJwtServicefrom the@nestjs/jwtmodule. - The
AuthGuardclass constructor injects an instance of theJwtServicethat is used to verify the JWT token. ThecanActivate()method receives theExecutionContextobject that contains the current request object. The method first extracts the JWT token from theAuthorizationheader of the request using theextractTokenFromHeader()method, and throws anUnauthorizedExceptionif no token is found. - If a token is found, the method then verifies the token using the
verifyAsync()method of theJwtServiceby passing the token and a secret key defined in thejwtConstantsmodule. If the token is successfully verified, thepayloadof the token is assigned to theuserproperty of the request object, which can then be accessed in the route handlers. If the token cannot be verified, the method throws anUnauthorizedException. - In summary, the
AuthGuardclass is a middleware that checks if an incoming request is authorized to access a protected route by validating the attached JWT token.
UserDecorator:
Inside the users module, create a file named users.decorator.ts.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const UserDecorator = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},);import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const UserDecorator = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},);- In NestJS, a param decorator is a function that is used to inject some value into a route handler method parameter. In this specific case, the
UserDecoratoris used to inject the user object into a route handler method parameter. - The decorator is created using the
createParamDecoratorfunction from the@nestjs/commonpackage. It takes two parameters: data: an optional argument that can be passed to the decorator.ctx: anExecutionContextobject that contains information about the current execution context of the request.
The UserDecorator function returns the request.user object, which is the user object that was set in the request object during the authentication process.
FriendsService:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from 'src/users/user.schema';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class FriendsService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
private userService: UsersService
) { }
async addFriend(userId: string, friendId: string) {
const user = await this.userModel.findById(userId);
const friend = await this.userModel.findById(friendId);
user.friends.push(friend.id);
friend.friends.push(user.id);
await user.save();
await friend.save();
return {
message: `${friend.email} has been added to your profile`,
friendId
}
}
async allFriends(userId: string) {
const user = (await this.userService.getUserById(userId));
return (await user.populate("friends")).friends;
}
async removeFriend(userId: string, friendId: string) {
const user = await this.userModel.findByIdAndUpdate(userId, { $pull: { friends: friendId } }, { new: true });
const friend = await this.userModel.findByIdAndUpdate(friendId, { $pull: { friends: userId } }, { new: true });
return { message: `${friend.email} has been removed from your profile`, friendId }
}
async listOfNonFriends(userId: string) {
const users = await this.userModel.find({ _id: { $ne: userId } });
const friends = await this.userModel.findById(userId);
return users.filter(user => !friends.friends.includes(user._id))
}
}import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from 'src/users/user.schema';
import { UsersService } from 'src/users/users.service';
@Injectable()
export class FriendsService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
private userService: UsersService
) { }
async addFriend(userId: string, friendId: string) {
const user = await this.userModel.findById(userId);
const friend = await this.userModel.findById(friendId);
user.friends.push(friend.id);
friend.friends.push(user.id);
await user.save();
await friend.save();
return {
message: `${friend.email} has been added to your profile`,
friendId
}
}
async allFriends(userId: string) {
const user = (await this.userService.getUserById(userId));
return (await user.populate("friends")).friends;
}
async removeFriend(userId: string, friendId: string) {
const user = await this.userModel.findByIdAndUpdate(userId, { $pull: { friends: friendId } }, { new: true });
const friend = await this.userModel.findByIdAndUpdate(friendId, { $pull: { friends: userId } }, { new: true });
return { message: `${friend.email} has been removed from your profile`, friendId }
}
async listOfNonFriends(userId: string) {
const users = await this.userModel.find({ _id: { $ne: userId } });
const friends = await this.userModel.findById(userId);
return users.filter(user => !friends.friends.includes(user._id))
}
}- The service is decorated with the
@Injectable()decorator to make it injectable using dependency injection. It has several methods for managing the friends of a user. - The constructor of the service takes two parameters,
@InjectModel(User.name) private userModel: Model<UserDocument>andprivate userService: UsersService. The@InjectModel()decorator is used to inject theUsermodel defined in a Mongoose schema. TheuserServiceis also injected to retrieve a user by ID. - The
addFriend(userId: string, friendId: string)method adds a friend to a user's profile. It finds the user and friend by their IDs, adds the friend's ID to the user'sfriendsarray, and vice versa. It then saves the user and friend and returns a message and the friend's ID. - The
allFriends(userId: string)method returns a list of all the friends of a user. It retrieves the user by ID using theuserServiceand populates thefriendsfield to return the list of friends. - The
removeFriend(userId: string, friendId: string)method removes a friend from a user's profile. It finds the user and friend by their IDs and removes the friend's ID from the user'sfriendsarray and vice versa. It then returns a message and the friend's ID. - The
listOfNonFriends(userId: string)method returns a list of all the users that are not friends of a user. It retrieves all the users except the user with the given ID using theuserModel. It then filters out the users that are already friends of the user.
FriendsModule.ts:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from 'src/users/user.schema';
import { FriendsController } from './friends.controller';
import { FriendsService } from './friends.service';
import { UsersModule } from 'src/users/users.module';
import { UsersService } from 'src/users/users.service';
@Module({
imports: [
MongooseModule.forFeature([
{ name: User.name, schema: UserSchema },
]),
UsersModule
],
controllers: [FriendsController],
providers: [FriendsService, UsersService]
})
export class FriendsModule { }import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from 'src/users/user.schema';
import { FriendsController } from './friends.controller';
import { FriendsService } from './friends.service';
import { UsersModule } from 'src/users/users.module';
import { UsersService } from 'src/users/users.service';
@Module({
imports: [
MongooseModule.forFeature([
{ name: User.name, schema: UserSchema },
]),
UsersModule
],
controllers: [FriendsController],
providers: [FriendsService, UsersService]
})
export class FriendsModule { }- In this module, the following are defined:
MongooseModule.forFeature: This method registers a Mongoose model for a specific feature or module of the application. In this case, it registers theUsermodel from theuser.schema.tsfile to be used by this module.UsersModule: This is another module of the application that is being imported into this module.FriendsController: This is a controller class that handles incoming requests for the/friendsendpoint.FriendsService: This is a service class that provides the business logic for theFriendsController.UsersService: This is another service class that provides the business logic for user-related functionalities and is being used as a dependency inFriendsService.
All of these components are grouped together in this module and can be imported and used in other parts of the application as needed.
UsersService.ts:
The UsersService class has an extra method.
import { BadRequestException, Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { User, UserDocument } from './user.schema';
import { InjectModel } from '@nestjs/mongoose';
import { AuthDto } from './auth.dto';
import { hash } from "bcryptjs";
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name) private userModel: Model<User>
) {
}
async createUser(data: AuthDto) {
try {
const isUserFound = await this.getUserByEmail(data.email);
if (isUserFound) {
throw new BadRequestException("User already exists");
}
const password = await hash(data.password, 10);
const newUser = new this.userModel({ ...data, password });
await newUser.save();
return {
success: true,
message: "User created successfully"
}
} catch (error) {
const err = error as Error;
console.log(err.message);
throw new BadRequestException(err.message)
}
}
async getUserByEmail(email: string) {
return await this.userModel.findOne({ email }).exec()
}
async getUserById(userId: string) {
return await this.userModel.findById(userId).exec();
}
}import { BadRequestException, Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { User, UserDocument } from './user.schema';
import { InjectModel } from '@nestjs/mongoose';
import { AuthDto } from './auth.dto';
import { hash } from "bcryptjs";
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name) private userModel: Model<User>
) {
}
async createUser(data: AuthDto) {
try {
const isUserFound = await this.getUserByEmail(data.email);
if (isUserFound) {
throw new BadRequestException("User already exists");
}
const password = await hash(data.password, 10);
const newUser = new this.userModel({ ...data, password });
await newUser.save();
return {
success: true,
message: "User created successfully"
}
} catch (error) {
const err = error as Error;
console.log(err.message);
throw new BadRequestException(err.message)
}
}
async getUserByEmail(email: string) {
return await this.userModel.findOne({ email }).exec()
}
async getUserById(userId: string) {
return await this.userModel.findById(userId).exec();
}
}- This is a method for a class that retrieves a user by their ID using the Mongoose ODM for MongoDB in Node.js.
- The
getUserByIdmethod takes in auserIdparameter of type string. It then uses thefindByIdmethod from the Mongoose model object (this.userModel) to query the database for the user with the given ID. Theexecmethod is called on the query to execute it and return a promise that resolves to the user document if found, ornullif not found. - The method is marked as
asyncto allow it to useawaitwhen callingfindByIdand wait for the database operation to complete before returning the result. This way, the method returns a promise that resolves to the user document ornull, depending on whether the user with the given ID was found in the database or not.
In order to use UsersService in other modules it should be exported.
Open your users.module.ts file and make a few changes.
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './user.schema';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: "7d" }
})
],
controllers: [UsersController],
providers: [UsersService, AuthService],
exports: [UsersService]
})
export class UsersModule { }import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './user.schema';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: "7d" }
})
],
controllers: [UsersController],
providers: [UsersService, AuthService],
exports: [UsersService]
})
export class UsersModule { }- the
UsersServiceis exported, allowing it to be used in other modules within the application.
UserSchema:
I have updated the user schema to add a friends field.
// user.schema.ts
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";
import * as mongoose from "mongoose";
export type UserDocument = HydratedDocument<User>;
@Schema({
timestamps: true, toJSON: {
transform(doc, ret, options) {
delete ret["password"]
},
}
})
export class User {
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true, })
password: string;
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], default: [], })
friends: mongoose.Types.ObjectId[]
}
export const UserSchema = SchemaFactory.createForClass(User);// user.schema.ts
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";
import * as mongoose from "mongoose";
export type UserDocument = HydratedDocument<User>;
@Schema({
timestamps: true, toJSON: {
transform(doc, ret, options) {
delete ret["password"]
},
}
})
export class User {
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true, })
password: string;
@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], default: [], })
friends: mongoose.Types.ObjectId[]
}
export const UserSchema = SchemaFactory.createForClass(User);friends: an array ofmongoose.Types.ObjectIdthat refers to other users. It has a default value of an empty array [].mongoose.Schema.Types.ObjectIdis a built-in data type in Mongoose used to store MongoDB ObjectIds. An ObjectId is a unique identifier for a MongoDB document that consists of a 12-byte hexadecimal string.
Creating a Contacts Page and connecting it to the API:
Open up your front-end folder and inside your pages folder create a new folder named contacts. Create a new file named index.tsx.
import { AuthLayout } from "@/components/AuthLayout";
import { ContactsPageComponent } from "@/page-components/contacts-page";
import Head from "next/head";
export default function ContactsPage() {
return <AuthLayout>
<Head>
<title>Chats</title>
</Head>
<ContactsPageComponent />
</AuthLayout>
}import { AuthLayout } from "@/components/AuthLayout";
import { ContactsPageComponent } from "@/page-components/contacts-page";
import Head from "next/head";
export default function ContactsPage() {
return <AuthLayout>
<Head>
<title>Chats</title>
</Head>
<ContactsPageComponent />
</AuthLayout>
}- The code exports a Next.js page component that renders the
ContactsPageComponentinside anAuthLayoutcomponent. - The
AuthLayoutcomponent is a layout component that provides common authentication-related functionality, such as rendering a navigation bar, checking if the user is authenticated, and redirecting to the login page if the user is not authenticated. - The
Headcomponent from Next.js is used to modify the document'sheadelement by setting the page title to "Chats". - Overall, this page component renders the
ContactsPageComponentwrapped inside anAuthLayoutwith a modifiedtitleelement in the document'shead.
Connecting an API:
Inside the API folder at the root of your folder, open up the index.ts file to make a few changes.
import { AuthLocalStorage } from '@/types/user.types';
import { AUTH_LOCAL_STORAGE_CONSTANT } from '@/utils/constants';
import { getValueFromLocalStorage } from '@/utils/localstorage';
import axios, { AxiosInstance } from 'axios';
export const axiosInstance: AxiosInstance = axios.create({
baseURL: "http://localhost:3001"
});
export class BaseApi {
protected _axiosInstance: AxiosInstance = axiosInstance;
constructor(protected url: string) {
this._axiosInstance.interceptors.request.use((config) => {
const authData = getValueFromLocalStorage<AuthLocalStorage>(AUTH_LOCAL_STORAGE_CONSTANT);
if (authData) {
config.headers.Authorization = `Bearer ${authData.token}`;
}
return config;
})
}
}import { AuthLocalStorage } from '@/types/user.types';
import { AUTH_LOCAL_STORAGE_CONSTANT } from '@/utils/constants';
import { getValueFromLocalStorage } from '@/utils/localstorage';
import axios, { AxiosInstance } from 'axios';
export const axiosInstance: AxiosInstance = axios.create({
baseURL: "http://localhost:3001"
});
export class BaseApi {
protected _axiosInstance: AxiosInstance = axiosInstance;
constructor(protected url: string) {
this._axiosInstance.interceptors.request.use((config) => {
const authData = getValueFromLocalStorage<AuthLocalStorage>(AUTH_LOCAL_STORAGE_CONSTANT);
if (authData) {
config.headers.Authorization = `Bearer ${authData.token}`;
}
return config;
})
}
}- The code imports
AuthLocalStorageandAUTH_LOCAL_STORAGE_CONSTANTtypes, which are used to read the user's authentication data from local storage.getValueFromLocalStorageis a utility function that retrieves a value from local storage and returns it in the specified type. - Next, it creates an instance of Axios with the
createmethod, specifying a base URL of[http://localhost:4000](http://localhost:4000.). - The
BaseApiclass has a single constructor that takes aurlparameter and sets it as an instance property. It also sets thethis._axiosInstanceproperty to the Axios instance created earlier. - The constructor sets an interceptor on the Axios instance, which intercepts requests before they are sent. The interceptor retrieves the user's authentication data from local storage using
getValueFromLocalStorageand sets theAuthorizationheader of the request to include the user's token. - This
BaseApiclass serves as a base class for other API classes and provides the necessary setup for them to make authenticated requests to the server.
FriendsApi:
Inside the API folder, create a new file named friends.api.ts.
import { IUser } from "@/types/user.types";
import { BaseApi } from ".";
export class FriendsApi extends BaseApi {
async getNonFriends() {
return await this._axiosInstance.get<IUser[]>(`${this.url}/non-friends`);
}
async addFriend(userId: string) {
return await this._axiosInstance.post<{ message: string, friendId: string }>(`${this.url}/new`, { userId })
}
async getAllFriends() {
return await this._axiosInstance.get<IUser[]>(`${this.url}/all`);
}
async removeFriend(userId: string) {
return await this._axiosInstance.post<{ message: string, friendId: string }>(`${this.url}/remove`, { userId })
}
}import { IUser } from "@/types/user.types";
import { BaseApi } from ".";
export class FriendsApi extends BaseApi {
async getNonFriends() {
return await this._axiosInstance.get<IUser[]>(`${this.url}/non-friends`);
}
async addFriend(userId: string) {
return await this._axiosInstance.post<{ message: string, friendId: string }>(`${this.url}/new`, { userId })
}
async getAllFriends() {
return await this._axiosInstance.get<IUser[]>(`${this.url}/all`);
}
async removeFriend(userId: string) {
return await this._axiosInstance.post<{ message: string, friendId: string }>(`${this.url}/remove`, { userId })
}
}- Here's an overview of each method:
getNonFriends(): sends a GET request to the API endpoint/non-friendsand returns a Promise that resolves to an array ofIUserobjects representing non-friends of the current user.addFriend(userId: string): sends a POST request to the API endpoint/newwith the provideduserIdin the request body, and returns a Promise that resolves to an object with amessageand afriendIdproperty indicating the success of the request.getAllFriends(): sends a GET request to the API endpoint/alland returns a Promise that resolves to an array ofIUserobjects representing all friends of the current user.removeFriend(userId: string): sends a POST request to the API endpoint/removewith the provideduserIdin the request body, and returns a Promise that resolves to an object with amessageand afriendIdproperty indicating the success of the request.
IUser Type:
The IUser type file is located at types/user.types.ts.
export type IUser = {
email: string,
_id: string,
friends: string[],
createdAt: string,
updatedAt: string
};
export type IUser = {
email: string,
_id: string,
friends: string[],
createdAt: string,
updatedAt: string
};The code defines a TypeScript interface IUser which describes the shape or structure of an object representing a user. The IUser interface has the following properties:
email: a required string property representing the user's email address._id: a required string property representing the user's ID.friends: an optional array of strings representing the user's friends.createdAt: a required string property representing the date and time the user was created.updatedAt: a required string property representing the date and time the user was last updated.
ContactsPageComponent:
Create a new file inside the page-components/contacts-page/index.tsx.
import { ActionIcon, Avatar, Button, Center, Container, Group, Loader, Modal, ScrollArea, Table, Text, TextInput, Title } from "@mantine/core";
import { IconPlus, IconSearch, IconUserCheck } from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks";
import { FriendsApi } from "@/api/friends.api";
import { useEffect, useState } from "react";
import { IUser } from "@/types/user.types";
import { AxiosError } from "axios";
import { useDispatch, useSelector } from "react-redux";
import { notifications } from "@mantine/notifications";
import { addNewFriendAction, removeFriendAction } from "@/store/auth/auth.slice";
import { IconUserX } from "@tabler/icons-react";
import { RootState } from "@/store";
const friendsApi = new FriendsApi("/friends");
export function ContactsPageComponent() {
const [opened, { open, close }] = useDisclosure(false);
const [loading, setLoading] = useState(false);
const [nonFriends, setNonFriends] = useState<IUser[]>([]);
const [friends, setFriends] = useState<IUser[]>([]);
const authState = useSelector((state: RootState) => state.auth);
const dispatch = useDispatch();
useEffect(() => {
async function getNonFriends() {
try {
const response = await friendsApi.getNonFriends();
console.log(response.data)
setNonFriends(response.data);
} catch (error) {
const err = error as AxiosError;
console.log("Error in Non Friends", err.response?.data)
}
}
getNonFriends();
}, [])
useEffect(() => {
async function getFriends() {
setLoading(true);
try {
const response = await friendsApi.getAllFriends();
console.log(response.data)
setFriends(response.data);
} catch (error) {
const err = error as AxiosError;
console.log("Error in Non Friends", err.response?.data)
} finally {
setLoading(false);
}
}
getFriends();
}, [])
async function addNewFriend(userId: string) {
try {
const response = await friendsApi.addFriend(userId);
dispatch(addNewFriendAction(response.data.friendId));
notifications.show({
title: "Success",
message: response.data.message
});
} catch (error) {
console.log(`Error in ${addNewFriend.name}`, (error as Error).message);
}
}
async function removeFriend(userId: string) {
try {
const response = await friendsApi.removeFriend(userId);
dispatch(removeFriendAction(response.data.friendId));
notifications.show({
title: "Success",
message: response.data.message
});
} catch (error) {
console.log(`Error in ${addNewFriend.name}`, (error as Error).message);
}
}
return <Container size={"lg"} my={"lg"} px={"xs"}>
{/* Display List of Non Friends */}
<Modal opened={opened} onClose={close}
scrollAreaComponent={ScrollArea.Autosize}
size="55%"
>
<Modal.Title>
<Text className="text-xl text-center text-[#495057]">Add Friends</Text>
</Modal.Title>
<Table verticalSpacing="sm">
<tbody>
{nonFriends.map(user => {
return <tr key={user._id}>
<td>
<Group position="center" spacing="sm">
<Avatar size={40} radius={40} >{user.email.slice(0, 2).toUpperCase()}</Avatar>
<div>
<Text fz="md" c="dimmed">
{user.email}
</Text>
</div>
</Group>
</td>
<td>
{authState.user!.friends.includes(user._id) ? <IconUserCheck size={35} className="text-blue-400" /> :
<Button onClick={() => addNewFriend(user._id)} color={"blue"} className={`"bg-blue-400" rounded-full`}>{"Add"}</Button>
}
</td>
</tr>
})}
</tbody>
</Table>
</Modal>
{/* Search Input */}
<Title order={2} color="#495057" lts={"0.09em"} fw={"normal"}>Contacts</Title>
<Group position="center">
<TextInput w={700} placeholder="Search here..."
rightSection={<IconSearch size={"1.2rem"} />}
/>
<ActionIcon onClick={open} variant="transparent">
<IconPlus size="2rem" />
</ActionIcon>
</Group>
{/* List Of Friends */}
<ScrollArea h={500} className="mt-10">
{
loading ? <Center>
<Loader />
</Center>
: <Table miw={800} verticalSpacing="sm">
<tbody>
{
friends.length > 0 ? friends.map(user => {
return <tr key={user._id}>
<td>
<Group position="center" spacing="sm">
<Avatar size={40} radius={40} >{user.email.slice(0, 2).toUpperCase()}</Avatar>
<div>
<Text fz="md" c="dimmed">
{user.email}
</Text>
</div>
</Group>
</td>
<td>
{!authState.user!.friends.includes(user._id) ? <IconUserX size={35} className="text-red-400" /> :
<Group className="space-x-3">
<Button onClick={() => removeFriend(user._id)} color={"red"} className={`bg-red-400 rounded-full`}>
Remove Friend
</Button>
</Group>
}
</td>
</tr>
})
: null
}
</tbody>
</Table>
}
</ScrollArea>
</Container>
}import { ActionIcon, Avatar, Button, Center, Container, Group, Loader, Modal, ScrollArea, Table, Text, TextInput, Title } from "@mantine/core";
import { IconPlus, IconSearch, IconUserCheck } from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks";
import { FriendsApi } from "@/api/friends.api";
import { useEffect, useState } from "react";
import { IUser } from "@/types/user.types";
import { AxiosError } from "axios";
import { useDispatch, useSelector } from "react-redux";
import { notifications } from "@mantine/notifications";
import { addNewFriendAction, removeFriendAction } from "@/store/auth/auth.slice";
import { IconUserX } from "@tabler/icons-react";
import { RootState } from "@/store";
const friendsApi = new FriendsApi("/friends");
export function ContactsPageComponent() {
const [opened, { open, close }] = useDisclosure(false);
const [loading, setLoading] = useState(false);
const [nonFriends, setNonFriends] = useState<IUser[]>([]);
const [friends, setFriends] = useState<IUser[]>([]);
const authState = useSelector((state: RootState) => state.auth);
const dispatch = useDispatch();
useEffect(() => {
async function getNonFriends() {
try {
const response = await friendsApi.getNonFriends();
console.log(response.data)
setNonFriends(response.data);
} catch (error) {
const err = error as AxiosError;
console.log("Error in Non Friends", err.response?.data)
}
}
getNonFriends();
}, [])
useEffect(() => {
async function getFriends() {
setLoading(true);
try {
const response = await friendsApi.getAllFriends();
console.log(response.data)
setFriends(response.data);
} catch (error) {
const err = error as AxiosError;
console.log("Error in Non Friends", err.response?.data)
} finally {
setLoading(false);
}
}
getFriends();
}, [])
async function addNewFriend(userId: string) {
try {
const response = await friendsApi.addFriend(userId);
dispatch(addNewFriendAction(response.data.friendId));
notifications.show({
title: "Success",
message: response.data.message
});
} catch (error) {
console.log(`Error in ${addNewFriend.name}`, (error as Error).message);
}
}
async function removeFriend(userId: string) {
try {
const response = await friendsApi.removeFriend(userId);
dispatch(removeFriendAction(response.data.friendId));
notifications.show({
title: "Success",
message: response.data.message
});
} catch (error) {
console.log(`Error in ${addNewFriend.name}`, (error as Error).message);
}
}
return <Container size={"lg"} my={"lg"} px={"xs"}>
{/* Display List of Non Friends */}
<Modal opened={opened} onClose={close}
scrollAreaComponent={ScrollArea.Autosize}
size="55%"
>
<Modal.Title>
<Text className="text-xl text-center text-[#495057]">Add Friends</Text>
</Modal.Title>
<Table verticalSpacing="sm">
<tbody>
{nonFriends.map(user => {
return <tr key={user._id}>
<td>
<Group position="center" spacing="sm">
<Avatar size={40} radius={40} >{user.email.slice(0, 2).toUpperCase()}</Avatar>
<div>
<Text fz="md" c="dimmed">
{user.email}
</Text>
</div>
</Group>
</td>
<td>
{authState.user!.friends.includes(user._id) ? <IconUserCheck size={35} className="text-blue-400" /> :
<Button onClick={() => addNewFriend(user._id)} color={"blue"} className={`"bg-blue-400" rounded-full`}>{"Add"}</Button>
}
</td>
</tr>
})}
</tbody>
</Table>
</Modal>
{/* Search Input */}
<Title order={2} color="#495057" lts={"0.09em"} fw={"normal"}>Contacts</Title>
<Group position="center">
<TextInput w={700} placeholder="Search here..."
rightSection={<IconSearch size={"1.2rem"} />}
/>
<ActionIcon onClick={open} variant="transparent">
<IconPlus size="2rem" />
</ActionIcon>
</Group>
{/* List Of Friends */}
<ScrollArea h={500} className="mt-10">
{
loading ? <Center>
<Loader />
</Center>
: <Table miw={800} verticalSpacing="sm">
<tbody>
{
friends.length > 0 ? friends.map(user => {
return <tr key={user._id}>
<td>
<Group position="center" spacing="sm">
<Avatar size={40} radius={40} >{user.email.slice(0, 2).toUpperCase()}</Avatar>
<div>
<Text fz="md" c="dimmed">
{user.email}
</Text>
</div>
</Group>
</td>
<td>
{!authState.user!.friends.includes(user._id) ? <IconUserX size={35} className="text-red-400" /> :
<Group className="space-x-3">
<Button onClick={() => removeFriend(user._id)} color={"red"} className={`bg-red-400 rounded-full`}>
Remove Friend
</Button>
</Group>
}
</td>
</tr>
})
: null
}
</tbody>
</Table>
}
</ScrollArea>
</Container>
}- This code defines a React functional component called
ContactsPageComponent. It uses the@mantine/coreand@tabler/icons-reactlibraries for UI components and icons respectively. It also uses@mantine/hooksfor a custom hook calleduseDisclosure. - The component first initializes some state variables using the
useStatehook:opened,loading,nonFriends, andfriends. It also usesuseSelectoranduseDispatchhooks from thereact-reduxlibrary to retrieve and update state from the Redux store. - The component has two
useEffecthooks, one that retrieves a list of non-friends and the other that retrieves a list of friends, using theFriendsApiclass defined in the code. The lists are then set as the state fornonFriendsandfriends. - The component defines two functions,
addNewFriendandremoveFriend, which send requests to the server to add or remove a friend respectively. They then dispatch corresponding Redux actions and display a notification to the user upon success. - The component renders a
Containercomponent from@mantine/core, which contains a modal for adding friends, a search input with a plus icon to add friends, and a table displaying non-friends. TheModalcomponent is opened or closed using theuseDisclosurehook. TheTablecomponent contains atbodyelement, which maps overnonFriendsto create a row for each user. Thetdelements contain aGroupelement with anAvatarandTextcomponent. The secondtdelement contains anIconUserCheckif the user is already a friend or a button to add the user as a friend otherwise. - The component is using the
ScrollAreacomponent from@mantine/coreto provide a scrollbar when there are many friends in the list. Theloadingvariable is a boolean that determines whether the component is still fetching the list of friends, and if so, it displays a loading spinner from theLoadercomponent. Once the list of friends is loaded, the component uses aTablecomponent from the same library to display the list of friends. - Each row in the table displays an avatar, the user's email address, and a button to remove them as a friend. The email address is displayed in a
Textcomponent with a medium font size and a "dimmed" color. The button is aButtoncomponent with a red background color and a rounded shape. If the currently authenticated user is not friends with the user displayed in the row, the button displays anIconUserXicon from the@tabler/icons-reactlibrary. If the user is already friends with the user displayed in the row, the button simply displays the text "Remove Friend". Clicking on the button triggers theremoveFriendfunction with the user's_idas an argument.
Auth State:
We also need to update the auth.slice.ts.
import { IUser } from "@/types/user.types";
import { AUTH_LOCAL_STORAGE_CONSTANT } from "@/utils/constants";
import { setValueToLocalStorage } from "@/utils/localstorage";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
const AUTH_SLICE_KEY = "AUTH_SLICE_KEY";
export type AuthState = {
token: string | null,
user: IUser | null
};
const initialState: AuthState = {
token: null,
user: null
}
export const authSlice = createSlice({
name: AUTH_SLICE_KEY,
initialState: initialState,
reducers: {
updateAuthAction(state, { payload }: PayloadAction<AuthState>) {
state.token = payload.token;
state.user = payload.user;
},
addNewFriendAction(state, { payload }: PayloadAction<string>) {
const friends = state.user!.friends;
friends.push(payload);
state.user!.friends = friends;
setValueToLocalStorage(AUTH_LOCAL_STORAGE_CONSTANT, state);
},
removeFriendAction(state, { payload }: PayloadAction<string>) {
state.user!.friends = state.user!.friends.filter(friend => friend !== payload);
setValueToLocalStorage(AUTH_LOCAL_STORAGE_CONSTANT, state);
}
}
});
export const { updateAuthAction, addNewFriendAction, removeFriendAction } = authSlice.actions;
export default authSlice.reducer;import { IUser } from "@/types/user.types";
import { AUTH_LOCAL_STORAGE_CONSTANT } from "@/utils/constants";
import { setValueToLocalStorage } from "@/utils/localstorage";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
const AUTH_SLICE_KEY = "AUTH_SLICE_KEY";
export type AuthState = {
token: string | null,
user: IUser | null
};
const initialState: AuthState = {
token: null,
user: null
}
export const authSlice = createSlice({
name: AUTH_SLICE_KEY,
initialState: initialState,
reducers: {
updateAuthAction(state, { payload }: PayloadAction<AuthState>) {
state.token = payload.token;
state.user = payload.user;
},
addNewFriendAction(state, { payload }: PayloadAction<string>) {
const friends = state.user!.friends;
friends.push(payload);
state.user!.friends = friends;
setValueToLocalStorage(AUTH_LOCAL_STORAGE_CONSTANT, state);
},
removeFriendAction(state, { payload }: PayloadAction<string>) {
state.user!.friends = state.user!.friends.filter(friend => friend !== payload);
setValueToLocalStorage(AUTH_LOCAL_STORAGE_CONSTANT, state);
}
}
});
export const { updateAuthAction, addNewFriendAction, removeFriendAction } = authSlice.actions;
export default authSlice.reducer;- There are three actions defined in the
reducersobject:updateAuthAction,addNewFriendAction, andremoveFriendAction. TheupdateAuthActionaction is used to update thetokenanduserproperties of the state. TheaddNewFriendActionandremoveFriendActionactions are used to add and remove friends from the user's friends list respectively. - Each of these actions updates the
AuthStateand also stores the updated state in the local storage using thesetValueToLocalStoragefunction.
setValueToLocalStorage:
I have added a new function named setValueToLocalStorage inside the localStorage.ts file.
export function setValueToLocalStorage<T>(key: string, value: T): boolean {
if (typeof window !== "undefined") {
localStorage.setItem(key, JSON.stringify(value));
return true;
} else {
return false;
}
}
export function setValueToLocalStorage<T>(key: string, value: T): boolean {
if (typeof window !== "undefined") {
localStorage.setItem(key, JSON.stringify(value));
return true;
} else {
return false;
}
}- The function takes two parameters:
keywhich is a string representing the key under which the value will be stored in local storage, andvaluewhich is the value to be stored. - The function first checks whether the
windowobject is defined before trying to access local storage. This is because local storage is only available in the browser environment, and not in server-side rendering. - If the
windowobject is defined, the function converts the value to a JSON string usingJSON.stringify()and stores it in local storage using thelocalStorage.setItem()method. The function then returnstrueto indicate that the value was successfully stored. - If the
windowobject is not defined, the function returnsfalseto indicate that the value could not be stored.
The Next Part will be about chatting between friends/contacts.