init
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.notification-icon:hover .anticon {
|
||||
color: #1677ff;
|
||||
}
|
||||
23
worklenz-frontend/src/features/navbar/help/HelpButton.tsx
Normal file
23
worklenz-frontend/src/features/navbar/help/HelpButton.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
33
worklenz-frontend/src/features/navbar/navRoutes.ts
Normal file
33
worklenz-frontend/src/features/navbar/navRoutes.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
45
worklenz-frontend/src/features/navbar/navbar-logo.tsx
Normal file
45
worklenz-frontend/src/features/navbar/navbar-logo.tsx
Normal 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;
|
||||
186
worklenz-frontend/src/features/navbar/navbar.tsx
Normal file
186
worklenz-frontend/src/features/navbar/navbar.tsx
Normal 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;
|
||||
117
worklenz-frontend/src/features/navbar/notificationSlice.ts
Normal file
117
worklenz-frontend/src/features/navbar/notificationSlice.ts
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
.profile-button:hover .anticon {
|
||||
color: #1677ff;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user