Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ap/wip #13

Merged
merged 3 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/src/post/post.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export class PostController {
return this.postService.getPostsFeed(query);
}

@Get('following')
getPostsFollowingFeed(
@Query(new ValidationPipe()) query: GetPostsFeedParams,
@Req() req: any,
) {
return this.postService.getPostsFollowingFeed(query, req.user);
}

@Post(':postId/reactions')
addPostReaction(
@Param('postId') postId: string,
Expand Down
2 changes: 2 additions & 0 deletions api/src/post/post.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { Post, PostSchema } from "./schema/post.schema";
import { ServicesModule } from "../services/services.module";
import { PostReaction, PostReactionSchema } from "./schema/post-reaction.schema";
import { PostComment, PostCommentSchema } from "./schema/post-comment.schema";
import { Follower, FollowerSchema } from "../user/schema/follower.schema";

@Module({
imports: [
MongooseModule.forFeature([
{ name: Post.name, schema: PostSchema },
{ name: PostReaction.name, schema: PostReactionSchema },
{ name: PostComment.name, schema: PostCommentSchema },
{ name: Follower.name, schema: FollowerSchema },
]),
ServicesModule,
],
Expand Down
19 changes: 17 additions & 2 deletions api/src/post/post.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import { PostReaction, PostReactionDocument } from "./schema/post-reaction.schem
import { PostReactionDto } from "./dto/post-reaction.dto";
import { PostCommentDto } from "./dto/post-comment.dto";
import { PostComment, PostCommentDocument } from "./schema/post-comment.schema";
import { Follower, FollowerDocument } from "../user/schema/follower.schema";

@Injectable()
export class PostService {
constructor(
@InjectModel(Post.name) private postModel: Model<PostDocument>,
@InjectModel(PostReaction.name) private postReactionModel: Model<PostReactionDocument>,
@InjectModel(PostComment.name) private postCommentModel: Model<PostCommentDocument>,
@InjectModel(Follower.name) private followerModel: Model<FollowerDocument>,
private firebaseService: FirebaseService,
) { }

Expand Down Expand Up @@ -49,8 +51,8 @@ export class PostService {
return { post: newPost };
}

async getPostsFeed(params: GetPostsFeedParams) {
const query: any = {};
async getPostsFeed(params: GetPostsFeedParams, extraQuery: any = {}) {
let query: any = {};
const limit = 5;
const skip = (params.page * limit) - limit;

Expand All @@ -64,6 +66,11 @@ export class PostService {
}
}

query = {
...query,
...extraQuery,
};

const posts = await this.postModel.aggregate([
{ $match: query },
{ $sort: { createdAt: -1 } },
Expand Down Expand Up @@ -137,6 +144,14 @@ export class PostService {
return { posts };
}

async getPostsFollowingFeed(params: GetPostsFeedParams, user: UserDocument) {
const following = await this.followerModel.find({
follower: user._id,
});
const followingIds = following.map(f => f.user);
return this.getPostsFeed(params, { user: { $in: followingIds } });
}

async addPostReaction(postId: string, user: UserDocument, body: PostReactionDto) {
const reaction = new this.postReactionModel({
post: postId,
Expand Down
2 changes: 1 addition & 1 deletion api/src/profile/profile.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class ProfileController {

@Get()
getProfile(@Req() req: any) {
return { user: req.user };
return this.profileService.getProfile(req.user);
}

@Put()
Expand Down
2 changes: 2 additions & 0 deletions api/src/profile/profile.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { MongooseModule } from "@nestjs/mongoose";
import { User, UserSchema } from "../user/schema/user.schema";
import { ServicesModule } from "../services/services.module";
import { Post, PostSchema } from "../post/schema/post.schema";
import { Follower, FollowerSchema } from "../user/schema/follower.schema";

@Module({
imports: [
MongooseModule.forFeature([
{ name: User.name, schema: UserSchema },
{ name: Post.name, schema: PostSchema },
{ name: Follower.name, schema: FollowerSchema },
]),
ServicesModule,
],
Expand Down
13 changes: 13 additions & 0 deletions api/src/profile/profile.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,28 @@ import { Model } from "mongoose";
import { FirebaseService } from "../services/firebase.service";
import { getDownloadURL } from "firebase-admin/storage";
import { Post, PostDocument } from "../post/schema/post.schema";
import { Follower, FollowerDocument } from "../user/schema/follower.schema";

@Injectable()
export class ProfileService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
@InjectModel(Post.name) private postModel: Model<PostDocument>,
@InjectModel(Follower.name) private followerModel: Model<FollowerDocument>,
private firebaseService: FirebaseService,
) {}

async getProfile(user: UserDocument) {
const followersInfo: any = {};
followersInfo.followers = await this.followerModel.countDocuments({ user: user._id });
followersInfo.following = await this.followerModel.countDocuments({ follower: user._id });
const userData = {
...user.toObject(),
followersInfo,
};
return { user: userData };
}

async updateProfile(body: UpdateProfileDto, avatar: any, user: UserDocument) {
const dbUser = await this.userModel.findById(user._id);

Expand Down
16 changes: 16 additions & 0 deletions api/src/user/schema/follower.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { HydratedDocument } from 'mongoose';
import { UserDocument } from './user.schema';

export type FollowerDocument = HydratedDocument<Follower>;

@Schema({ timestamps: true })
export class Follower {
@Prop({ required: true, type: mongoose.Schema.Types.ObjectId, ref: 'User' })
user: UserDocument;

@Prop({ required: true, type: mongoose.Schema.Types.ObjectId, ref: 'User' })
follower: UserDocument;
}

export const FollowerSchema = SchemaFactory.createForClass(Follower);
17 changes: 14 additions & 3 deletions api/src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { Controller, Get, Param } from "@nestjs/common";
import { Controller, Get, Param, Put, Req } from "@nestjs/common";
import { UserService } from "./user.service";

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

@Get(':userId')
getUserById(@Param('userId') userId: string) {
return this.userService.getUserById(userId);
getUserById(
@Param('userId') userId: string,
@Req() req: any,
) {
return this.userService.getUserById(userId, req.user);
}

@Get(':userId/posts')
getUserPosts(@Param('userId') userId: string) {
return this.userService.getUserPosts(userId);
}

@Put(':userId/followers')
toggleFollowing(
@Param('userId') userId: string,
@Req() req: any,
) {
return this.userService.toggleFollowing(userId, req.user);
}
}
2 changes: 2 additions & 0 deletions api/src/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { User, UserSchema } from "./schema/user.schema";
import { UserController } from "./user.controller";
import { UserService } from "./user.service";
import { Post, PostSchema } from "../post/schema/post.schema";
import { Follower, FollowerSchema } from "./schema/follower.schema";

@Module({
imports: [
MongooseModule.forFeature([
{ name: User.name, schema: UserSchema },
{ name: Post.name, schema: PostSchema },
{ name: Follower.name, schema: FollowerSchema },
]),
],
controllers: [UserController],
Expand Down
34 changes: 32 additions & 2 deletions api/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,37 @@ import { InjectModel } from "@nestjs/mongoose";
import { User, UserDocument } from "./schema/user.schema";
import mongoose, { Model } from "mongoose";
import { Post, PostDocument } from "../post/schema/post.schema";
import { Follower, FollowerDocument } from "./schema/follower.schema";

@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
@InjectModel(Post.name) private postModel: Model<PostDocument>,
@InjectModel(Follower.name) private followerModel: Model<FollowerDocument>,
) { }

async getUserById(userId: string) {
async getUserById(userId: string, currentUser: UserDocument) {
let user = await this.userModel.findById(userId).lean();
if (!user.settings.public) {
return {
user: {
_id: user._id,
avatar: user.avatar,
username: user.username,
settings: { public: false },
},
};
}
return { user };
const followersInfo: any = {};
followersInfo.followers = await this.followerModel.countDocuments({ user: user._id });
followersInfo.following = await this.followerModel.countDocuments({ follower: user._id });
followersInfo.isFollowing = !!(await this.followerModel.countDocuments({ user: user._id, follower: currentUser._id }));
const userData = {
...user,
followersInfo,
};
return { user: userData };
}

async getUserPosts(userId: string) {
Expand Down Expand Up @@ -62,4 +73,23 @@ export class UserService {
]);
return { posts };
}

async toggleFollowing(userId: string, currentUser: UserDocument) {
const follower = await this.followerModel.findOne({
user: userId,
follower: currentUser._id,
});
if (follower) {
await follower.deleteOne();
} else {
const newFollower = new this.followerModel({
user: userId,
follower: currentUser._id,
});
await newFollower.save();
}
const { user } = await this.getUserById(userId, currentUser);

return { user };
}
}
7 changes: 7 additions & 0 deletions frontend/src/api/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export class BaseApi {
return response;
}

async put(url, data) {
const response = await this.client.put(url, JSON.stringify(data));
this.parseResponse(response);
this.checkResponse(response);
return response;
}

async postForm(url, form) {
const response = await this.client.post(url, form);
this.parseResponse(response);
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/api/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ class PostApi extends BaseApi {
return this.postForm('/posts', form);
}

getPostsFeed(page, filter) {
getPostsFeed(page, filter, isFollowing) {
if (isFollowing) {
return this.get('/posts/following', { page, filter });
}
return this.get('/posts', { page, filter });
}

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/api/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ class UserApi extends BaseApi {
getPosts(userId) {
return this.get(`/users/${userId}/posts`);
}

updateFollowing(userId) {
return this.put(`/users/${userId}/followers`);
}
}

export const userApi = new UserApi();
Binary file added frontend/src/assets/images/Rectangle 224.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/Rectangle 246.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/Rectangle 30.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/src/components/FeedFilterModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function clear() {
</script>

<template>
<Button @click="isVisible = true" class="float-end" label="Filter" icon="pi pi-filter" :severity="isActive ? 'primary' : 'secondary'" outlined />
<Button @click="isVisible = true" label="Filter" icon="pi pi-filter" :severity="isActive ? 'primary' : 'secondary'" outlined />
<Dialog v-model:visible="isVisible" modal class="feed-filter-dialog">
<template #header>
<h4 class="mb-0">Feed filter</h4>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function logout() {
</div>
</div>
<!--Sign in/sign up authentication links-->
<div>
<div class="text-end">
<router-link v-if="userStore.user" to="/account">
<Avatar :label="avatarLabel" :image="userStore.user.avatar" size="large" shape="circle" />
</router-link>
Expand Down
15 changes: 10 additions & 5 deletions frontend/src/components/UserProfileView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Tag from 'primevue/tag';
import Card from 'primevue/card';
import Button from 'primevue/button';

defineEmits(['following']);

const { user, isEditable } = defineProps({
user: {
type: Object,
Expand All @@ -17,21 +19,24 @@ const { user, isEditable } = defineProps({
</script>

<template>
<div class="mb-2">
<div class="mb-3">
<div class="avatar m-auto">
<div v-if="!user.avatar" class="avatar-placeholder">
<span>{{ user.username.toUpperCase().charAt(0) }}</span>
</div>
<img v-else :src="user.avatar" alt="Avatar" preview />
</div>
</div>
<div class="mb-2">
<p class="text-center fs-5 m-0">{{ user.username }}</p>
</div>
<div class="text-center mb-4">
<div class="mb-3 d-flex justify-content-center">
<p class="text-center fs-5 m-0 me-2">{{ user.username }}</p>
<Tag v-if="user.settings.public" value="Public" severity="info"/>
<Tag v-else value="Private" severity="contrast"/>
</div>
<div class="mb-4 text-center">
<span class="me-3"><strong>{{ user.followersInfo.following }}</strong> following</span>
<Button :label="!user.followersInfo.isFollowing ? 'Follow' : 'Unfollow'" :outlined="user.followersInfo.isFollowing" size="small" v-if="!isEditable" @click="$emit('following')"/>
<span class="ms-3"><strong>{{ user.followersInfo.followers }}</strong> followers</span>
</div>
<div v-if="isEditable" class="text-center mb-4">
<Button as="router-link" to="/account/edit" label="Edit profile" icon="pi pi-user-edit" size="small" severity="secondary" outlined/>
</div>
Expand Down
Loading