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
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 };
62 changes: 32 additions & 30 deletions front-end/lib/api.mjs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -70,19 +70,19 @@ 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
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) {
Expand All @@ -91,25 +91,26 @@ 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 };
}
}

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 [];
}
}
Expand All @@ -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) {
Expand All @@ -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;
}

Expand All @@ -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 [];
}
Expand All @@ -189,15 +191,15 @@ async function getBloomsByHashtag(hashtag) {
return blooms;
} catch (error) {
// Error already handled by _apiRequest
return {success: false};
return { success: false };
}
}

async function postBloom(content) {
try {
const data = await _apiRequest("/bloom", {
method: "POST",
body: JSON.stringify({content}),
body: JSON.stringify({ content }),
});

if (data.success) {
Expand All @@ -208,7 +210,7 @@ async function postBloom(content) {
return data;
} catch (error) {
// Error already handled by _apiRequest
return {success: false};
return { success: false };
}
}

Expand All @@ -225,24 +227,24 @@ 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 };
}
}

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) {
Expand All @@ -255,7 +257,7 @@ async function followUser(username) {

return data;
} catch (error) {
return {success: false};
return { success: false };
}
}

Expand All @@ -277,7 +279,7 @@ async function unfollowUser(username) {
return data;
} catch (error) {
// Error already handled by _apiRequest
return {success: false};
return { success: false };
}
}

Expand All @@ -300,4 +302,4 @@ const apiService = {
getWhoToFollow,
};

export {apiService};
export { apiService };
18 changes: 10 additions & 8 deletions front-end/views/hashtag.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {renderOne, renderEach, destroy} from "../lib/render.mjs";
import { renderOne, renderEach, destroy } from "../lib/render.mjs";
import {
state,
apiService,
Expand All @@ -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,
Expand Down Expand Up @@ -52,4 +54,4 @@ function hashtagView(hashtag) {
);
}

export {hashtagView};
export { hashtagView };