From acbded65a52f9a8ab54f8a4b739414c406189bd5 Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Thu, 2 Apr 2026 13:24:27 +0100 Subject: [PATCH] add unfollow feature --- backend/data/follows.py | 8 +++++ backend/endpoints.py | 21 +++++++++++++- backend/main.py | 2 ++ front-end/components/profile.mjs | 50 ++++++++++++++++++++++++++------ front-end/index.html | 17 +++++------ 5 files changed, 78 insertions(+), 20 deletions(-) diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314..f3916e4 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -20,6 +20,14 @@ def follow(follower: User, followee: User): # Already following - treat as idempotent request. pass +#unfollow function +def unfollow(follower: User, followee: User): + """Remove a follow relationship between follower and followee.""" + with db_cursor()as cur: + cur.execute( + "DELETE FROM follows WHERE follower = %s AND followee = %s", + (follower.id, followee.id), + ) def get_followed_usernames(follower: User) -> List[str]: """get_followed_usernames returns a list of usernames followee follows.""" diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..aad5439 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, @@ -150,6 +150,25 @@ def do_follow(): ) +@jwt_required() +def do_unfollow(unfollow_username): + """Remove a follow relationship - unfollow a user.""" + 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(): type_check_error = verify_request_fields({"content": str}) 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) diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index ec4f200..8819717 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -1,4 +1,4 @@ -import {apiService} from "../index.mjs"; +import { apiService } from "../index.mjs"; /** * Create a profile component @@ -6,7 +6,7 @@ import {apiService} from "../index.mjs"; * @param {Object} profileData - The profile data to display * @returns {DocumentFragment} - The profile UI */ -function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { +function createProfile(template, { profileData, whoToFollow, isLoggedIn }) { if (!template || !profileData) return; const profileElement = document .getElementById(template) @@ -15,26 +15,39 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { const usernameEl = profileElement.querySelector("[data-username]"); const bloomCountEl = profileElement.querySelector("[data-bloom-count]"); const followingCountEl = profileElement.querySelector( - "[data-following-count]" + "[data-following-count]", ); const followerCountEl = profileElement.querySelector("[data-follower-count]"); const followButtonEl = profileElement.querySelector("[data-action='follow']"); - const whoToFollowContainer = profileElement.querySelector(".profile__who-to-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 || ""; 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.dataset.username = profileData.username || ""; followButtonEl.hidden = profileData.is_self || profileData.is_following; followButtonEl.addEventListener("click", handleFollow); + + unfollowButtonEl.dataset.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) { - const whoToFollowList = whoToFollowContainer.querySelector("[data-who-to-follow]"); + const whoToFollowList = whoToFollowContainer.querySelector( + "[data-who-to-follow]", + ); const whoToFollowTemplate = document.querySelector("#who-to-follow-chip"); for (const userToFollow of whoToFollow) { const wtfElement = whoToFollowTemplate.content.cloneNode(true); @@ -42,7 +55,7 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { usernameLink.innerText = userToFollow.username; usernameLink.setAttribute("href", `/profile/${userToFollow.username}`); const followButton = wtfElement.querySelector("button"); - followButton.setAttribute("data-username", userToFollow.username); + followButton.dataset.username = userToFollow.username; followButton.addEventListener("click", handleFollow); if (!isLoggedIn) { followButton.style.display = "none"; @@ -59,11 +72,30 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { async function handleFollow(event) { const button = event.target; - const username = button.getAttribute("data-username"); + const username = button.dataset.username; if (!username) return; await apiService.followUser(username); await apiService.getWhoToFollow(); } -export {createProfile, handleFollow}; +async function handleUnfollow(event) { + const button = event.target; + const username = button.dataset.username; + if (!username) return; + + const originalText = button.textContent; + + try { + button.disabled = true; + button.textContent = "Unfollowing..."; + + await apiService.unfollowUser(username); + await apiService.getWhoToFollow(); + } finally { + button.textContent = originalText; + button.disabled = false; + } +} + +export { createProfile, handleFollow, handleUnfollow }; diff --git a/front-end/index.html b/front-end/index.html index 89d6b13..e2bd3e5 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -1,4 +1,4 @@ - + @@ -10,12 +10,7 @@

- Purple Forest + Purple Forest PurpleForest

@@ -186,11 +181,11 @@

Create your account

+

Who to follow

-
    -
+
    @@ -236,7 +231,9 @@

    Share a Bloom