diff --git a/backend/social_db_utils.py b/backend/social_db_utils.py new file mode 100644 index 0000000..a8d0f1e --- /dev/null +++ b/backend/social_db_utils.py @@ -0,0 +1,202 @@ +import os +import psycopg2 +from psycopg2.extras import RealDictCursor +from psycopg2 import errors + + +def get_db_connection(): + """Get database connection""" + return psycopg2.connect( + host=os.getenv("DB_HOST", "localhost"), + port=int(os.getenv("DB_PORT", "5432")), + database=os.getenv("DB_NAME", "recipes_db"), + user=os.getenv("DB_USER", "recipes_user"), + password=os.getenv("DB_PASSWORD", "recipes_password"), + ) + + +# ============= Friends System ============= + +def send_friend_request(sender_id: int, receiver_id: int): + """Send a friend request""" + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + try: + # Check if already friends + cur.execute( + "SELECT 1 FROM friendships WHERE (user_id = %s AND friend_id = %s) OR (user_id = %s AND friend_id = %s)", + (sender_id, receiver_id, receiver_id, sender_id) + ) + if cur.fetchone(): + return {"error": "Already friends"} + + # Check if request already exists + cur.execute( + "SELECT * FROM friend_requests WHERE sender_id = %s AND receiver_id = %s AND status = 'pending'", + (sender_id, receiver_id) + ) + existing = cur.fetchone() + if existing: + return dict(existing) + + try: + cur.execute( + """ + INSERT INTO friend_requests (sender_id, receiver_id) + VALUES (%s, %s) + RETURNING id, sender_id, receiver_id, status, created_at + """, + (sender_id, receiver_id) + ) + request = cur.fetchone() + conn.commit() + return dict(request) + except errors.UniqueViolation: + # Request already exists, fetch and return it + conn.rollback() + cur.execute( + "SELECT id, sender_id, receiver_id, status, created_at FROM friend_requests WHERE sender_id = %s AND receiver_id = %s", + (sender_id, receiver_id) + ) + existing_request = cur.fetchone() + if existing_request: + return dict(existing_request) + return {"error": "Friend request already exists"} + finally: + cur.close() + conn.close() + + +def accept_friend_request(request_id: int): + """Accept a friend request and create friendship""" + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + try: + # Get request details + cur.execute( + "SELECT sender_id, receiver_id FROM friend_requests WHERE id = %s AND status = 'pending'", + (request_id,) + ) + request = cur.fetchone() + if not request: + return {"error": "Request not found or already processed"} + + sender_id = request["sender_id"] + receiver_id = request["receiver_id"] + + # Create bidirectional friendship + cur.execute( + "INSERT INTO friendships (user_id, friend_id) VALUES (%s, %s), (%s, %s) ON CONFLICT DO NOTHING", + (sender_id, receiver_id, receiver_id, sender_id) + ) + + # Update request status + cur.execute( + "UPDATE friend_requests SET status = 'accepted', updated_at = CURRENT_TIMESTAMP WHERE id = %s", + (request_id,) + ) + + conn.commit() + return {"success": True} + finally: + cur.close() + conn.close() + + +def reject_friend_request(request_id: int): + """Reject a friend request""" + conn = get_db_connection() + cur = conn.cursor() + try: + cur.execute( + "UPDATE friend_requests SET status = 'rejected', updated_at = CURRENT_TIMESTAMP WHERE id = %s AND status = 'pending'", + (request_id,) + ) + conn.commit() + return {"success": True} + finally: + cur.close() + conn.close() + + +def get_friend_requests(user_id: int): + """Get pending friend requests for a user""" + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + try: + cur.execute( + """ + SELECT fr.id AS request_id, fr.sender_id, fr.receiver_id, fr.status, fr.created_at, + u.username AS sender_username, u.display_name AS sender_display_name, u.email AS sender_email + FROM friend_requests fr + JOIN users u ON u.id = fr.sender_id + WHERE fr.receiver_id = %s AND fr.status = 'pending' + ORDER BY fr.created_at DESC + """, + (user_id,) + ) + return [dict(row) for row in cur.fetchall()] + finally: + cur.close() + conn.close() + + +def get_friends(user_id: int): + """Get list of user's friends""" + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + try: + cur.execute( + """ + SELECT u.id, u.username, u.display_name, u.email, f.created_at AS friends_since + FROM friendships f + JOIN users u ON u.id = f.friend_id + WHERE f.user_id = %s + ORDER BY u.display_name + """, + (user_id,) + ) + return [dict(row) for row in cur.fetchall()] + finally: + cur.close() + conn.close() + + +def remove_friend(user_id: int, friend_id: int): + """Remove a friend""" + conn = get_db_connection() + cur = conn.cursor() + try: + cur.execute( + "DELETE FROM friendships WHERE (user_id = %s AND friend_id = %s) OR (user_id = %s AND friend_id = %s)", + (user_id, friend_id, friend_id, user_id) + ) + conn.commit() + return {"success": True} + finally: + cur.close() + conn.close() + + +def search_users(query: str, current_user_id: int, limit: int = 20): + """Search for users by username or display name""" + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + try: + search_pattern = f"%{query}%" + cur.execute( + """ + SELECT u.id, u.username, u.display_name, u.email, + EXISTS(SELECT 1 FROM friendships WHERE user_id = %s AND friend_id = u.id) AS is_friend, + EXISTS(SELECT 1 FROM friend_requests WHERE sender_id = %s AND receiver_id = u.id AND status = 'pending') AS request_sent + FROM users u + WHERE (u.username ILIKE %s OR u.display_name ILIKE %s) AND u.id != %s + ORDER BY u.display_name + LIMIT %s + """, + (current_user_id, current_user_id, search_pattern, search_pattern, current_user_id, limit) + ) + return [dict(row) for row in cur.fetchall()] + finally: + cur.close() + conn.close() diff --git a/frontend/index.html b/frontend/index.html index a6c3b1f..6d6c88a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,8 @@ - + + My Recipes | המתכונים שלי diff --git a/frontend/src/App.css b/frontend/src/App.css index d97c31e..ad167e3 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -96,6 +96,18 @@ body { } } +.topbar-logo { + height: 2.5rem; + width: auto; + object-fit: contain; +} + +@media (min-width: 768px) { + .topbar-logo { + height: 3rem; + } +} + .logo-emoji { font-size: 1.8rem; } diff --git a/frontend/src/assets/my-recipes-logo-dark.png b/frontend/src/assets/my-recipes-logo-dark.png new file mode 100644 index 0000000..b8d30a1 Binary files /dev/null and b/frontend/src/assets/my-recipes-logo-dark.png differ diff --git a/frontend/src/assets/my-recipes-logo-light.png b/frontend/src/assets/my-recipes-logo-light.png new file mode 100644 index 0000000..60a23e1 Binary files /dev/null and b/frontend/src/assets/my-recipes-logo-light.png differ diff --git a/frontend/src/assets/placeholder-dark.png b/frontend/src/assets/placeholder-dark.png new file mode 100644 index 0000000..d37006b Binary files /dev/null and b/frontend/src/assets/placeholder-dark.png differ diff --git a/frontend/src/assets/placeholder-light.png b/frontend/src/assets/placeholder-light.png new file mode 100644 index 0000000..2684105 Binary files /dev/null and b/frontend/src/assets/placeholder-light.png differ diff --git a/frontend/src/components/GroceryLists.jsx b/frontend/src/components/GroceryLists.jsx index 8467a0d..ac44e05 100644 --- a/frontend/src/components/GroceryLists.jsx +++ b/frontend/src/components/GroceryLists.jsx @@ -582,7 +582,7 @@ function GroceryLists({ user, onShowToast }) { )} - ); } diff --git a/frontend/src/components/PinnedGroceryLists.jsx b/frontend/src/components/PinnedGroceryLists.jsx index c6abf14..e00d0f2 100644 --- a/frontend/src/components/PinnedGroceryLists.jsx +++ b/frontend/src/components/PinnedGroceryLists.jsx @@ -79,7 +79,7 @@ function PinnedGroceryLists({ onShowToast }) { ))} -