From df7510da2e8bdb0dcd76de84f5b17b0f9aee0cc2 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Thu, 11 Dec 2025 15:51:54 +0200 Subject: [PATCH] Add grocery-lists --- .../grocery_db_utils.cpython-313.pyc | Bin 10981 -> 12379 bytes backend/__pycache__/main.cpython-313.pyc | Bin 28034 -> 29202 bytes .../notification_db_utils.cpython-313.pyc | Bin 4076 -> 4076 bytes backend/add_is_pinned_column.sql | 5 + backend/grocery_db_utils.py | 37 ++- backend/main.py | 27 ++- backend/schema.sql | 1 + frontend/src/App.css | 18 ++ frontend/src/App.jsx | 6 + frontend/src/components/GroceryLists.jsx | 52 +++- .../src/components/PinnedGroceryLists.jsx | 222 ++++++++++++++++++ frontend/src/groceryApi.js | 22 ++ 12 files changed, 373 insertions(+), 17 deletions(-) create mode 100644 backend/add_is_pinned_column.sql create mode 100644 frontend/src/components/PinnedGroceryLists.jsx diff --git a/backend/__pycache__/grocery_db_utils.cpython-313.pyc b/backend/__pycache__/grocery_db_utils.cpython-313.pyc index 9c1f8d99bfd3f632adaaaebc0d0b54505d5ed02d..7d147f149c091957f0642292ef288406677b558c 100644 GIT binary patch delta 898 zcmY*YO-LI-6rR~6>t?h0(PT}u$vO?DW@{=2TPR9tf~E?Vq`0OPDn>DB8j(bH6GXw< zmL9YyHl2gVdX%1oTzahZBJ|J*J)|kvUV7+lK`peW&L&ax!OR=pd*3&2zM1(I|2}Ow zHk(;M%sSAY8Sxe^F8IFK6@qQWrv=zvJc__8xDzIz8^3~nM&c$}iJfGNDsQGGS38)~ z%dKfBdP;jRLczh(A6*|!y{7QKw-txkJ|?3i7Bbmvs+48_>8Uv_;leMi{)>rku(Z+~ za=KxSU&-8Qh3)Fz|SlUo%8}A037QGxzf70Ueap_e*@hO zvWB=0KezJ+!^DFcN;J@MYRrs-9iA@wtZv2xz+-Cz1mg59SYOAWq^pr{#^~kXcrKlu zPa$#;q~vGv1qD6H$!G?p<=kvaUPAL3C6D~M#cWDOx$OLsC|@Hq%fnLSeq?YQWs(7O zrb80HFgf}kjYP&GB#!PNpMt{CAyfk=8j)z#cwWC;Wzhx0D(|>@DI!J44h@y-gaL%h zynut89Zxqq@L$?@t~L&}wplO1MFxLvZnyQ2hUI`Z-5`_Lgw*X*8rfMY2E0P`+T)?!Y#uW%Vax z+A^)1DqOr`c9c2c#Go^>Dg#>hZFkX)g*E!f%2(`8)eDT=mT}#Fc4G56mkyJq3USG|9xczbHzCx-w4 diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index c376c59ec94caabbeda84c9fecd7bc695b831ab8..1aac91cfa73fe9838a1061abd5ef10f35f689f66 100644 GIT binary patch delta 8176 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

wtm@2TD9D}J#-_*0+3vye zhC~|5VHUbOkp_Z-KLct~o3lR|tk4@^qL}?~P<^jFRRSq5o`*}I59sla4n}yVx>o{F zDcFUJ!dQGBe!J5+NtMv7ogfbFsp=lsyru&7j)7s(K1%}=Q^Cn2Vf#dg+Ci;7Fyo&M z!H#o>Z_;mqJ_n&j^$XpVL9bVco+p;jY&Zxn+ITCyzhoJ}whrF>`1S1$EQe1FLT15> z+UusOC0*6$JjzFmnZyS?@sA!x*!=gt$=U}x*g>~(elPq0iR7jz-;8z)`-p!$T2asE zqM_pRT9~Pk9MDZiv8C>Zlc@f zI-uLOAAI)am75DU&)`r8ELk_ECSE!3E5x zN})qaBfUf0HP)5;lD#N)=e!H-^gj^z9{x-AnK(ik*shV~8$^aD5k5fRhgt=WxMr*% zzm2OqXu9e{CZ_f(PT^WoOkpp@jRcQJ`X1}@)EnAiQn4{2mFV+)VQMd0V4-(`ch!}i%9vw=ZKg4%GIW{d4o{M_}Hca456 zkgv1Py_g;Q;d%9te2)G3;hOw)=p5m};+~3ID*M&pb$SOp+-BK66xr;Hi+s;ll2?oF zRl|J3$Q@sWz7{ASvK&83H2Yh$57M{UF@L+0Z`(@IgctU<*?n0Yy8T}FDCp##QV{X{N)0BGLSp3_6BaMO5sche^~H& z@xU363-i5z*A5FoF{zZNsSZaS2t5e>2tx=*5oQo%gcAr4Bg`Y5LwE||89a;VMV3FY zZk}J3z6Df8HyeVNhwmW6EBLIPoeR@ee0&2R;cXA!WawK+!A7F*Abf=I8-#lZc%|ca zDSoq|c%`6t&MRrr^OWN830Hq_*ms1Q2zW+u$D+92DXu+=Ye0Rw$F`){XB0b!Vl&V! zU;y4?=hUA7sSEA3BQ3pUI^>_53bfLlzz2VV{I)?5?rI4kw*~Y2LixKw=|{rOe-?JW zD{OjSa9$Ujw*^)+S(R-fn$LtdSlQU*Bbi20aNhBW0AHV2*`3L;A|q=<$9GHuA3c?&8;C0b0yM!ESO_1H-%4$l!zZ8Xd;@f8ZF~ A)Bpeg delta 7795 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 diff --git a/backend/add_is_pinned_column.sql b/backend/add_is_pinned_column.sql new file mode 100644 index 0000000..a8761d7 --- /dev/null +++ b/backend/add_is_pinned_column.sql @@ -0,0 +1,5 @@ +-- Add is_pinned column to grocery_lists table +ALTER TABLE grocery_lists ADD COLUMN IF NOT EXISTS is_pinned BOOLEAN DEFAULT FALSE; + +-- Verify the column was added +\d grocery_lists diff --git a/backend/grocery_db_utils.py b/backend/grocery_db_utils.py index cdb2922..ff80334 100644 --- a/backend/grocery_db_utils.py +++ b/backend/grocery_db_utils.py @@ -44,7 +44,7 @@ def get_user_grocery_lists(user_id: int) -> List[Dict[str, Any]]: try: cur.execute( """ - SELECT DISTINCT gl.id, gl.name, gl.items, gl.owner_id, gl.created_at, gl.updated_at, + SELECT DISTINCT gl.id, gl.name, gl.items, gl.owner_id, gl.is_pinned, gl.created_at, gl.updated_at, u.display_name as owner_display_name, CASE WHEN gl.owner_id = %s THEN TRUE ELSE gls.can_edit END as can_edit, CASE WHEN gl.owner_id = %s THEN TRUE ELSE FALSE END as is_owner @@ -70,7 +70,7 @@ def get_grocery_list_by_id(list_id: int, user_id: int) -> Optional[Dict[str, Any try: cur.execute( """ - SELECT gl.id, gl.name, gl.items, gl.owner_id, gl.created_at, gl.updated_at, + SELECT gl.id, gl.name, gl.items, gl.owner_id, gl.is_pinned, gl.created_at, gl.updated_at, u.display_name as owner_display_name, CASE WHEN gl.owner_id = %s THEN TRUE ELSE gls.can_edit END as can_edit, CASE WHEN gl.owner_id = %s THEN TRUE ELSE FALSE END as is_owner @@ -218,3 +218,36 @@ def search_users(query: str, limit: int = 10) -> List[Dict[str, Any]]: finally: cur.close() conn.close() + + +def toggle_grocery_list_pin(list_id: int, user_id: int) -> Optional[Dict[str, Any]]: + """Toggle pin status for a grocery list (owner only)""" + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + try: + # Check if user is owner + cur.execute( + "SELECT id, is_pinned FROM grocery_lists WHERE id = %s AND owner_id = %s", + (list_id, user_id) + ) + result = cur.fetchone() + if not result: + return None + + # Toggle pin status + new_pin_status = not result["is_pinned"] + cur.execute( + """ + UPDATE grocery_lists + SET is_pinned = %s, updated_at = CURRENT_TIMESTAMP + WHERE id = %s + RETURNING id, name, items, owner_id, is_pinned, created_at, updated_at + """, + (new_pin_status, list_id) + ) + updated = cur.fetchone() + conn.commit() + return dict(updated) if updated else None + finally: + cur.close() + conn.close() diff --git a/backend/main.py b/backend/main.py index 3ac6c3d..99fee42 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,7 +2,7 @@ import random from typing import List, Optional from datetime import timedelta -from fastapi import FastAPI, HTTPException, Query, Depends +from fastapi import FastAPI, HTTPException, Query, Depends, Response from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, EmailStr, field_validator import os @@ -42,6 +42,7 @@ from grocery_db_utils import ( unshare_grocery_list, get_grocery_list_shares, search_users, + toggle_grocery_list_pin, ) from notification_db_utils import ( @@ -133,6 +134,7 @@ class GroceryList(BaseModel): name: str items: List[str] owner_id: int + is_pinned: bool = False owner_display_name: Optional[str] = None can_edit: bool = False is_owner: bool = False @@ -190,8 +192,9 @@ app.add_middleware( CORSMiddleware, allow_origins=allowed_origins, allow_credentials=True, - allow_methods=["*"], + allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allow_headers=["*"], + max_age=0, # Disable CORS preflight caching ) @@ -548,6 +551,26 @@ def delete_grocery_list_endpoint(list_id: int, current_user: dict = Depends(get_ return +@app.options("/grocery-lists/{list_id}/pin") +async def options_pin_grocery_list(list_id: int): + """Handle CORS preflight for pin endpoint""" + return Response(status_code=200) + + +@app.patch("/grocery-lists/{list_id}/pin", response_model=GroceryList) +def toggle_pin_grocery_list_endpoint(list_id: int, current_user: dict = Depends(get_current_user)): + """Toggle pin status for a grocery list (owner only)""" + updated = toggle_grocery_list_pin(list_id, current_user["user_id"]) + if not updated: + raise HTTPException(status_code=404, detail="רשימת קניות לא נמצאה או שאין לך הרשאה") + + # Get full details with permissions + result = get_grocery_list_by_id(list_id, current_user["user_id"]) + result["created_at"] = str(result["created_at"]) + result["updated_at"] = str(result["updated_at"]) + return result + + @app.post("/grocery-lists/{list_id}/share", response_model=GroceryListShare) def share_grocery_list_endpoint( list_id: int, diff --git a/backend/schema.sql b/backend/schema.sql index 70a2aa8..5b61e17 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS grocery_lists ( name TEXT NOT NULL, items TEXT[] NOT NULL DEFAULT '{}', -- Array of grocery items owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + is_pinned BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/frontend/src/App.css b/frontend/src/App.css index d686785..6040486 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -104,6 +104,24 @@ body { .layout { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } + + .layout:has(.pinned-lists-sidebar) { + grid-template-columns: 320px minmax(0, 1fr) minmax(0, 1fr); + } +} + +.pinned-lists-sidebar { + display: none; +} + +@media (min-width: 960px) { + .pinned-lists-sidebar { + display: block; + position: sticky; + top: 1rem; + max-height: calc(100vh - 2rem); + overflow-y: auto; + } } .sidebar, diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1ed96cf..5d890ee 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,6 +6,7 @@ import RecipeSearchList from "./components/RecipeSearchList"; import RecipeDetails from "./components/RecipeDetails"; import RecipeFormDrawer from "./components/RecipeFormDrawer"; import GroceryLists from "./components/GroceryLists"; +import PinnedGroceryLists from "./components/PinnedGroceryLists"; import Modal from "./components/Modal"; import ToastContainer from "./components/ToastContainer"; import ThemeToggle from "./components/ThemeToggle"; @@ -367,6 +368,11 @@ function App() { ) : ( <> + {isAuthenticated && ( +

+ )}
{ + try { + const updated = await togglePinGroceryList(list.id); + setLists(lists.map((l) => (l.id === updated.id ? updated : l))); + if (selectedList?.id === updated.id) { + setSelectedList(updated); + } + const message = updated.is_pinned + ? "רשימה הוצמדה לדף הבית" + : "רשימה הוסרה מדף הבית"; + onShowToast(message, "success"); + } catch (error) { + onShowToast(error.message, "error"); + } + }; + const handleShowShareModal = async (list) => { setShowShareModal(list); setUserSearch(""); @@ -411,19 +428,28 @@ function GroceryLists({ user, onShowToast }) {
{selectedList.is_owner && ( - + <> + + + )} {selectedList.can_edit && (