This commit is contained in:
chamikaJ
2025-04-17 18:28:54 +05:30
parent f583291d8a
commit 8825b0410a
2837 changed files with 241385 additions and 127578 deletions

View File

@@ -0,0 +1,3 @@
.notification-icon:hover .anticon {
color: #1677ff;
}

View File

@@ -0,0 +1,23 @@
import { QuestionCircleOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import './HelpButton.css';
const HelpButton = () => {
// localization
const { t } = useTranslation('navbar');
return (
<Tooltip title={t('help')}>
<Button
className="notification-icon"
style={{ height: '62px', width: '60px' }}
type="text"
icon={<QuestionCircleOutlined style={{ fontSize: 20 }} />}
/>
</Tooltip>
);
};
export default HelpButton;

View File

@@ -0,0 +1,32 @@
import { UsergroupAddOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { colors } from '../../../styles/colors';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { toggleInviteMemberDrawer } from '../../settings/member/memberSlice';
const InviteButton = () => {
const dispatch = useAppDispatch();
// localization
const { t } = useTranslation('navbar');
return (
<Tooltip title={t('inviteTooltip')}>
<Button
type="dashed"
icon={<UsergroupAddOutlined />}
style={{
color: colors.skyBlue,
borderColor: colors.skyBlue,
}}
onClick={() => dispatch(toggleInviteMemberDrawer())}
>
{t('invite')}
</Button>
</Tooltip>
);
};
export default InviteButton;

View File

@@ -0,0 +1,99 @@
import {
ClockCircleOutlined,
HomeOutlined,
MenuOutlined,
ProjectOutlined,
QuestionCircleOutlined,
ReadOutlined,
} from '@ant-design/icons';
import { Button, Card, Dropdown, Flex, MenuProps, Space, Typography } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { colors } from '../../../styles/colors';
import { NavLink } from 'react-router-dom';
import InviteButton from '../invite/InviteButton';
import SwitchTeamButton from '../switchTeam/SwitchTeamButton';
// custom css
import './mobileMenu.css';
const MobileMenuButton = () => {
// localization
const { t } = useTranslation('navbar');
const navLinks = [
{
name: 'home',
icon: React.createElement(HomeOutlined),
},
{
name: 'projects',
icon: React.createElement(ProjectOutlined),
},
{
name: 'schedule',
icon: React.createElement(ClockCircleOutlined),
},
{
name: 'reporting',
icon: React.createElement(ReadOutlined),
},
{
name: 'help',
icon: React.createElement(QuestionCircleOutlined),
},
];
const mobileMenu: MenuProps['items'] = [
{
key: '1',
label: (
<Card className="mobile-menu-card" bordered={false} style={{ width: 230 }}>
{navLinks.map((navEl, index) => (
<NavLink key={index} to={`/worklenz/${navEl.name}`}>
<Typography.Text strong>
<Space>
{navEl.icon}
{t(navEl.name)}
</Space>
</Typography.Text>
</NavLink>
))}
<Flex
vertical
gap={12}
style={{
width: '90%',
marginInlineStart: 12,
marginBlock: 6,
}}
>
<Button
style={{
backgroundColor: colors.lightBeige,
color: 'black',
}}
>
{t('upgradePlan')}
</Button>
<InviteButton />
<SwitchTeamButton />
</Flex>
</Card>
),
},
];
return (
<Dropdown
overlayClassName="mobile-menu-dropdown"
menu={{ items: mobileMenu }}
placement="bottomRight"
trigger={['click']}
>
<Button className="borderless-icon-btn" icon={<MenuOutlined style={{ fontSize: 20 }} />} />
</Dropdown>
);
};
export default MobileMenuButton;

View File

@@ -0,0 +1,38 @@
.mobile-menu-dropdown .ant-dropdown-menu {
padding: 0 !important;
margin-top: 2px !important;
}
.mobile-menu-dropdown .ant-dropdown-menu-item {
padding: 0 !important;
}
.mobile-menu-card .ant-card-head {
padding: 0 16px;
}
.mobile-menu-card .ant-card-body {
padding: 4px 0;
}
.mobile-menu-card .ant-card-body a {
display: block;
margin: 0;
padding: 10px 15px;
}
.mobile-menu-card .ant-card-body a:hover {
color: #1890ff !important;
background-color: #edebf0 !important;
box-sizing: border-box;
}
html[data-theme="dark"] .mobile-menu-card .ant-card-body a:hover {
background-color: #333 !important;
color: #1890ff !important;
}
.mobile-menu-card {
border: none !important;
box-shadow: none !important;
}

View File

@@ -0,0 +1,33 @@
export type NavRoutesType = {
name: string;
path: string;
adminOnly: boolean;
freePlanFeature?: boolean;
};
export const navRoutes: NavRoutesType[] = [
{
name: 'home',
path: '/worklenz/home',
adminOnly: false,
freePlanFeature: true,
},
{
name: 'projects',
path: '/worklenz/projects',
adminOnly: false,
freePlanFeature: true,
},
// {
// name: 'schedule',
// path: '/worklenz/schedule',
// adminOnly: true,
// freePlanFeature: false,
// },
{
name: 'reporting',
path: '/worklenz/reporting/overview',
adminOnly: true,
freePlanFeature: false,
},
];

View File

@@ -0,0 +1,45 @@
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import logo from '@/assets/images/logo.png';
import logoDark from '@/assets/images/logo-dark-mode.png';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useSelector } from 'react-redux';
import { RootState } from '@/app/store';
const NavbarLogo = () => {
const { t } = useTranslation('navbar');
const themeMode = useSelector((state: RootState) => state.themeReducer.mode);
return (
<Link to={'/worklenz/home'}>
<div style={{ position: 'relative', display: 'inline-block' }}>
<img
src={themeMode === 'dark' ? logoDark : logo}
alt={t('logoAlt')}
style={{ width: '100%', maxWidth: 140 }}
/>
<span
style={{
position: 'absolute',
top: -1,
right: 0,
backgroundColor: '#ff5722',
color: 'white',
fontSize: '7px',
padding: '0px 3px',
borderRadius: '3px',
fontWeight: 'bold',
textTransform: 'uppercase',
lineHeight: '1.8',
}}
>
Beta
</span>
</div>
</Link>
);
};
export default NavbarLogo;

View File

@@ -0,0 +1,186 @@
import React, { useEffect, useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Col, ConfigProvider, Flex, Menu, MenuProps, Alert } from 'antd';
import { createPortal } from 'react-dom';
import InviteTeamMembers from '../../components/common/invite-team-members/invite-team-members';
import HelpButton from './help/HelpButton';
import InviteButton from './invite/InviteButton';
import MobileMenuButton from './mobileMenu/MobileMenuButton';
import NavbarLogo from './navbar-logo';
import NotificationButton from '../../components/navbar/notifications/notifications-drawer/notification/notification-button';
import ProfileButton from './user-profile/profile-button';
import SwitchTeamButton from './switchTeam/SwitchTeamButton';
import UpgradePlanButton from './upgradePlan/UpgradePlanButton';
import NotificationDrawer from '../../components/navbar/notifications/notifications-drawer/notification/notfication-drawer';
import { useResponsive } from '@/hooks/useResponsive';
import { getJSONFromLocalStorage } from '@/utils/localStorageFunctions';
import { navRoutes, NavRoutesType } from './navRoutes';
import { useAuthService } from '@/hooks/useAuth';
import { authApiService } from '@/api/auth/auth.api.service';
import { ISUBSCRIPTION_TYPE } from '@/shared/constants';
import logger from '@/utils/errorLogger';
const Navbar = () => {
const [current, setCurrent] = useState<string>('home');
const currentSession = useAuthService().getCurrentSession();
const [daysUntilExpiry, setDaysUntilExpiry] = useState<number | null>(null);
const location = useLocation();
const { isDesktop, isMobile, isTablet } = useResponsive();
const { t } = useTranslation('navbar');
const authService = useAuthService();
const [navRoutesList, setNavRoutesList] = useState<NavRoutesType[]>(navRoutes);
const [isOwnerOrAdmin, setIsOwnerOrAdmin] = useState<boolean>(authService.isOwnerOrAdmin());
const showUpgradeTypes = [ISUBSCRIPTION_TYPE.TRIAL]
useEffect(() => {
authApiService.verify().then(authorizeResponse => {
if (authorizeResponse.authenticated) {
authService.setCurrentSession(authorizeResponse.user);
setIsOwnerOrAdmin(!!(authorizeResponse.user.is_admin || authorizeResponse.user.owner));
}
}).catch(error => {
logger.error('Error during authorization', error);
});
}, []);
useEffect(() => {
const storedNavRoutesList: NavRoutesType[] = getJSONFromLocalStorage('navRoutes') || navRoutes;
setNavRoutesList(storedNavRoutesList);
}, []);
useEffect(() => {
if (currentSession?.trial_expire_date) {
const today = new Date();
const expiryDate = new Date(currentSession.trial_expire_date);
const diffTime = expiryDate.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
setDaysUntilExpiry(diffDays);
}
}, [currentSession?.trial_expire_date]);
const navlinkItems = useMemo(
() =>
navRoutesList
.filter(route => {
if (!route.freePlanFeature && currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE) return false;
if (route.adminOnly && !isOwnerOrAdmin) return false;
return true;
})
.map((route, index) => ({
key: route.path.split('/').pop() || index,
label: (
<Link to={route.path} style={{ fontWeight: 600 }}>
{t(route.name)}
</Link>
),
})),
[navRoutesList, t, isOwnerOrAdmin, currentSession?.subscription_type]
);
useEffect(() => {
const afterWorklenzString = location.pathname.split('/worklenz/')[1];
const pathKey = afterWorklenzString.split('/')[0];
setCurrent(pathKey ?? 'home');
}, [location]);
return (
<Col
style={{
width: '100%',
display: 'flex',
flexDirection: 'column',
paddingInline: isDesktop ? 48 : 24,
gap: 12,
alignItems: 'center',
justifyContent: 'space-between',
}}
>
{daysUntilExpiry !== null && ((daysUntilExpiry <= 3 && daysUntilExpiry > 0) || (daysUntilExpiry >= -7 && daysUntilExpiry < 0)) && (
<Alert
message={daysUntilExpiry > 0 ? `Your license will expire in ${daysUntilExpiry} days` : `Your license has expired ${Math.abs(daysUntilExpiry)} days ago`}
type="warning"
showIcon
style={{ width: '100%', marginTop: 12 }}
/>
)}
<Flex
style={{
width: '100%',
display: 'flex',
gap: 12,
alignItems: 'center',
justifyContent: 'space-between',
}}
>
{/* logo */}
<NavbarLogo />
<Flex
align="center"
justify={isDesktop ? 'space-between' : 'flex-end'}
style={{ width: '100%' }}
>
{/* navlinks menu */}
{isDesktop && (
<Menu
selectedKeys={[current]}
mode="horizontal"
style={{
flex: 10,
maxWidth: 720,
minWidth: 0,
border: 'none',
}}
items={navlinkItems}
/>
)}
<Flex gap={20} align="center">
<ConfigProvider wave={{ disabled: true }}>
{isDesktop && (
<Flex gap={20} align="center">
{isOwnerOrAdmin && showUpgradeTypes.includes(currentSession?.subscription_type as ISUBSCRIPTION_TYPE) && (
<UpgradePlanButton />
)}
{isOwnerOrAdmin && <InviteButton />}
<Flex align="center">
<SwitchTeamButton />
<NotificationButton />
<HelpButton />
<ProfileButton isOwnerOrAdmin={isOwnerOrAdmin} />
</Flex>
</Flex>
)}
{isTablet && !isDesktop && (
<Flex gap={12} align="center">
<SwitchTeamButton />
<NotificationButton />
<ProfileButton isOwnerOrAdmin={isOwnerOrAdmin} />
<MobileMenuButton />
</Flex>
)}
{isMobile && (
<Flex gap={12} align="center">
<NotificationButton />
<ProfileButton isOwnerOrAdmin={isOwnerOrAdmin} />
<MobileMenuButton />
</Flex>
)}
</ConfigProvider>
</Flex>
</Flex>
</Flex>
{isOwnerOrAdmin && createPortal(<InviteTeamMembers />, document.body, 'invite-team-members')}
{createPortal(<NotificationDrawer />, document.body, 'notification-drawer')}
</Col>
);
};
export default Navbar;

View File

@@ -0,0 +1,117 @@
import { ITeamInvitationViewModel } from '@/types/notifications/notifications.types';
import { IWorklenzNotification } from '@/types/notifications/notifications.types';
import { NotificationsDataModel } from '@/types/notifications/notifications.types';
import { NotificationType } from '../../types/notification.types';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { teamsApiService } from '@/api/teams/teams.api.service';
import { notificationsApiService } from '@/api/notifications/notifications.api.service';
type NotificationState = {
notificationType: 'Read' | 'Unread';
loading: boolean;
loadingInvitations: boolean;
notifications: IWorklenzNotification[];
notificationsCount: number;
isDrawerOpen: boolean;
invitations: ITeamInvitationViewModel[];
invitationsCount: number;
showBrowserPush: boolean;
_dataset: NotificationsDataModel;
dataset: NotificationsDataModel;
loadersMap: { [x: string]: boolean };
unreadNotificationsCount: number;
};
const initialState: NotificationState = {
notificationType: 'Unread',
loading: false,
loadingInvitations: false,
notifications: [],
notificationsCount: 0,
isDrawerOpen: false,
invitations: [],
invitationsCount: 0,
showBrowserPush: false,
_dataset: [],
dataset: [],
loadersMap: {},
unreadNotificationsCount: 0,
};
export const fetchInvitations = createAsyncThunk('notification/fetchInvitations', async () => {
const res = await teamsApiService.getInvitations();
return res.body;
});
export const fetchNotifications = createAsyncThunk(
'notification/fetchNotifications',
async (filter: string) => {
const res = await notificationsApiService.getNotifications(filter);
return res.body;
}
);
export const fetchUnreadCount = createAsyncThunk('notification/fetchUnreadCount', async () => {
const res = await notificationsApiService.getUnreadCount();
return res.body;
});
const notificationSlice = createSlice({
name: 'notificationReducer',
initialState,
reducers: {
toggleDrawer: state => {
state.isDrawerOpen ? (state.isDrawerOpen = false) : (state.isDrawerOpen = true);
},
setNotificationType: (state, action) => {
state.notificationType = action.payload;
},
},
extraReducers: builder => {
builder.addCase(fetchInvitations.pending, state => {
state.loading = true;
});
builder.addCase(fetchInvitations.fulfilled, (state, action) => {
state.loading = false;
state.invitations = action.payload;
state.invitationsCount = action.payload.length;
state.invitations.map(invitation => {
state._dataset.push({
type: 'invitation',
data: invitation,
});
});
});
builder.addCase(fetchInvitations.rejected, state => {
state.loading = false;
});
builder.addCase(fetchNotifications.pending, state => {
state.loading = true;
});
builder.addCase(fetchNotifications.fulfilled, (state, action) => {
state.loading = false;
state.notifications = action.payload;
state.notificationsCount = action.payload.length;
state.notifications.map(notification => {
state._dataset.push({
type: 'notification',
data: notification,
});
});
});
builder.addCase(fetchUnreadCount.pending, state => {
state.unreadNotificationsCount = 0;
});
builder.addCase(fetchUnreadCount.fulfilled, (state, action) => {
state.unreadNotificationsCount = action.payload;
});
builder.addCase(fetchUnreadCount.rejected, state => {
state.unreadNotificationsCount = 0;
});
},
});
export const { toggleDrawer, setNotificationType } = notificationSlice.actions;
export default notificationSlice.reducer;

View File

@@ -0,0 +1,137 @@
// Ant Design Icons
import { BankOutlined, CaretDownFilled, CheckCircleFilled } from '@ant-design/icons';
// Ant Design Components
import { Card, Divider, Dropdown, Flex, Tooltip, Typography } from 'antd';
// Redux Hooks
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
// Redux Actions
import { fetchTeams, setActiveTeam } from '@/features/teams/teamSlice';
import { verifyAuthentication } from '@/features/auth/authSlice';
import { setUser } from '@/features/user/userSlice';
// Hooks & Services
import { useAuthService } from '@/hooks/useAuth';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { createAuthService } from '@/services/auth/auth.service';
// Components
import CustomAvatar from '@/components/CustomAvatar';
// Styles
import { colors } from '@/styles/colors';
import './switchTeam.css';
import { useEffect } from 'react';
const SwitchTeamButton = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const authService = createAuthService(navigate);
const { getCurrentSession } = useAuthService();
const session = getCurrentSession();
const { t } = useTranslation('navbar');
// Selectors
const teamsList = useAppSelector(state => state.teamReducer.teamsList);
const themeMode = useAppSelector(state => state.themeReducer.mode);
useEffect(() => {
dispatch(fetchTeams());
}, [dispatch]);
const isActiveTeam = (teamId: string): boolean => {
if (!teamId || !session?.team_id) return false;
return teamId === session.team_id;
};
const handleVerifyAuth = async () => {
const result = await dispatch(verifyAuthentication()).unwrap();
if (result.authenticated) {
dispatch(setUser(result.user));
authService.setCurrentSession(result.user);
}
};
const handleTeamSelect = async (id: string) => {
if (!id) return;
await dispatch(setActiveTeam(id));
await handleVerifyAuth();
window.location.reload();
};
const renderTeamCard = (team: any, index: number) => (
<Card
className="switch-team-card"
onClick={() => handleTeamSelect(team.id)}
bordered={false}
style={{ width: 230 }}
>
<Flex vertical>
<Flex gap={12} align="center" justify="space-between" style={{ padding: '4px 12px' }}>
<Flex gap={8} align="center">
<CustomAvatar avatarName={team.name || ''} />
<Flex vertical>
<Typography.Text style={{ fontSize: 11, fontWeight: 300 }}>
Owned by {team.owns_by}
</Typography.Text>
<Typography.Text>{team.name}</Typography.Text>
</Flex>
</Flex>
<CheckCircleFilled
style={{
fontSize: 16,
color: isActiveTeam(team.id) ? colors.limeGreen : colors.lightGray,
}}
/>
</Flex>
{index < teamsList.length - 1 && <Divider style={{ margin: 0 }} />}
</Flex>
</Card>
);
const dropdownItems =
teamsList?.map((team, index) => ({
key: team.id || '',
label: renderTeamCard(team, index),
type: 'item' as const,
})) || [];
return (
<Dropdown
overlayClassName="switch-team-dropdown"
menu={{ items: dropdownItems }}
trigger={['click']}
placement="bottomRight"
>
<Tooltip title={t('switchTeamTooltip')} trigger={'hover'}>
<Flex
gap={12}
align="center"
justify="center"
style={{
color: themeMode === 'dark' ? '#e6f7ff' : colors.skyBlue,
backgroundColor: themeMode === 'dark' ? '#153450' : colors.paleBlue,
fontWeight: 500,
borderRadius: '50rem',
padding: '10px 16px',
height: '39px',
cursor: 'pointer',
}}
>
<BankOutlined />
<Typography.Text strong style={{ color: colors.skyBlue, cursor: 'pointer' }}>
{session?.team_name}
</Typography.Text>
<CaretDownFilled />
</Flex>
</Tooltip>
</Dropdown>
);
};
export default SwitchTeamButton;

