From f9fbd413c1acf4a7fb4b19ed7e8fbe403c99f5d1 Mon Sep 17 00:00:00 2001 From: Iheb Hamdi <67348203+hamdiheb@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:55:55 +0200 Subject: [PATCH 1/4] Created new function that unfollow --- backend/data/follows.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314e..8377907d 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -20,6 +20,20 @@ def follow(follower: User, followee: User): # Already following - treat as idempotent request. pass +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 get_followed_usernames(follower: User) -> List[str]: """get_followed_usernames returns a list of usernames followee follows.""" From abf5a9850e01d9a62d2b9ffa2e39bbd8268eab1b Mon Sep 17 00:00:00 2001 From: Iheb Hamdi <67348203+hamdiheb@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:57:16 +0200 Subject: [PATCH 2/4] End point created to unfollow --- backend/endpoints.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a07..21a333cf 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -1,6 +1,11 @@ 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 +155,24 @@ def do_follow(): ) +@jwt_required() +def do_unfollow(profile_username): + current_user = get_current_user() + + follow_user = get_user(profile_username) + if follow_user is None: + return make_response( + (f"Cannot unfollow {profile_username} - user does not exist", 404) + ) + + unfollow(current_user, follow_user) + + return jsonify( + { + "success": True, + } + ) + @jwt_required() def send_bloom(): type_check_error = verify_request_fields({"content": str}) From 4e03669b2253384b051adb8aa96173be33aa31fd Mon Sep 17 00:00:00 2001 From: Iheb Hamdi <67348203+hamdiheb@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:58:21 +0200 Subject: [PATCH 3/4] Added the unfollow route --- backend/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/main.py b/backend/main.py index 7ba155fa..4bc4b627 100644 --- a/backend/main.py +++ b/backend/main.py @@ -54,6 +54,11 @@ 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 49262e2c054ba7fdddb9cd15217e934b063b28a2 Mon Sep 17 00:00:00 2001 From: Iheb Hamdi <67348203+hamdiheb@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:14:53 +0200 Subject: [PATCH 4/4] Function unfollow added to the profile --- backend/main.py | 1 + front-end/components/profile.mjs | 97 ++++++++++++++++++-------------- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/backend/main.py b/backend/main.py index 4bc4b627..eab82226 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, diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index ec4f2009..e240d435 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,64 +6,75 @@ import {apiService} from "../index.mjs"; * @param {Object} profileData - The profile data to display * @returns {DocumentFragment} - The profile UI */ -function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { - if (!template || !profileData) return; - const profileElement = document - .getElementById(template) - .content.cloneNode(true); +function createProfile(template, { profileData, whoToFollow, isLoggedIn }) { + if (!template || !profileData) return + const profileElement = document.getElementById(template).content.cloneNode(true) - const usernameEl = profileElement.querySelector("[data-username]"); - const bloomCountEl = profileElement.querySelector("[data-bloom-count]"); - const followingCountEl = profileElement.querySelector( - "[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 usernameEl = profileElement.querySelector('[data-username]') + const bloomCountEl = profileElement.querySelector('[data-bloom-count]') + const followingCountEl = profileElement.querySelector('[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') // 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); - if (!isLoggedIn) { - followButtonEl.style.display = "none"; + 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 || '') + if (profileData.is_following) { + followButtonEl.textContent = 'Unfollow' + } else { + followButtonEl.textContent = 'Follow' + } + followButtonEl.hidden = profileData.is_self + if (profileData.is_following) { + followButtonEl.addEventListener('click', handleUnfollow) + } else { + followButtonEl.addEventListener('click', handleFollow) } if (whoToFollow.length > 0) { - const whoToFollowList = whoToFollowContainer.querySelector("[data-who-to-follow]"); - const whoToFollowTemplate = document.querySelector("#who-to-follow-chip"); + 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); - const usernameLink = wtfElement.querySelector("a[data-username]"); - usernameLink.innerText = userToFollow.username; - usernameLink.setAttribute("href", `/profile/${userToFollow.username}`); - const followButton = wtfElement.querySelector("button"); - followButton.setAttribute("data-username", userToFollow.username); - followButton.addEventListener("click", handleFollow); + const wtfElement = whoToFollowTemplate.content.cloneNode(true) + const usernameLink = wtfElement.querySelector('a[data-username]') + usernameLink.innerText = userToFollow.username + usernameLink.setAttribute('href', `/profile/${userToFollow.username}`) + const followButton = wtfElement.querySelector('button') + followButton.setAttribute('data-username', userToFollow.username) + followButton.addEventListener('click', handleFollow) if (!isLoggedIn) { - followButton.style.display = "none"; + followButton.style.display = 'none' } - whoToFollowList.appendChild(wtfElement); + whoToFollowList.appendChild(wtfElement) } } else { - whoToFollowContainer.innerText = ""; + whoToFollowContainer.innerText = '' } - return profileElement; + return profileElement } async function handleFollow(event) { - const button = event.target; - const username = button.getAttribute("data-username"); - if (!username) return; + const button = event.target + const username = button.getAttribute('data-username') + if (!username) return + + await apiService.followUser(username) + await apiService.getWhoToFollow() +} + +async function handleUnfollow(event) { + const button = event.target + const username = button.getAttribute('data-username') + if (!username) return - await apiService.followUser(username); - await apiService.getWhoToFollow(); + await apiService.unfollowUser(username) + await apiService.getWhoToFollow() } -export {createProfile, handleFollow}; +export { createProfile, handleFollow, handleUnfollow }