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 };