200 lines
7.3 KiB
JavaScript
200 lines
7.3 KiB
JavaScript
import PropTypes from 'prop-types';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useDispatch } from 'react-redux';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
|
|
import { TopNav } from '@astryxdesign/core/TopNav';
|
|
import { TopNavHeading } from '@astryxdesign/core/TopNav';
|
|
import { DropdownMenu } from '@astryxdesign/core/DropdownMenu';
|
|
import { Popover } from '@astryxdesign/core/Popover';
|
|
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
import { Avatar } from '@astryxdesign/core/Avatar';
|
|
import { HStack } from '@astryxdesign/core/HStack';
|
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
import { Text } from '@astryxdesign/core/Text';
|
|
import { Heading } from '@astryxdesign/core/Heading';
|
|
import { Divider } from '@astryxdesign/core/Divider';
|
|
|
|
import { TbBoxMultiple1 } from 'react-icons/tb';
|
|
import { GrMultiple } from 'react-icons/gr';
|
|
import { BellOutlined, WindowsOutlined, EditOutlined, CommentOutlined, LogoutOutlined, GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
|
|
|
|
import avatar1 from 'assets/images/users/avatar-1.png';
|
|
import logo from 'assets/images/doormile-logo.png';
|
|
import navbarMark from 'assets/images/doormile-mark.png';
|
|
import { clearFcmToken } from 'store/reducers/fcmSlice';
|
|
import { logoutUser } from 'store/reducers/loginUserSlice';
|
|
import { performSessionLogout } from 'utils/session';
|
|
import { DT } from 'themes/dt/tokens';
|
|
|
|
// doormile-logo.png is a white asset; recolour to brand red for this
|
|
// light TopNav surface (same trick login.js uses for its white-card logo).
|
|
const logoStyle = {
|
|
height: 24,
|
|
width: 'auto',
|
|
filter: 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)'
|
|
};
|
|
|
|
// Collapsed rail: doormile-mark.png (red-on-white disc) needs a white
|
|
// backing + scale(1.4) crop to fill its circle cleanly — same composition
|
|
// the old DrawerHeader used, just sized for the nav-bar row instead of the
|
|
// full-height sidebar rail.
|
|
const markWrapperStyle = {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: '50%',
|
|
background: '#fff',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
overflow: 'hidden',
|
|
flexShrink: 0
|
|
};
|
|
|
|
const markStyle = {
|
|
width: '100%',
|
|
height: '100%',
|
|
objectFit: 'cover',
|
|
transform: 'scale(1.4)',
|
|
display: 'block'
|
|
};
|
|
|
|
const NotificationPanel = () => (
|
|
<VStack gap={0} padding={0} width={320}>
|
|
<HStack justify="between" vAlign="center" padding={2}>
|
|
<Heading level={5}>Notifications</Heading>
|
|
</HStack>
|
|
<Divider />
|
|
<VStack gap={0} padding={0}>
|
|
{[
|
|
{ icon: <GiftOutlined />, text: "It's Cristina danny's birthday today.", time: '2 min ago' },
|
|
{ icon: <MessageOutlined />, text: 'Aida Burg commented your post.', time: '5 August' },
|
|
{ icon: <SettingOutlined />, text: 'Your profile is 60% complete.', time: '7 hours ago' }
|
|
].map((n) => (
|
|
<HStack key={n.text} gap={1.5} vAlign="center" padding={2}>
|
|
<Avatar name={n.text} size="sm" />
|
|
<VStack gap={0} padding={0}>
|
|
<Text type="body">{n.text}</Text>
|
|
<Text type="supporting">{n.time}</Text>
|
|
</VStack>
|
|
</HStack>
|
|
))}
|
|
</VStack>
|
|
<Divider />
|
|
<HStack justify="center" padding={1.5}>
|
|
<Text type="label" color="accent">
|
|
View all
|
|
</Text>
|
|
</HStack>
|
|
</VStack>
|
|
);
|
|
|
|
// ==============================|| MAIN LAYOUT - TOP NAV ||============================== //
|
|
|
|
const AppTopNav = ({ isSidebarCollapsed }) => {
|
|
const navigate = useNavigate();
|
|
const dispatch = useDispatch();
|
|
const queryClient = useQueryClient();
|
|
const topNavRef = useRef(null);
|
|
const [headerHeight, setHeaderHeight] = useState(null);
|
|
|
|
const firstname = localStorage.getItem('firstname') || '';
|
|
|
|
const handleLogout = () => {
|
|
performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser });
|
|
};
|
|
|
|
// AppShell (height="auto") pins the sticky side nav below the header using
|
|
// a --appshell-header-height CSS var it measures itself via ResizeObserver
|
|
// (AppShell.tsx). On some routes that measurement lags/settles wrong, so
|
|
// the side nav's sticky offset is off and its bottom-anchored collapse
|
|
// button visibly jumps on scroll. Re-measure the header independently here
|
|
// and force the same variable with !important on the shell root — an
|
|
// author-stylesheet !important rule beats AppShell's own non-important
|
|
// inline style.setProperty() call on that same element, so this always
|
|
// wins regardless of what the internal measurement produced.
|
|
useEffect(() => {
|
|
const el = topNavRef.current;
|
|
if (!el) return undefined;
|
|
|
|
const updateHeight = () => setHeaderHeight(el.getBoundingClientRect().height);
|
|
updateHeight();
|
|
|
|
const observer = new ResizeObserver(updateHeight);
|
|
observer.observe(el);
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
<TopNav
|
|
ref={topNavRef}
|
|
label="Doormile Express"
|
|
style={{
|
|
backgroundColor: DT.surface,
|
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
|
boxShadow: DT.shadowMd
|
|
}}
|
|
heading={
|
|
<TopNavHeading
|
|
logo={
|
|
isSidebarCollapsed ? (
|
|
<HStack gap={1} vAlign="center" padding={0}>
|
|
<span style={markWrapperStyle}>
|
|
<img src={navbarMark} alt="" style={markStyle} />
|
|
</span>
|
|
<img src={logo} alt="Doormile Express" style={logoStyle} />
|
|
</HStack>
|
|
) : (
|
|
<img src={logo} alt="Doormile Express" style={logoStyle} />
|
|
)
|
|
}
|
|
headingHref="/doormile/dispatch"
|
|
/>
|
|
}
|
|
endContent={
|
|
<HStack gap={1} vAlign="center" padding={0}>
|
|
<DropdownMenu
|
|
hasChevron={false}
|
|
button={{ label: 'Quick create', icon: <WindowsOutlined />, isIconOnly: true, variant: 'ghost' }}
|
|
items={[
|
|
{ label: 'Create Order', icon: <TbBoxMultiple1 />, onClick: () => navigate('/doormile/orders/create') },
|
|
{ label: 'Create Multiple Order', icon: <GrMultiple />, onClick: () => navigate('/doormile/orders/createorders') }
|
|
]}
|
|
/>
|
|
|
|
<Popover placement="below" alignment="end" content={<NotificationPanel />} label="Notifications">
|
|
<IconButton label="Notifications" tooltip="Notifications" variant="ghost" icon={<BellOutlined />} />
|
|
</Popover>
|
|
|
|
<DropdownMenu
|
|
hasChevron={false}
|
|
button={{ label: firstname || 'Profile', icon: <Avatar src={avatar1} name={firstname} size="xsm" tooltip={false} />, variant: 'ghost' }}
|
|
items={[
|
|
{ label: 'View Profile', icon: <EditOutlined />, onClick: () => navigate('/viewprofile') },
|
|
{ label: 'Support Ticket', icon: <CommentOutlined /> },
|
|
{ type: 'divider' },
|
|
{ label: 'Logout', icon: <LogoutOutlined />, onClick: handleLogout }
|
|
]}
|
|
/>
|
|
</HStack>
|
|
}
|
|
/>
|
|
{headerHeight != null && (
|
|
<style>{`
|
|
.astryx-app-shell {
|
|
--appshell-header-height: ${headerHeight}px !important;
|
|
}
|
|
`}</style>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
AppTopNav.propTypes = {
|
|
isSidebarCollapsed: PropTypes.bool
|
|
};
|
|
|
|
export default AppTopNav;
|