View File

@@ -0,0 +1,26 @@
.switch-team-dropdown .ant-dropdown-menu {
padding: 0 !important;
border-radius: 12px;
max-height: 255px;
overflow: hidden;
overflow-y: auto;
scrollbar-width: thin;
}
.switch-team-dropdown .ant-dropdown-menu::-webkit-scrollbar {
width: 6px;
}
.switch-team-dropdown .ant-dropdown-menu::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.switch-team-dropdown .ant-dropdown-menu-item {
padding: 0 !important;
transition: background-color 0.2s ease;
}
.switch-team-card .ant-card-body {
padding: 0 !important;
}

View File

@@ -0,0 +1,33 @@
import { Button, Tooltip } from 'antd';
import React from 'react';
import { colors } from '../../../styles/colors';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useAppSelector } from '@/hooks/useAppSelector';
const UpgradePlanButton = () => {
// localization
const { t } = useTranslation('navbar');
const navigate = useNavigate();
const themeMode = useAppSelector(state => state.themeReducer.mode);
return (
<Tooltip title={t('upgradePlanTooltip')}>
<Button
style={{
backgroundColor: themeMode === 'dark' ? '#b38750' : colors.lightBeige,
color: '#000000d9',
padding: '4px 11px',
}}
size="small"
type="text"
onClick={() => navigate('/worklenz/admin-center/billing')}
>
{t('upgradePlan')}
</Button>
</Tooltip>
);
};
export default UpgradePlanButton;

