From 0321a8a69dacd0a0f8aa6624d9aa948649044d8a Mon Sep 17 00:00:00 2001 From: donarbl Date: Thu, 29 Jan 2026 14:29:06 +0000 Subject: [PATCH 1/5] changed the login actions --- front-end/views/profile.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front-end/views/profile.mjs b/front-end/views/profile.mjs index dd2b92a..31139a9 100644 --- a/front-end/views/profile.mjs +++ b/front-end/views/profile.mjs @@ -39,8 +39,8 @@ function profileView(username) { createLogin ); document - .querySelector("[data-action='login']") - ?.addEventListener("click", handleLogin); + .querySelector("[data-form='login']") + ?.addEventListener("submit", handleLogin); const profileData = state.profiles.find((p) => p.username === username); if (profileData) { From bc09dc929e9cda449b505d0961ffb0da08c9b576 Mon Sep 17 00:00:00 2001 From: donarbl Date: Thu, 29 Jan 2026 15:27:58 +0000 Subject: [PATCH 2/5] fixed 280 symbols in both places --- backend/data/blooms.py | 5 +++++ backend/endpoints.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf..62b4238 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -14,8 +14,13 @@ class Bloom: content: str sent_timestamp: datetime.datetime +MAX_BLOOM_LENGTH = 280 # this is to ensure the extra safety def add_bloom(*, sender: User, content: str) -> Bloom: + + if len(content) > MAX_BLOOM_LENGTH: + raise ValueError(f"blooms content is too long(max {MAX_BLOOM_LENGTH})") + hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] now = datetime.datetime.now(tz=datetime.UTC) diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..c237739 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -149,13 +149,22 @@ def do_follow(): } ) - +MAX_BLOOM_LENGTH = 280 @jwt_required() def send_bloom(): type_check_error = verify_request_fields({"content": str}) if type_check_error is not None: return type_check_error + content = request.json ["content"] + + if len(content) > MAX_BLOOM_LENGTH: + return make_response( + ({"success": False, + "message": f"blloms can't be longe than {MAX_BLOOM_LENGTH} symbols", + }, 400,) + ) + user = get_current_user() blooms.add_bloom(sender=user, content=request.json["content"]) From fca0c4d1b754b4fbaba3481376b7f41a55c86b44 Mon Sep 17 00:00:00 2001 From: donarbl Date: Fri, 30 Jan 2026 09:16:02 +0000 Subject: [PATCH 3/5] fixed the hashtag --- front-end/components/bloom.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c..4d1a537 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -37,8 +37,8 @@ const createBloom = (template, bloom) => { function _formatHashtags(text) { if (!text) return text; return text.replace( - /\B#[^#]+/g, - (match) => `${match}` + /\B#([a-zA-Z0-9_]+)/g,// gets SwizBiz, without # or trailing love!!. + (match, tag) => `[${match}](/hashtag/${tag})` ); } From 8c6373615a734b82fbe993322967be50cce1709b Mon Sep 17 00:00:00 2001 From: donarbl Date: Fri, 30 Jan 2026 09:31:35 +0000 Subject: [PATCH 4/5] fixed hashtag that slows the browser --- front-end/views/hashtag.mjs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/front-end/views/hashtag.mjs b/front-end/views/hashtag.mjs index 7b7e996..83ebf5f 100644 --- a/front-end/views/hashtag.mjs +++ b/front-end/views/hashtag.mjs @@ -16,9 +16,7 @@ import {createHeading} from "../components/heading.mjs"; function hashtagView(hashtag) { destroy(); - - apiService.getBloomsByHashtag(hashtag); - +// it will show the basic structure or loading msg renderOne( state.isLoggedIn, getLogoutContainer(), @@ -28,6 +26,7 @@ function hashtagView(hashtag) { document .querySelector("[data-action='logout']") ?.addEventListener("click", handleLogout); + renderOne( state.isLoggedIn, getLoginContainer(), @@ -39,17 +38,25 @@ function hashtagView(hashtag) { ?.addEventListener("click", handleLogin); renderOne( - state.currentHashtag, + `#${hashtag}`, getHeadingContainer(), "heading-template", createHeading ); + apiService.getBloomsByHashtag(hashtag).then(() => { + renderOne( + state.currentHashtag, + getHeadingContainer(), + "heading-template", + createHeading + ); renderEach( state.hashtagBlooms || [], getTimelineContainer(), "bloom-template", createBloom ); +}); } export {hashtagView}; From a6cf15d7accfcaf03c1e1a1f9de7c0ea981698f4 Mon Sep 17 00:00:00 2001 From: donarbl Date: Sat, 31 Jan 2026 18:02:20 +0000 Subject: [PATCH 5/5] implemented rebloom featrure in back and frontend --- backend/data/blooms.py | 72 +++++++++++++++++++++++++++++++--- backend/endpoints.py | 15 +++++++ backend/main.py | 2 + front-end/components/bloom.mjs | 30 ++++++++++++++ front-end/index.html | 7 +++- front-end/lib/api.mjs | 21 ++++++++++ 6 files changed, 140 insertions(+), 7 deletions(-) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 62b4238..cdc89b5 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -13,34 +13,94 @@ class Bloom: sender: User content: str sent_timestamp: datetime.datetime + original_bloom_id: Optional[int] = None + original_sender: Optional[User] = None + rebloom_count: int = 0 MAX_BLOOM_LENGTH = 280 # this is to ensure the extra safety def add_bloom(*, sender: User, content: str) -> Bloom: - - if len(content) > MAX_BLOOM_LENGTH: + if len(content) > MAX_BLOOM_LENGTH: raise ValueError(f"blooms content is too long(max {MAX_BLOOM_LENGTH})") hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] now = datetime.datetime.now(tz=datetime.UTC) - bloom_id = int(now.timestamp() * 1000000) + with db_cursor() as cur: + # Let the database generate the id cur.execute( - "INSERT INTO blooms (id, sender_id, content, send_timestamp) VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s)", + """ + INSERT INTO blooms (sender_id, content, send_timestamp) + VALUES (%(sender_id)s, %(content)s, %(timestamp)s) + RETURNING id + """, dict( - bloom_id=bloom_id, sender_id=sender.id, content=content, - timestamp=datetime.datetime.now(datetime.UTC), + timestamp=now, ), ) + bloom_id = cur.fetchone()[0] + for hashtag in hashtags: cur.execute( "INSERT INTO hashtags (hashtag, bloom_id) VALUES (%(hashtag)s, %(bloom_id)s)", dict(hashtag=hashtag, bloom_id=bloom_id), ) + return Bloom( + id=bloom_id, + sender=sender.username, + content=content, + sent_timestamp=now, + ) + +def add_rebloom(*, rebloomer: User, original_bloom: Bloom) -> Bloom: + now = datetime.datetime.now(tz=datetime.UTC) + + with db_cursor() as cur: + # Insert rebloom without id, DB generates it + cur.execute( + """ + INSERT INTO blooms ( + sender_id, content, send_timestamp, original_bloom_id + ) + VALUES ( + %(sender_id)s, %(content)s, %(timestamp)s, %(original_bloom_id)s + ) + RETURNING id + """, + dict( + sender_id=rebloomer.id, + content=original_bloom.content, + timestamp=now, + original_bloom_id=original_bloom.id, + ), + ) + new_id = cur.fetchone()[0] + + # Increase rebloom_count on the original + cur.execute( + """ + UPDATE blooms + SET rebloom_count = COALESCE(rebloom_count, 0) + 1 + WHERE id = %(id)s + """, + dict(id=original_bloom.id), + ) + + return Bloom( + id=new_id, + sender=rebloomer.username, + content=original_bloom.content, + sent_timestamp=now, + original_bloom_id=original_bloom.id, + original_sender=original_bloom.sender, + rebloom_count=(original_bloom.rebloom_count or 0) + 1, + ) + + def get_blooms_for_user( username: str, *, before: Optional[int] = None, limit: Optional[int] = None diff --git a/backend/endpoints.py b/backend/endpoints.py index c237739..2020579 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -175,6 +175,21 @@ def send_bloom(): } ) +@jwt_required() +def rebloom(id_str): + try: + bloom_id = int(id_str) + except ValueError: + return make_response(("Invalid bloom id", 400)) + + original = blooms.get_bloom(bloom_id) + if original is None: + return make_response(("Bloom not found", 404)) + + user = get_current_user() + new_bloom = blooms.add_rebloom(rebloomer=user, original_bloom=original) + + return jsonify({"success": True, "bloom": new_bloom}) def get_bloom(id_str): try: diff --git a/backend/main.py b/backend/main.py index 7ba155f..2c86576 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 @@ -57,6 +58,7 @@ def main(): app.add_url_rule("/suggested-follows/", view_func=suggested_follows) app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom) + app.add_url_rule("/bloom//rebloom", methods=["POST"], view_func=rebloom) app.add_url_rule("/bloom/", methods=["GET"], view_func=get_bloom) app.add_url_rule("/blooms/", view_func=user_blooms) app.add_url_rule("/hashtag/", view_func=hashtag) diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 4d1a537..5c0e96e 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -1,3 +1,5 @@ +import { apiService } from "../lib/api.mjs"; + /** * Create a bloom component * @param {string} template - The ID of the template to clone @@ -20,6 +22,34 @@ const createBloom = (template, bloom) => { const bloomTime = bloomFrag.querySelector("[data-time]"); const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])"); const bloomContent = bloomFrag.querySelector("[data-content]"); + const rebloomInfo = bloomFrag.querySelector("[data-rebloom-info]"); + const rebloomCountEl = bloomFrag.querySelector("[data-rebloom-count]"); + const rebloomButton = bloomFrag.querySelector("[data-action='rebloom']"); + + if (bloom.original_bloom_id && bloom.original_sender) { + // This is a rebloom + rebloomInfo.textContent = + `Re-bloomed by ${bloom.sender}, originally by ${bloom.original_sender}`; + } else { + // Original bloom: no rebloom info + rebloomInfo.textContent = "";//it gives an origianl bloom, no rebllom information + } + + if (bloom.rebloom_count && bloom.rebloom_count > 0) { + rebloomCountEl.textContent = `Re-bloomed ${bloom.rebloom_count} times`; + } else { + rebloomCountEl.textContent = ""; + } + + rebloomButton?.addEventListener("click", async () => { + try { + await apiService.rebloom(bloom.id); // implement this in your API service + // TODO: refresh timeline / current view after rebloom + } catch (error) { + console.error("Failed to re-bloom", error); + } + }); + bloomArticle.setAttribute("data-bloom-id", bloom.id); bloomUsername.setAttribute("href", `/profile/${bloom.sender}`); diff --git a/front-end/index.html b/front-end/index.html index 89d6b13..9dcc841 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -239,6 +239,11 @@

Share a Bloom

+

+

+ @@ -256,6 +261,6 @@

Share a Bloom

Please enable JavaScript in your browser.

- + diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index f4b5339..4243a6e 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -211,6 +211,26 @@ async function postBloom(content) { return {success: false}; } } +async function rebloom(bloomId) { + const endpoint = `/bloom/${bloomId}/rebloom`; + + try { + const data = await _apiRequest(endpoint, { + method: "POST", + }); + + if (data.success) { + // refresh main timeline so the new bloom appears + await getBlooms(); + } + + return data; + } catch (error) { + // Error already handled by _apiRequest + return { success: false }; + } +} + // ======= USER methods async function getProfile(username) { @@ -292,6 +312,7 @@ const apiService = { getBlooms, postBloom, getBloomsByHashtag, + rebloom, // User methods getProfile,