From d1d411adca43f95090b994ff2b4774dfa9d442b1 Mon Sep 17 00:00:00 2001 From: zohrehKazemianpour <129424353+zohrehKazemianpour@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:08:21 +0000 Subject: [PATCH 1/6] Fix: Replace psycopg2 with psycopg2-binary for macOS compatibility and update README --- backend/README.md | 3 ++- backend/requirements.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/README.md b/backend/README.md index 8e31e11..08f0aef 100644 --- a/backend/README.md +++ b/backend/README.md @@ -13,7 +13,8 @@ To run: 3. Activate the virtual environment: `. .venv/bin/activate` 4. Install dependencies: `pip install -r requirements.txt` 5. Run the database: `../db/run.sh` (you must have Docker installed and running). -6. Create the database schema: `../db/create-schema.sh` +6.run the server python3 main.py +7. Create the database schema: `../db/create-schema.sh` You may want to run `python3 populate.py` to populate sample data. diff --git a/backend/requirements.txt b/backend/requirements.txt index e03836c..5e11b05 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,7 +11,7 @@ idna==3.10 itsdangerous==2.2.0 Jinja2==3.1.5 MarkupSafe==3.0.2 -psycopg2==2.9.10 +psycopg2-binary==2.9.10 pycparser==2.22 PyJWT==2.10.1 python-dotenv==1.0.1 From 706b031bc1623e3076982ee89c9d56506679099a Mon Sep 17 00:00:00 2001 From: zohrehKazemianpour <129424353+zohrehKazemianpour@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:10:04 +0000 Subject: [PATCH 2/6] feat: add original_bloom_id column to blooms table for rebloom support --- db/schema.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/schema.sql b/db/schema.sql index 61e7580..1351eb2 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -10,7 +10,8 @@ CREATE TABLE blooms ( id BIGSERIAL NOT NULL PRIMARY KEY, sender_id INT NOT NULL REFERENCES users(id), content TEXT NOT NULL, - send_timestamp TIMESTAMP NOT NULL + send_timestamp TIMESTAMP NOT NULL, + original_bloom_id BIGINT REFERENCES blooms(id) ); CREATE TABLE follows ( From 29c262818117eff1844b62fa2c7da7e9c2f3a337 Mon Sep 17 00:00:00 2001 From: zohrehKazemianpour <129424353+zohrehKazemianpour@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:48:59 +0000 Subject: [PATCH 3/6] feat: Enhance SQL queries to support re-bloom data and counts --- backend/data/blooms.py | 61 +++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf..7368186 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -13,21 +13,25 @@ class Bloom: sender: User content: str sent_timestamp: datetime.datetime + original_bloom_id: Optional[int] =None + original_sender: Optional[str] = None + rebloom_count: int = 0 -def add_bloom(*, sender: User, content: str) -> Bloom: +def add_bloom(*, sender: User, content: str, original_bloom_id: Optional[int] = None) -> Bloom: 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: 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 (id, sender_id, content, send_timestamp, original_bloom_id) VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s, %(original_bloom_id)s)", dict( bloom_id=bloom_id, sender_id=sender.id, content=content, timestamp=datetime.datetime.now(datetime.UTC), + original_bloom_id=original_bloom_id, ), ) for hashtag in hashtags: @@ -35,6 +39,7 @@ def add_bloom(*, sender: User, content: str) -> Bloom: "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, original_bloom_id=original_bloom_id) def get_blooms_for_user( @@ -51,16 +56,23 @@ def get_blooms_for_user( before_clause = "" 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 + b.id, + u.username, + b.content, + b.send_timestamp, + b.original_bloom_id, + ou.username AS original_sender, + (SELECT COUNT(*) FROM blooms WHERE original_bloom_id = COALESCE(b.original_bloom_id, b.id)) AS rebloom_count + FROM blooms b + INNER JOIN users u ON u.id = b.sender_id + LEFT JOIN blooms ob ON b.original_bloom_id = ob.id + LEFT JOIN users ou ON ob.sender_id = ou.id WHERE - username = %(sender_username)s + u.username = %(sender_username)s {before_clause} - ORDER BY send_timestamp DESC + ORDER BY b.send_timestamp DESC {limit_clause} """, kwargs, @@ -68,13 +80,16 @@ def get_blooms_for_user( rows = cur.fetchall() blooms = [] for row in rows: - bloom_id, sender_username, content, timestamp = row + bloom_id, sender_username, content, timestamp, original_bloom_id, original_sender, rebloom_count = row blooms.append( Bloom( id=bloom_id, sender=sender_username, content=content, sent_timestamp=timestamp, + original_bloom_id=original_bloom_id, + original_sender=original_sender, + rebloom_count=rebloom_count, ) ) return blooms @@ -83,18 +98,21 @@ def get_blooms_for_user( 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 b.id, u.username, b.content, b.send_timestamp, b.original_bloom_id, ou.username AS original_sender, (SELECT COUNT(*) FROM blooms WHERE original_bloom_id = COALESCE(b.original_bloom_id, b.id)) AS rebloom_count FROM blooms b INNER JOIN users u ON u.id = b.sender_id LEFT JOIN blooms ob ON b.original_bloom_id = ob.id LEFT JOIN users ou ON ob.sender_id = ou.id WHERE b.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, original_bloom_id, original_sender, rebloom_count = row return Bloom( id=bloom_id, sender=sender_username, content=content, sent_timestamp=timestamp, + original_bloom_id=original_bloom_id, + original_sender=original_sender, + rebloom_count=rebloom_count, ) @@ -108,12 +126,20 @@ def get_blooms_with_hashtag( with db_cursor() as cur: cur.execute( f"""SELECT - blooms.id, users.username, content, send_timestamp - FROM - blooms INNER JOIN hashtags ON blooms.id = hashtags.bloom_id INNER JOIN users ON blooms.sender_id = users.id + b.id, + u.username, + b.content, + b.send_timestamp, + b.original_bloom_id, + ou.username AS original_sender, + (SELECT COUNT(*) FROM blooms WHERE original_bloom_id = COALESCE(b.original_bloom_id, b.id)) AS rebloom_count + FROM blooms b + INNER JOIN hashtags ON b.id = hashtags.bloom_id INNER JOIN users u ON b.sender_id = u.id + LEFT JOIN blooms ob ON b.original_bloom_id = ob.id + LEFT JOIN users ou ON ob.sender_id = ou.id WHERE hashtag = %(hashtag_without_leading_hash)s - ORDER BY send_timestamp DESC + ORDER BY b.send_timestamp DESC {limit_clause} """, kwargs, @@ -121,13 +147,16 @@ def get_blooms_with_hashtag( rows = cur.fetchall() blooms = [] for row in rows: - bloom_id, sender_username, content, timestamp = row + bloom_id, sender_username, content, timestamp, original_bloom_id, original_sender, rebloom_count = row blooms.append( Bloom( id=bloom_id, sender=sender_username, content=content, sent_timestamp=timestamp, + original_bloom_id=original_bloom_id, + original_sender=original_sender, + rebloom_count=rebloom_count, ) ) return blooms From 41dfe4aac0c0763f3d06415e6eb2ef3abe8637be Mon Sep 17 00:00:00 2001 From: zohrehKazemianpour <129424353+zohrehKazemianpour@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:09:45 +0000 Subject: [PATCH 4/6] feat(backend): Add POST /bloom//rebloom endpoint to create reblooms --- backend/endpoints.py | 19 +++++++++++++++++++ backend/main.py | 2 ++ 2 files changed, 21 insertions(+) diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..ba23e86 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -178,6 +178,25 @@ def get_bloom(id_str): return jsonify(bloom) +@jwt_required() +def do_rebloom(id_str): + try: + id_int = int(id_str) + except ValueError: + return make_response((f"Invalid bloom id", 400)) + + original = blooms.get_bloom(id_int) + if original is None: + return make_response((f"Bloom not found", 404)) + + user = get_current_user() + + # Create a new bloom with the same content, referencing the original bloom + blooms.add_bloom(sender=user, content=original.content, original_bloom_id=id_int) + + return jsonify({"success": True}) + + @jwt_required() def home_timeline(): current_user = get_current_user() diff --git a/backend/main.py b/backend/main.py index 7ba155f..57b7fe3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,6 +12,7 @@ register, self_profile, send_bloom, + do_rebloom, suggested_follows, user_blooms, ) @@ -58,6 +59,7 @@ 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("/bloom//rebloom", methods=["POST"], view_func=do_rebloom) app.add_url_rule("/blooms/", view_func=user_blooms) app.add_url_rule("/hashtag/", view_func=hashtag) From a4d672425834c18e1cf3871312ebb56f6ac5f8a8 Mon Sep 17 00:00:00 2001 From: zohrehKazemianpour <129424353+zohrehKazemianpour@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:57:43 +0000 Subject: [PATCH 5/6] feat(front-end): add accessible Re-bloom button to bloom template --- front-end/index.html | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/front-end/index.html b/front-end/index.html index 89d6b13..b830414 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -234,11 +234,27 @@

Share a Bloom

From b2f24d5204637cdf91aad632611498799c768380 Mon Sep 17 00:00:00 2001 From: zohrehKazemianpour <129424353+zohrehKazemianpour@users.noreply.github.com> Date: Sun, 22 Feb 2026 09:59:41 +0000 Subject: [PATCH 6/6] feat: integrate rebloom button with backend --- front-end/components/bloom.mjs | 35 +++++++++++++++++++++++++++++++--- front-end/index.mjs | 19 ++++++++++++++++++ front-end/lib/api.mjs | 19 ++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c..e0c291d 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -21,16 +21,45 @@ const createBloom = (template, bloom) => { const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])"); const bloomContent = bloomFrag.querySelector("[data-content]"); + + const rebloomInfo = bloomFrag.querySelector("[data-rebloom-info]"); + const rebloomSender = bloomFrag.querySelector("[data-rebloom-sender]"); + const rebloomCount = bloomFrag.querySelector("[data-rebloom-count]"); + bloomArticle.setAttribute("data-bloom-id", bloom.id); - bloomUsername.setAttribute("href", `/profile/${bloom.sender}`); - bloomUsername.textContent = bloom.sender; + + + if (bloom.original_sender) { + + bloomUsername.setAttribute("href", `/profile/${bloom.original_sender}`); + bloomUsername.textContent = bloom.original_sender; + + + if (rebloomInfo && rebloomSender) { + rebloomInfo.hidden = false; + rebloomSender.setAttribute("href", `/profile/${bloom.sender}`); + rebloomSender.textContent = bloom.sender; + } + } else { + bloomUsername.setAttribute("href", `/profile/${bloom.sender}`); + bloomUsername.textContent = bloom.sender; + if (rebloomInfo) rebloomInfo.hidden = true; + } + bloomTime.textContent = _formatTimestamp(bloom.sent_timestamp); - bloomTimeLink.setAttribute("href", `/bloom/${bloom.id}`); + + const timeLink = bloomTime ? bloomTime.closest("a") : null; + if (timeLink) timeLink.setAttribute("href", `/bloom/${bloom.id}`); bloomContent.replaceChildren( ...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html") .body.childNodes ); + + if (rebloomCount) { + rebloomCount.textContent = String(bloom.rebloom_count ?? 0); + } + return bloomFrag; }; diff --git a/front-end/index.mjs b/front-end/index.mjs index be49922..524c0d2 100644 --- a/front-end/index.mjs +++ b/front-end/index.mjs @@ -43,6 +43,25 @@ async function init() { document.addEventListener("state-change", () => { handleRouteChange(); }); + + + document.addEventListener("click", async (event) => { + const btn = event.target.closest && event.target.closest('[data-action="rebloom"]'); + if (!btn) return; + + const article = btn.closest && btn.closest('[data-bloom]'); + if (!article) return; + + const bloomId = article.getAttribute('data-bloom-id'); + if (!bloomId) return; + + try { + await apiService.postRebloom(bloomId); + } catch (err) { + // errors are already handled/displayed by apiService + console.error('Rebloom failed', err); + } + }); } // TODO Check any unhandled errors bubble up to this central handler diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index f4b5339..6b15133 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -212,6 +212,24 @@ async function postBloom(content) { } } +async function postRebloom(bloomId) { + try { + const data = await _apiRequest(`/bloom/${bloomId}/rebloom`, { + method: "POST", + }); + + if (data.success) { + // Refresh timeline and current user's profile + await Promise.all([getBlooms(), getProfile(state.currentUser)]); + } + + return data; + } catch (error) { + // Error already handled by _apiRequest + return {success: false}; + } +} + // ======= USER methods async function getProfile(username) { const endpoint = username ? `/profile/${username}` : "/profile"; @@ -291,6 +309,7 @@ const apiService = { getBloom, getBlooms, postBloom, + postRebloom, getBloomsByHashtag, // User methods