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,