diff --git a/spec.json b/spec.json index 95468154a5d3f5a038d1aaa8cd3767ebca3055e4..1ca2502ef0cfab946bf2ed05dc8941862d2ff8ce 100644 --- a/spec.json +++ b/spec.json @@ -596,22 +596,22 @@ "required": true }, "responses": { - "409": { - "description": "Email already exists", + "201": { + "description": "Successfully signed up", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ExceptionResponse" + "$ref": "#/components/schemas/AuthenticationResponse" } } } }, - "201": { - "description": "Successfully signed up", + "409": { + "description": "Email already exists", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AuthenticationResponse" + "$ref": "#/components/schemas/ExceptionResponse" } } } @@ -649,8 +649,8 @@ } } }, - "401": { - "description": "Invalid credentials", + "404": { + "description": "User not found", "content": { "application/json": { "schema": { @@ -659,8 +659,8 @@ } } }, - "404": { - "description": "User not found", + "401": { + "description": "Invalid credentials", "content": { "application/json": { "schema": { @@ -868,6 +868,49 @@ } } }, + "/api/users/search/{searchTerm}/{filter}": { + "get": { + "tags": [ + "User" + ], + "summary": "Search for users by name and filter", + "description": "Returns a list of users whose names contain the specified search term and match the filter.", + "operationId": "getUsersByNameAndFilter", + "parameters": [ + { + "name": "searchTerm", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved list of users", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDTO" + } + } + } + } + } + } + } + }, "/api/users/me": { "get": { "tags": [ @@ -1700,6 +1743,9 @@ "role": { "type": "string" }, + "subscriptionLevel": { + "type": "string" + }, "token": { "type": "string" } @@ -1758,6 +1804,9 @@ }, "role": { "type": "string" + }, + "subscriptionLevel": { + "type": "string" } } }, diff --git a/src/api/models/AuthenticationResponse.ts b/src/api/models/AuthenticationResponse.ts index 47169ae37b90b20523c9be17ff5da7c2b15bcd4a..d6785f617996241509d3c5a02058c70cde76c90f 100644 --- a/src/api/models/AuthenticationResponse.ts +++ b/src/api/models/AuthenticationResponse.ts @@ -6,6 +6,7 @@ export type AuthenticationResponse = { firstName?: string; lastName?: string; role?: string; + subscriptionLevel?: string; token?: string; }; diff --git a/src/api/models/UserDTO.ts b/src/api/models/UserDTO.ts index aee3ea036af88cba3a66ffeb071041105d318d8b..2020ee7c36cf2f4a1a8304f61f398d76c8d5cda2 100644 --- a/src/api/models/UserDTO.ts +++ b/src/api/models/UserDTO.ts @@ -10,5 +10,6 @@ export type UserDTO = { email?: string; createdAt?: string; role?: string; + subscriptionLevel?: string; }; diff --git a/src/api/services/UserService.ts b/src/api/services/UserService.ts index e7b80041bff94cbc4a17e041ead118b85b9db970..93cd7cea1dd29a31a0dcfc46ab19d53a0edbbc0f 100644 --- a/src/api/services/UserService.ts +++ b/src/api/services/UserService.ts @@ -199,6 +199,28 @@ export class UserService { }, }); } + /** + * Search for users by name and filter + * Returns a list of users whose names contain the specified search term and match the filter. + * @returns UserDTO Successfully retrieved list of users + * @throws ApiError + */ + public static getUsersByNameAndFilter({ + searchTerm, + filter, + }: { + searchTerm: string, + filter: string, + }): CancelablePromise<Array<UserDTO>> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/users/search/{searchTerm}/{filter}', + path: { + 'searchTerm': searchTerm, + 'filter': filter, + }, + }); + } /** * Get the authenticated user * Get all user information for the authenticated user diff --git a/src/views/User/UserAddFriend.vue b/src/views/User/UserAddFriend.vue index cb8eb7872e11cf3ee6bfde1ca6f9a9d319f50667..585f5f8c7ec6f69bebe290fe917e46cad1f8eb5e 100644 --- a/src/views/User/UserAddFriend.vue +++ b/src/views/User/UserAddFriend.vue @@ -1,11 +1,22 @@ +<script setup lang="ts"> +import { ref } from 'vue' +import { FriendService } from '@/api'; + +async function addFriend(friendID: number) { + const response = await FriendService.addFriendRequest({ userId: friendID }); + console.log(response); +} +</script> + + <template> <div class="container"> <h1>Add Friend</h1> <div class="row"> <form class="col-md-5" id="searchBox" role="search"> - <input class="form-control me-2 custom-border" type="search" placeholder="Search" aria-label="Search"> - <button class="btn btn-success" type="submit">Search</button> -</form> + <input class="form-control me-2 custom-border" type="search" placeholder="Search" aria-label="Search"> + <button class="btn btn-success" type="submit">Search</button> + </form> <div class="col-md-8"> <div class="people-nearby"> <div class="nearby-user"> @@ -20,7 +31,7 @@ <p class="text-muted">500m away</p> </div> <div class="col-md-3 col-sm-3"> - <button class="btn btn-primary pull-right">Add Friend</button> + <button class="btn btn-primary pull-right" @click="addFriend(1)">Add Friend</button> </div> </div> </div> @@ -221,6 +232,7 @@ img.profile-photo-lg { } .form-control.custom-border { - border-color: #222223; /* Change to your desired color */ + border-color: #222223; + /* Change to your desired color */ } </style> \ No newline at end of file diff --git a/src/views/User/UserFriendsView.vue b/src/views/User/UserFriendsView.vue index ae17297ad9dbe29a6a9ad36e72cda666db433fcd..7cd60ebc235650474548eb74427d14078cb558e6 100644 --- a/src/views/User/UserFriendsView.vue +++ b/src/views/User/UserFriendsView.vue @@ -1,29 +1,108 @@ <template> <div class="container"> <h1>Your Friends</h1> - <button class="btn btn-primary pull-right my-3" @click="addFriend">+ Add Friend</button> - <div class="row"> - <div class="col-lg-3" v-for="friend in friends" :key="friend.id"> - <div class="card card-one"> - <div class="header"> - <div v-if="friend.profileImage" class="avatar"> - <img :src="'http://localhost:8080/api/images/' + friend.profileImage" alt=""> - </div> - <div v-else class="avatar"> - <img :src="'../src/assets/userprofile.png'" alt=""> + <div> + <button class="btn btn-primary pull-right" @click="addNewFriends">+ Add Friend</button> + <div class="my-3"> + <button class="btn pages" @click="setupFriends">Your Friends</button> + <button class="btn pages" @click="requestFriend">Friend Requests</button> + </div> + </div> + <div v-if="showFriends"> + <div v-if="elementsInFriends"> + <div class="row"> + <div class="col-lg-3" v-for="friend in friends" :key="friend.id"> + <div class="card card-one"> + <div class="header"> + <div v-if="friend.profileImage" class="avatar"> + <img :src="'http://localhost:8080/api/images/' + friend.profileImage" alt=""> + </div> + <div v-else class="avatar"> + <img :src="'../src/assets/userprofile.png'" alt=""> + </div> + </div> + <h3><a href="#" class="btn stretched-link" id="profileName" + @click="navigateToFriend(friend.id)">{{ + friend.firstName }}</a></h3> + <div class="desc">{{ friend.firstName }} {{ friend.lastName }}</div> + <div class="contacts"> + <a class="text removeFriend" data-bs-toggle="collapse" + :href="'#collapseExample' + friend.id" role="button" aria-expanded="false" + :aria-controls="'collapseExample' + friend.id"> + See more + </a> + <div class="collapse" :id="'collapseExample' + friend.id"> + <button class="btn btn-danger" @click="removeFriend(friend.id)"> + <h5><img src="@/assets/icons/remove-white.svg" style="width: 30px"> Remove + friend + </h5> + </button> + </div> + </div> </div> </div> - <h3><a href="#" class="btn stretched-link" id="profileName" @click="navigateToFriend(friend.id)">{{ friend.firstName }}</a></h3> - <div class="desc">{{ friend.firstName }} {{ friend.lastName }}</div> - <div class="contacts"> - <a class="text removeFriend" data-bs-toggle="collapse" - :href="'#collapseExample' + friend.id" role="button" aria-expanded="false" :aria-controls="'collapseExample' + friend.id"> - See more - </a> - <div class="collapse" :id="'collapseExample' + friend.id"> - <button class="btn btn-danger" @click="removeFriend(friend.id)"> - <h5><img src="@/assets/icons/remove-white.svg" style="width: 30px"> Remove friend</h5> - </button> + </div> + </div> + <div v-else>No Friends</div> + </div> + <div v-else-if="showRequests" class="row"> + <div class="content-body"> + <div v-if="elementsInFriendRequest" id="requests"> + <div class="request" v-for="(friend) in friendRequests" :key="friend.id"> + <div v-if="friend.profileImage !== null"><img id="profilePicture" + :src="'http://localhost:8080/api/images/' + friend.profileImage" alt="user" + class="profile-photo-lg"></div> + <div v-else><img id="profilePicture" :src="'../src/assets/userprofile.png'" alt="user" + class="profile-photo-lg"></div> + <h2>{{ friend.firstName }}</h2> - <button class="btn btn-success mx-2" + @click="acceptRequest(friend.id)">Accept</button> + <button class="btn btn-danger" @click="rejectRequest(friend.id)">Reject</button> + </div> + </div> + <div v-else>No friend requests</div> + </div> + </div> + <div v-if="showAddFriend" class="modal" tabindex="-1" role="dialog" + style="display:block; background-color: rgba(0,0,0,0.5);"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Add Friend</h5> + <button type="button" class="close" @click="showAddFriend = false"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body d-flex justify-content-center align-items-center flex-column"> + <form class="col-md-10 d-flex justify-content-center align-items-center flex-row my-4" + id="searchBox" role="search" @submit.prevent="searchProfile(searchWord)"> + <input class="form-control me-2 custom-border" type="search" placeholder="Search" + aria-label="Search" v-model="searchWord"> + <button class="btn btn-success" type="submit">Search</button> + </form> + <div class="col-md-12"> + <div class="people-nearby"> + <div v-for="user in searchedUsers" :key="user.id" class="nearby-user"> + <div class="row d-flex align-items-center"> + <div class="col-md-2 col-sm-2"> + <div v-if="user.profileImage !== null"><img id="profilePicture" + :src="'http://localhost:8080/api/images/' + user.profileImage" + alt="user" class="profile-photo-lg"></div> + <div v-else><img id="profilePicture" :src="'../src/assets/userprofile.png'" + alt="user" class="profile-photo-lg"></div> + + </div> + <div class="col-md-7 col-sm-7"> + <h5><a href="#" class="profile-link" @click="toUserProfile(user.id)">{{ + user.firstName }}</a> + </h5> + </div> + <div class="col-md-3 col-sm-3"> + <button class="btn btn-primary pull-right" @click="addFriend(user.id)">Add + Friend</button> + </div> + </div> + </div> + </div> </div> </div> </div> @@ -37,14 +116,65 @@ <script setup lang="ts"> import { ref, onMounted } from 'vue'; import { useRouter } from 'vue-router'; -import { FriendService } from '@/api'; +import { FriendService, UserService } from '@/api'; import type { UserDTO } from '@/api'; const router = useRouter(); const friends = ref(); +const showFriends = ref(true); +const showRequests = ref(false); +const showAddFriend = ref(false); +const friendRequests = ref([] as any); +const addFriends = ref([] as any); +const searchedUsers = ref([] as any); + +const searchWord = ref(""); -function addFriend() { - router.push('/add-friend'); +const elementsInFriendRequest = ref(false); +const elementsInFriends = ref(false); + +const toUserProfile = (userId: number) => { + router.push('/profile/' + userId); +}; + +const searchProfile = async (searchTerm: string) => { + const userPayload = { + searchTerm: searchTerm as string, + filter: 'NON_FRIENDS' as string, + }; + try { + const response = await UserService.getUsersByNameAndFilter(userPayload); + searchedUsers.value = response; + console.log(response); + } catch (error) { + console.error('Failed to search for profile', error); + } +}; + +const addNewFriends = async () => { + try { + //const response = await FriendService.(); + showAddFriend.value = true; + } catch (error) { + console.error('Failed to add friend', error); + } +}; + +async function addFriend(friendID: number) { + const response = await FriendService.addFriendRequest({ userId: friendID }); +} + +async function requestFriend() { + showRequests.value = true; + showFriends.value = false; + try { + const response = await FriendService.getFriendRequests(); + friendRequests.value = response; + elementsInFriendRequest.value = response.length > 0; + console.log("Friend requests: " + response); + } catch (error) { + console.error('Failed to fetch friend requests', error); + } } const navigateToFriend = (friendID: number) => { @@ -53,26 +183,49 @@ const navigateToFriend = (friendID: number) => { const removeFriend = async (friendID: number) => { try { - // Attempt to delete the friend from the backend. await FriendService.deleteFriendOrFriendRequest({ friendId: friendID }); - // Update the friends list by filtering out the removed friend. - friends.value = friends.value.filter((friend: UserDTO) => friend.id !== friendID); + const responseFriends = await FriendService.getFriends(); + friends.value = responseFriends; } catch (error) { console.error('Failed to remove friend', error); } }; - const setupFriends = async () => { + showFriends.value = true; + showRequests.value = false; try { const response = await FriendService.getFriends(); friends.value = response; + elementsInFriends.value = response.length > 0; console.log(response); } catch (error) { console.error('Failed to fetch friends', error); } }; +const acceptRequest = async (requestID: number) => { + try { + await FriendService.acceptFriendRequest({ friendId: requestID }); + const responseRequest = await FriendService.getFriendRequests(); + friendRequests.value = responseRequest; + const responseFriends = await FriendService.getFriends(); + friends.value = responseFriends; + } catch (error) { + console.error('Failed to accept friend request', error); + } +}; + +const rejectRequest = async (requestID: number) => { + try { + await FriendService.deleteFriendOrFriendRequest({ friendId: requestID }); + const response = await FriendService.getFriendRequests(); + friendRequests.value = response; + } catch (error) { + console.error('Failed to reject friend request', error); + } +}; + onMounted(() => { setupFriends(); }); @@ -374,4 +527,39 @@ ul.friend-list .right p { font-weight: 600; width: 100%; } + +#requests { + display: flex; + flex-direction: column; + align-items: center; +} + +.request { + display: flex; + justify-content: center; + align-items: center; + margin: 1rem; +} + +#profilePicture { + width: 70px; + height: 70px; + border-radius: 50%; + margin-right: 1rem; + border: 2px solid #000; +} + +.modal-content { + padding: 1rem; +} + +.modal-header { + margin-bottom: 5px; +} + +.pages { + border-bottom: 1px solid #000; + border-radius: 0px; + margin: 0px 5px; +} </style> \ No newline at end of file