mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-01 14:23:11 +00:00
Add user dropdown menu and notification panel to topbar
- Click user section to open dropdown with profile, settings, password, sign out - Shows user avatar, name, and email in dropdown header - Click notification bell to open panel showing empty state - Both use portal-rendered dropdowns, close on outside click - Removed standalone logout button, moved into user dropdown
This commit is contained in:
parent
11e3d3d31f
commit
2477bcf5a6
2 changed files with 261 additions and 23 deletions
|
|
@ -1,16 +1,57 @@
|
|||
import React from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { logout, getCurrentUser } from "../services/authService";
|
||||
|
||||
export default function Topbar({ onToggleNav }) {
|
||||
const navigate = useNavigate();
|
||||
const currentUser = getCurrentUser();
|
||||
function NotificationDropdown() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const btnRef = useRef(null);
|
||||
const menuRef = useRef(null);
|
||||
const [menuPos, setMenuPos] = useState({ top: 0, right: 0 });
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate("/login");
|
||||
useEffect(() => {
|
||||
const handleClick = (e) => {
|
||||
if (btnRef.current?.contains(e.target)) return;
|
||||
if (menuRef.current?.contains(e.target)) return;
|
||||
setOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, []);
|
||||
|
||||
const handleOpen = () => {
|
||||
if (btnRef.current) {
|
||||
const rect = btnRef.current.getBoundingClientRect();
|
||||
setMenuPos({ top: rect.bottom + 8, right: window.innerWidth - rect.right });
|
||||
}
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button ref={btnRef} className="topbarIconBtn" title="Notifications" onClick={handleOpen}>
|
||||
<i className="fa fa-bell-o"></i>
|
||||
</button>
|
||||
{open && ReactDOM.createPortal(
|
||||
<div ref={menuRef} className="notifDropdown" style={{ top: menuPos.top, right: menuPos.right }}>
|
||||
<div className="notifHeader">Notifications</div>
|
||||
<div className="notifEmpty">
|
||||
<i className="fa fa-check-circle notifEmptyIcon"></i>
|
||||
<span>No new notifications</span>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UserDropdown({ currentUser, onLogout, onNavigate }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const btnRef = useRef(null);
|
||||
const menuRef = useRef(null);
|
||||
const [menuPos, setMenuPos] = useState({ top: 0, right: 0 });
|
||||
|
||||
const getInitials = (fullname) => {
|
||||
if (!fullname) return "U";
|
||||
const parts = fullname.split(" ");
|
||||
|
|
@ -20,6 +61,78 @@ export default function Topbar({ onToggleNav }) {
|
|||
return fullname.substring(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e) => {
|
||||
if (btnRef.current?.contains(e.target)) return;
|
||||
if (menuRef.current?.contains(e.target)) return;
|
||||
setOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, []);
|
||||
|
||||
const handleOpen = () => {
|
||||
if (btnRef.current) {
|
||||
const rect = btnRef.current.getBoundingClientRect();
|
||||
setMenuPos({ top: rect.bottom + 8, right: window.innerWidth - rect.right });
|
||||
}
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
const handleNav = (path) => {
|
||||
setOpen(false);
|
||||
onNavigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button ref={btnRef} className="topbarUserBtn" onClick={handleOpen}>
|
||||
<div className="avatar">{getInitials(currentUser?.fullname)}</div>
|
||||
<div className="topbarUserInfo">
|
||||
<span className="topbarUserName">{currentUser?.fullname || "User"}</span>
|
||||
<span className="topbarUserRole">{currentUser?.userRoles || ""}</span>
|
||||
</div>
|
||||
<i className="fa fa-angle-down topbarUserCaret"></i>
|
||||
</button>
|
||||
{open && ReactDOM.createPortal(
|
||||
<div ref={menuRef} className="userDropdown" style={{ top: menuPos.top, right: menuPos.right }}>
|
||||
<div className="userDropdownHeader">
|
||||
<div className="userDropdownAvatar">{getInitials(currentUser?.fullname)}</div>
|
||||
<div>
|
||||
<div className="userDropdownName">{currentUser?.fullname || "User"}</div>
|
||||
<div className="userDropdownEmail">{currentUser?.email || ""}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="userDropdownDivider" />
|
||||
<button className="userDropdownItem" onClick={() => handleNav("/profile")}>
|
||||
<i className="fa fa-user"></i> My Profile
|
||||
</button>
|
||||
<button className="userDropdownItem" onClick={() => handleNav("/settings")}>
|
||||
<i className="fa fa-cog"></i> Account Settings
|
||||
</button>
|
||||
<button className="userDropdownItem" onClick={() => handleNav("/change-password")}>
|
||||
<i className="fa fa-lock"></i> Change Password
|
||||
</button>
|
||||
<div className="userDropdownDivider" />
|
||||
<button className="userDropdownItem userDropdownLogout" onClick={() => { setOpen(false); onLogout(); }}>
|
||||
<i className="fa fa-sign-out"></i> Sign Out
|
||||
</button>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Topbar({ onToggleNav }) {
|
||||
const navigate = useNavigate();
|
||||
const currentUser = getCurrentUser();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="topbar">
|
||||
<div className="topLeft">
|
||||
|
|
@ -34,23 +147,9 @@ export default function Topbar({ onToggleNav }) {
|
|||
</div>
|
||||
|
||||
<div className="topRight">
|
||||
<button className="topbarIconBtn" title="Notifications">
|
||||
<i className="fa fa-bell-o"></i>
|
||||
</button>
|
||||
|
||||
<NotificationDropdown />
|
||||
<div className="topbarDivider" />
|
||||
|
||||
<div className="topbarUser">
|
||||
<div className="avatar">{getInitials(currentUser?.fullname)}</div>
|
||||
<div className="topbarUserInfo">
|
||||
<span className="topbarUserName">{currentUser?.fullname || "User"}</span>
|
||||
<span className="topbarUserRole">{currentUser?.userRoles || ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="topbarIconBtn topbarLogout" onClick={handleLogout} title="Logout">
|
||||
<i className="fa fa-sign-out"></i>
|
||||
</button>
|
||||
<UserDropdown currentUser={currentUser} onLogout={handleLogout} onNavigate={navigate} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
|||
139
src/index.css
139
src/index.css
|
|
@ -263,6 +263,145 @@ body {
|
|||
background: rgba(255,100,100,0.1);
|
||||
}
|
||||
|
||||
.notifDropdown {
|
||||
position: fixed;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||
z-index: 99999;
|
||||
width: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notifHeader {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.notifEmpty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 30px 16px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.notifEmptyIcon {
|
||||
font-size: 24px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.topbarUserBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.topbarUserBtn:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.topbarUserCaret {
|
||||
color: rgba(255,255,255,0.5);
|
||||
font-size: 12px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.userDropdown {
|
||||
position: fixed;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||
z-index: 99999;
|
||||
width: 260px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.userDropdownHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.userDropdownAvatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #1fa8de;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.userDropdownName {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.userDropdownEmail {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.userDropdownDivider {
|
||||
height: 1px;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.userDropdownItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.userDropdownItem:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.userDropdownItem i {
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.userDropdownLogout {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.userDropdownLogout:hover {
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.userDropdownLogout i {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.topbarDivider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue