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 @@ -
- - -