Add logo
This commit is contained in:
parent
3270788902
commit
653e4f0ea0
202
backend/social_db_utils.py
Normal file
202
backend/social_db_utils.py
Normal file
@ -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()
|
||||||
@ -2,7 +2,8 @@
|
|||||||
<html lang="he" dir="rtl">
|
<html lang="he" dir="rtl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍽️</text></svg>">
|
<link rel="icon" type="image/png" href="/src/assets/my-recipes-logo-light.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/src/assets/my-recipes-logo-light.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="google-site-verification" content="xzgN8wy4yNKqR0_qZZeUqj-wfnje0v7koYpUyU1ti2I" />
|
<meta name="google-site-verification" content="xzgN8wy4yNKqR0_qZZeUqj-wfnje0v7koYpUyU1ti2I" />
|
||||||
<title>My Recipes | המתכונים שלי</title>
|
<title>My Recipes | המתכונים שלי</title>
|
||||||
|
|||||||
@ -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 {
|
.logo-emoji {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
frontend/src/assets/my-recipes-logo-dark.png
Normal file
BIN
frontend/src/assets/my-recipes-logo-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
BIN
frontend/src/assets/my-recipes-logo-light.png
Normal file
BIN
frontend/src/assets/my-recipes-logo-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
BIN
frontend/src/assets/placeholder-dark.png
Normal file
BIN
frontend/src/assets/placeholder-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
frontend/src/assets/placeholder-light.png
Normal file
BIN
frontend/src/assets/placeholder-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
@ -582,7 +582,7 @@ function GroceryLists({ user, onShowToast }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<style jsx>{`
|
<style>{`
|
||||||
.grocery-lists-container {
|
.grocery-lists-container {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
|
|||||||
221
frontend/src/components/NotificationBell.css
Normal file
221
frontend/src/components/NotificationBell.css
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
.notification-bell-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell-btn {
|
||||||
|
position: relative;
|
||||||
|
background: var(--card-soft);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell-btn:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.2rem 0.45rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 10px);
|
||||||
|
right: 0;
|
||||||
|
width: 420px;
|
||||||
|
max-height: 550px;
|
||||||
|
background: var(--panel-bg);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0.98;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--card-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link:hover {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-list::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-list::-webkit-scrollbar-track {
|
||||||
|
background: var(--panel-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-list::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
transition: all 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 3px;
|
||||||
|
background: transparent;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item.unread::before {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item.unread {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item.unread:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-message {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-item.read .notification-message {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-time {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-small {
|
||||||
|
background: var(--panel-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.4rem 0.65rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-small:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-small:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-small.delete {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-small.delete:hover {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import {
|
|||||||
markAllNotificationsAsRead,
|
markAllNotificationsAsRead,
|
||||||
deleteNotification,
|
deleteNotification,
|
||||||
} from "../notificationApi";
|
} from "../notificationApi";
|
||||||
|
import "./NotificationBell.css";
|
||||||
|
|
||||||
function NotificationBell({ onShowToast }) {
|
function NotificationBell({ onShowToast }) {
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
@ -174,225 +175,6 @@ function NotificationBell({ onShowToast }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<style jsx>{`
|
|
||||||
.notification-bell-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-bell-btn {
|
|
||||||
position: relative;
|
|
||||||
background: var(--card-soft);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
line-height: 1;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-bell-btn:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: -6px;
|
|
||||||
right: -6px;
|
|
||||||
background: #ef4444;
|
|
||||||
color: white;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0.2rem 0.45rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
min-width: 20px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + 10px);
|
|
||||||
right: 0;
|
|
||||||
width: 420px;
|
|
||||||
max-height: 550px;
|
|
||||||
background: var(--panel-bg);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
opacity: 0.98;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
background: var(--card-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-link {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--accent);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-link:hover {
|
|
||||||
background: var(--accent-soft);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-list {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 450px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-list::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-list::-webkit-scrollbar-track {
|
|
||||||
background: var(--panel-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-list::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-list::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-empty {
|
|
||||||
text-align: center;
|
|
||||||
padding: 3rem 2rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
transition: all 0.2s;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-item::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 3px;
|
|
||||||
background: transparent;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-item.unread::before {
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-item:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-item.unread {
|
|
||||||
background: var(--accent-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-item.unread:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-message {
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-item.read .notification-message {
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-time {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.4rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon-small {
|
|
||||||
background: var(--panel-bg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
padding: 0.4rem 0.65rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
line-height: 1;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon-small:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon-small.delete {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon-small.delete:hover {
|
|
||||||
background: #fef2f2;
|
|
||||||
color: #dc2626;
|
|
||||||
border-color: #fecaca;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,7 +79,7 @@ function PinnedGroceryLists({ onShowToast }) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<style jsx>{`
|
<style>{`
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@400;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@400;700&display=swap');
|
||||||
|
|
||||||
.pinned-grocery-lists {
|
.pinned-grocery-lists {
|
||||||
|
|||||||
@ -1,6 +1,19 @@
|
|||||||
import placeholderImage from "../assets/placeholder.svg";
|
import { useEffect, useState } from "react";
|
||||||
|
import placeholderLight from "../assets/placeholder-light.png";
|
||||||
|
import placeholderDark from "../assets/placeholder-dark.png";
|
||||||
|
|
||||||
function RecipeDetails({ recipe, onEditClick, onDeleteClick, onShowDeleteModal, isAuthenticated, currentUser }) {
|
function RecipeDetails({ recipe, onEditClick, onDeleteClick, onShowDeleteModal, isAuthenticated, currentUser }) {
|
||||||
|
const [theme, setTheme] = useState(document.documentElement.getAttribute('data-theme') || 'dark');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
setTheme(document.documentElement.getAttribute('data-theme') || 'dark');
|
||||||
|
});
|
||||||
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const placeholderImage = theme === 'dark' ? placeholderDark : placeholderLight;
|
||||||
if (!recipe) {
|
if (!recipe) {
|
||||||
return (
|
return (
|
||||||
<section className="panel placeholder">
|
<section className="panel placeholder">
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import placeholderImage from "../assets/placeholder.svg";
|
import placeholderLight from "../assets/placeholder-light.png";
|
||||||
|
import placeholderDark from "../assets/placeholder-dark.png";
|
||||||
|
|
||||||
function RecipeSearchList({
|
function RecipeSearchList({
|
||||||
allRecipes,
|
allRecipes,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user