From 80c4b766d7df2fb515520edd2eaae671b14600a3 Mon Sep 17 00:00:00 2001 From: gagasieuga Date: Fri, 6 Sep 2024 00:52:28 +0700 Subject: [PATCH] add follow and modify fyp page --- src/app.ts | 6 ++ src/controllers/follow.controller.ts | 80 +++++++++++++++++++++++ src/controllers/user.controller.ts | 21 +++++- src/locales/en/translation.json | 3 +- src/public/js/script.js | 20 ++++++ src/routes/follow.route.ts | 25 ++++++++ src/routes/index.ts | 2 + src/services/follow.service.ts | 95 ++++++++++++++++++++++++++++ src/services/post.service.ts | 35 +++++++--- src/views/users/index.hbs | 12 ++++ src/views/users/show.hbs | 35 +++++----- 11 files changed, 306 insertions(+), 28 deletions(-) create mode 100644 src/controllers/follow.controller.ts create mode 100644 src/routes/follow.route.ts create mode 100644 src/services/follow.service.ts diff --git a/src/app.ts b/src/app.ts index ee28949..ac39595 100644 --- a/src/app.ts +++ b/src/app.ts @@ -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(); @@ -37,6 +38,11 @@ hbs.registerHelper('t', (key: string) => { return i18next.t(key); }); +hbs.registerHelper('isFollowing', function(user: User, followingUsers: Array) { + return followingUsers.some(followingUser => followingUser.userId === user.userId); +}); + + app.use( session({ resave: false, diff --git a/src/controllers/follow.controller.ts b/src/controllers/follow.controller.ts new file mode 100644 index 0000000..0a01872 --- /dev/null +++ b/src/controllers/follow.controller.ts @@ -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')); + } +}); diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 0ec15a9..e118b33 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -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); @@ -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), @@ -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, @@ -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) }); diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 089fdac..ba3a734 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -45,7 +45,8 @@ "bookmark": "Bookmark", "latestPost": "See latest posts", "addNewUser": "Add New User", - "update": "Update" + "update": "Update", + "unfollow": "Unfollow" }, "content" : { "username": "Username", diff --git a/src/public/js/script.js b/src/public/js/script.js index b3ac0bd..456298d 100644 --- a/src/public/js/script.js +++ b/src/public/js/script.js @@ -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`); + } + }); + }); + + + }); diff --git a/src/routes/follow.route.ts b/src/routes/follow.route.ts new file mode 100644 index 0000000..3bad364 --- /dev/null +++ b/src/routes/follow.route.ts @@ -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; diff --git a/src/routes/index.ts b/src/routes/index.ts index 46dc470..7244015 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -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(); @@ -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; diff --git a/src/services/follow.service.ts b/src/services/follow.service.ts new file mode 100644 index 0000000..ab15c09 --- /dev/null +++ b/src/services/follow.service.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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); + } +} diff --git a/src/services/post.service.ts b/src/services/post.service.ts index 258a397..da57c01 100644 --- a/src/services/post.service.ts +++ b/src/services/post.service.ts @@ -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'; @@ -10,6 +10,7 @@ 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(); @@ -17,6 +18,7 @@ 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({ @@ -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 { const user = await userService.getUserById(userId); diff --git a/src/views/users/index.hbs b/src/views/users/index.hbs index 95789f7..ce42288 100644 --- a/src/views/users/index.hbs +++ b/src/views/users/index.hbs @@ -17,6 +17,16 @@ + {{!-- Check if this is not the current user before showing follow/unfollow buttons --}} + {{#unless (eq this.userId ../currentUserId)}} + + {{/unless}} {{/each}} @@ -42,3 +52,5 @@ + + diff --git a/src/views/users/show.hbs b/src/views/users/show.hbs index 3e2b292..89df2a5 100644 --- a/src/views/users/show.hbs +++ b/src/views/users/show.hbs @@ -20,22 +20,27 @@ {{/if}} {{/if}} - {{#if userRole}}{{#unless isOwner}}{{#if userActive}} - - - {{/if}}{{/unless}}{{/if}} + {{#if userRole}} + {{#unless isOwner}} + {{#if userActive}} + {{#if isFollowing}} + + {{else}} + + {{/if}} + + {{/if}} + {{/unless}} + {{/if}} {{#if isAdmin}} {{#unless userAdmin}} - {{#if userActive}} - - {{/if}}
@@ -52,7 +57,6 @@ - {{#if isOwner}}
@@ -75,6 +79,7 @@ {{/if}} +
{{#if userActive}} @@ -130,5 +135,3 @@
- -