Skip to content

Commit 94c82b5

Browse files
authored
Merge pull request #13 from deuxroux/ap/wip
Ap/wip
2 parents 4b244ad + a8ad7e1 commit 94c82b5

22 files changed

+220
-28
lines changed

api/src/post/post.controller.ts

+8
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ export class PostController {
2525
return this.postService.getPostsFeed(query);
2626
}
2727

28+
@Get('following')
29+
getPostsFollowingFeed(
30+
@Query(new ValidationPipe()) query: GetPostsFeedParams,
31+
@Req() req: any,
32+
) {
33+
return this.postService.getPostsFollowingFeed(query, req.user);
34+
}
35+
2836
@Post(':postId/reactions')
2937
addPostReaction(
3038
@Param('postId') postId: string,

api/src/post/post.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { Post, PostSchema } from "./schema/post.schema";
66
import { ServicesModule } from "../services/services.module";
77
import { PostReaction, PostReactionSchema } from "./schema/post-reaction.schema";
88
import { PostComment, PostCommentSchema } from "./schema/post-comment.schema";
9+
import { Follower, FollowerSchema } from "../user/schema/follower.schema";
910

1011
@Module({
1112
imports: [
1213
MongooseModule.forFeature([
1314
{ name: Post.name, schema: PostSchema },
1415
{ name: PostReaction.name, schema: PostReactionSchema },
1516
{ name: PostComment.name, schema: PostCommentSchema },
17+
{ name: Follower.name, schema: FollowerSchema },
1618
]),
1719
ServicesModule,
1820
],

api/src/post/post.service.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import { PostReaction, PostReactionDocument } from "./schema/post-reaction.schem
1111
import { PostReactionDto } from "./dto/post-reaction.dto";
1212
import { PostCommentDto } from "./dto/post-comment.dto";
1313
import { PostComment, PostCommentDocument } from "./schema/post-comment.schema";
14+
import { Follower, FollowerDocument } from "../user/schema/follower.schema";
1415

1516
@Injectable()
1617
export class PostService {
1718
constructor(
1819
@InjectModel(Post.name) private postModel: Model<PostDocument>,
1920
@InjectModel(PostReaction.name) private postReactionModel: Model<PostReactionDocument>,
2021
@InjectModel(PostComment.name) private postCommentModel: Model<PostCommentDocument>,
22+
@InjectModel(Follower.name) private followerModel: Model<FollowerDocument>,
2123
private firebaseService: FirebaseService,
2224
) { }
2325

@@ -49,8 +51,8 @@ export class PostService {
4951
return { post: newPost };
5052
}
5153

52-
async getPostsFeed(params: GetPostsFeedParams) {
53-
const query: any = {};
54+
async getPostsFeed(params: GetPostsFeedParams, extraQuery: any = {}) {
55+
let query: any = {};
5456
const limit = 5;
5557
const skip = (params.page * limit) - limit;
5658

@@ -64,6 +66,11 @@ export class PostService {
6466
}
6567
}
6668

69+
query = {
70+
...query,
71+
...extraQuery,
72+
};
73+
6774
const posts = await this.postModel.aggregate([
6875
{ $match: query },
6976
{ $sort: { createdAt: -1 } },
@@ -137,6 +144,14 @@ export class PostService {
137144
return { posts };
138145
}
139146

147+
async getPostsFollowingFeed(params: GetPostsFeedParams, user: UserDocument) {
148+
const following = await this.followerModel.find({
149+
follower: user._id,
150+
});
151+
const followingIds = following.map(f => f.user);
152+
return this.getPostsFeed(params, { user: { $in: followingIds } });
153+
}
154+
140155
async addPostReaction(postId: string, user: UserDocument, body: PostReactionDto) {
141156
const reaction = new this.postReactionModel({
142157
post: postId,

api/src/profile/profile.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export class ProfileController {
99

1010
@Get()
1111
getProfile(@Req() req: any) {
12-
return { user: req.user };
12+
return this.profileService.getProfile(req.user);
1313
}
1414

1515
@Put()

api/src/profile/profile.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import { MongooseModule } from "@nestjs/mongoose";
55
import { User, UserSchema } from "../user/schema/user.schema";
66
import { ServicesModule } from "../services/services.module";
77
import { Post, PostSchema } from "../post/schema/post.schema";
8+
import { Follower, FollowerSchema } from "../user/schema/follower.schema";
89

910
@Module({
1011
imports: [
1112
MongooseModule.forFeature([
1213
{ name: User.name, schema: UserSchema },
1314
{ name: Post.name, schema: PostSchema },
15+
{ name: Follower.name, schema: FollowerSchema },
1416
]),
1517
ServicesModule,
1618
],

api/src/profile/profile.service.ts

+13
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,28 @@ import { Model } from "mongoose";
66
import { FirebaseService } from "../services/firebase.service";
77
import { getDownloadURL } from "firebase-admin/storage";
88
import { Post, PostDocument } from "../post/schema/post.schema";
9+
import { Follower, FollowerDocument } from "../user/schema/follower.schema";
910

1011
@Injectable()
1112
export class ProfileService {
1213
constructor(
1314
@InjectModel(User.name) private userModel: Model<UserDocument>,
1415
@InjectModel(Post.name) private postModel: Model<PostDocument>,
16+
@InjectModel(Follower.name) private followerModel: Model<FollowerDocument>,
1517
private firebaseService: FirebaseService,
1618
) {}
1719

20+
async getProfile(user: UserDocument) {
21+
const followersInfo: any = {};
22+
followersInfo.followers = await this.followerModel.countDocuments({ user: user._id });
23+
followersInfo.following = await this.followerModel.countDocuments({ follower: user._id });
24+
const userData = {
25+
...user.toObject(),
26+
followersInfo,
27+
};
28+
return { user: userData };
29+
}
30+
1831
async updateProfile(body: UpdateProfileDto, avatar: any, user: UserDocument) {
1932
const dbUser = await this.userModel.findById(user._id);
2033

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2+
import mongoose, { HydratedDocument } from 'mongoose';
3+
import { UserDocument } from './user.schema';
4+
5+
export type FollowerDocument = HydratedDocument<Follower>;
6+
7+
@Schema({ timestamps: true })
8+
export class Follower {
9+
@Prop({ required: true, type: mongoose.Schema.Types.ObjectId, ref: 'User' })
10+
user: UserDocument;
11+
12+
@Prop({ required: true, type: mongoose.Schema.Types.ObjectId, ref: 'User' })
13+
follower: UserDocument;
14+
}
15+
16+
export const FollowerSchema = SchemaFactory.createForClass(Follower);

api/src/user/user.controller.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
1-
import { Controller, Get, Param } from "@nestjs/common";
1+
import { Controller, Get, Param, Put, Req } from "@nestjs/common";
22
import { UserService } from "./user.service";
33

44
@Controller('users')
55
export class UserController {
66
constructor(private userService: UserService) {}
77

88
@Get(':userId')
9-
getUserById(@Param('userId') userId: string) {
10-
return this.userService.getUserById(userId);
9+
getUserById(
10+
@Param('userId') userId: string,
11+
@Req() req: any,
12+
) {
13+
return this.userService.getUserById(userId, req.user);
1114
}
1215

1316
@Get(':userId/posts')
1417
getUserPosts(@Param('userId') userId: string) {
1518
return this.userService.getUserPosts(userId);
1619
}
20+
21+
@Put(':userId/followers')
22+
toggleFollowing(
23+
@Param('userId') userId: string,
24+
@Req() req: any,
25+
) {
26+
return this.userService.toggleFollowing(userId, req.user);
27+
}
1728
}

api/src/user/user.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { User, UserSchema } from "./schema/user.schema";
44
import { UserController } from "./user.controller";
55
import { UserService } from "./user.service";
66
import { Post, PostSchema } from "../post/schema/post.schema";
7+
import { Follower, FollowerSchema } from "./schema/follower.schema";
78

89
@Module({
910
imports: [
1011
MongooseModule.forFeature([
1112
{ name: User.name, schema: UserSchema },
1213
{ name: Post.name, schema: PostSchema },
14+
{ name: Follower.name, schema: FollowerSchema },
1315
]),
1416
],
1517
controllers: [UserController],

api/src/user/user.service.ts

+32-2
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,37 @@ import { InjectModel } from "@nestjs/mongoose";
33
import { User, UserDocument } from "./schema/user.schema";
44
import mongoose, { Model } from "mongoose";
55
import { Post, PostDocument } from "../post/schema/post.schema";
6+
import { Follower, FollowerDocument } from "./schema/follower.schema";
67

78
@Injectable()
89
export class UserService {
910
constructor(
1011
@InjectModel(User.name) private userModel: Model<UserDocument>,
1112
@InjectModel(Post.name) private postModel: Model<PostDocument>,
13+
@InjectModel(Follower.name) private followerModel: Model<FollowerDocument>,
1214
) { }
1315

14-
async getUserById(userId: string) {
16+
async getUserById(userId: string, currentUser: UserDocument) {
1517
let user = await this.userModel.findById(userId).lean();
1618
if (!user.settings.public) {
1719
return {
1820
user: {
21+
_id: user._id,
1922
avatar: user.avatar,
2023
username: user.username,
2124
settings: { public: false },
2225
},
2326
};
2427
}
25-
return { user };
28+
const followersInfo: any = {};
29+
followersInfo.followers = await this.followerModel.countDocuments({ user: user._id });
30+
followersInfo.following = await this.followerModel.countDocuments({ follower: user._id });
31+
followersInfo.isFollowing = !!(await this.followerModel.countDocuments({ user: user._id, follower: currentUser._id }));
32+
const userData = {
33+
...user,
34+
followersInfo,
35+
};
36+
return { user: userData };
2637
}
2738

2839
async getUserPosts(userId: string) {
@@ -62,4 +73,23 @@ export class UserService {
6273
]);
6374
return { posts };
6475
}
76+
77+
async toggleFollowing(userId: string, currentUser: UserDocument) {
78+
const follower = await this.followerModel.findOne({
79+
user: userId,
80+
follower: currentUser._id,
81+
});
82+
if (follower) {
83+
await follower.deleteOne();
84+
} else {
85+
const newFollower = new this.followerModel({
86+
user: userId,
87+
follower: currentUser._id,
88+
});
89+
await newFollower.save();
90+
}
91+
const { user } = await this.getUserById(userId, currentUser);
92+
93+
return { user };
94+
}
6595
}

frontend/src/api/base.js

+7
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ export class BaseApi {
4343
return response;
4444
}
4545

46+
async put(url, data) {
47+
const response = await this.client.put(url, JSON.stringify(data));
48+
this.parseResponse(response);
49+
this.checkResponse(response);
50+
return response;
51+
}
52+
4653
async postForm(url, form) {
4754
const response = await this.client.post(url, form);
4855
this.parseResponse(response);

frontend/src/api/post.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ class PostApi extends BaseApi {
1313
return this.postForm('/posts', form);
1414
}
1515

16-
getPostsFeed(page, filter) {
16+
getPostsFeed(page, filter, isFollowing) {
17+
if (isFollowing) {
18+
return this.get('/posts/following', { page, filter });
19+
}
1720
return this.get('/posts', { page, filter });
1821
}
1922

frontend/src/api/user.js

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ class UserApi extends BaseApi {
88
getPosts(userId) {
99
return this.get(`/users/${userId}/posts`);
1010
}
11+
12+
updateFollowing(userId) {
13+
return this.put(`/users/${userId}/followers`);
14+
}
1115
}
1216

1317
export const userApi = new UserApi();
2.68 MB
Loading
2.53 MB
Loading
1.3 MB
Loading

frontend/src/components/FeedFilterModal.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function clear() {
3737
</script>
3838

3939
<template>
40-
<Button @click="isVisible = true" class="float-end" label="Filter" icon="pi pi-filter" :severity="isActive ? 'primary' : 'secondary'" outlined />
40+
<Button @click="isVisible = true" label="Filter" icon="pi pi-filter" :severity="isActive ? 'primary' : 'secondary'" outlined />
4141
<Dialog v-model:visible="isVisible" modal class="feed-filter-dialog">
4242
<template #header>
4343
<h4 class="mb-0">Feed filter</h4>

frontend/src/components/Header.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function logout() {
6161
</div>
6262
</div>
6363
<!--Sign in/sign up authentication links-->
64-
<div>
64+
<div class="text-end">
6565
<router-link v-if="userStore.user" to="/account">
6666
<Avatar :label="avatarLabel" :image="userStore.user.avatar" size="large" shape="circle" />
6767
</router-link>

frontend/src/components/UserProfileView.vue

+10-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import Tag from 'primevue/tag';
33
import Card from 'primevue/card';
44
import Button from 'primevue/button';
55
6+
defineEmits(['following']);
7+
68
const { user, isEditable } = defineProps({
79
user: {
810
type: Object,
@@ -17,21 +19,24 @@ const { user, isEditable } = defineProps({
1719
</script>
1820

1921
<template>
20-
<div class="mb-2">
22+
<div class="mb-3">
2123
<div class="avatar m-auto">
2224
<div v-if="!user.avatar" class="avatar-placeholder">
2325
<span>{{ user.username.toUpperCase().charAt(0) }}</span>
2426
</div>
2527
<img v-else :src="user.avatar" alt="Avatar" preview />
2628
</div>
2729
</div>
28-
<div class="mb-2">
29-
<p class="text-center fs-5 m-0">{{ user.username }}</p>
30-
</div>
31-
<div class="text-center mb-4">
30+
<div class="mb-3 d-flex justify-content-center">
31+
<p class="text-center fs-5 m-0 me-2">{{ user.username }}</p>
3232
<Tag v-if="user.settings.public" value="Public" severity="info"/>
3333
<Tag v-else value="Private" severity="contrast"/>
3434
</div>
35+
<div class="mb-4 text-center">
36+
<span class="me-3"><strong>{{ user.followersInfo.following }}</strong> following</span>
37+
<Button :label="!user.followersInfo.isFollowing ? 'Follow' : 'Unfollow'" :outlined="user.followersInfo.isFollowing" size="small" v-if="!isEditable" @click="$emit('following')"/>
38+
<span class="ms-3"><strong>{{ user.followersInfo.followers }}</strong> followers</span>
39+
</div>
3540
<div v-if="isEditable" class="text-center mb-4">
3641
<Button as="router-link" to="/account/edit" label="Edit profile" icon="pi pi-user-edit" size="small" severity="secondary" outlined/>
3742
</div>

0 commit comments

Comments
 (0)