diff --git a/.vscode/settings.json b/.vscode/settings.json index 166be53..94b8107 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "liveServer.settings.root": "front-end/", - "python.defaultInterpreterPath": "backend/.venv/bin/python" + "python.defaultInterpreterPath": "backend/.venv/bin/python", + "python-envs.defaultEnvManager": "ms-python.python:system" } diff --git a/backend/.gitignore b/backend/.gitignore index 30a3427..ac6bb9d 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,3 +1,3 @@ /.env -/.venv/ +/venv/ *.pyc diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf..b2bc26b 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -6,6 +6,7 @@ from data.connection import db_cursor from data.users import User +MAX_BLOOM_LENGTH = 280 @dataclass class Bloom: @@ -16,9 +17,13 @@ class Bloom: def add_bloom(*, sender: User, content: str) -> Bloom: + # reject any new posts that are longer than 280 characters + if len(content) > MAX_BLOOM_LENGTH: + raise ValueError("Bloom content cannot exceed 280 characters.") + hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] - now = datetime.datetime.now(tz=datetime.UTC) + now = datetime.datetime.now(tz=datetime.timezone.utc) bloom_id = int(now.timestamp() * 1000000) with db_cursor() as cur: cur.execute( @@ -27,7 +32,7 @@ def add_bloom(*, sender: User, content: str) -> Bloom: bloom_id=bloom_id, sender_id=sender.id, content=content, - timestamp=datetime.datetime.now(datetime.UTC), + timestamp=datetime.datetime.now(datetime.timezone.utc), ), ) for hashtag in hashtags: diff --git a/front-end/components/bloom-form.mjs b/front-end/components/bloom-form.mjs index e047f9a..93561d9 100644 --- a/front-end/components/bloom-form.mjs +++ b/front-end/components/bloom-form.mjs @@ -1,5 +1,5 @@ import {apiService} from "../index.mjs"; - +import { MAX_BLOOM_LENGTH } from "./bloom.mjs"; /** * Create a bloom form component * @param {string} template - The ID of the template to clone @@ -51,7 +51,7 @@ function handleTyping(event) { const counter = textarea .closest("[data-form]") ?.querySelector("[data-counter]"); - const maxLength = parseInt(textarea.getAttribute("maxlength"), 10); + const maxLength = parseInt(textarea.getAttribute("maxlength"), 10) || MAX_BLOOM_LENGTH; counter.textContent = `${textarea.value.length} / ${maxLength}`; } diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c..acde223 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -10,6 +10,7 @@ * "sent_timestamp": "datetime as ISO 8601 formatted string"} */ +export const MAX_BLOOM_LENGTH = 280; const createBloom = (template, bloom) => { if (!bloom) return; const bloomFrag = document.getElementById(template).content.cloneNode(true); @@ -26,6 +27,11 @@ const createBloom = (template, bloom) => { bloomUsername.textContent = bloom.sender; bloomTime.textContent = _formatTimestamp(bloom.sent_timestamp); bloomTimeLink.setAttribute("href", `/bloom/${bloom.id}`); + // FIX: If a bloom from the database is over 280 characters, cut it short + let displayContent = bloom.content || ""; + if (displayContent.length > MAX_BLOOM_LENGTH) { + displayContent = displayContent.slice(0, MAX_BLOOM_LENGTH - 3) + "..."; + } bloomContent.replaceChildren( ...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html") .body.childNodes @@ -36,10 +42,10 @@ const createBloom = (template, bloom) => { function _formatHashtags(text) { if (!text) return text; - return text.replace( - /\B#[^#]+/g, - (match) => `${match}` - ); + return text.replace(/(^|\s)(#[A-Za-z0-9_]+)/g, (full, prefix, tag) => { + const encoded = encodeURIComponent(tag.slice(1)); + return `${prefix}${tag}`; + }); } function _formatTimestamp(timestamp) { diff --git a/front-end/components/login.mjs b/front-end/components/login.mjs index 165b16a..93911b5 100644 --- a/front-end/components/login.mjs +++ b/front-end/components/login.mjs @@ -30,6 +30,8 @@ async function handleLogin(event) { const password = formData.get("password"); await apiService.login(username, password); + //the browser refreshes on that exact same URL + window.location.reload(); } catch (error) { throw error; } finally { diff --git a/front-end/components/logout.mjs b/front-end/components/logout.mjs index 2c5ebe9..fd395d2 100644 --- a/front-end/components/logout.mjs +++ b/front-end/components/logout.mjs @@ -16,6 +16,7 @@ function createLogout(template, isLoggedIn) { async function handleLogout(event) { try { apiService.logout(); + window.location.href = "/"; } catch (error) { throw error; } diff --git a/front-end/views/hashtag.mjs b/front-end/views/hashtag.mjs index 7b7e996..d5dec39 100644 --- a/front-end/views/hashtag.mjs +++ b/front-end/views/hashtag.mjs @@ -14,10 +14,13 @@ import {createHeading} from "../components/heading.mjs"; // Hashtag view: show all tweets containing this tag -function hashtagView(hashtag) { +async function hashtagView(hashtag) { destroy(); - apiService.getBloomsByHashtag(hashtag); + const normalizedHashtag = hashtag.startsWith("#") ? hashtag : `#${hashtag}`; + if (state.currentHashtag !== normalizedHashtag) { + await apiService.getBloomsByHashtag(hashtag); + } renderOne( state.isLoggedIn,