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: 2 additions & 0 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions backend/data/follows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)

15 changes: 15 additions & 0 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})

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_unfollow,
)

from dotenv import load_dotenv
Expand Down Expand Up @@ -54,6 +55,8 @@ def main():
app.add_url_rule("/profile", view_func=self_profile)
app.add_url_rule("/profile/<profile_username>", view_func=other_profile)
app.add_url_rule("/follow", methods=["POST"], view_func=do_follow)
app.add_url_rule("/unfollow/<username>", view_func=do_unfollow, methods=["POST"])

app.add_url_rule("/suggested-follows/<limit_str>", view_func=suggested_follows)

app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom)
Expand Down
9 changes: 7 additions & 2 deletions front-end/components/bloom-form.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {apiService} from "../index.mjs";
import { apiService } from "../index.mjs";

/**
* Create a bloom form component
Expand Down Expand Up @@ -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 be 280 characters or less.`);
return;
}

try {
// Make form inert while we call the back end
Expand Down Expand Up @@ -55,4 +60,4 @@ function handleTyping(event) {
counter.textContent = `${textarea.value.length} / ${maxLength}`;
}

export {createBloomForm, handleBloomSubmit, handleTyping};
export { createBloomForm, handleBloomSubmit, handleTyping };
7 changes: 4 additions & 3 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => `<a href="/hashtag/${match.slice(1)}">${match}</a>`
/\B#(\w+)/g,
(match, tag) => `<a href="/hashtag/${encodeURIComponent(tag)}">${match}</a>`
);
}

Expand Down Expand Up @@ -84,4 +85,4 @@ function _formatTimestamp(timestamp) {
}
}

export {createBloom};
export { createBloom };
37 changes: 30 additions & 7 deletions front-end/components/profile.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {apiService} from "../index.mjs";
import { apiService } from "../index.mjs";

/**
* Create a profile component
* @param {string} template - The ID of the template to clone
* @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)
Expand All @@ -19,30 +19,44 @@ 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}`);
bloomCountEl.textContent = profileData.total_blooms || 0;
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";
Expand All @@ -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 };
Loading