From cae13b73590b8c0447d7069ae4e3e40b860a2341 Mon Sep 17 00:00:00 2001 From: shaden-pr Date: Thu, 9 Oct 2025 23:12:32 +0100 Subject: [PATCH 1/6] implement unfollow --- backend/data/follows.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314..69456b0 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -20,6 +20,19 @@ def follow(follower: User, followee: User): # Already following - treat as idempotent request. pass +def unfollow(follower: User, followee: User): + with db_cursor() as cur: + try: + cur.execute( + "DELETE FROM follows WHERE follower = %(follower_id)s AND followee = %(followee_id)s", + dict( + follower_id=follower.id, + followee_id=followee.id, + ), + ) + except Exception as err: + # handle unexpected errors. + pass def get_followed_usernames(follower: User) -> List[str]: """get_followed_usernames returns a list of usernames followee follows.""" From f8774640da04ca0743863989663b56bf9d9af625 Mon Sep 17 00:00:00 2001 From: shaden-pr Date: Thu, 9 Oct 2025 23:12:58 +0100 Subject: [PATCH 2/6] add unfollow endpoint --- backend/endpoints.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..98c3c53 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, @@ -149,6 +149,22 @@ def do_follow(): } ) +@jwt_required() +def do_unfollow(unfollow_username): + current_user = get_current_user() + 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(): From 9511842a958bbb972a3aa2960d52bfbc401e6e37 Mon Sep 17 00:00:00 2001 From: shaden-pr Date: Thu, 9 Oct 2025 23:14:19 +0100 Subject: [PATCH 3/6] Add URL rule for unfollowing users --- backend/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/main.py b/backend/main.py index 7ba155f..a69c9e9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,6 +4,7 @@ from data.users import lookup_user from endpoints import ( do_follow, + do_unfollow, get_bloom, hashtag, home_timeline, @@ -54,6 +55,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) From 5d90e939a503d8ab98ddccd7397274804a033f22 Mon Sep 17 00:00:00 2001 From: shaden-pr Date: Thu, 9 Oct 2025 23:15:53 +0100 Subject: [PATCH 4/6] feat(ui): add unfollow button to profile component template --- front-end/index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/front-end/index.html b/front-end/index.html index 89d6b13..85966d3 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -186,6 +186,7 @@

Create your account

+

Who to follow

@@ -199,6 +200,7 @@

Who to follow

+
From 03b0e3801a595f768e654eb65beb2d207cc39636 Mon Sep 17 00:00:00 2001 From: shaden-pr Date: Thu, 9 Oct 2025 23:16:47 +0100 Subject: [PATCH 5/6] feat(frontend): handle unfollow in profile component --- front-end/components/profile.mjs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index ec4f200..4ddb8a7 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -19,6 +19,7 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { ); const followerCountEl = profileElement.querySelector("[data-follower-count]"); const followButtonEl = profileElement.querySelector("[data-action='follow']"); + const unfollowButtonEl = profileElement.querySelector("[data-action='unfollow']") const whoToFollowContainer = profileElement.querySelector(".profile__who-to-follow"); // Populate with data usernameEl.querySelector("h2").textContent = profileData.username || ""; @@ -29,8 +30,15 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { followButtonEl.setAttribute("data-username", profileData.username || ""); followButtonEl.hidden = profileData.is_self || profileData.is_following; followButtonEl.addEventListener("click", handleFollow); + + unfollowButtonEl.setAttribute("data-username", profileData.username || ""); + unfollowButtonEl.hidden = profileData.is_self || !profileData.is_following; + unfollowButtonEl.addEventListener("click", handleUnfollow); + if (!isLoggedIn) { followButtonEl.style.display = "none"; + unfollowButtonEl.style.display = "none"; + } if (whoToFollow.length > 0) { @@ -41,9 +49,13 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { const usernameLink = wtfElement.querySelector("a[data-username]"); usernameLink.innerText = userToFollow.username; usernameLink.setAttribute("href", `/profile/${userToFollow.username}`); - const followButton = wtfElement.querySelector("button"); + const followButton = wtfElement.querySelector("button[data-action='follow']"); followButton.setAttribute("data-username", userToFollow.username); followButton.addEventListener("click", handleFollow); + const unfollowButton = wtfElement.querySelector("button[data-action='unfollow']"); + unfollowButton.setAttribute("data-username", userToFollow.username); + unfollowButton.addEventListener("click", handleUnfollow); + if (!isLoggedIn) { followButton.style.display = "none"; } @@ -66,4 +78,13 @@ async function handleFollow(event) { await apiService.getWhoToFollow(); } -export {createProfile, handleFollow}; +async function handleUnfollow(event){ + const button = event.target; + const username = button.getAttribute("data-username"); + if(!username) return; + + await apiService.unfollowUser(username); + await apiService.getWhoToFollow(); +} + +export { createProfile, handleFollow, handleUnfollow}; From 4d89113d1cb5729fe5a3ac844803477d697e8496 Mon Sep 17 00:00:00 2001 From: shaden-pr Date: Fri, 10 Oct 2025 17:06:51 +0100 Subject: [PATCH 6/6] remove: unfollow button and handler from who-to-follow section --- front-end/components/profile.mjs | 3 --- front-end/index.html | 1 - 2 files changed, 4 deletions(-) diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index 4ddb8a7..ce28635 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -52,9 +52,6 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { const followButton = wtfElement.querySelector("button[data-action='follow']"); followButton.setAttribute("data-username", userToFollow.username); followButton.addEventListener("click", handleFollow); - const unfollowButton = wtfElement.querySelector("button[data-action='unfollow']"); - unfollowButton.setAttribute("data-username", userToFollow.username); - unfollowButton.addEventListener("click", handleUnfollow); if (!isLoggedIn) { followButton.style.display = "none"; diff --git a/front-end/index.html b/front-end/index.html index 85966d3..1b83f4c 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -200,7 +200,6 @@

Who to follow

-