Add feature to shared lists
This commit is contained in:
parent
c65cce9de7
commit
d159cadacc
@ -136,7 +136,7 @@ def delete_grocery_list(list_id: int) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def share_grocery_list(list_id: int, shared_with_user_id: int, can_edit: bool = False) -> Dict[str, Any]:
|
def share_grocery_list(list_id: int, shared_with_user_id: int, can_edit: bool = False) -> Dict[str, Any]:
|
||||||
"""Share a grocery list with another user"""
|
"""Share a grocery list with another user or update existing share permissions"""
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
try:
|
try:
|
||||||
@ -158,6 +158,28 @@ def share_grocery_list(list_id: int, shared_with_user_id: int, can_edit: bool =
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def update_share_permission(list_id: int, shared_with_user_id: int, can_edit: bool) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Update edit permission for an existing share"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
try:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE grocery_list_shares
|
||||||
|
SET can_edit = %s
|
||||||
|
WHERE list_id = %s AND shared_with_user_id = %s
|
||||||
|
RETURNING id, list_id, shared_with_user_id, can_edit, shared_at
|
||||||
|
""",
|
||||||
|
(can_edit, list_id, shared_with_user_id)
|
||||||
|
)
|
||||||
|
share = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
return dict(share) if share else None
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def unshare_grocery_list(list_id: int, user_id: int) -> bool:
|
def unshare_grocery_list(list_id: int, user_id: int) -> bool:
|
||||||
"""Remove sharing access for a user"""
|
"""Remove sharing access for a user"""
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
|
|||||||
@ -45,6 +45,7 @@ from grocery_db_utils import (
|
|||||||
get_grocery_list_shares,
|
get_grocery_list_shares,
|
||||||
search_users,
|
search_users,
|
||||||
toggle_grocery_list_pin,
|
toggle_grocery_list_pin,
|
||||||
|
update_share_permission,
|
||||||
)
|
)
|
||||||
|
|
||||||
from notification_db_utils import (
|
from notification_db_utils import (
|
||||||
@ -178,6 +179,10 @@ class ShareGroceryList(BaseModel):
|
|||||||
can_edit: bool = False
|
can_edit: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateSharePermission(BaseModel):
|
||||||
|
can_edit: bool
|
||||||
|
|
||||||
|
|
||||||
class GroceryListShare(BaseModel):
|
class GroceryListShare(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
list_id: int
|
list_id: int
|
||||||
@ -1037,6 +1042,45 @@ def unshare_grocery_list_endpoint(
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.patch("/grocery-lists/{list_id}/shares/{user_id}", response_model=GroceryListShare)
|
||||||
|
def update_share_permission_endpoint(
|
||||||
|
list_id: int,
|
||||||
|
user_id: int,
|
||||||
|
share_data: UpdateSharePermission,
|
||||||
|
current_user: dict = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Update share permissions for a user"""
|
||||||
|
# Check if current user is the owner
|
||||||
|
existing = get_grocery_list_by_id(list_id, current_user["user_id"])
|
||||||
|
if not existing:
|
||||||
|
raise HTTPException(status_code=404, detail="רשימת קניות לא נמצאה")
|
||||||
|
|
||||||
|
if not existing["is_owner"]:
|
||||||
|
raise HTTPException(status_code=403, detail="רק הבעלים יכול לשנות הרשאות")
|
||||||
|
|
||||||
|
updated_share = update_share_permission(list_id, user_id, share_data.can_edit)
|
||||||
|
if not updated_share:
|
||||||
|
raise HTTPException(status_code=404, detail="שיתוף לא נמצא")
|
||||||
|
|
||||||
|
# Get user details for response
|
||||||
|
shares = get_grocery_list_shares(list_id)
|
||||||
|
share_with_details = next((s for s in shares if s["shared_with_user_id"] == user_id), None)
|
||||||
|
|
||||||
|
if not share_with_details:
|
||||||
|
raise HTTPException(status_code=404, detail="שיתוף לא נמצא")
|
||||||
|
|
||||||
|
return GroceryListShare(
|
||||||
|
id=share_with_details["id"],
|
||||||
|
list_id=list_id,
|
||||||
|
shared_with_user_id=user_id,
|
||||||
|
username=share_with_details["username"],
|
||||||
|
display_name=share_with_details["display_name"],
|
||||||
|
email=share_with_details["email"],
|
||||||
|
can_edit=share_data.can_edit,
|
||||||
|
shared_at=share_with_details["shared_at"].isoformat() if hasattr(share_with_details["shared_at"], 'isoformat') else str(share_with_details["shared_at"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/users/search", response_model=List[UserSearch])
|
@app.get("/users/search", response_model=List[UserSearch])
|
||||||
def search_users_endpoint(
|
def search_users_endpoint(
|
||||||
q: str = Query(..., min_length=1, description="Search query for username or display name"),
|
q: str = Query(..., min_length=1, description="Search query for username or display name"),
|
||||||
|
|||||||
@ -1909,3 +1909,72 @@ html {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Share Modal Search Results Styles */
|
||||||
|
.share-search .search-results {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-search .search-results li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-search .search-results li:hover:not(.disabled) {
|
||||||
|
background-color: var(--accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-search .search-results li.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-search .search-results li.disabled:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-search .search-results .username {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-search .search-results .badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Share Actions */
|
||||||
|
.share-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shares-list .share-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shares-list .share-item .username {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,8 @@ function App() {
|
|||||||
}
|
}
|
||||||
}); // "recipes" or "grocery-lists"
|
}); // "recipes" or "grocery-lists"
|
||||||
|
|
||||||
|
const [selectedGroceryListId, setSelectedGroceryListId] = useState(null);
|
||||||
|
|
||||||
const [recipes, setRecipes] = useState([]);
|
const [recipes, setRecipes] = useState([]);
|
||||||
const [selectedRecipe, setSelectedRecipe] = useState(null);
|
const [selectedRecipe, setSelectedRecipe] = useState(null);
|
||||||
|
|
||||||
@ -395,7 +397,11 @@ function App() {
|
|||||||
onAddClick={() => setDrawerOpen(true)}
|
onAddClick={() => setDrawerOpen(true)}
|
||||||
user={user}
|
user={user}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
onShowToast={addToast}
|
onShowToast={addToast}
|
||||||
|
onNotificationClick={(listId) => {
|
||||||
|
setCurrentView("grocery-lists");
|
||||||
|
setSelectedGroceryListId(listId);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -457,7 +463,12 @@ function App() {
|
|||||||
|
|
||||||
<main className="layout">
|
<main className="layout">
|
||||||
{currentView === "grocery-lists" ? (
|
{currentView === "grocery-lists" ? (
|
||||||
<GroceryLists user={user} onShowToast={addToast} />
|
<GroceryLists
|
||||||
|
user={user}
|
||||||
|
onShowToast={addToast}
|
||||||
|
selectedListIdFromNotification={selectedGroceryListId}
|
||||||
|
onListSelected={() => setSelectedGroceryListId(null)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
|
|||||||
@ -10,9 +10,10 @@ import {
|
|||||||
unshareGroceryList,
|
unshareGroceryList,
|
||||||
searchUsers,
|
searchUsers,
|
||||||
togglePinGroceryList,
|
togglePinGroceryList,
|
||||||
|
updateSharePermission,
|
||||||
} from "../groceryApi";
|
} from "../groceryApi";
|
||||||
|
|
||||||
function GroceryLists({ user, onShowToast }) {
|
function GroceryLists({ user, onShowToast, selectedListIdFromNotification, onListSelected }) {
|
||||||
const [lists, setLists] = useState([]);
|
const [lists, setLists] = useState([]);
|
||||||
const [selectedList, setSelectedList] = useState(null);
|
const [selectedList, setSelectedList] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -21,6 +22,7 @@ function GroceryLists({ user, onShowToast }) {
|
|||||||
const [shares, setShares] = useState([]);
|
const [shares, setShares] = useState([]);
|
||||||
const [userSearch, setUserSearch] = useState("");
|
const [userSearch, setUserSearch] = useState("");
|
||||||
const [searchResults, setSearchResults] = useState([]);
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
|
const [allUsers, setAllUsers] = useState([]);
|
||||||
const [sharePermission, setSharePermission] = useState(false);
|
const [sharePermission, setSharePermission] = useState(false);
|
||||||
|
|
||||||
// New list form
|
// New list form
|
||||||
@ -45,9 +47,20 @@ function GroceryLists({ user, onShowToast }) {
|
|||||||
loadLists();
|
loadLists();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle list selection from notification
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedListIdFromNotification && lists.length > 0) {
|
||||||
|
const listToSelect = lists.find(list => list.id === selectedListIdFromNotification);
|
||||||
|
if (listToSelect) {
|
||||||
|
setSelectedList(listToSelect);
|
||||||
|
onListSelected?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedListIdFromNotification, lists, onListSelected]);
|
||||||
|
|
||||||
// Restore selected list from localStorage after lists are loaded
|
// Restore selected list from localStorage after lists are loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lists.length > 0) {
|
if (lists.length > 0 && !selectedListIdFromNotification) {
|
||||||
try {
|
try {
|
||||||
const savedListId = localStorage.getItem("selectedGroceryListId");
|
const savedListId = localStorage.getItem("selectedGroceryListId");
|
||||||
if (savedListId) {
|
if (savedListId) {
|
||||||
@ -60,7 +73,7 @@ function GroceryLists({ user, onShowToast }) {
|
|||||||
console.error("Failed to restore selected list", err);
|
console.error("Failed to restore selected list", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [lists]);
|
}, [lists, selectedListIdFromNotification]);
|
||||||
|
|
||||||
const loadLists = async () => {
|
const loadLists = async () => {
|
||||||
try {
|
try {
|
||||||
@ -215,26 +228,46 @@ function GroceryLists({ user, onShowToast }) {
|
|||||||
setSharePermission(false);
|
setSharePermission(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Load shares for this list
|
||||||
const sharesData = await getGroceryListShares(list.id);
|
const sharesData = await getGroceryListShares(list.id);
|
||||||
setShares(sharesData);
|
setShares(sharesData);
|
||||||
|
|
||||||
|
// Load all users (use a space to bypass minimum length requirement)
|
||||||
|
const users = await searchUsers(" ");
|
||||||
|
|
||||||
|
// Sort users: recent shares first (those in sharesData), then others alphabetically
|
||||||
|
const sharedUserIds = new Set(sharesData.map(s => s.shared_with_user_id));
|
||||||
|
const recentUsers = users.filter(u => sharedUserIds.has(u.id));
|
||||||
|
const otherUsers = users.filter(u => !sharedUserIds.has(u.id));
|
||||||
|
|
||||||
|
const sortedUsers = [
|
||||||
|
...recentUsers.sort((a, b) => a.display_name.localeCompare(b.display_name)),
|
||||||
|
...otherUsers.sort((a, b) => a.display_name.localeCompare(b.display_name))
|
||||||
|
];
|
||||||
|
|
||||||
|
setAllUsers(sortedUsers);
|
||||||
|
setSearchResults(sortedUsers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onShowToast(error.message, "error");
|
console.error("Failed to load users:", error);
|
||||||
|
onShowToast("לא הצלחנו לטעון את רשימת המשתמשים", "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchUsers = async (query) => {
|
const handleSearchUsers = async (query) => {
|
||||||
setUserSearch(query);
|
setUserSearch(query);
|
||||||
if (query.trim().length < 2) {
|
|
||||||
setSearchResults([]);
|
if (query.trim().length === 0) {
|
||||||
|
// Show all users when search is empty
|
||||||
|
setSearchResults(allUsers);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Filter from all users
|
||||||
const results = await searchUsers(query);
|
const filtered = allUsers.filter(user =>
|
||||||
setSearchResults(results);
|
user.display_name.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
} catch (error) {
|
user.username.toLowerCase().includes(query.toLowerCase())
|
||||||
onShowToast(error.message, "error");
|
);
|
||||||
}
|
setSearchResults(filtered);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShareWithUser = async (userId, username) => {
|
const handleShareWithUser = async (userId, username) => {
|
||||||
@ -264,6 +297,22 @@ function GroceryLists({ user, onShowToast }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTogglePermission = async (userId, currentCanEdit) => {
|
||||||
|
try {
|
||||||
|
const updatedShare = await updateSharePermission(showShareModal.id, userId, !currentCanEdit);
|
||||||
|
// Update the shares list with the new permission
|
||||||
|
setShares(shares.map((s) =>
|
||||||
|
s.shared_with_user_id === userId ? { ...s, can_edit: updatedShare.can_edit } : s
|
||||||
|
));
|
||||||
|
const message = updatedShare.can_edit
|
||||||
|
? "הרשאת עריכה ניתנה"
|
||||||
|
: "הרשאת עריכה הוסרה";
|
||||||
|
onShowToast(message, "success");
|
||||||
|
} catch (error) {
|
||||||
|
onShowToast(error.message, "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="loading">טוען רשימות קניות...</div>;
|
return <div className="loading">טוען רשימות קניות...</div>;
|
||||||
}
|
}
|
||||||
@ -542,31 +591,26 @@ function GroceryLists({ user, onShowToast }) {
|
|||||||
value={userSearch}
|
value={userSearch}
|
||||||
onChange={(e) => handleSearchUsers(e.target.value)}
|
onChange={(e) => handleSearchUsers(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<label className="checkbox-label">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={sharePermission}
|
|
||||||
onChange={(e) => setSharePermission(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>אפשר עריכה</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{searchResults.length > 0 && (
|
<ul className="search-results">
|
||||||
<ul className="search-results">
|
{searchResults.map((user) => {
|
||||||
{searchResults.map((user) => (
|
const alreadyShared = shares.some(s => s.shared_with_user_id === user.id);
|
||||||
|
return (
|
||||||
<li
|
<li
|
||||||
key={user.id}
|
key={user.id}
|
||||||
onClick={() => handleShareWithUser(user.id, user.username)}
|
onClick={() => !alreadyShared && handleShareWithUser(user.id, user.username)}
|
||||||
|
className={alreadyShared ? "disabled" : ""}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<strong>{user.display_name}</strong>
|
<strong>{user.display_name}</strong>
|
||||||
<span className="username">@{user.username}</span>
|
<span className="username">@{user.username}</span>
|
||||||
|
{alreadyShared && <span className="badge">משותף</span>}
|
||||||
</div>
|
</div>
|
||||||
<button className="btn small">שתף</button>
|
{!alreadyShared && <button className="btn small">שתף</button>}
|
||||||
</li>
|
</li>
|
||||||
))}
|
);
|
||||||
</ul>
|
})}
|
||||||
)}
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shares-list">
|
<div className="shares-list">
|
||||||
@ -580,14 +624,22 @@ function GroceryLists({ user, onShowToast }) {
|
|||||||
<div>
|
<div>
|
||||||
<strong>{share.display_name}</strong>
|
<strong>{share.display_name}</strong>
|
||||||
<span className="username">@{share.username}</span>
|
<span className="username">@{share.username}</span>
|
||||||
{share.can_edit && <span className="badge">עורך</span>}
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="share-actions">
|
||||||
className="btn danger small"
|
<button
|
||||||
onClick={() => handleUnshare(share.shared_with_user_id)}
|
className={`btn small ${share.can_edit ? 'secondary' : 'ghost'}`}
|
||||||
>
|
onClick={() => handleTogglePermission(share.shared_with_user_id, share.can_edit)}
|
||||||
הסר
|
title={share.can_edit ? "הסר הרשאת עריכה" : "אפשר עריכה"}
|
||||||
</button>
|
>
|
||||||
|
{share.can_edit ? "עורך" : "צופה"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn danger small"
|
||||||
|
onClick={() => handleUnshare(share.shared_with_user_id)}
|
||||||
|
>
|
||||||
|
הסר
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -125,6 +125,14 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-item.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item.clickable:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.notification-item::before {
|
.notification-item::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
} from "../notificationApi";
|
} from "../notificationApi";
|
||||||
import "./NotificationBell.css";
|
import "./NotificationBell.css";
|
||||||
|
|
||||||
function NotificationBell({ onShowToast }) {
|
function NotificationBell({ onShowToast, onNotificationClick }) {
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [unreadCount, setUnreadCount] = useState(0);
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
@ -67,6 +67,19 @@ function NotificationBell({ onShowToast }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNotificationClick = async (notification) => {
|
||||||
|
// Mark as read
|
||||||
|
if (!notification.is_read) {
|
||||||
|
await handleMarkAsRead(notification.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle grocery share notifications
|
||||||
|
if (notification.type === "grocery_share" && notification.related_id) {
|
||||||
|
setShowDropdown(false);
|
||||||
|
onNotificationClick?.(notification.related_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleMarkAllAsRead = async () => {
|
const handleMarkAllAsRead = async () => {
|
||||||
try {
|
try {
|
||||||
await markAllNotificationsAsRead();
|
await markAllNotificationsAsRead();
|
||||||
@ -141,7 +154,8 @@ function NotificationBell({ onShowToast }) {
|
|||||||
key={notification.id}
|
key={notification.id}
|
||||||
className={`notification-item ${
|
className={`notification-item ${
|
||||||
notification.is_read ? "read" : "unread"
|
notification.is_read ? "read" : "unread"
|
||||||
}`}
|
} ${notification.type === "grocery_share" ? "clickable" : ""}`}
|
||||||
|
onClick={() => handleNotificationClick(notification)}
|
||||||
>
|
>
|
||||||
<div className="notification-content">
|
<div className="notification-content">
|
||||||
<p className="notification-message">
|
<p className="notification-message">
|
||||||
@ -155,7 +169,10 @@ function NotificationBell({ onShowToast }) {
|
|||||||
{!notification.is_read && (
|
{!notification.is_read && (
|
||||||
<button
|
<button
|
||||||
className="btn-icon-small"
|
className="btn-icon-small"
|
||||||
onClick={() => handleMarkAsRead(notification.id)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleMarkAsRead(notification.id);
|
||||||
|
}}
|
||||||
title="סמן כנקרא"
|
title="סמן כנקרא"
|
||||||
>
|
>
|
||||||
✓
|
✓
|
||||||
@ -163,7 +180,10 @@ function NotificationBell({ onShowToast }) {
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
className="btn-icon-small delete"
|
className="btn-icon-small delete"
|
||||||
onClick={() => handleDelete(notification.id)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(notification.id);
|
||||||
|
}}
|
||||||
title="מחק"
|
title="מחק"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import NotificationBell from "./NotificationBell";
|
import NotificationBell from "./NotificationBell";
|
||||||
|
|
||||||
function TopBar({ onAddClick, user, onLogout, onShowToast }) {
|
function TopBar({ onAddClick, user, onLogout, onShowToast, onNotificationClick }) {
|
||||||
return (
|
return (
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div className="topbar-left">
|
<div className="topbar-left">
|
||||||
@ -14,7 +14,7 @@ function TopBar({ onAddClick, user, onLogout, onShowToast }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="topbar-actions">
|
<div className="topbar-actions">
|
||||||
{user && <NotificationBell onShowToast={onShowToast} />}
|
{user && <NotificationBell onShowToast={onShowToast} onNotificationClick={onNotificationClick} />}
|
||||||
{user && (
|
{user && (
|
||||||
<button className="btn primary btn-mobile-compact" onClick={onAddClick}>
|
<button className="btn primary btn-mobile-compact" onClick={onAddClick}>
|
||||||
<span className="btn-text-desktop">+ מתכון חדש</span>
|
<span className="btn-text-desktop">+ מתכון חדש</span>
|
||||||
|
|||||||
@ -121,6 +121,20 @@ export const unshareGroceryList = async (listId, userId) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update share permissions
|
||||||
|
export const updateSharePermission = async (listId, userId, canEdit) => {
|
||||||
|
const res = await fetch(`${API_URL}/grocery-lists/${listId}/shares/${userId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
body: JSON.stringify({ can_edit: canEdit }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json();
|
||||||
|
throw new Error(error.detail || "Failed to update share permission");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
// Search users
|
// Search users
|
||||||
export const searchUsers = async (query) => {
|
export const searchUsers = async (query) => {
|
||||||
const res = await fetch(`${API_URL}/users/search?q=${encodeURIComponent(query)}`, {
|
const res = await fetch(`${API_URL}/users/search?q=${encodeURIComponent(query)}`, {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user