From 8db3541410255ca2d7ed84635072016b7d5ae03c Mon Sep 17 00:00:00 2001 From: Addison Ferrell Date: Fri, 14 Aug 2026 14:22:04 -0400 Subject: [PATCH] feat(notice-board): add persistent emoji reactions --- .../backend/lambda_function.py | 46 ++++++++++- .../02-notice-board/frontend/src/App.jsx | 77 +++++++++++++++++-- .../02-notice-board/frontend/src/api.js | 11 +++ 3 files changed, 124 insertions(+), 10 deletions(-) diff --git a/workshops/fullstack-aws/projects/02-notice-board/backend/lambda_function.py b/workshops/fullstack-aws/projects/02-notice-board/backend/lambda_function.py index 1f2cf81..fb04b67 100644 --- a/workshops/fullstack-aws/projects/02-notice-board/backend/lambda_function.py +++ b/workshops/fullstack-aws/projects/02-notice-board/backend/lambda_function.py @@ -2,11 +2,12 @@ import os from bson import ObjectId from bson.errors import InvalidId -from pymongo import MongoClient +from pymongo import MongoClient, ReturnDocument from datetime import datetime, timezone MONGO_HOST = os.environ["MONGO_HOST"] MONGO_PORT = int(os.environ.get("MONGO_PORT", 27017)) +REACTION_TYPES = ("thumbs_up", "heart", "smile", "fire") def get_collection(): @@ -39,6 +40,12 @@ def lambda_handler(event, context): body = json.loads(event.get("body") or "{}") return create_notice(body) + if method == "POST" and path.startswith("/notices/") and path.endswith("/reactions"): + parts = path.strip("/").split("/") + if len(parts) == 3 and parts[0] == "notices" and parts[2] == "reactions": + body = json.loads(event.get("body") or "{}") + return add_reaction(parts[1], body) + if method == "DELETE" and path.startswith("/notices/"): notice_id = path.split("/")[-1] return delete_notice(notice_id) @@ -51,9 +58,13 @@ def lambda_handler(event, context): def get_notices(): - notices = list(get_collection().find({}, {"_id": 1, "name": 1, "message": 1, "created_at": 1})) + notices = list(get_collection().find( + {}, + {"_id": 1, "name": 1, "message": 1, "created_at": 1, "reactions": 1}, + )) for n in notices: n["id"] = str(n.pop("_id")) + n["reactions"] = normalized_reactions(n.get("reactions")) return response(200, {"notices": notices}) def create_notice(data): @@ -66,6 +77,7 @@ def create_notice(data): "name": name, "message": message, "created_at": datetime.now(timezone.utc).isoformat(), + "reactions": {reaction: 0 for reaction in REACTION_TYPES}, } get_collection().insert_one(notice) @@ -73,6 +85,36 @@ def create_notice(data): return response(201, {"notice": notice}) +def add_reaction(notice_id, data): + try: + object_id = ObjectId(notice_id) + except InvalidId: + return response(400, {"error": f"Invalid notice id: {notice_id}"}) + + reaction = data.get("reaction") + if reaction not in REACTION_TYPES: + return response(400, {"error": "Invalid reaction"}) + + notice = get_collection().find_one_and_update( + {"_id": object_id}, + {"$inc": {f"reactions.{reaction}": 1}}, + projection={"reactions": 1}, + return_document=ReturnDocument.AFTER, + ) + if notice is None: + return response(404, {"error": "Notice not found"}) + + return response(200, { + "id": notice_id, + "reactions": normalized_reactions(notice.get("reactions")), + }) + + +def normalized_reactions(reactions): + reactions = reactions or {} + return {reaction: reactions.get(reaction, 0) for reaction in REACTION_TYPES} + + def delete_notice(notice_id): try: object_id = ObjectId(notice_id) diff --git a/workshops/fullstack-aws/projects/02-notice-board/frontend/src/App.jsx b/workshops/fullstack-aws/projects/02-notice-board/frontend/src/App.jsx index 49fc9a4..396da0b 100644 --- a/workshops/fullstack-aws/projects/02-notice-board/frontend/src/App.jsx +++ b/workshops/fullstack-aws/projects/02-notice-board/frontend/src/App.jsx @@ -1,11 +1,19 @@ import { useEffect, useState } from 'react' -import { getNotices, createNotice, deleteNotice } from './api' +import { getNotices, createNotice, deleteNotice, addReaction } from './api' + +const REACTIONS = [ + { key: 'thumbs_up', emoji: '👍', label: 'Thumbs up' }, + { key: 'heart', emoji: '❤️', label: 'Heart' }, + { key: 'smile', emoji: '😊', label: 'Smile' }, + { key: 'fire', emoji: '🔥', label: 'Fire' }, +] export default function App() { const [notices, setNotices] = useState([]) const [error, setError] = useState(null) const [name, setName] = useState('') const [message, setMessage] = useState('') + const [openReactionPicker, setOpenReactionPicker] = useState(null) const loadNotices = () => { getNotices() @@ -40,6 +48,18 @@ export default function App() { } } + const handleReaction = async (id, reaction) => { + try { + const data = await addReaction(id, reaction) + setNotices((current) => current.map((notice) => ( + notice.id === id ? { ...notice, reactions: data.reactions } : notice + ))) + setOpenReactionPicker(null) + } catch (err) { + setError(err.message) + } + } + return (

Notice Board

@@ -64,13 +84,54 @@ export default function App() { {notices.length === 0 && !error &&

No notices yet.

}
) diff --git a/workshops/fullstack-aws/projects/02-notice-board/frontend/src/api.js b/workshops/fullstack-aws/projects/02-notice-board/frontend/src/api.js index f7e9af6..03f1f8f 100644 --- a/workshops/fullstack-aws/projects/02-notice-board/frontend/src/api.js +++ b/workshops/fullstack-aws/projects/02-notice-board/frontend/src/api.js @@ -19,3 +19,14 @@ export async function deleteNotice(id) { const res = await fetch(`${API_URL}/notices/${id}`, { method: 'DELETE' }) return res.json() } + +export async function addReaction(id, reaction) { + const res = await fetch(`${API_URL}/notices/${id}/reactions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ reaction }), + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error || 'Failed to add reaction') + return data +}