Skip to content

Commit

Permalink
add follow and modify fyp page
Browse files Browse the repository at this point in the history
  • Loading branch information
namanhpham committed Sep 11, 2024
1 parent 581bab2 commit 80c4b76
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 28 deletions.
6 changes: 6 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import indexRouter from './routes/index';
import { config } from 'dotenv';
import { AppDataSource } from './config/data-source';
import handlebarsHelpers from 'handlebars-helpers';
import { User } from './entities/user.entity';
config();

const app = express();
Expand All @@ -37,6 +38,11 @@ hbs.registerHelper('t', (key: string) => {
return i18next.t(key);
});

hbs.registerHelper('isFollowing', function(user: User, followingUsers: Array<User>) {
return followingUsers.some(followingUser => followingUser.userId === user.userId);
});


app.use(
session({
resave: false,
Expand Down
80 changes: 80 additions & 0 deletions src/controllers/follow.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Request, Response } from 'express';
import { FollowService } from '../services/follow.service';
import asyncHandler from 'express-async-handler';
import i18next from 'i18next';
import { isAuthenticated } from '../middlewares/auth.middleware';

const { t } = i18next;
const followService = new FollowService();

// Follow a user
export const followUser = [
isAuthenticated,
asyncHandler(async (req: Request, res: Response) => {
const followerId = req.session.user?.id || 0;
const followedId = parseInt(req.params.userId, 10);

try {
await followService.followUser(followerId, followedId);
req.flash('flashMessage', t('message.followSuccess'));
res.redirect(`/users/${followedId}`);
} catch (error) {
console.error(error);
const err = error as Error;
req.flash('flashMessage', err.message);
res.redirect(`/users/${followedId}`);
}
}),
];

// Unfollow a user
export const unfollowUser = [
isAuthenticated,
asyncHandler(async (req: Request, res: Response) => {
const followerId = req.session.user?.id || 0;
const followedId = parseInt(req.params.userId, 10);

try {
await followService.unfollowUser(followerId, followedId);
req.flash('flashMessage', t('message.unfollowSuccess'));
res.redirect(`/users/${followedId}`);
} catch (error) {
console.error(error);
res.redirect(`/users/${followedId}`);
}
}),
];

// Get followers of a user
export const getFollowers = asyncHandler(async (req: Request, res: Response) => {
const userId = parseInt(req.params.userId, 10);

try {
const followers = await followService.getFollowers(userId);
res.render('user/followers', {
followers,
title: t('title.followers'),
flashMessage: req.flash('flashMessage'),
});
} catch (error) {
console.error(error);
res.status(500).send(t('error.serverError'));
}
});

// Get following users of a user
export const getFollowing = asyncHandler(async (req: Request, res: Response) => {
const userId = parseInt(req.params.userId, 10);

try {
const following = await followService.getFollowing(userId);
res.render('user/following', {
following,
title: t('title.following'),
flashMessage: req.flash('flashMessage'),
});
} catch (error) {
console.error(error);
res.status(500).send(t('error.serverError'));
}
});
21 changes: 19 additions & 2 deletions src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ import { validateSessionRole,
validateActiveUser,
sanitizeContent } from '../utils/';
import { PostService } from '../services/post.service';
import { FollowService } from '../services/follow.service';
import { User } from '../entities/user.entity';
import { PostVisibility } from '../constants/post-visibility';

const userService = new UserService();
const postService = new PostService();
const followService = new FollowService();

async function validateUserById(req: Request, res: Response) {
const id = parseInt(req.params.id);
Expand All @@ -36,9 +39,18 @@ export const getUsers = asyncHandler(async (req: Request, res: Response) => {
const page = parseInt(req.query.page as string, 10) || 1;
try {
const users = await userService.getAllUsers(page);
const currentUserId = req.session.user?.id;

let followingUsers: User[] = [];
if (req.session.user) {
followingUsers = await followService.getFollowing(req.session.user?.id);
}

res.render('users/index', {
title: t ('title.users'),
users: users,
followingUsers: followingUsers,
currentUserId: currentUserId,
userRole: req.session.user?.role,
userStatus: UserStatus,
isAdmin: validateAdminRole(req),
Expand All @@ -61,8 +73,12 @@ export const getUserById = asyncHandler(async (req: Request, res: Response) => {
const sanitizedPosts = posts.map(post => ({
...post,
content: sanitizeContent(post.content)
}));;
// Admin and page owner can Edit profile.
}));
let isFollowing = false;
if (req.session.user) {
isFollowing = await followService.isFollowing(req.session.user?.id, id);
}

res.render('users/show', {
title: 'title.userProfile',
postVisibility: PostVisibility,
Expand All @@ -72,6 +88,7 @@ export const getUserById = asyncHandler(async (req: Request, res: Response) => {
userRole: req.session.user?.role,
userPosts: sanitizedPosts,
userStatus: UserStatus,
isFollowing: isFollowing,
isOwner: id == req.session.user?.id,
isAdmin: validateAdminRole(req)
});
Expand Down
3 changes: 2 additions & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"bookmark": "Bookmark",
"latestPost": "See latest posts",
"addNewUser": "Add New User",
"update": "Update"
"update": "Update",
"unfollow": "Unfollow"
},
"content" : {
"username": "Username",
Expand Down
20 changes: 20 additions & 0 deletions src/public/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,24 @@ $(document).ready(function() {
$('#commentForm').toggle();
});

$('.btn-follow, .btn-unfollow, .follow-btn, .unfollow-btn').on('click', function() {
const userId = $(this).data('user-id');
const isFollowBtn = $(this).hasClass('btn-follow') || $(this).hasClass('follow-btn');
const url = isFollowBtn ? `/follow/follow/${userId}` : `/follow/unfollow/${userId}`;
const actionText = isFollowBtn ? 'followed' : 'unfollowed';

$.ajax({
url: url,
method: 'POST',
success: function() {
alert(`User ${actionText} successfully`);
},
error: function() {
alert(`Error ${actionText === 'followed' ? 'following' : 'unfollowing'} user`);
}
});
});



});
25 changes: 25 additions & 0 deletions src/routes/follow.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// routes/follow.routes.ts

import express from 'express';
import {
followUser,
unfollowUser,
getFollowers,
getFollowing,
} from '../controllers/follow.controller';

const router = express.Router();

// Follow a user
router.post('/follow/:userId', followUser);

// Unfollow a user
router.post('/unfollow/:userId', unfollowUser);

// Get followers of a user
router.get('/:userId/followers', getFollowers);

// Get following users of a user
router.get('/:userId/following', getFollowing);

export default router;
2 changes: 2 additions & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import postRouter from './post.route';
import userRouter from './user.route';
import commentRouter from './comment.route';
import tagRouter from './tag.route';
import followRouter from './follow.route';

const router: Router = Router();

Expand All @@ -17,5 +18,6 @@ router.use('/posts', postRouter);
router.use('/users', userRouter);
router.use('/comments', commentRouter);
router.use('/tags', tagRouter);
router.use('/follow', followRouter);

export default router;
95 changes: 95 additions & 0 deletions src/services/follow.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { AppDataSource } from '../config/data-source';
import { User } from '../entities/user.entity';

export class FollowService {
private userRepository = AppDataSource.getRepository(User);

async followUser(followerId: number, followedId: number): Promise<void> {
if (followerId === followedId) {
throw new Error('You cannot follow yourself');
}

const follower = await this.userRepository.findOne({
where: { userId: followerId },
relations: ['following'],
});

const followed = await this.userRepository.findOne({
where: { userId: followedId },
});

if (!follower || !followed) {
throw new Error('User not found');
}

if (follower.following.some(user => user.userId === followedId)) {
throw new Error('You are already following this user');
}

follower.following.push(followed);
await this.userRepository.save(follower);
}

async unfollowUser(followerId: number, followedId: number): Promise<void> {
if (followerId === followedId) {
throw new Error('You cannot unfollow yourself');
}

const follower = await this.userRepository.findOne({
where: { userId: followerId },
relations: ['following'],
});

if (!follower) {
throw new Error('User not found');
}

follower.following = follower.following.filter(user => user.userId !== followedId);
await this.userRepository.save(follower);
}

async getFollowers(userId: number): Promise<User[]> {
const user = await this.userRepository.findOne({
where: { userId },
relations: ['followers'],
});

if (!user) {
throw new Error('User not found');
}

return user.followers;
}

async getFollowing(userId: number): Promise<User[]> {
const user = await this.userRepository.findOne({
where: { userId },
relations: ['following'],
select: {
following: {
userId: true,
username: true,
}
}
});

if (!user) {
throw new Error('User not found');
}

return user.following;
}

async isFollowing(followerId: number, followedId: number): Promise<boolean> {
const follower = await this.userRepository.findOne({
where: { userId: followerId },
relations: ['following'],
});

if (!follower) {
throw new Error('User not found');
}

return follower.following.some(user => user.userId === followedId);
}
}
35 changes: 26 additions & 9 deletions src/services/post.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Not } from 'typeorm';
import { In, Not } from 'typeorm';
import { AppDataSource } from '../config/data-source';
import { PostVisibility } from '../constants/post-visibility';
import { CreatePostDto } from '../dtos/post/create-post.dto';
Expand All @@ -10,13 +10,15 @@ import { TagService } from './tag.service';
import { PAGE_SIZE } from '../constants/post-constant';
import { extractIMG } from '../utils';
import Fuse from 'fuse.js'
import { User } from '../entities/user.entity';

const userService = new UserService();
const tagService = new TagService();

export class PostService {
private postRepository = AppDataSource.getRepository(Post);
private postStatsRepository = AppDataSource.getRepository(PostStats);
private userRepository = AppDataSource.getRepository(User);

async getPostById(userId: number | undefined, postId: number) {
const post = await this.postRepository.findOne({
Expand Down Expand Up @@ -136,21 +138,36 @@ export class PostService {
async getFYPPosts(userId: number, page: number = 1) {
const pageSize = PAGE_SIZE;
const offset = (page - 1) * pageSize;


// Retrieve the current user's following list
const following = await this.userRepository.findOne({
where: { userId },
relations: ['following']
});

if (!following) {
throw new Error('User not found');
}

const followingIds = following.following.map(user => user.userId);

// Find posts from the users the current user is following, as well as their own posts
const posts = await this.postRepository.find({
relations: ['user'],
where: [
// Retrieve public or pinned posts from other users
{ user: { userId: Not(userId) }, visible: PostVisibility.PUBLIC },
{ user: { userId: Not(userId) }, visible: PostVisibility.PINNED },
{ user: { userId } }, // All posts from the current user
// Posts from followed users
{ user: { userId: In(followingIds) }, visible: In([PostVisibility.PUBLIC, PostVisibility.PINNED]) },
// Posts from the current user
{ user: { userId } }
],
order: { createdAt: 'DESC' },
skip: offset,
take: pageSize,
order: { createdAt: 'DESC' },
skip: offset,
take: pageSize,
});

return posts;
}


async create(createPostDto: CreatePostDto, userId: number): Promise<Post> {
const user = await userService.getUserById(userId);
Expand Down
Loading

0 comments on commit 80c4b76

Please sign in to comment.