diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314..10b50c6 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -5,6 +5,18 @@ from psycopg2.errors import UniqueViolation +# Unfollow + +def unfollow(follower: User, followee: User): + with db_cursor() as cur: + cur.execute( + "DELETE FROM follows WHERE follower = %(follower_id)s AND followee = %(followee_id)s", + dict( + follower_id=follower.id, + followee_id=followee.id, + ), + ) + def follow(follower: User, followee: User): with db_cursor() as cur: @@ -41,3 +53,4 @@ def get_inverse_followed_usernames(followee: User) -> List[str]: ) rows = cur.fetchall() return [row[0] for row in rows] + diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..606eadf 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -1,6 +1,6 @@ from typing import Dict, Union from data import blooms -from data.follows import follow, get_followed_usernames, get_inverse_followed_usernames +from data.follows import follow, unfollow, get_followed_usernames, get_inverse_followed_usernames from data.users import ( UserRegistrationError, get_suggested_follows, @@ -24,6 +24,7 @@ def login(): type_check_error = verify_request_fields({"username": str, "password": str}) if type_check_error is not None: return type_check_error + assert request.json is not None user = get_user(request.json["username"]) if user is None: return make_response(({"success": False, "message": "Unknown user"}, 403)) @@ -150,6 +151,30 @@ def do_follow(): ) +# Unfollow +@jwt_required() +def do_unfollow(): + type_check_error = verify_request_fields({"unfollow_username": str}) + if type_check_error is not None: + return type_check_error + + current_user = get_current_user() + + unfollow_username = request.json["unfollow_username"] + unfollow_user = get_user(unfollow_username) + if unfollow_user is None: + return make_response( + (f"Cannot unfollow {unfollow_username} - user does not exist", 404) + ) + + unfollow(current_user, unfollow_user) + return jsonify( + { + "success": True, + } + ) + + @jwt_required() def send_bloom(): type_check_error = verify_request_fields({"content": str}) @@ -244,4 +269,4 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No 400, ) ) - return None + return None \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 7ba155f..23c42e7 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,5 +1,5 @@ import os - +from endpoints import do_unfollow from custom_json_provider import CustomJsonProvider from data.users import lookup_user from endpoints import ( @@ -54,6 +54,7 @@ def main(): app.add_url_rule("/profile", view_func=self_profile) app.add_url_rule("/profile/", view_func=other_profile) app.add_url_rule("/follow", methods=["POST"], view_func=do_follow) + app.add_url_rule("/unfollow", methods=["POST"], view_func=do_unfollow) app.add_url_rule("/suggested-follows/", view_func=suggested_follows) app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom) diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index ec4f200..f1cd281 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -20,15 +20,25 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { const followerCountEl = profileElement.querySelector("[data-follower-count]"); const followButtonEl = profileElement.querySelector("[data-action='follow']"); const whoToFollowContainer = profileElement.querySelector(".profile__who-to-follow"); - // Populate with data + usernameEl.querySelector("h2").textContent = profileData.username || ""; usernameEl.setAttribute("href", `/profile/${profileData.username}`); bloomCountEl.textContent = profileData.total_blooms || 0; followerCountEl.textContent = profileData.followers?.length || 0; followingCountEl.textContent = profileData.follows?.length || 0; + followButtonEl.setAttribute("data-username", profileData.username || ""); - followButtonEl.hidden = profileData.is_self || profileData.is_following; - followButtonEl.addEventListener("click", handleFollow); + followButtonEl.setAttribute("data-action", "follow"); + followButtonEl.hidden = profileData.is_self; + + if (profileData.is_following) { + followButtonEl.textContent = "Unfollow"; + } else { + followButtonEl.textContent = "Follow"; + } + + // followButtonEl.addEventListener("click", handleFollow); + if (!isLoggedIn) { followButtonEl.style.display = "none"; } @@ -43,11 +53,11 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { usernameLink.setAttribute("href", `/profile/${userToFollow.username}`); const followButton = wtfElement.querySelector("button"); followButton.setAttribute("data-username", userToFollow.username); + followButton.setAttribute("data-action", "follow"); followButton.addEventListener("click", handleFollow); if (!isLoggedIn) { followButton.style.display = "none"; } - whoToFollowList.appendChild(wtfElement); } } else { @@ -62,8 +72,16 @@ async function handleFollow(event) { const username = button.getAttribute("data-username"); if (!username) return; - await apiService.followUser(username); + if (button.textContent === "Unfollow") { + await apiService.unfollowUser(username); + button.textContent = "Follow"; + } else { + await apiService.followUser(username); + button.textContent = "Unfollow"; + } + await apiService.getWhoToFollow(); } -export {createProfile, handleFollow}; +// export {createProfile, handleFollow}; +export {createProfile}; \ No newline at end of file diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index f4b5339..67f8a2b 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -261,12 +261,13 @@ async function followUser(username) { async function unfollowUser(username) { try { - const data = await _apiRequest(`/unfollow/${username}`, { + const data = await _apiRequest("/unfollow", { method: "POST", + body: JSON.stringify({follow_username: username}), }); if (data.success) { - // Update both the unfollowed user's profile and the current user's profile + // Update both the unfollowed user's profile and the current user's profile await Promise.all([ getProfile(username), getProfile(state.currentUser), @@ -276,7 +277,7 @@ async function unfollowUser(username) { return data; } catch (error) { - // Error already handled by _apiRequest + // Error already handled by _apiRequest return {success: false}; } } diff --git a/front-end/views/profile.mjs b/front-end/views/profile.mjs index dd2b92a..0ce0fa0 100644 --- a/front-end/views/profile.mjs +++ b/front-end/views/profile.mjs @@ -9,7 +9,7 @@ import { } from "../index.mjs"; import {createLogin, handleLogin} from "../components/login.mjs"; import {createLogout, handleLogout} from "../components/logout.mjs"; -import {createProfile, handleFollow} from "../components/profile.mjs"; +import {createProfile} from "../components/profile.mjs"; import {createBloom} from "../components/bloom.mjs"; // Profile view - just this person's blooms and their profile