View File

@@ -0,0 +1,3 @@
.profile-button:hover .anticon {
color: #1677ff;
}

View File

@@ -0,0 +1,117 @@
import { UserOutlined } from '@ant-design/icons';
import { Button, Card, Dropdown, Flex, MenuProps, Tooltip, Typography } from 'antd';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAppSelector } from '@/hooks/useAppSelector';
import { RootState } from '@/app/store';
import { getRole } from '@/utils/session-helper';
import './profile-dropdown.css';
import './profile-button.css';
import SingleAvatar from '@/components/common/single-avatar/single-avatar';
import { useAuthService } from '@/hooks/useAuth';
import { useEffect, useState } from 'react';
interface ProfileButtonProps {
isOwnerOrAdmin: boolean;
}
const ProfileButton = ({ isOwnerOrAdmin }: ProfileButtonProps) => {
const { t } = useTranslation('navbar');
const authService = useAuthService();
const currentSession = useAppSelector((state: RootState) => state.userReducer);
const role = getRole();
const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode);
const getLinkStyle = () => ({
color: themeMode === 'dark' ? '#ffffffd9' : '#181818',
});
const profile: MenuProps['items'] = [
{
key: '1',
label: (
<Card
className={`profile-card ${themeMode === 'dark' ? 'dark' : ''}`}
title={
<div style={{ paddingBlock: '16px' }}>
<Typography.Text>Account</Typography.Text>
<Flex gap={8} align="center" justify="flex-start" style={{ width: '100%' }}>
<SingleAvatar
avatarUrl={currentSession?.avatar_url}
name={currentSession?.name}
email={currentSession?.email}
/>
<Flex vertical style={{ flex: 1, minWidth: 0 }}>
<Typography.Text
ellipsis={{ tooltip: currentSession?.name }} // Show tooltip on hover
style={{ width: '100%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{currentSession?.name}
</Typography.Text>
<Typography.Text
ellipsis={{ tooltip: currentSession?.email }} // Show tooltip on hover
style={{ fontSize: 12, width: '100%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{currentSession?.email}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
({role})
</Typography.Text>
</Flex>
</Flex>
</div>
}
variant="borderless"
style={{ width: 230 }}
>
{isOwnerOrAdmin && (
<Link to="/worklenz/admin-center/overview" style={getLinkStyle()}>
{t('adminCenter')}
</Link>
)}
<Link to="/worklenz/settings/profile" style={getLinkStyle()}>
{t('settings')}
</Link>
<Link to="/auth/logging-out" style={getLinkStyle()}>
{t('logOut')}
</Link>
</Card>
),
},
];
return (
<Dropdown
overlayClassName="profile-dropdown"
menu={{ items: profile }}
placement="bottomRight"
trigger={['click']}
>
<Tooltip title={t('profileTooltip')}>
<Button
className="profile-button"
style={{ height: '62px', width: '60px' }}
type="text"
icon={
currentSession?.avatar_url ? (
<SingleAvatar
avatarUrl={currentSession.avatar_url}
name={currentSession.name}
email={currentSession.email}
/>
) : (
<UserOutlined style={{ fontSize: 20 }} />
)
}
/>
</Tooltip>
</Dropdown>
);
};
export default ProfileButton;

View File

@@ -0,0 +1,39 @@
.profile-dropdown .ant-dropdown-menu {
padding: 0 !important;
margin-top: 2px !important;
}
.profile-dropdown .ant-dropdown-menu-item {
padding: 0 !important;
}
.profile-card .ant-card-head {
padding: 0 16px;
}
.profile-card .ant-card-body {
padding: 4px 0;
}
.profile-card .ant-card-body a {
display: block;
margin: 0;
padding: 10px 15px;
}
.profile-card .ant-card-body a:hover {
color: #1890ff !important;
background-color: #f8f7f9 !important;
box-sizing: border-box;
}
.profile-card.dark .ant-card-body a:hover {
color: #1890ff !important;
background-color: #424242 !important;
box-sizing: border-box;
}
.profile-card {
border: none !important;
box-shadow: none !important;
}