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
115 changes: 58 additions & 57 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,77 +11,78 @@

*/
const createBloom = (template, bloom) => {
if (!bloom) return;
const bloomFrag = document.getElementById(template).content.cloneNode(true);
const bloomParser = new DOMParser();
if (!bloom) return;
const bloomFrag = document.getElementById(template).content.cloneNode(true);
const bloomParser = new DOMParser();

const bloomArticle = bloomFrag.querySelector("[data-bloom]");
const bloomUsername = bloomFrag.querySelector("[data-username]");
const bloomTime = bloomFrag.querySelector("[data-time]");
const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])");
const bloomContent = bloomFrag.querySelector("[data-content]");
const bloomArticle = bloomFrag.querySelector("[data-bloom]");
const bloomUsername = bloomFrag.querySelector("[data-username]");
const bloomTime = bloomFrag.querySelector("[data-time]");
const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])");
const bloomContent = bloomFrag.querySelector("[data-content]");

bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
bloomUsername.textContent = bloom.sender;
bloomTime.textContent = _formatTimestamp(bloom.sent_timestamp);
bloomTimeLink.setAttribute("href", `/bloom/${bloom.id}`);
bloomContent.replaceChildren(
...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html")
.body.childNodes
);
bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
bloomUsername.textContent = bloom.sender;
bloomTime.textContent = _formatTimestamp(bloom.sent_timestamp);
bloomTimeLink.setAttribute("href", `/bloom/${bloom.id}`);
bloomContent.replaceChildren(
...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html")
.body.childNodes
);

return bloomFrag;
return bloomFrag;
};

function _formatHashtags(text) {
if (!text) return text;
return text.replace(
/\B#[^#]+/g,
(match) => `<a href="/hashtag/${match.slice(1)}">${match}</a>`
);
if (!text) return text;
return text.replace(
/\B#[a-zA-Z0-9_]+/g,

(match) => `<a href="/hashtag/${match.slice(1)}">${match}</a>`
);
}

function _formatTimestamp(timestamp) {
if (!timestamp) return "";
if (!timestamp) return "";

try {
const date = new Date(timestamp);
const now = new Date();
const diffSeconds = Math.floor((now - date) / 1000);
try {
const date = new Date(timestamp);
const now = new Date();
const diffSeconds = Math.floor((now - date) / 1000);

// Less than a minute
if (diffSeconds < 60) {
return `${diffSeconds}s`;
}
// Less than a minute
if (diffSeconds < 60) {
return `${diffSeconds}s`;
}

// Less than an hour
const diffMinutes = Math.floor(diffSeconds / 60);
if (diffMinutes < 60) {
return `${diffMinutes}m`;
}
// Less than an hour
const diffMinutes = Math.floor(diffSeconds / 60);
if (diffMinutes < 60) {
return `${diffMinutes}m`;
}

// Less than a day
const diffHours = Math.floor(diffMinutes / 60);
if (diffHours < 24) {
return `${diffHours}h`;
}
// Less than a day
const diffHours = Math.floor(diffMinutes / 60);
if (diffHours < 24) {
return `${diffHours}h`;
}

// Less than a week
const diffDays = Math.floor(diffHours / 24);
if (diffDays < 7) {
return `${diffDays}d`;
}
// Less than a week
const diffDays = Math.floor(diffHours / 24);
if (diffDays < 7) {
return `${diffDays}d`;
}

// Format as month and day for older dates
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
}).format(date);
} catch (error) {
console.error("Failed to format timestamp:", error);
return "";
}
// Format as month and day for older dates
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
}).format(date);
} catch (error) {
console.error("Failed to format timestamp:", error);
return "";
}
}

export {createBloom};
export { createBloom };
50 changes: 25 additions & 25 deletions front-end/components/login.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 login component
Expand All @@ -7,36 +7,36 @@ import {apiService} from "../index.mjs";
* @returns {DocumentFragment} - The login fragment
*/
function createLogin(template, isLoggedIn) {
if (isLoggedIn) return;
const loginElement = document
.getElementById(template)
.content.cloneNode(true);
if (isLoggedIn) return;
const loginElement = document
.getElementById(template)
.content.cloneNode(true);

return loginElement;
return loginElement;
}
// HANDLER
async function handleLogin(event) {
event.preventDefault();
const form = event.target;
const submitButton = form.querySelector("[data-submit]");
const originalText = submitButton.textContent;
event.preventDefault();
const form = event.target;
const submitButton = form.querySelector("[data-submit]");
const originalText = submitButton.textContent;

try {
form.inert = true;
submitButton.textContent = "Logging in...";
try {
form.inert = true;
submitButton.textContent = "Logging in...";

const formData = new FormData(form);
const username = formData.get("username");
const password = formData.get("password");
const formData = new FormData(form);
const username = formData.get("username");
const password = formData.get("password");

await apiService.login(username, password);
} catch (error) {
throw error;
} finally {
// Always reset UI state regardless of success/failure
submitButton.textContent = originalText;
form.inert = false;
}
await apiService.login(username, password);
} catch (error) {
throw error;
} finally {
// Always reset UI state regardless of success/failure
submitButton.textContent = originalText;
form.inert = false;
}
}

