From 15fb667064d99b7fc4d9cf1ddb41e0195dd4b835 Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Sun, 28 Sep 2025 23:33:55 +0100 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 05/11] 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 ee529a766417b703d1e487cefd54844850d686b4 Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Wed, 8 Oct 2025 13:21:31 +0100 Subject: [PATCH 06/11] add rebloom function --- backend/endpoints.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..39b9490 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -175,7 +175,7 @@ def get_bloom(id_str): bloom = blooms.get_bloom(id_int) if bloom is None: return make_response((f"Bloom not found", 404)) - return jsonify(bloom) + return jsonify(blooms.bloom_to_dict(bloom)) @jwt_required() @@ -245,3 +245,25 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No ) ) return None + +@jwt_required() +def rebloom(id_str): + try: + original_id = int(id_str) + except ValueError: + return make_response(("Invalid bloom id", 400)) + + user = get_current_user() + + # Check original bloom exists + original = blooms.get_bloom(original_id) + if original is None: + return make_response(("Original bloom not found", 404)) + + new_rebloom = blooms.add_rebloom(sender=user, original_bloom_id=original_id) + + return jsonify({ + "success": True, + "rebloom_id": new_rebloom.id, + "original_bloom_id": original_id + }) From 8b06624ca19fa98edcddc5b09b4f1a13784de797 Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Wed, 8 Oct 2025 13:28:53 +0100 Subject: [PATCH 07/11] add endpoint for rebloom function --- backend/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/main.py b/backend/main.py index 7ba155f..1bacc4b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -14,6 +14,7 @@ send_bloom, suggested_follows, user_blooms, + rebloom, ) from dotenv import load_dotenv @@ -59,6 +60,8 @@ def main(): app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom) app.add_url_rule("/bloom/", methods=["GET"], view_func=get_bloom) app.add_url_rule("/blooms/", view_func=user_blooms) + app.add_url_rule("/rebloom/", methods=["POST"], view_func=rebloom) + app.add_url_rule("/hashtag/", view_func=hashtag) app.run(host="0.0.0.0", port="3000", debug=True) From a14df378bf074c68a26ca98f3f0c1886db09a0a9 Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Wed, 8 Oct 2025 13:29:51 +0100 Subject: [PATCH 08/11] add rebloom and bloomtoDict function --- backend/data/blooms.py | 70 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 6121ffb..c4789b2 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -13,6 +13,10 @@ class Bloom: sender: User content: str sent_timestamp: datetime.datetime + type:str= "bloom" + original_bloom_id:Optional[int] = None + rebloomed_by: Optional[User] = None + def add_bloom(*, sender: User, content: str) -> Bloom: @@ -39,13 +43,13 @@ def add_bloom(*, sender: User, content: str) -> Bloom: ) + def get_blooms_for_user( username: str, *, before: Optional[int] = None, limit: Optional[int] = None ) -> List[Bloom]: with db_cursor() as cur: - kwargs = { - "sender_username": username, - } + kwargs = {"sender_username": username} + if before is not None: before_clause = "AND send_timestamp < %(before_limit)s" kwargs["before_limit"] = before @@ -55,48 +59,59 @@ def get_blooms_for_user( limit_clause = make_limit_clause(limit, kwargs) cur.execute( - f"""SELECT - blooms.id, users.username, content, send_timestamp - FROM - blooms INNER JOIN users ON users.id = blooms.sender_id - WHERE - username = %(sender_username)s - {before_clause} + f""" + SELECT + blooms.id, + users.username, + blooms.content, + blooms.send_timestamp, + blooms.type, + blooms.original_bloom_id + FROM blooms + INNER JOIN users ON users.id = blooms.sender_id + WHERE username = %(sender_username)s + {before_clause} ORDER BY send_timestamp DESC {limit_clause} """, kwargs, ) + rows = cur.fetchall() blooms = [] for row in rows: - bloom_id, sender_username, content, timestamp = row + bloom_id, sender_username, content, timestamp, type_, original_id = row blooms.append( Bloom( id=bloom_id, sender=sender_username, content=content, sent_timestamp=timestamp, + type=type_, + original_bloom_id=original_id, ) ) return blooms + def get_bloom(bloom_id: int) -> Optional[Bloom]: with db_cursor() as cur: cur.execute( - "SELECT blooms.id, users.username, content, send_timestamp FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE blooms.id = %s", + "SELECT blooms.id, users.username, content, send_timestamp, type, original_bloom_id FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE blooms.id = %s", (bloom_id,), ) row = cur.fetchone() if row is None: return None - bloom_id, sender_username, content, timestamp = row + bloom_id, sender_username, content, timestamp, type_, original_id = row return Bloom( id=bloom_id, sender=sender_username, content=content, sent_timestamp=timestamp, + type=type_, + original_bloom_id=original_id, ) @@ -142,3 +157,32 @@ def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str: else: limit_clause = "" return limit_clause + +def add_rebloom(*, sender:User, original_bloom_id: int)-> Bloom: + now = datetime.datetime.now(tz=datetime.UTC) + rebloom_id= int(now.timestamp()* 1000000) + with db_cursor() as cur: + # Insert rebloom + cur.execute( + """INSERT INTO blooms (id, sender_id, content, send_timestamp, type, original_bloom_id) + VALUES (%(rebloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s, 'rebloom', %(original_bloom_id)s)""", + dict( + rebloom_id=rebloom_id, + sender_id=sender.id, + content="", # rebloom doesn’t need its own text + timestamp=now, + original_bloom_id=original_bloom_id, + ), + ) + return get_bloom(rebloom_id) + +def bloom_to_dict(bloom: Bloom) -> Dict[str, Any]: + return { + "id": bloom.id, + "sender": bloom.sender if isinstance(bloom.sender, str) else bloom.sender.username, + "content": bloom.content, + "sent_timestamp": bloom.sent_timestamp.isoformat(), + "type": bloom.type, + "original_bloom_id": bloom.original_bloom_id, + } + From 21e7f720dce7151dc5a475800383cff1e59d7d81 Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Wed, 8 Oct 2025 13:30:28 +0100 Subject: [PATCH 09/11] add design for rebloom button --- front-end/index.css | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/front-end/index.css b/front-end/index.css index 65c7fb4..c0bf081 100644 --- a/front-end/index.css +++ b/front-end/index.css @@ -254,6 +254,35 @@ dialog { color: var(--error); } +/* REBLOOM BUTTON */ +.rebloom-button { + font: 600 90% monospace, system-ui; + background-color: var(--paper); + color: var(--brand); + border: 0.5px solid var(--brand); + border-radius: var(--pill); + box-shadow: 2px 3px var(--brand); + padding: calc(var(--space) / 4) calc(var(--space)); + cursor: pointer; + transition: all 0.3s ease; + margin-top: calc(var(--space) / 3); + align-self: start; +} + +.rebloom-button:hover, +.rebloom-button:focus { + background-color: var(--brand); + color: var(--paper); + box-shadow: 0 0 var(--accent); + border-color: var(--brand); +} + +.rebloom-button:disabled { + opacity: 0.6; + cursor: not-allowed; + box-shadow: none; +} + @keyframes fade { from { opacity: 0%; From a6e5104c6c9dae4758f289a83b2359432f2a60dc Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Wed, 8 Oct 2025 13:31:39 +0100 Subject: [PATCH 10/11] code update in create bloom function for rebloom --- front-end/components/bloom.mjs | 65 +++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 3d5951b..8a8ecc0 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -1,3 +1,4 @@ +import { apiService } from "../lib/api.mjs"; /** * Create a bloom component * @param {string} template - The ID of the template to clone @@ -26,10 +27,66 @@ const createBloom = (template, bloom) => { bloomUsername.textContent = bloom.sender; bloomTime.textContent = _formatTimestamp(bloom.sent_timestamp); bloomTimeLink.setAttribute("href", `/bloom/${bloom.id}`); - bloomContent.replaceChildren( - ...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html") - .body.childNodes - ); + + if (bloom.type === "rebloom" && bloom.original_bloom_id) { + if (!window._bloomCache) window._bloomCache = {}; + + const cached = window._bloomCache[bloom.original_bloom_id]; + if (cached) { + bloomContent.innerHTML = ` + Reblooomed @${cached.sender}
+ ${cached.content} + `; + } else { + apiService + .getBloom(bloom.original_bloom_id) + .then((original) => { + window._bloomCache[bloom.original_bloom_id] = original; + bloomContent.innerHTML = ` + Reblooomed @${original.sender}
+ ${original.content} + `; + }) + .catch(() => { + bloomContent.textContent = " Rebloom (original not found)"; + }); + } + } else { + bloomContent.replaceChildren( + ...bloomParser.parseFromString( + _formatHashtags(bloom.content), + "text/html" + ).body.childNodes + ); + } + + // --- Add a Rebloom button --- + const rebloomButton = document.createElement("button"); + rebloomButton.textContent = " Rebloom"; + rebloomButton.classList.add("rebloom-button"); + rebloomButton.addEventListener("click", async () => { + try { + rebloomButton.disabled = true; + rebloomButton.textContent = "Reblooming..."; + const result = await apiService.rebloomBloom(bloom.id); + + if (result.success) { + rebloomButton.textContent = "Rebloomed!"; + } else { + alert("Rebloom failed"); + rebloomButton.textContent = " Rebloom"; + } + } catch (err) { + console.error(err); + alert("Failed to rebloom."); + rebloomButton.textContent = " Rebloom"; + } finally { + rebloomButton.disabled = false; + } + }); + + // Add the button to the bloom card (e.g., at the bottom) + bloomArticle.appendChild(rebloomButton); return bloomFrag; }; From 7f59afe143cd83959b932d2a2326ddcecdc09d8e Mon Sep 17 00:00:00 2001 From: sheetalkharab Date: Wed, 8 Oct 2025 13:32:53 +0100 Subject: [PATCH 11/11] add function rebllomBloom for post request --- front-end/lib/api.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index 8a5f5bc..4b060ab 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -283,6 +283,22 @@ async function unfollowUser(username) { } } +async function rebloomBloom(originalBloomId) { + try { + const data = await _apiRequest(`/rebloom/${originalBloomId}`, { + method: "POST", + }); + + if (data.success) { + await getProfile(state.currentUser); + } + + return data; + } catch (error) { + return { success: false }; + } +} + const apiService = { // Auth methods login, @@ -294,6 +310,7 @@ const apiService = { getBlooms, postBloom, getBloomsByHashtag, + rebloomBloom, // User methods getProfile,