diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf..fea43c2 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -13,21 +13,24 @@ class Bloom: sender: User content: str sent_timestamp: datetime.datetime + original_sender: Optional[str] = None + rebloom_count: int = 0 -def add_bloom(*, sender: User, content: str) -> Bloom: +def add_bloom(*, sender: User, content: str, original_sender: str = None) -> Bloom: hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] now = datetime.datetime.now(tz=datetime.UTC) bloom_id = int(now.timestamp() * 1000000) with db_cursor() as cur: cur.execute( - "INSERT INTO blooms (id, sender_id, content, send_timestamp) VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s)", + "INSERT INTO blooms (id, sender_id, content, send_timestamp, original_sender) VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s, %(og_sender)s)", dict( bloom_id=bloom_id, sender_id=sender.id, content=content, timestamp=datetime.datetime.now(datetime.UTC), + og_sender=original_sender, ), ) for hashtag in hashtags: @@ -54,7 +57,7 @@ def get_blooms_for_user( cur.execute( f"""SELECT - blooms.id, users.username, content, send_timestamp + blooms.id, users.username, content, send_timestamp, original_sender, rebloom_count FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE @@ -68,13 +71,15 @@ def get_blooms_for_user( rows = cur.fetchall() blooms = [] for row in rows: - bloom_id, sender_username, content, timestamp = row + bloom_id, sender_username, content, timestamp, og_sender, count = row blooms.append( Bloom( id=bloom_id, sender=sender_username, content=content, sent_timestamp=timestamp, + original_sender=og_sender, + rebloom_count=count, ) ) return blooms @@ -83,18 +88,21 @@ def get_blooms_for_user( def get_bloom(bloom_id: int) -> Optional[Bloom]: with db_cursor() as cur: cur.execute( - "SELECT blooms.id, users.username, content, send_timestamp FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE blooms.id = %s", + "SELECT blooms.id, users.username, content, send_timestamp, original_sender, rebloom_count FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE blooms.id = %s", (bloom_id,), ) row = cur.fetchone() if row is None: return None - bloom_id, sender_username, content, timestamp = row + bloom_id, sender_username, content, timestamp, og_sender, count = row return Bloom( id=bloom_id, sender=sender_username, content=content, sent_timestamp=timestamp, + original_sender=og_sender, + rebloom_count=count, + ) @@ -108,7 +116,7 @@ def get_blooms_with_hashtag( with db_cursor() as cur: cur.execute( f"""SELECT - blooms.id, users.username, content, send_timestamp + blooms.id, users.username, content, send_timestamp, original_sender, rebloom_count FROM blooms INNER JOIN hashtags ON blooms.id = hashtags.bloom_id INNER JOIN users ON blooms.sender_id = users.id WHERE @@ -121,13 +129,15 @@ def get_blooms_with_hashtag( rows = cur.fetchall() blooms = [] for row in rows: - bloom_id, sender_username, content, timestamp = row + bloom_id, sender_username, content, timestamp, og_sender, count = row blooms.append( Bloom( id=bloom_id, sender=sender_username, content=content, sent_timestamp=timestamp, + original_sender=og_sender, + rebloom_count=count, ) ) return blooms diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..cb97f4a 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -157,8 +157,10 @@ def send_bloom(): return type_check_error user = get_current_user() - - blooms.add_bloom(sender=user, content=request.json["content"]) + # //rebloom get new filed for repost + original_sender = request.json.get("original_sender") + # //add og sender to add bloom + blooms.add_bloom(sender=user, content=request.json["content"], original_sender=original_sender) return jsonify( { diff --git a/db/schema.sql b/db/schema.sql index 61e7580..63bdf90 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,3 +1,10 @@ +DROP TABLE IF EXISTS hashtags CASCADE; +DROP TABLE IF EXISTS follows CASCADE; +DROP TABLE IF EXISTS blooms CASCADE; +DROP TABLE IF EXISTS users CASCADE; + +-- //so I can add sender and count + CREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR NOT NULL, @@ -10,7 +17,9 @@ CREATE TABLE blooms ( id BIGSERIAL NOT NULL PRIMARY KEY, sender_id INT NOT NULL REFERENCES users(id), content TEXT NOT NULL, - send_timestamp TIMESTAMP NOT NULL + send_timestamp TIMESTAMP NOT NULL, + riginal_sender VARCHAR, + rebloom_count INT DEFAULT 0 ); CREATE TABLE follows ( diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c..a35be93 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -8,80 +8,109 @@ * "sender": username, * "content": "string from textarea", * "sent_timestamp": "datetime as ISO 8601 formatted string"} - +// to rebloom I add 2 fields + "original_sender": "original_username", + "rebloom_count": 0 */ const createBloom = (template, bloom) => { - 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]"); - - 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; + 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]"); + + 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 + ); + + // rebloom function + //grab new fields + const rebloomLabel = bloomFrag.querySelector("[data-rebloom-label]"); + const rebloomCounter = bloomFrag.querySelector("[data-rebloom-count]"); + + //see if the otiginal senser field is filled with repost, show el + if (bloom.original_sender) { + rebloomLabel.textContent = `(rebloomed ${bloom.original_sender})`; + rebloomLabel.classList.remove("hidden"); + } else { + rebloomLabel.classList.add("hidden"); + } + + //see if count has been increased and show el + if (bloom.rebloom_count >= 1) { + rebloomCounter.textContent = bloom.rebloom_count; + rebloomCounter.classList.remove("hidden"); + } else { + rebloomCounter.classList.add("hidden"); + } + + const rebloomBtn = bloomFrag.querySelector("[data-action='rebloom-btn']"); + rebloomBtn.addEventListener("click", async () => { + await apiService.postBloom(bloom.content, bloom.sender); + + window.location.reload(); + }); + return bloomFrag; }; function _formatHashtags(text) { - if (!text) return text; - return text.replace( - /\B#[^#]+/g, - (match) => `${match}` - ); + if (!text) return text; + return text.replace( + /\B#[^#]+/g, + (match) => `${match}` + ); } function _formatTimestamp(timestamp) { - if (!timestamp) return ""; - - 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 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 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 ""; - } + if (!timestamp) return ""; + + 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 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 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 ""; + } } -export {createBloom}; +export { createBloom }; diff --git a/front-end/index.css b/front-end/index.css index 65c7fb4..13b075e 100644 --- a/front-end/index.css +++ b/front-end/index.css @@ -1,269 +1,269 @@ /* design palette */ :root { - --space: clamp(4px, 1vw + 4px, 30px); - --container: clamp(280px, calc(100dvw - calc(var(--space) * 2)), 1280px); - --key: 260; - --brand: hsl(var(--key), 65%, 35%); - --accent: hsl(var(--key), 15%, 50%); - --error: hsla(0, 100%, 67%, 1); - --paper: hsla(var(--key), 45%, 96%); - --ink: hsl(var(--key), 15%, 10%); - --outline: hsl(var(--key), 25%, 90%); - --shadow: var(--outline) 0px 0px 0px 1px, var(--outline) 0px 1px 0px 0px, - var(--outline) 0px 0px 2.5px 0px, 0px 3.25px 6px var(--outline); - --inset: inset 0px -3.25px 12px var(--outline); - --corner: 2.5px; - --pill: 9999px; + --space: clamp(4px, 1vw + 4px, 30px); + --container: clamp(280px, calc(100dvw - calc(var(--space) * 2)), 1280px); + --key: 260; + --brand: hsl(var(--key), 65%, 35%); + --accent: hsl(var(--key), 15%, 50%); + --error: hsla(0, 100%, 67%, 1); + --paper: hsla(var(--key), 45%, 96%); + --ink: hsl(var(--key), 15%, 10%); + --outline: hsl(var(--key), 25%, 90%); + --shadow: var(--outline) 0px 0px 0px 1px, var(--outline) 0px 1px 0px 0px, + var(--outline) 0px 0px 2.5px 0px, 0px 3.25px 6px var(--outline); + --inset: inset 0px -3.25px 12px var(--outline); + --corner: 2.5px; + --pill: 9999px; } * { - box-sizing: border-box; + box-sizing: border-box; } /* MAIN LAYOUT */ /* // layout */ body { - background: var(--paper); - animation: fade 600ms cubic-bezier(0.075, 0.82, 0.165, 1); - margin: 0; - display: grid; - grid-template: - ". header ." auto - ". main ." 1fr - ". footer ." auto / minmax(var(--space), 1fr) var(--container) minmax(var(--space), 1fr); - header { - grid-area: header; - display: flex; - justify-content: space-between; - align-items: center; - h1 { - display: flex; - align-items: center; - } - } + background: var(--paper); + animation: fade 600ms cubic-bezier(0.075, 0.82, 0.165, 1); + margin: 0; + display: grid; + grid-template: + ". header ." auto + ". main ." 1fr + ". footer ." auto / minmax(var(--space), 1fr) var(--container) minmax(var(--space), 1fr); + header { + grid-area: header; + display: flex; + justify-content: space-between; + align-items: center; + h1 { + display: flex; + align-items: center; + } + } - main { - grid-area: main; - } - footer { - grid-area: footer; - } + main { + grid-area: main; + } + footer { + grid-area: footer; + } } main { - display: grid; - grid-template: - "heading" min-content - "profile" auto - "bloom-form" min-content - "timeline" 1fr - "signup" auto / var(--container); - gap: var(--space); + display: grid; + grid-template: + "heading" min-content + "profile" auto + "bloom-form" min-content + "timeline" 1fr + "signup" auto / var(--container); + gap: var(--space); } @media (min-width: 37.5em) { - main { - grid-template: - "profile bloom-form" min-content - "profile heading" min-content - "profile timeline" auto - "....... timeline" auto - "....... signup" minmax(80vh, 1fr) / 280px auto; - } + main { + grid-template: + "profile bloom-form" min-content + "profile heading" min-content + "profile timeline" auto + "....... timeline" auto + "....... signup" minmax(80vh, 1fr) / 280px auto; + } } .profile__component { - grid-area: profile; + grid-area: profile; } .bloom-form__component { - grid-area: bloom-form; + grid-area: bloom-form; } .signup__component { - grid-area: signup; + grid-area: signup; } .timeline__component { - grid-area: timeline; + grid-area: timeline; } .logout__component { - margin: auto 0 auto auto; + margin: auto 0 auto auto; } .heading__component { - grid-area: heading; + grid-area: heading; } /* base elements */ html { - box-sizing: border-box; - font: 100%/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", - Roboto, sans-serif; - background-color: var(--paper); - color: var(--ink); + box-sizing: border-box; + font: 100%/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, sans-serif; + background-color: var(--paper); + color: var(--ink); } a:any-link { - color: var(--brand); - font-weight: bold; + color: var(--brand); + font-weight: bold; } ul[class], li[class] { - list-style: none; + list-style: none; } button { - appearance: none; - border: 1px solid var(--outline); - padding: calc(var(--space) / 2) var(--space); - border-radius: var(--corner); + appearance: none; + border: 1px solid var(--outline); + padding: calc(var(--space) / 2) var(--space); + border-radius: var(--corner); } label, legend { - font-family: monospace; - font-weight: 900; + font-family: monospace; + font-weight: 900; } input, textarea { - width: 100%; - padding: calc(var(--space) / 1.5); - background-color: var(--paper); - box-shadow: var(--inset); - border-radius: var(--corner); - border: var(--border); + width: 100%; + padding: calc(var(--space) / 1.5); + background-color: var(--paper); + box-shadow: var(--inset); + border-radius: var(--corner); + border: var(--border); } button { - font: 600 100% monospace, system-ui; - white-space: nowrap; - color: var(--ink); - background-color: transparent; - border: 0.5px solid var(--brand); - border-radius: var(--pill); - box-shadow: 2px 3px var(--brand); - padding: calc(var(--space) / 3) var(--space); - transition: all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275), - border-color 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); + font: 600 100% monospace, system-ui; + white-space: nowrap; + color: var(--ink); + background-color: transparent; + border: 0.5px solid var(--brand); + border-radius: var(--pill); + box-shadow: 2px 3px var(--brand); + padding: calc(var(--space) / 3) var(--space); + transition: all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275), + border-color 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); } button.is-active, button:active, button:focus { - box-shadow: 0 0 var(--accent); - border-color: initial; - text-decoration: none; + box-shadow: 0 0 var(--accent); + border-color: initial; + text-decoration: none; } @media (hover) { - button:hover { - box-shadow: 0 0 var(--accent); - border-color: initial; - text-decoration: none; - } + button:hover { + box-shadow: 0 0 var(--accent); + border-color: initial; + text-decoration: none; + } } button:focus { - outline: 3px dotted var(--accent); + outline: 3px dotted var(--accent); } dialog { - min-width: var(--container); + min-width: var(--container); } .system-message { - font-family: monospace; - color: var(--brand); + font-family: monospace; + color: var(--brand); } /* COMPONENTS */ /* BLOOM FORM */ .bloom-form__form { - display: grid; - grid-template: - "textarea textarea" auto - "char-count submit" auto / 1fr min-content; + display: grid; + grid-template: + "textarea textarea" auto + "char-count submit" auto / 1fr min-content; } .bloom-form__field { - grid-area: textarea; + grid-area: textarea; } .bloom-form__submit { - grid-area: submit; - margin: 0 auto auto 0; + grid-area: submit; + margin: 0 auto auto 0; } .bloom-form__char-count { - grid-area: char-count; + grid-area: char-count; } /* LOGIN */ .login__container { - margin: 1em 0 auto auto; + margin: 1em 0 auto auto; } .login__form { - display: flex; - gap: calc(var(--space) / 3); + display: flex; + gap: calc(var(--space) / 3); } .login__footer { - margin: 0; + margin: 0; } /* PROFILE */ .profile__avatar { - font-size: 3em; + font-size: 3em; } /* TIMELINE */ .timeline__content { - min-height: 80dvh; - display: grid; - gap: var(--space); + min-height: 80dvh; + display: grid; + gap: var(--space); } /* states, helpers*/ .flex { - display: flex; - justify-content: space-between; + display: flex; + justify-content: space-between; } .grid { - display: grid; - gap: var(--space); + display: grid; + gap: var(--space); } .box { - padding: var(--space); - background-color: var(--paper); - box-shadow: var(--shadow); - border-radius: var(--corner); - border: 0; - display: grid; - gap: var(--space); + padding: var(--space); + background-color: var(--paper); + box-shadow: var(--shadow); + border-radius: var(--corner); + border: 0; + display: grid; + gap: var(--space); } .is-invisible { - clip: rect(0 0 0 0); - clip-path: inset(50%); - height: 1px; - overflow: hidden; - position: absolute; - white-space: nowrap; - width: 1px; + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; } .is-error, :invalid, [data-error] { - color: var(--error); + color: var(--error); } @keyframes fade { - from { - opacity: 0%; - } - to { - opacity: 100%; - } + from { + opacity: 0%; + } + to { + opacity: 100%; + } } /* We always want attribute hidden to sync with display:none, no matter what */ [hidden] { - display: none !important; + display: none !important; } diff --git a/front-end/index.html b/front-end/index.html index 89d6b13..b5fdd41 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -1,261 +1,268 @@ - - - - Purple Forest - - - -
-

- Purple Forest - PurpleForest -

-
-
-
+ + + + Purple Forest + + + +
+

+ Purple Forest + PurpleForest +

+
+
+
-
-
-
-
-
-
-
- -
- - - - + + + - - + + - + - - + + diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index f4b5339..fcc3793 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 @@ -14,290 +14,291 @@ import {handleErrorDialog} from "../components/error.mjs"; // Helper function for making API requests async function _apiRequest(endpoint, options = {}) { - const token = state.token; - const baseUrl = "http://localhost:3000"; - - const defaultOptions = { - headers: { - "Content-Type": "application/json", - ...(token ? {Authorization: `Bearer ${token}`} : {}), - }, - mode: "cors", - credentials: "include", - }; - - const fetchOptions = {...defaultOptions, ...options}; - const url = endpoint.startsWith("http") ? endpoint : `${baseUrl}${endpoint}`; - - try { - const response = await fetch(url, fetchOptions); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - const error = new Error( - errorData.message || `API error: ${response.status}` - ); - error.status = response.status; - - // Handle auth errors - if (error.status === 401 || error.status === 403) { - if (!endpoint.includes("/login") && !endpoint.includes("/register")) { - state.destroyState(); - } - } - - // Pass all errors forward to a dialog on the screen - handleErrorDialog(error); - throw error; - } - - const contentType = response.headers.get("content-type"); - return contentType?.includes("application/json") - ? await response.json() - : {success: true}; - } catch (error) { - if (!error.status) { - // Only handle network errors here, response errors are handled above - handleErrorDialog(error); - } - throw error; // Re-throw so it can be caught by the calling function - } + const token = state.token; + const baseUrl = "http://localhost:3000"; + + const defaultOptions = { + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + mode: "cors", + credentials: "include", + }; + + const fetchOptions = { ...defaultOptions, ...options }; + const url = endpoint.startsWith("http") ? endpoint : `${baseUrl}${endpoint}`; + + try { + const response = await fetch(url, fetchOptions); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + const error = new Error( + errorData.message || `API error: ${response.status}` + ); + error.status = response.status; + + // Handle auth errors + if (error.status === 401 || error.status === 403) { + if (!endpoint.includes("/login") && !endpoint.includes("/register")) { + state.destroyState(); + } + } + + // Pass all errors forward to a dialog on the screen + handleErrorDialog(error); + throw error; + } + + const contentType = response.headers.get("content-type"); + return contentType?.includes("application/json") + ? await response.json() + : { success: true }; + } catch (error) { + if (!error.status) { + // Only handle network errors here, response errors are handled above + handleErrorDialog(error); + } + throw error; // Re-throw so it can be caught by the calling function + } } // Local helper to update a profile in the profiles array function _updateProfile(username, profileData) { - const profiles = [...state.profiles]; - const index = profiles.findIndex((p) => p.username === username); - - if (index !== -1) { - profiles[index] = {...profiles[index], ...profileData}; - } else { - profiles.push({username, ...profileData}); - } - state.updateState({profiles}); + const profiles = [...state.profiles]; + const index = profiles.findIndex((p) => p.username === username); + + if (index !== -1) { + profiles[index] = { ...profiles[index], ...profileData }; + } else { + profiles.push({ username, ...profileData }); + } + state.updateState({ profiles }); } // ====== AUTH methods async function login(username, password) { - try { - const data = await _apiRequest("/login", { - method: "POST", - body: JSON.stringify({username, password}), - }); - - if (data.success && data.token) { - state.updateState({ - token: data.token, - currentUser: username, - isLoggedIn: true, - }); - await Promise.all([getBlooms(), getProfile(username), getWhoToFollow()]); - } - - return data; - } catch (error) { - return {success: false}; - } + try { + const data = await _apiRequest("/login", { + method: "POST", + body: JSON.stringify({ username, password }), + original_sender, //added sender for rebloom + }); + + if (data.success && data.token) { + state.updateState({ + token: data.token, + currentUser: username, + isLoggedIn: true, + }); + await Promise.all([getBlooms(), getProfile(username), getWhoToFollow()]); + } + + return data; + } catch (error) { + return { success: false }; + } } async function getWhoToFollow() { - try { - const usernamesToFollow = await _apiRequest("/suggested-follows/3"); + 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: []}); - return []; - } + return usernamesToFollow; + } catch (error) { + // Error already handled by _apiRequest + state.updateState({ usernamesToFollow: [] }); + return []; + } } async function signup(username, password) { - try { - const data = await _apiRequest("/register", { - method: "POST", - body: JSON.stringify({username, password}), - }); - - if (data.success && data.token) { - state.updateState({ - token: data.token, - currentUser: username, - isLoggedIn: true, - }); - await getProfile(username); - } - - return data; - } catch (error) { - return {success: false}; - } + try { + const data = await _apiRequest("/register", { + method: "POST", + body: JSON.stringify({ username, password }), + }); + + if (data.success && data.token) { + state.updateState({ + token: data.token, + currentUser: username, + isLoggedIn: true, + }); + await getProfile(username); + } + + return data; + } catch (error) { + return { success: false }; + } } function logout() { - state.destroyState(); - return {success: true}; + state.destroyState(); + return { success: true }; } // ===== BLOOM methods async function getBloom(bloomId) { - const endpoint = `/bloom/${bloomId}`; - const bloom = await _apiRequest(endpoint); - state.updateState({singleBloomToShow: bloom}); - return bloom; + const endpoint = `/bloom/${bloomId}`; + const bloom = await _apiRequest(endpoint); + state.updateState({ singleBloomToShow: bloom }); + return bloom; } async function getBlooms(username) { - const endpoint = username ? `/blooms/${username}` : "/home"; - - try { - const blooms = await _apiRequest(endpoint); - - if (username) { - _updateProfile(username, {blooms}); - } else { - state.updateState({timelineBlooms: blooms}); - } - - return blooms; - } catch (error) { - // Error already handled by _apiRequest - if (username) { - _updateProfile(username, {blooms: []}); - } else { - state.updateState({timelineBlooms: []}); - } - return []; - } + const endpoint = username ? `/blooms/${username}` : "/home"; + + try { + const blooms = await _apiRequest(endpoint); + + if (username) { + _updateProfile(username, { blooms }); + } else { + state.updateState({ timelineBlooms: blooms }); + } + + return blooms; + } catch (error) { + // Error already handled by _apiRequest + if (username) { + _updateProfile(username, { blooms: [] }); + } else { + state.updateState({ timelineBlooms: [] }); + } + return []; + } } /** * Fetches blooms containing a specific hashtag */ async function getBloomsByHashtag(hashtag) { - const tag = hashtag.startsWith("#") ? hashtag.substring(1) : hashtag; - const endpoint = `/hashtag/${encodeURIComponent(tag)}`; - - try { - const blooms = await _apiRequest(endpoint); - state.updateState({ - hashtagBlooms: blooms, - currentHashtag: `#${tag}`, - }); - return blooms; - } catch (error) { - // Error already handled by _apiRequest - return {success: false}; - } + const tag = hashtag.startsWith("#") ? hashtag.substring(1) : hashtag; + const endpoint = `/hashtag/${encodeURIComponent(tag)}`; + + try { + const blooms = await _apiRequest(endpoint); + state.updateState({ + hashtagBlooms: blooms, + currentHashtag: `#${tag}`, + }); + return blooms; + } catch (error) { + // Error already handled by _apiRequest + return { success: false }; + } } async function postBloom(content) { - try { - const data = await _apiRequest("/bloom", { - method: "POST", - body: JSON.stringify({content}), - }); - - if (data.success) { - await getBlooms(); - await getProfile(state.currentUser); - } - - return data; - } catch (error) { - // Error already handled by _apiRequest - return {success: false}; - } + try { + const data = await _apiRequest("/bloom", { + method: "POST", + body: JSON.stringify({ content }), + }); + + if (data.success) { + await getBlooms(); + await getProfile(state.currentUser); + } + + return data; + } catch (error) { + // Error already handled by _apiRequest + return { success: false }; + } } // ======= USER methods async function getProfile(username) { - const endpoint = username ? `/profile/${username}` : "/profile"; - - try { - const profileData = await _apiRequest(endpoint); - - if (username) { - _updateProfile(username, profileData); - } else { - const currentUsername = profileData.username; - const fullProfileData = await _apiRequest(`/profile/${currentUsername}`); - _updateProfile(currentUsername, fullProfileData); - state.updateState({currentUser: currentUsername, isLoggedIn: true}); - } - - return profileData; - } catch (error) { - // Error already handled by _apiRequest - if (!username) { - state.updateState({isLoggedIn: false, currentUser: null}); - } - return {success: false}; - } + const endpoint = username ? `/profile/${username}` : "/profile"; + + try { + const profileData = await _apiRequest(endpoint); + + if (username) { + _updateProfile(username, profileData); + } else { + const currentUsername = profileData.username; + const fullProfileData = await _apiRequest(`/profile/${currentUsername}`); + _updateProfile(currentUsername, fullProfileData); + state.updateState({ currentUser: currentUsername, isLoggedIn: true }); + } + + return profileData; + } catch (error) { + // Error already handled by _apiRequest + if (!username) { + state.updateState({ isLoggedIn: false, currentUser: null }); + } + return { success: false }; + } } async function followUser(username) { - try { - const data = await _apiRequest("/follow", { - method: "POST", - body: JSON.stringify({follow_username: username}), - }); - - if (data.success) { - await Promise.all([ - getProfile(username), - getProfile(state.currentUser), - getBlooms(), - ]); - } - - return data; - } catch (error) { - return {success: false}; - } + try { + const data = await _apiRequest("/follow", { + method: "POST", + body: JSON.stringify({ follow_username: username }), + }); + + if (data.success) { + await Promise.all([ + getProfile(username), + getProfile(state.currentUser), + getBlooms(), + ]); + } + + return data; + } catch (error) { + return { success: false }; + } } async function unfollowUser(username) { - try { - const data = await _apiRequest(`/unfollow/${username}`, { - method: "POST", - }); - - if (data.success) { - // Update both the unfollowed user's profile and the current user's profile - await Promise.all([ - getProfile(username), - getProfile(state.currentUser), - getBlooms(), - ]); - } - - return data; - } catch (error) { - // Error already handled by _apiRequest - return {success: false}; - } + try { + const data = await _apiRequest(`/unfollow/${username}`, { + method: "POST", + }); + + if (data.success) { + // Update both the unfollowed user's profile and the current user's profile + await Promise.all([ + getProfile(username), + getProfile(state.currentUser), + getBlooms(), + ]); + } + + return data; + } catch (error) { + // Error already handled by _apiRequest + return { success: false }; + } } const apiService = { - // Auth methods - login, - signup, - logout, - - // Bloom methods - getBloom, - getBlooms, - postBloom, - getBloomsByHashtag, - - // User methods - getProfile, - followUser, - unfollowUser, - getWhoToFollow, + // Auth methods + login, + signup, + logout, + + // Bloom methods + getBloom, + getBlooms, + postBloom, + getBloomsByHashtag, + + // User methods + getProfile, + followUser, + unfollowUser, + getWhoToFollow, }; -export {apiService}; +export { apiService };