From 15fb667064d99b7fc4d9cf1ddb41e0195dd4b835 Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Sun, 28 Sep 2025 23:33:55 +0100 Subject: [PATCH 1/9] code update to fix login issue --- front-end/lib/api.mjs | 62 ++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index f4b5339..8a5f5bc 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -1,5 +1,5 @@ -import {state} from "../index.mjs"; -import {handleErrorDialog} from "../components/error.mjs"; +import { state } from "../index.mjs"; +import { handleErrorDialog } from "../components/error.mjs"; // === ABOUT THE STATE // state gives you these two functions only @@ -20,13 +20,13 @@ async function _apiRequest(endpoint, options = {}) { const defaultOptions = { headers: { "Content-Type": "application/json", - ...(token ? {Authorization: `Bearer ${token}`} : {}), + ...(token ? { Authorization: `Bearer ${token}` } : {}), }, mode: "cors", credentials: "include", }; - const fetchOptions = {...defaultOptions, ...options}; + const fetchOptions = { ...defaultOptions, ...options }; const url = endpoint.startsWith("http") ? endpoint : `${baseUrl}${endpoint}`; try { @@ -54,7 +54,7 @@ async function _apiRequest(endpoint, options = {}) { const contentType = response.headers.get("content-type"); return contentType?.includes("application/json") ? await response.json() - : {success: true}; + : { success: true }; } catch (error) { if (!error.status) { // Only handle network errors here, response errors are handled above @@ -70,11 +70,11 @@ function _updateProfile(username, profileData) { const index = profiles.findIndex((p) => p.username === username); if (index !== -1) { - profiles[index] = {...profiles[index], ...profileData}; + profiles[index] = { ...profiles[index], ...profileData }; } else { - profiles.push({username, ...profileData}); + profiles.push({ username, ...profileData }); } - state.updateState({profiles}); + state.updateState({ profiles }); } // ====== AUTH methods @@ -82,7 +82,7 @@ async function login(username, password) { try { const data = await _apiRequest("/login", { method: "POST", - body: JSON.stringify({username, password}), + body: JSON.stringify({ username, password }), }); if (data.success && data.token) { @@ -91,12 +91,13 @@ async function login(username, password) { currentUser: username, isLoggedIn: true, }); + window.location.hash = "/"; // ensures hash router loads home view await Promise.all([getBlooms(), getProfile(username), getWhoToFollow()]); } return data; } catch (error) { - return {success: false}; + return { success: false }; } } @@ -104,12 +105,12 @@ async function getWhoToFollow() { try { const usernamesToFollow = await _apiRequest("/suggested-follows/3"); - state.updateState({whoToFollow: usernamesToFollow}); + state.updateState({ whoToFollow: usernamesToFollow }); return usernamesToFollow; } catch (error) { // Error already handled by _apiRequest - state.updateState({usernamesToFollow: []}); + state.updateState({ usernamesToFollow: [] }); return []; } } @@ -118,7 +119,7 @@ async function signup(username, password) { try { const data = await _apiRequest("/register", { method: "POST", - body: JSON.stringify({username, password}), + body: JSON.stringify({ username, password }), }); if (data.success && data.token) { @@ -132,20 +133,21 @@ async function signup(username, password) { return data; } catch (error) { - return {success: false}; + return { success: false }; } } function logout() { state.destroyState(); - return {success: true}; + window.location.hash = "/"; // redirect to home page after logout + return { success: true }; } // ===== BLOOM methods async function getBloom(bloomId) { const endpoint = `/bloom/${bloomId}`; const bloom = await _apiRequest(endpoint); - state.updateState({singleBloomToShow: bloom}); + state.updateState({ singleBloomToShow: bloom }); return bloom; } @@ -156,18 +158,18 @@ async function getBlooms(username) { const blooms = await _apiRequest(endpoint); if (username) { - _updateProfile(username, {blooms}); + _updateProfile(username, { blooms }); } else { - state.updateState({timelineBlooms: blooms}); + state.updateState({ timelineBlooms: blooms }); } return blooms; } catch (error) { // Error already handled by _apiRequest if (username) { - _updateProfile(username, {blooms: []}); + _updateProfile(username, { blooms: [] }); } else { - state.updateState({timelineBlooms: []}); + state.updateState({ timelineBlooms: [] }); } return []; } @@ -189,7 +191,7 @@ async function getBloomsByHashtag(hashtag) { return blooms; } catch (error) { // Error already handled by _apiRequest - return {success: false}; + return { success: false }; } } @@ -197,7 +199,7 @@ async function postBloom(content) { try { const data = await _apiRequest("/bloom", { method: "POST", - body: JSON.stringify({content}), + body: JSON.stringify({ content }), }); if (data.success) { @@ -208,7 +210,7 @@ async function postBloom(content) { return data; } catch (error) { // Error already handled by _apiRequest - return {success: false}; + return { success: false }; } } @@ -225,16 +227,16 @@ async function getProfile(username) { const currentUsername = profileData.username; const fullProfileData = await _apiRequest(`/profile/${currentUsername}`); _updateProfile(currentUsername, fullProfileData); - state.updateState({currentUser: currentUsername, isLoggedIn: true}); + state.updateState({ currentUser: currentUsername, isLoggedIn: true }); } return profileData; } catch (error) { // Error already handled by _apiRequest if (!username) { - state.updateState({isLoggedIn: false, currentUser: null}); + state.updateState({ isLoggedIn: false, currentUser: null }); } - return {success: false}; + return { success: false }; } } @@ -242,7 +244,7 @@ async function followUser(username) { try { const data = await _apiRequest("/follow", { method: "POST", - body: JSON.stringify({follow_username: username}), + body: JSON.stringify({ follow_username: username }), }); if (data.success) { @@ -255,7 +257,7 @@ async function followUser(username) { return data; } catch (error) { - return {success: false}; + return { success: false }; } } @@ -277,7 +279,7 @@ async function unfollowUser(username) { return data; } catch (error) { // Error already handled by _apiRequest - return {success: false}; + return { success: false }; } } @@ -300,4 +302,4 @@ const apiService = { getWhoToFollow, }; -export {apiService}; +export { apiService }; From 0e95faf0213fd96c16ccc4d1f7fd62f8bac1eecb Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Mon, 29 Sep 2025 01:27:09 +0100 Subject: [PATCH 2/9] update code for exceed bloom length --- backend/data/blooms.py | 2 ++ front-end/components/bloom-form.mjs | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf..6121ffb 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -16,6 +16,8 @@ class Bloom: def add_bloom(*, sender: User, content: str) -> Bloom: + if len(content) > 280: + raise ValueError("Bloom content must not exceed 280 character limit.") hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] now = datetime.datetime.now(tz=datetime.UTC) diff --git a/front-end/components/bloom-form.mjs b/front-end/components/bloom-form.mjs index e047f9a..5671e57 100644 --- a/front-end/components/bloom-form.mjs +++ b/front-end/components/bloom-form.mjs @@ -1,4 +1,4 @@ -import {apiService} from "../index.mjs"; +import { apiService } from "../index.mjs"; /** * Create a bloom form component @@ -26,6 +26,11 @@ async function handleBloomSubmit(event) { const originalText = submitButton.textContent; const textarea = form.querySelector("textarea"); const content = textarea.value.trim(); + const charMaxLength = 280; + if (content.length > charMaxLength) { + alert(`Bloom content must not be ${charMaxLength} characters.`); + return; + } try { // Make form inert while we call the back end @@ -55,4 +60,4 @@ function handleTyping(event) { counter.textContent = `${textarea.value.length} / ${maxLength}`; } -export {createBloomForm, handleBloomSubmit, handleTyping}; +export { createBloomForm, handleBloomSubmit, handleTyping }; From f9b3e0fe37f08c43b03a7d03235b410884c979fe Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Mon, 29 Sep 2025 10:28:49 +0100 Subject: [PATCH 3/9] chane in alert message --- front-end/components/bloom-form.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/components/bloom-form.mjs b/front-end/components/bloom-form.mjs index 5671e57..6a71b98 100644 --- a/front-end/components/bloom-form.mjs +++ b/front-end/components/bloom-form.mjs @@ -28,7 +28,7 @@ async function handleBloomSubmit(event) { const content = textarea.value.trim(); const charMaxLength = 280; if (content.length > charMaxLength) { - alert(`Bloom content must not be ${charMaxLength} characters.`); + alert(`Bloom content must be 280 characters or less.`); return; } From 7e8ce68a5f38aa206735948cb0e91a57f70cda2c Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Mon, 29 Sep 2025 11:56:43 +0100 Subject: [PATCH 4/9] update in code for hashtag work properly --- front-end/components/bloom.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c..3d5951b 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -36,9 +36,10 @@ const createBloom = (template, bloom) => { function _formatHashtags(text) { if (!text) return text; + // special character in hashtag convert into url friendly format return text.replace( - /\B#[^#]+/g, - (match) => `${match}` + /\B#(\w+)/g, + (match, tag) => `${match}` ); } @@ -84,4 +85,4 @@ function _formatTimestamp(timestamp) { } } -export {createBloom}; +export { createBloom }; From 59760e8169f09727b8aa49a264051f6d7746b7b8 Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Mon, 29 Sep 2025 15:04:45 +0100 Subject: [PATCH 5/9] update in code for hashtag slow down --- front-end/views/hashtag.mjs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/front-end/views/hashtag.mjs b/front-end/views/hashtag.mjs index 7b7e996..181b8e7 100644 --- a/front-end/views/hashtag.mjs +++ b/front-end/views/hashtag.mjs @@ -1,4 +1,4 @@ -import {renderOne, renderEach, destroy} from "../lib/render.mjs"; +import { renderOne, renderEach, destroy } from "../lib/render.mjs"; import { state, apiService, @@ -7,17 +7,19 @@ import { getTimelineContainer, getHeadingContainer, } from "../index.mjs"; -import {createLogin, handleLogin} from "../components/login.mjs"; -import {createLogout, handleLogout} from "../components/logout.mjs"; -import {createBloom} from "../components/bloom.mjs"; -import {createHeading} from "../components/heading.mjs"; +import { createLogin, handleLogin } from "../components/login.mjs"; +import { createLogout, handleLogout } from "../components/logout.mjs"; +import { createBloom } from "../components/bloom.mjs"; +import { createHeading } from "../components/heading.mjs"; // Hashtag view: show all tweets containing this tag function hashtagView(hashtag) { + const formattedHashtag = hashtag.startsWith("#") ? hashtag : `#${hashtag}`; destroy(); - - apiService.getBloomsByHashtag(hashtag); + if (formattedHashtag !== state.currentHashtag) { + apiService.getBloomsByHashtag(hashtag); + } renderOne( state.isLoggedIn, @@ -52,4 +54,4 @@ function hashtagView(hashtag) { ); } -export {hashtagView}; +export { hashtagView }; From 46068f3b5649a35cb71956a337909d9a64652698 Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Tue, 30 Sep 2025 21:25:19 +0100 Subject: [PATCH 6/9] add unfollow function --- backend/endpoints.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..8560e1a 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -245,3 +245,18 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No ) ) return None + +@jwt_required() +def do_unfollow(username): + current_user = get_current_user() + target_user = get_user(username) + if target_user is None: + return make_response( + ({"success": False, "message": f"Cannot unfollow {username} - user does not exist"}, 404) + ) + + from data.follows import unfollow + unfollow(current_user, target_user) + + return jsonify({"success": True}) + From 70ea242f2a9036b97afa3a24f23b392b86311ad5 Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Tue, 30 Sep 2025 21:25:49 +0100 Subject: [PATCH 7/9] add api url for unfollow --- backend/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/main.py b/backend/main.py index 7ba155f..b07525f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -14,6 +14,7 @@ send_bloom, suggested_follows, user_blooms, + do_unfollow, ) from dotenv import load_dotenv @@ -54,6 +55,8 @@ 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/", view_func=do_unfollow, methods=["POST"]) + app.add_url_rule("/suggested-follows/", view_func=suggested_follows) app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom) From f359aaf56d1d6d5e17150436d8b99125fff6caf8 Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Tue, 30 Sep 2025 21:26:17 +0100 Subject: [PATCH 8/9] add unfollow function --- backend/data/follows.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314..916ef73 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -41,3 +41,11 @@ def get_inverse_followed_usernames(followee: User) -> List[str]: ) rows = cur.fetchall() return [row[0] for row in rows] + +def unfollow(follower: User, followee: User): + with db_cursor() as cur: + cur.execute( + "DELETE FROM follows WHERE follower = %s AND followee = %s", + (follower.id, followee.id), + ) + From 1ff8426c33a06e174c1afba108438ec97085425d Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Wed, 1 Oct 2025 10:53:10 +0100 Subject: [PATCH 9/9] add unfollow code in createProfile --- front-end/components/profile.mjs | 37 ++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index ec4f200..3aea09e 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) @@ -19,7 +19,9 @@ 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"); + const whoToFollowContainer = profileElement.querySelector( + ".profile__who-to-follow" + ); // Populate with data usernameEl.querySelector("h2").textContent = profileData.username || ""; usernameEl.setAttribute("href", `/profile/${profileData.username}`); @@ -27,22 +29,34 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { 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 (profileData.is_self) { + followButtonEl.style.display = "none"; + } else if (profileData.is_following) { + followButtonEl.textContent = "Unfollow"; + followButtonEl.addEventListener("click", handleUnfollow); + } else { + // Not following → show follow + followButtonEl.textContent = "Follow"; + followButtonEl.addEventListener("click", handleFollow); + } if (!isLoggedIn) { followButtonEl.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) { + if (userToFollow.username === profileData.username) continue; 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.textContent = "Follow"; followButton.addEventListener("click", handleFollow); if (!isLoggedIn) { followButton.style.display = "none"; @@ -65,5 +79,14 @@ async function handleFollow(event) { 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.unfollowUser(username); + await apiService.getProfile(username); // refresh profile state + await apiService.getWhoToFollow(); +} -export {createProfile, handleFollow}; +export { createProfile, handleFollow, handleUnfollow };