export {createLogin, handleLogin};
export { createLogin, handleLogin };
108 changes: 56 additions & 52 deletions front-end/components/profile.mjs
Original file line number Diff line number Diff line change
@@ -1,69 +1,73 @@
import {apiService} from "../index.mjs";
import { apiService } from "../index.mjs";

/**
* Create a profile component
* @param {string} template - The ID of the template to clone
* @param {Object} profileData - The profile data to display
* @returns {DocumentFragment} - The profile UI
*/
function createProfile(template, {profileData, whoToFollow, isLoggedIn}) {
if (!template || !profileData) return;
const profileElement = document
.getElementById(template)
.content.cloneNode(true);
function createProfile(template, { profileData, whoToFollow, isLoggedIn }) {
if (!template || !profileData) return;
const profileElement = document
.getElementById(template)
.content.cloneNode(true);

const usernameEl = profileElement.querySelector("[data-username]");
const bloomCountEl = profileElement.querySelector("[data-bloom-count]");
const followingCountEl = profileElement.querySelector(
"[data-following-count]"
);
const followerCountEl = profileElement.querySelector("[data-follower-count]");
const followButtonEl = profileElement.querySelector("[data-action='follow']");
const whoToFollowContainer = profileElement.querySelector(".profile__who-to-follow");
// Populate with data
usernameEl.querySelector("h2").textContent = profileData.username || "";
usernameEl.setAttribute("href", `/profile/${profileData.username}`);
bloomCountEl.textContent = profileData.total_blooms || 0;
followerCountEl.textContent = profileData.followers?.length || 0;
followingCountEl.textContent = profileData.follows?.length || 0;
followButtonEl.setAttribute("data-username", profileData.username || "");
followButtonEl.hidden = profileData.is_self || profileData.is_following;
followButtonEl.addEventListener("click", handleFollow);
if (!isLoggedIn) {
followButtonEl.style.display = "none";
}
const usernameEl = profileElement.querySelector("[data-username]");
const bloomCountEl = profileElement.querySelector("[data-bloom-count]");
const followingCountEl = profileElement.querySelector(
"[data-following-count]"
);
const followerCountEl = profileElement.querySelector("[data-follower-count]");
const followButtonEl = profileElement.querySelector("[data-action='follow']");
const whoToFollowContainer = profileElement.querySelector(
".profile__who-to-follow"
);
// Populate with data
usernameEl.querySelector("h2").textContent = profileData.username || "";
usernameEl.setAttribute("href", `/profile/${profileData.username}`);
bloomCountEl.textContent = profileData.total_blooms || 0;
followerCountEl.textContent = profileData.followers?.length || 0;
followingCountEl.textContent = profileData.follows?.length || 0;
followButtonEl.setAttribute("data-username", profileData.username || "");
followButtonEl.hidden = profileData.is_self || profileData.is_following;
followButtonEl.addEventListener("click", handleFollow);
if (!isLoggedIn) {
followButtonEl.style.display = "none";
}

if (whoToFollow.length > 0) {
const whoToFollowList = whoToFollowContainer.querySelector("[data-who-to-follow]");
const whoToFollowTemplate = document.querySelector("#who-to-follow-chip");
for (const userToFollow of whoToFollow) {
const wtfElement = whoToFollowTemplate.content.cloneNode(true);
const usernameLink = wtfElement.querySelector("a[data-username]");
usernameLink.innerText = userToFollow.username;
usernameLink.setAttribute("href", `/profile/${userToFollow.username}`);
const followButton = wtfElement.querySelector("button");
followButton.setAttribute("data-username", userToFollow.username);
followButton.addEventListener("click", handleFollow);
if (!isLoggedIn) {
followButton.style.display = "none";
}
if (whoToFollow.length > 0) {
const whoToFollowList = whoToFollowContainer.querySelector(
"[data-who-to-follow]"
);
const whoToFollowTemplate = document.querySelector("#who-to-follow-chip");
for (const userToFollow of whoToFollow) {
const wtfElement = whoToFollowTemplate.content.cloneNode(true);
const usernameLink = wtfElement.querySelector("a[data-username]");
usernameLink.innerText = userToFollow.username;
usernameLink.setAttribute("href", `/profile/${userToFollow.username}`);
const followButton = wtfElement.querySelector("button");
followButton.setAttribute("data-username", userToFollow.username);
followButton.addEventListener("click", handleFollow);
if (!isLoggedIn) {
followButton.style.display = "none";
}

whoToFollowList.appendChild(wtfElement);
}
} else {
whoToFollowContainer.innerText = "";
}
whoToFollowList.appendChild(wtfElement);
}
} else {
whoToFollowContainer.innerText = "";
}

return profileElement;
return profileElement;
}

async function handleFollow(event) {
const button = event.target;
const username = button.getAttribute("data-username");
if (!username) return;
const button = event.target;
const username = button.getAttribute("data-username");
if (!username) return;

await apiService.followUser(username);
await apiService.getWhoToFollow();
await apiService.followUser(username);
await apiService.getWhoToFollow();
}

export {createProfile, handleFollow};
export { createProfile, handleFollow };
Loading