diff --git a/backend/data/blooms.py b/backend/data/blooms.py
index 7e280cf..6121ffb 100644
--- a/backend/data/blooms.py
+++ b/backend/data/blooms.py
@@ -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)
diff --git a/front-end/components/bloom-form.mjs b/front-end/components/bloom-form.mjs
index e047f9a..6a71b98 100644
--- a/front-end/components/bloom-form.mjs
+++ b/front-end/components/bloom-form.mjs
@@ -1,4 +1,4 @@
-import {apiService} from "../index.mjs";
+import { apiService } from "../index.mjs";
/**
* Create a bloom form component
@@ -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
@@ -55,4 +60,4 @@ function handleTyping(event) {
counter.textContent = `${textarea.value.length} / ${maxLength}`;
}
-export {createBloomForm, handleBloomSubmit, handleTyping};
+export { createBloomForm, handleBloomSubmit, handleTyping };
diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs
index 0b4166c..3d5951b 100644
--- a/front-end/components/bloom.mjs
+++ b/front-end/components/bloom.mjs
@@ -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) => `${match}`
+ /\B#(\w+)/g,
+ (match, tag) => `${match}`
);
}
@@ -84,4 +85,4 @@ function _formatTimestamp(timestamp) {
}
}
-export {createBloom};
+export { createBloom };
diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs
index f4b5339..8a5f5bc 100644
--- a/front-end/lib/api.mjs
+++ b/front-end/lib/api.mjs
@@ -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
@@ -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 {
@@ -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
@@ -70,11 +70,11 @@ 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
@@ -82,7 +82,7 @@ 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) {
@@ -91,12 +91,13 @@ 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 };
}
}
@@ -104,12 +105,12 @@ 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 [];
}
}
@@ -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) {
@@ -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;
}
@@ -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 [];
}
@@ -189,7 +191,7 @@ async function getBloomsByHashtag(hashtag) {
return blooms;
} catch (error) {
// Error already handled by _apiRequest
- return {success: false};
+ return { success: false };
}
}
@@ -197,7 +199,7 @@ async function postBloom(content) {
try {
const data = await _apiRequest("/bloom", {
method: "POST",
- body: JSON.stringify({content}),
+ body: JSON.stringify({ content }),
});
if (data.success) {
@@ -208,7 +210,7 @@ async function postBloom(content) {
return data;
} catch (error) {
// Error already handled by _apiRequest
- return {success: false};
+ return { success: false };
}
}
@@ -225,16 +227,16 @@ 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 };
}
}
@@ -242,7 +244,7 @@ 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) {
@@ -255,7 +257,7 @@ async function followUser(username) {
return data;
} catch (error) {
- return {success: false};
+ return { success: false };
}
}
@@ -277,7 +279,7 @@ async function unfollowUser(username) {
return data;
} catch (error) {
// Error already handled by _apiRequest
- return {success: false};
+ return { success: false };
}
}
@@ -300,4 +302,4 @@ const apiService = {
getWhoToFollow,
};
-export {apiService};
+export { apiService };
diff --git a/front-end/views/hashtag.mjs b/front-end/views/hashtag.mjs
index 7b7e996..181b8e7 100644
--- a/front-end/views/hashtag.mjs
+++ b/front-end/views/hashtag.mjs
@@ -1,4 +1,4 @@
-import {renderOne, renderEach, destroy} from "../lib/render.mjs";
+import { renderOne, renderEach, destroy } from "../lib/render.mjs";
import {
state,
apiService,
@@ -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,
@@ -52,4 +54,4 @@ function hashtagView(hashtag) {
);
}
-export {hashtagView};
+export { hashtagView };