Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/.env
/.venv/
/venv/
*.pyc
45 changes: 40 additions & 5 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class Bloom:
sender: User
content: str
sent_timestamp: datetime.datetime
rebloomed_by: Optional[str] = None # Tracks if someone shared it
rebloom_count: int = 0 # Tracks share count


def add_bloom(*, sender: User, content: str) -> Bloom:
Expand All @@ -36,6 +38,29 @@ def add_bloom(*, sender: User, content: str) -> Bloom:
dict(hashtag=hashtag, bloom_id=bloom_id),
)

def add_rebloom(*, rebloomer: User, original_bloom_id: int) -> None:
original = get_bloom(original_bloom_id)
if not original:
return

now = datetime.datetime.now(tz=datetime.UTC)
new_bloom_id = int(now.timestamp() * 1000000)

with db_cursor() as cur:
# 1. Insert the shared copy as a new timeline entry attributed to the rebloomer
cur.execute(
"""INSERT INTO blooms
(id, sender_id, content, send_timestamp, rebloomed_by_id)
VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s, %(rebloomed_by_id)s)""",
dict(
bloom_id=new_bloom_id,
sender_id=original.sender_id, # Keep original author tracking if needed, or mapping structure
content=original.content,
timestamp=now,
rebloomed_by_id=rebloomer.id
),
)
# 2. Increment a counter system or handle via a tracking table aggregation

def get_blooms_for_user(
username: str, *, before: Optional[int] = None, limit: Optional[int] = None
Expand All @@ -54,27 +79,37 @@ def get_blooms_for_user(

cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
b.id,
u.username AS sender_username,
b.content,
b.send_timestamp,
rb_u.username AS rebloomed_by_username,
(SELECT COUNT(*) FROM blooms WHERE rebloomed_from_id = b.id) AS rebloom_count
FROM
blooms INNER JOIN users ON users.id = blooms.sender_id
blooms b
INNER JOIN users u ON u.id = b.sender_id
LEFT JOIN users rb_u ON rb_u.id = b.rebloomed_by_id
WHERE
username = %(sender_username)s
(u.username = %(sender_username)s AND b.rebloomed_by_id IS NULL)
OR rb_u.username = %(sender_username)s
{before_clause}
ORDER BY send_timestamp DESC
ORDER BY b.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, rebloomed_by, rebloom_count = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloomed_by=rebloomed_by,
rebloom_count=rebloom_count
)
)
return blooms
Expand Down
23 changes: 23 additions & 0 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,29 @@ def get_bloom(id_str):
return make_response((f"Bloom not found", 404))
return jsonify(bloom)

@jwt_required()
def do_rebloom(id_str):
"""
Endpoint to handle reblooming an existing bloom post
"""
# 1. Parse the incoming original bloom ID
try:
original_id_int = int(id_str)
except ValueError:
return make_response(("Invalid bloom id", 400))

# 2. Grab the logged-in user who clicked the rebloom button
current_user = get_current_user()

# 3. Call your core dataclass layer function to perform the clone insert
try:
blooms.add_rebloom(rebloomer=current_user, original_bloom_id=original_id_int)
return jsonify({
"success": True,
"message": "Rebloomed successfully!"
})
except Exception as error:
return make_response(({"success": False, "message": str(error)}, 500))

@jwt_required()
def home_timeline():
Expand Down
3 changes: 3 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
send_bloom,
suggested_follows,
user_blooms,
do_rebloom
)

from dotenv import load_dotenv
Expand Down Expand Up @@ -61,6 +62,8 @@ def main():
app.add_url_rule("/blooms/<profile_username>", view_func=user_blooms)
app.add_url_rule("/hashtag/<hashtag>", view_func=hashtag)

app.add_url_rule("/api/blooms/<id_str>/rebloom", view_func=do_rebloom, methods=["POST"])

app.run(host="0.0.0.0", port="3000", debug=True)


Expand Down
4 changes: 3 additions & 1 deletion db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ 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,
rebloomed_from_id BIGINT REFERENCES blooms(id) ON DELETE CASCADE,
rebloomed_by_id INT REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE follows (
Expand Down
52 changes: 52 additions & 0 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ const createBloom = (template, bloom) => {
const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])");
const bloomContent = bloomFrag.querySelector("[data-content]");

// REBLOOM UI EXTENSIONS
// Look for a top notification banner anchor inside your HTML template template
const rebloomHeader = bloomFrag.querySelector("[data-rebloom-header]");
if (bloom.rebloomed_by) {
if (rebloomHeader) {
rebloomHeader.textContent = `🔄 ${bloom.rebloomed_by} re-bloomed`;
rebloomHeader.setAttribute("href", `/profile/${bloom.rebloomed_by}`);
rebloomHeader.classList.remove("hidden"); // Ensure it's visible
}
// Visual indicator to distinctively dim or border wrap the shared block card
bloomArticle.classList.add("rebloom-card-style");
} else if (rebloomHeader) {
rebloomHeader.classList.add("hidden");
}

// Look for your counter UI node element
const rebloomCounter = bloomFrag.querySelector("[data-rebloom-count]");
if (rebloomCounter) {
rebloomCounter.textContent = bloom.rebloom_count > 0 ? `🔄 ${bloom.rebloom_count}` : "🔄";
}

bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
bloomUsername.textContent = bloom.sender;
Expand All @@ -31,6 +52,37 @@ const createBloom = (template, bloom) => {
.body.childNodes
);

const rebloomButton = bloomFrag.querySelector("[data-rebloom-button]");
if (rebloomButton) {
rebloomButton.addEventListener("click", async (event) => {
// Prevent standard browser button click bubbles or form submissions
event.preventDefault();

try {
// Send a POST network ping request straight to your Flask Endpoint.py
const response = await fetch(`/api/blooms/${bloom.id}/rebloom`, {
method: "POST",
headers: {
"Authorization": `Bearer ${localStorage.getItem("token")}`,
"Content-Type": "application/json"
}
});

const result = await response.json();

if (response.ok && result.success) {
// Instantly refresh the timeline viewport so the newly shared item renders at the top
window.location.reload();
} else {
alert(`Could not rebloom: ${result.message || "Unknown error"}`);
}
} catch (error) {
console.error("Network error executing rebloom:", error);
alert("A network connection problem occurred. Please try again.");
}
});
}

return bloomFrag;
};

Expand Down
8 changes: 8 additions & 0 deletions front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,19 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<!-- Bloom Template -->
<template id="bloom-template">
<article class="bloom box" data-bloom data-bloom-id="">
<a href="#" data-rebloom-header class="bloom__rebloom-meta hidden" style="font-size: 0.85rem; color: #666; display: block; margin-bottom: 5px;"></a>

<div class="bloom__header flex">
<a href="#" class="bloom__username" data-username>Username</a>
<a href="#" class="bloom__time"><time class="bloom__time" data-time>2m</time></a>
</div>
<div class="bloom__content" data-content></div>

<div class="bloom__actions" style="margin-top: 8px;">
<button type="button" data-action="rebloom" data-rebloom-button style="background: none; border: none; cursor: pointer; color: #666;">
<span data-rebloom-count>🔄</span>
</button>
</div>
</article>
</template>

Expand Down