From eeec5b2b841bfb179995c059f281d04ebd10c8e1 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Fri, 25 Jul 2025 17:01:15 +0530 Subject: [PATCH] feat(navbar): implement new notification and invitation components - Added NotificationDrawer and InvitationItem components to enhance the notification system. - Refactored existing notification handling to improve user experience and maintainability. - Introduced new styles and structure for notifications using Tailwind CSS for better visual consistency. - Updated Navbar to include new components and improve overall layout. - Created a centralized navRoutes file for better route management within the navbar. --- worklenz-frontend/sonar-project.properties | 55 ++++ .../src/components/PinRouteToNavbarButton.tsx | 2 +- .../invite-team-members.tsx | 13 +- ...invitation-item.tsx => InvitationItem.tsx} | 2 +- .../navbar/Navbar.tsx} | 41 +-- .../navbar/NavbarLogo.tsx} | 8 +- ...tion-button.tsx => NotificationButton.tsx} | 0 .../navbar/help/HelpButton.css | 0 .../navbar/help/HelpButton.tsx | 8 +- .../navbar/invite/InviteButton.tsx | 12 +- .../navbar/mobileMenu/MobileMenuButton.css} | 0 .../navbar/mobileMenu/MobileMenuButton.tsx | 112 ++++++++ .../navbar/notifications/STYLING_FIXES.md | 128 ++++++++++ .../{notification => }/notfication-drawer.tsx | 239 ++++++++++-------- ...fication-item.css => NotificationItem.css} | 0 .../notification/NotificationItem.tsx | 165 ++++++++++++ .../notification/NotificationTemplate.tsx | 152 +++++++++++ ...plate.css => PushNotificationTemplate.css} | 0 .../notification/PushNotificationTemplate.tsx | 176 +++++++++++++ .../notification/notification-item.tsx | 127 ---------- .../notification/notification-template.tsx | 95 ------- .../push-notification-template.tsx | 105 -------- .../navbar/switchTeam/SwitchTeamButton.css} | 0 .../navbar/switchTeam/SwitchTeamButton.tsx | 75 +++--- .../navbar/timers/TimerButton.tsx} | 14 +- .../navbar/upgradePlan/UpgradePlanButton.tsx | 10 +- .../navbar/user-profile/ProfileButton.css} | 0 .../navbar/user-profile/ProfileButton.tsx} | 34 ++- .../navbar/user-profile/ProfileDropdown.css} | 0 .../navbar/mobileMenu/MobileMenuButton.tsx | 99 -------- .../src/features/navbar/notificationSlice.ts | 18 +- worklenz-frontend/src/layouts/MainLayout.tsx | 6 +- .../src/layouts/ReportingLayout.tsx | 12 +- .../src/{features => lib}/navbar/navRoutes.ts | 0 34 files changed, 1074 insertions(+), 634 deletions(-) create mode 100644 worklenz-frontend/sonar-project.properties rename worklenz-frontend/src/components/navbar/{notifications/notifications-drawer/notification/invitation-item.tsx => InvitationItem.tsx} (97%) rename worklenz-frontend/src/{features/navbar/navbar.tsx => components/navbar/Navbar.tsx} (85%) rename worklenz-frontend/src/{features/navbar/navbar-logo.tsx => components/navbar/NavbarLogo.tsx} (87%) rename worklenz-frontend/src/components/navbar/{notifications/notifications-drawer/notification/notification-button.tsx => NotificationButton.tsx} (100%) rename worklenz-frontend/src/{features => components}/navbar/help/HelpButton.css (100%) rename worklenz-frontend/src/{features => components}/navbar/help/HelpButton.tsx (82%) rename worklenz-frontend/src/{features => components}/navbar/invite/InviteButton.tsx (69%) rename worklenz-frontend/src/{features/navbar/mobileMenu/mobileMenu.css => components/navbar/mobileMenu/MobileMenuButton.css} (100%) create mode 100644 worklenz-frontend/src/components/navbar/mobileMenu/MobileMenuButton.tsx create mode 100644 worklenz-frontend/src/components/navbar/notifications/STYLING_FIXES.md rename worklenz-frontend/src/components/navbar/notifications/notifications-drawer/{notification => }/notfication-drawer.tsx (58%) rename worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/{notification-item.css => NotificationItem.css} (100%) create mode 100644 worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/NotificationItem.tsx create mode 100644 worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/NotificationTemplate.tsx rename worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/{push-notification-template.css => PushNotificationTemplate.css} (100%) create mode 100644 worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/PushNotificationTemplate.tsx delete mode 100644 worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-item.tsx delete mode 100644 worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-template.tsx delete mode 100644 worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.tsx rename worklenz-frontend/src/{features/navbar/switchTeam/switchTeam.css => components/navbar/switchTeam/SwitchTeamButton.css} (100%) rename worklenz-frontend/src/{features => components}/navbar/switchTeam/SwitchTeamButton.tsx (75%) rename worklenz-frontend/src/{features/navbar/timers/timer-button.tsx => components/navbar/timers/TimerButton.tsx} (98%) rename worklenz-frontend/src/{features => components}/navbar/upgradePlan/UpgradePlanButton.tsx (77%) rename worklenz-frontend/src/{features/navbar/user-profile/profile-button.css => components/navbar/user-profile/ProfileButton.css} (100%) rename worklenz-frontend/src/{features/navbar/user-profile/profile-button.tsx => components/navbar/user-profile/ProfileButton.tsx} (86%) rename worklenz-frontend/src/{features/navbar/user-profile/profile-dropdown.css => components/navbar/user-profile/ProfileDropdown.css} (100%) delete mode 100644 worklenz-frontend/src/features/navbar/mobileMenu/MobileMenuButton.tsx rename worklenz-frontend/src/{features => lib}/navbar/navRoutes.ts (100%) diff --git a/worklenz-frontend/sonar-project.properties b/worklenz-frontend/sonar-project.properties new file mode 100644 index 00000000..4c0b2f54 --- /dev/null +++ b/worklenz-frontend/sonar-project.properties @@ -0,0 +1,55 @@ +# SonarQube Configuration for Worklenz Frontend +sonar.projectKey=worklenz-frontend +sonar.projectName=Worklenz Frontend +sonar.projectVersion=1.0.0 + +# Source code configuration +sonar.sources=src +sonar.tests=src +sonar.test.inclusions=**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx + +# Language-specific configurations +sonar.typescript.node=node +sonar.typescript.lcov.reportPaths=coverage/lcov.info +sonar.javascript.lcov.reportPaths=coverage/lcov.info + +# Exclusions +sonar.exclusions=**/node_modules/**,\ + **/build/**,\ + **/dist/**,\ + **/public/**,\ + **/*.d.ts,\ + src/react-app-env.d.ts,\ + src/vite-env.d.ts,\ + **/*.config.js,\ + **/*.config.ts,\ + **/*.config.mts,\ + scripts/** + +# Test exclusions from coverage +sonar.coverage.exclusions=**/*.test.ts,\ + **/*.test.tsx,\ + **/*.spec.ts,\ + **/*.spec.tsx,\ + **/*.config.*,\ + src/index.tsx,\ + src/reportWebVitals.ts,\ + src/serviceWorkerRegistration.ts,\ + src/setupTests.ts + +# Code quality rules +sonar.qualitygate.wait=true + +# File encoding +sonar.sourceEncoding=UTF-8 + +# JavaScript/TypeScript specific settings +sonar.javascript.environments=browser,node,jest +sonar.typescript.tsconfigPath=tsconfig.json + +# ESLint configuration (if available) +# sonar.eslint.reportPaths=eslint-report.json + +# Additional settings for React projects +sonar.javascript.file.suffixes=.js,.jsx +sonar.typescript.file.suffixes=.ts,.tsx \ No newline at end of file diff --git a/worklenz-frontend/src/components/PinRouteToNavbarButton.tsx b/worklenz-frontend/src/components/PinRouteToNavbarButton.tsx index 4e8f8dc1..f26d48fc 100644 --- a/worklenz-frontend/src/components/PinRouteToNavbarButton.tsx +++ b/worklenz-frontend/src/components/PinRouteToNavbarButton.tsx @@ -3,7 +3,7 @@ import { getJSONFromLocalStorage, saveJSONToLocalStorage } from '../utils/localS import { Button, ConfigProvider, Tooltip } from '@/shared/antd-imports'; import { PushpinFilled, PushpinOutlined } from '@/shared/antd-imports'; import { colors } from '../styles/colors'; -import { navRoutes, NavRoutesType } from '../features/navbar/navRoutes'; +import { navRoutes, NavRoutesType } from '../lib/navbar/navRoutes'; // Props type for the component type PinRouteToNavbarButtonProps = { diff --git a/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx b/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx index ab4ff36a..a45e44fc 100644 --- a/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx +++ b/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx @@ -1,4 +1,14 @@ -import { AutoComplete, Button, Drawer, Flex, Form, message, Modal, Select, Spin, Typography } from '@/shared/antd-imports'; +import { + AutoComplete, + Button, + Flex, + Form, + message, + Modal, + Select, + Spin, + Typography, +} from '@/shared/antd-imports'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { @@ -11,7 +21,6 @@ import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.se import { IJobTitle } from '@/types/job.types'; import { teamMembersApiService } from '@/api/team-members/teamMembers.api.service'; import { ITeamMemberCreateRequest } from '@/types/teamMembers/team-member-create-request'; -import { LinkOutlined } from '@ant-design/icons'; interface FormValues { email: string[]; diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/invitation-item.tsx b/worklenz-frontend/src/components/navbar/InvitationItem.tsx similarity index 97% rename from worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/invitation-item.tsx rename to worklenz-frontend/src/components/navbar/InvitationItem.tsx index 894b722f..eea2da7d 100644 --- a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/invitation-item.tsx +++ b/worklenz-frontend/src/components/navbar/InvitationItem.tsx @@ -17,7 +17,7 @@ interface InvitationItemProps { t: TFunction; } -const InvitationItem: React.FC = ({ item, isUnreadNotifications, t }) => { +const InvitationItem = ({ item, isUnreadNotifications, t }: InvitationItemProps) => { const [accepting, setAccepting] = useState(false); const [joining, setJoining] = useState(false); const dispatch = useAppDispatch(); diff --git a/worklenz-frontend/src/features/navbar/navbar.tsx b/worklenz-frontend/src/components/navbar/Navbar.tsx similarity index 85% rename from worklenz-frontend/src/features/navbar/navbar.tsx rename to worklenz-frontend/src/components/navbar/Navbar.tsx index 1630c25e..a0df07b4 100644 --- a/worklenz-frontend/src/features/navbar/navbar.tsx +++ b/worklenz-frontend/src/components/navbar/Navbar.tsx @@ -1,55 +1,60 @@ -import React, { useEffect, useState, useMemo } from 'react'; +import React, { useEffect, useState, useMemo, memo } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Col, ConfigProvider, Flex, Menu, MenuProps, Alert } from '@/shared/antd-imports'; +import { Col, ConfigProvider, Flex, Menu } from '@/shared/antd-imports'; import { createPortal } from 'react-dom'; -import InviteTeamMembers from '../../components/common/invite-team-members/invite-team-members'; +import InviteTeamMembers from '../common/invite-team-members/invite-team-members'; 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 NavbarLogo from './NavbarLogo'; +import NotificationButton from './NotificationButton'; +import ProfileButton from './user-profile/ProfileButton'; import SwitchTeamButton from './switchTeam/SwitchTeamButton'; import UpgradePlanButton from './upgradePlan/UpgradePlanButton'; -import NotificationDrawer from '../../components/navbar/notifications/notifications-drawer/notification/notfication-drawer'; +import NotificationDrawer from './notifications/notifications-drawer/notfication-drawer'; import { useResponsive } from '@/hooks/useResponsive'; import { getJSONFromLocalStorage } from '@/utils/localStorageFunctions'; -import { navRoutes, NavRoutesType } from './navRoutes'; +import { navRoutes, NavRoutesType } from '@/lib/navbar/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'; -import TimerButton from './timers/timer-button'; import HelpButton from './help/HelpButton'; -const Navbar = () => { +const Navbar = memo(() => { const [current, setCurrent] = useState('home'); - const currentSession = useAuthService().getCurrentSession(); + const authService = useAuthService(); + const currentSession = authService.getCurrentSession(); const [daysUntilExpiry, setDaysUntilExpiry] = useState(null); const location = useLocation(); const { isDesktop, isMobile, isTablet } = useResponsive(); const { t } = useTranslation('navbar'); - const authService = useAuthService(); const [navRoutesList, setNavRoutesList] = useState(navRoutes); const [isOwnerOrAdmin, setIsOwnerOrAdmin] = useState(authService.isOwnerOrAdmin()); - const showUpgradeTypes = [ISUBSCRIPTION_TYPE.TRIAL]; + const showUpgradeTypes = useMemo(() => [ISUBSCRIPTION_TYPE.TRIAL], []); useEffect(() => { + let mounted = true; authApiService .verify() .then(authorizeResponse => { - if (authorizeResponse.authenticated) { + if (mounted && authorizeResponse.authenticated) { authService.setCurrentSession(authorizeResponse.user); setIsOwnerOrAdmin(!!(authorizeResponse.user.is_admin || authorizeResponse.user.owner)); } }) .catch(error => { - logger.error('Error during authorization', error); + if (mounted) { + logger.error('Error during authorization', error); + } }); - }, []); + return () => { + mounted = false; + }; + }, [authService]); useEffect(() => { const storedNavRoutesList: NavRoutesType[] = getJSONFromLocalStorage('navRoutes') || navRoutes; @@ -183,6 +188,8 @@ const Navbar = () => { {createPortal(, document.body, 'notification-drawer')} ); -}; +}); + +Navbar.displayName = 'Navbar'; export default Navbar; diff --git a/worklenz-frontend/src/features/navbar/navbar-logo.tsx b/worklenz-frontend/src/components/navbar/NavbarLogo.tsx similarity index 87% rename from worklenz-frontend/src/features/navbar/navbar-logo.tsx rename to worklenz-frontend/src/components/navbar/NavbarLogo.tsx index afa8b1c0..183ac0a7 100644 --- a/worklenz-frontend/src/features/navbar/navbar-logo.tsx +++ b/worklenz-frontend/src/components/navbar/NavbarLogo.tsx @@ -1,14 +1,14 @@ +import { memo } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import logo from '@/assets/images/worklenz-light-mode.png'; import logoDark from '@/assets/images/worklenz-dark-mode.png'; -import { useAppSelector } from '@/hooks/useAppSelector'; import { useSelector } from 'react-redux'; import { RootState } from '@/app/store'; -const NavbarLogo = () => { +const NavbarLogo = memo(() => { const { t } = useTranslation('navbar'); const themeMode = useSelector((state: RootState) => state.themeReducer.mode); @@ -23,6 +23,8 @@ const NavbarLogo = () => { ); -}; +}); + +NavbarLogo.displayName = 'NavbarLogo'; export default NavbarLogo; diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-button.tsx b/worklenz-frontend/src/components/navbar/NotificationButton.tsx similarity index 100% rename from worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-button.tsx rename to worklenz-frontend/src/components/navbar/NotificationButton.tsx diff --git a/worklenz-frontend/src/features/navbar/help/HelpButton.css b/worklenz-frontend/src/components/navbar/help/HelpButton.css similarity index 100% rename from worklenz-frontend/src/features/navbar/help/HelpButton.css rename to worklenz-frontend/src/components/navbar/help/HelpButton.css diff --git a/worklenz-frontend/src/features/navbar/help/HelpButton.tsx b/worklenz-frontend/src/components/navbar/help/HelpButton.tsx similarity index 82% rename from worklenz-frontend/src/features/navbar/help/HelpButton.tsx rename to worklenz-frontend/src/components/navbar/help/HelpButton.tsx index aa66e7dc..60749026 100644 --- a/worklenz-frontend/src/features/navbar/help/HelpButton.tsx +++ b/worklenz-frontend/src/components/navbar/help/HelpButton.tsx @@ -1,10 +1,10 @@ import { QuestionCircleOutlined } from '@/shared/antd-imports'; import { Button, Tooltip } from '@/shared/antd-imports'; -import React from 'react'; +import React, { memo } from 'react'; import { useTranslation } from 'react-i18next'; import './HelpButton.css'; -const HelpButton = () => { +const HelpButton = memo(() => { // localization const { t } = useTranslation('navbar'); @@ -18,6 +18,8 @@ const HelpButton = () => { /> ); -}; +}); + +HelpButton.displayName = 'HelpButton'; export default HelpButton; diff --git a/worklenz-frontend/src/features/navbar/invite/InviteButton.tsx b/worklenz-frontend/src/components/navbar/invite/InviteButton.tsx similarity index 69% rename from worklenz-frontend/src/features/navbar/invite/InviteButton.tsx rename to worklenz-frontend/src/components/navbar/invite/InviteButton.tsx index 22412822..03263d20 100644 --- a/worklenz-frontend/src/features/navbar/invite/InviteButton.tsx +++ b/worklenz-frontend/src/components/navbar/invite/InviteButton.tsx @@ -1,12 +1,12 @@ import { UsergroupAddOutlined } from '@/shared/antd-imports'; import { Button, Tooltip } from '@/shared/antd-imports'; -import React from 'react'; +import React, { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { colors } from '../../../styles/colors'; import { useAppDispatch } from '@/hooks/useAppDispatch'; -import { toggleInviteMemberDrawer } from '../../settings/member/memberSlice'; +import { toggleInviteMemberDrawer } from '../../../features/settings/member/memberSlice'; -const InviteButton = () => { +const InviteButton = memo(() => { const dispatch = useAppDispatch(); // localization @@ -21,12 +21,14 @@ const InviteButton = () => { color: colors.skyBlue, borderColor: colors.skyBlue, }} - onClick={() => dispatch(toggleInviteMemberDrawer())} + onClick={useCallback(() => dispatch(toggleInviteMemberDrawer()), [dispatch])} > {t('invite')} ); -}; +}); + +InviteButton.displayName = 'InviteButton'; export default InviteButton; diff --git a/worklenz-frontend/src/features/navbar/mobileMenu/mobileMenu.css b/worklenz-frontend/src/components/navbar/mobileMenu/MobileMenuButton.css similarity index 100% rename from worklenz-frontend/src/features/navbar/mobileMenu/mobileMenu.css rename to worklenz-frontend/src/components/navbar/mobileMenu/MobileMenuButton.css diff --git a/worklenz-frontend/src/components/navbar/mobileMenu/MobileMenuButton.tsx b/worklenz-frontend/src/components/navbar/mobileMenu/MobileMenuButton.tsx new file mode 100644 index 00000000..103a4f6b --- /dev/null +++ b/worklenz-frontend/src/components/navbar/mobileMenu/MobileMenuButton.tsx @@ -0,0 +1,112 @@ +import { + Button, + Card, + Dropdown, + Flex, + MenuProps, + Space, + Typography, + HomeOutlined, + MenuOutlined, + ProjectOutlined, + QuestionCircleOutlined, + ReadOutlined, +} from '@/shared/antd-imports'; +import React, { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { colors } from '@/styles/colors'; +import { NavLink } from 'react-router-dom'; +import InviteButton from '@/components/navbar/invite/InviteButton'; +import SwitchTeamButton from '@/components/navbar/switchTeam/SwitchTeamButton'; +// custom css +import './MobileMenuButton.css'; + +const MobileMenuButton = memo(() => { + // localization + const { t } = useTranslation('navbar'); + + const navLinks = useMemo( + () => [ + { + 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'] = useMemo( + () => [ + { + key: '1', + label: ( + + {navLinks.map((navEl, index) => ( + + + + {navEl.icon} + {t(navEl.name)} + + + + ))} + + + + + + + + ), + }, + ], + [navLinks, t] + ); + + return ( + + + )} + + {formattedDate} + + + + + ); +}); + +NotificationItem.displayName = 'NotificationItem'; + +export default NotificationItem; diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/NotificationTemplate.tsx b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/NotificationTemplate.tsx new file mode 100644 index 00000000..eb0da22f --- /dev/null +++ b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/NotificationTemplate.tsx @@ -0,0 +1,152 @@ +import React, { memo, useCallback, useMemo } from 'react'; +import { Button, Typography, Tag } from '@/shared/antd-imports'; +import { BankOutlined } from '@/shared/antd-imports'; +import { IWorklenzNotification } from '@/types/notifications/notifications.types'; +import { useNavigate } from 'react-router-dom'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { toggleDrawer } from '@features/navbar/notificationSlice'; +import { teamsApiService } from '@/api/teams/teams.api.service'; +import { formatDistanceToNow } from 'date-fns'; +import { tagBackground } from '@/utils/colorUtils'; +import logger from '@/utils/errorLogger'; +import './NotificationItem.css'; + +interface NotificationTemplateProps { + item: IWorklenzNotification; + isUnreadNotifications: boolean; + markNotificationAsRead: (id: string) => Promise; + loadersMap: Record; +} + +const NotificationTemplate = memo(({ + item, + isUnreadNotifications, + markNotificationAsRead, + loadersMap, +}) => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + + const goToUrl = useCallback( + async (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (!item.url) return; + + try { + dispatch(toggleDrawer()); + + if (item.team_id) { + await teamsApiService.setActiveTeam(item.team_id); + } + + navigate(item.url, { + state: item.params || null, + }); + } catch (error) { + logger.error('Error navigating to notification URL', error); + } + }, + [item.url, item.team_id, item.params, dispatch, navigate] + ); + + const formattedDate = useMemo(() => { + if (!item.created_at) return ''; + try { + return formatDistanceToNow(new Date(item.created_at), { addSuffix: true }); + } catch (error) { + logger.error('Error formatting date', error); + return ''; + } + }, [item.created_at]); + + const handleMarkAsRead = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + markNotificationAsRead(item.id); + }, + [markNotificationAsRead, item.id] + ); + + const containerStyle = useMemo( + () => ({ + border: item.color ? `2px solid ${item.color}4d` : undefined, + }), + [item.color] + ); + + const containerClassName = useMemo( + () => [ + 'w-auto p-3 mb-3 rounded border border-gray-200 bg-white shadow-sm transition-all duration-300', + 'hover:shadow-md hover:bg-gray-50', + item.url ? 'cursor-pointer' : 'cursor-default', + 'dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700' + ].join(' '), + [item.url] + ); + + const messageHtml = useMemo( + () => ({ __html: item.message }), + [item.message] + ); + + const tagStyle = useMemo( + () => (item.color ? { backgroundColor: tagBackground(item.color) } : {}), + [item.color] + ); + + const shouldShowProject = useMemo( + () => Boolean(item.project && item.color), + [item.project, item.color] + ); + + const isLoading = useMemo( + () => Boolean(loadersMap[item.id]), + [loadersMap, item.id] + ); + + return ( +
+
+
+ + {item.team} + +
+ {shouldShowProject && ( +
+ {item.project} +
+ )} +
+ +
+ {isUnreadNotifications && ( + + )} + + {formattedDate} + +
+
+
+ ); +}); + +NotificationTemplate.displayName = 'NotificationTemplate'; + +export default NotificationTemplate; diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.css b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/PushNotificationTemplate.css similarity index 100% rename from worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.css rename to worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/PushNotificationTemplate.css diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/PushNotificationTemplate.tsx b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/PushNotificationTemplate.tsx new file mode 100644 index 00000000..7128b7bb --- /dev/null +++ b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/PushNotificationTemplate.tsx @@ -0,0 +1,176 @@ +import React, { memo, useCallback, useMemo } from 'react'; +import { notification } from '@/shared/antd-imports'; +import { IWorklenzNotification } from '@/types/notifications/notifications.types'; +import { teamsApiService } from '@/api/teams/teams.api.service'; +import { toQueryString } from '@/utils/toQueryString'; +import { BankOutlined } from '@/shared/antd-imports'; +import './PushNotificationTemplate.css'; + +interface PushNotificationTemplateProps { + notification: IWorklenzNotification; +} + +const PushNotificationTemplate = memo(({ + notification: notificationData, +}: PushNotificationTemplateProps) => { + const handleClick = useCallback(async () => { + if (!notificationData.url) return; + + try { + let url = notificationData.url; + if (notificationData.params && Object.keys(notificationData.params).length) { + const q = toQueryString(notificationData.params); + url += q; + } + + if (notificationData.team_id) { + await teamsApiService.setActiveTeam(notificationData.team_id); + } + + window.location.href = url; + } catch (error) { + console.error('Error handling notification click:', error); + } + }, [notificationData.url, notificationData.params, notificationData.team_id]); + + const containerStyle = useMemo( + () => ({ + cursor: notificationData.url ? 'pointer' : 'default', + padding: '8px 0', + borderRadius: '8px', + }), + [notificationData.url] + ); + + const headerStyle = useMemo( + () => ({ + display: 'flex', + alignItems: 'center', + marginBottom: '8px', + color: '#262626', + fontSize: '14px', + fontWeight: 500, + }), + [] + ); + + const iconStyle = useMemo( + () => ({ marginRight: '8px', color: '#1890ff' }), + [] + ); + + const messageStyle = useMemo( + () => ({ + color: '#595959', + fontSize: '13px', + lineHeight: '1.5', + marginTop: '4px', + }), + [] + ); + + const className = useMemo( + () => `notification-content ${notificationData.url ? 'clickable' : ''}`, + [notificationData.url] + ); + + const messageHtml = useMemo( + () => ({ __html: notificationData.message }), + [notificationData.message] + ); + + return ( +
+
+ {notificationData.team ? ( + <> + + {notificationData.team} + + ) : ( + 'Worklenz' + )} +
+
+
+ ); +}); + +PushNotificationTemplate.displayName = 'PushNotificationTemplate'; + +// Notification queue management +class NotificationQueueManager { + private queue: IWorklenzNotification[] = []; + private isProcessing = false; + private readonly maxQueueSize = 10; + private readonly notificationStyle = { + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', + padding: '12px 16px', + minWidth: '300px', + maxWidth: '400px', + }; + + private processQueue = () => { + if (this.isProcessing || this.queue.length === 0) return; + + this.isProcessing = true; + const notificationData = this.queue.shift(); + + if (notificationData) { + notification.info({ + message: null, + description: , + placement: 'topRight', + duration: 5, + style: this.notificationStyle, + onClose: () => { + this.isProcessing = false; + // Use setTimeout to prevent stack overflow with rapid notifications + setTimeout(() => this.processQueue(), 0); + }, + }); + } else { + this.isProcessing = false; + } + }; + + public addNotification = (notificationData: IWorklenzNotification) => { + // Prevent queue overflow + if (this.queue.length >= this.maxQueueSize) { + console.warn('Notification queue is full, dropping oldest notification'); + this.queue.shift(); + } + + this.queue.push(notificationData); + this.processQueue(); + }; + + public clearQueue = () => { + this.queue.length = 0; + this.isProcessing = false; + }; + + public getQueueLength = () => this.queue.length; +} + +const notificationManager = new NotificationQueueManager(); + +export const showNotification = (notificationData: IWorklenzNotification) => { + notificationManager.addNotification(notificationData); +}; + +export const clearNotificationQueue = () => { + notificationManager.clearQueue(); +}; + +export const getNotificationQueueLength = () => { + return notificationManager.getQueueLength(); +}; diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-item.tsx b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-item.tsx deleted file mode 100644 index 39023968..00000000 --- a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-item.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { IWorklenzNotification } from '@/types/notifications/notifications.types'; -import { BankOutlined } from '@/shared/antd-imports'; -import { Button, Tag, Typography, theme } from '@/shared/antd-imports'; -import DOMPurify from 'dompurify'; -import React, { useState } from 'react'; -import { fromNow } from '@/utils/dateUtils'; -import './notification-item.css'; - -const { Text } = Typography; - -interface NotificationItemProps { - notification: IWorklenzNotification; - isUnreadNotifications?: boolean; - markNotificationAsRead?: (id: string) => Promise; - goToUrl?: (e: React.MouseEvent, notification: IWorklenzNotification) => Promise; -} - -const NotificationItem = ({ - notification, - isUnreadNotifications = true, - markNotificationAsRead, - goToUrl, -}: NotificationItemProps) => { - const { token } = theme.useToken(); - const [loading, setLoading] = useState(false); - const isDarkMode = - token.colorBgContainer === '#141414' || - token.colorBgContainer.includes('dark') || - document.documentElement.getAttribute('data-theme') === 'dark'; - - const handleNotificationClick = async (e: React.MouseEvent) => { - await goToUrl?.(e, notification); - await markNotificationAsRead?.(notification.id); - }; - - const handleMarkAsRead = async (e: React.MouseEvent) => { - e.stopPropagation(); - if (!notification.id) return; - - setLoading(true); - try { - await markNotificationAsRead?.(notification.id); - } finally { - setLoading(false); - } - }; - - const createSafeHtml = (html: string) => { - return { __html: DOMPurify.sanitize(html) }; - }; - - const getTagBackground = (color?: string) => { - if (!color) return {}; - - // Create a more transparent version of the color for the background - // This is equivalent to the color + '4d' in the Angular template - const bgColor = `${color}4d`; - - // For dark mode, we might need to adjust the text color for better contrast - if (isDarkMode) { - return { - backgroundColor: bgColor, - color: '#ffffff', - borderColor: 'transparent', - }; - } - - return { - backgroundColor: bgColor, - borderColor: 'transparent', - }; - }; - - return ( -
-
-
- {/* Team name */} -
- - {notification.team} - -
- - {/* Message with HTML content */} -
- - {/* Project tag */} - {notification.project && ( -
- {notification.project} -
- )} -
- - {/* Footer with mark as read button and timestamp */} -
- {isUnreadNotifications && markNotificationAsRead && ( - - )} - - {notification.created_at ? fromNow(notification.created_at) : ''} - -
-
-
- ); -}; - -export default NotificationItem; diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-template.tsx b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-template.tsx deleted file mode 100644 index f4a6e60a..00000000 --- a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/notification-template.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Button, Typography, Tag } from '@/shared/antd-imports'; -import { BankOutlined } from '@/shared/antd-imports'; -import { IWorklenzNotification } from '@/types/notifications/notifications.types'; -import { useNavigate } from 'react-router-dom'; -import { useAppDispatch } from '@/hooks/useAppDispatch'; -import { toggleDrawer } from '../../../../../features/navbar/notificationSlice'; -import { teamsApiService } from '@/api/teams/teams.api.service'; -import { formatDistanceToNow } from 'date-fns'; -import { tagBackground } from '@/utils/colorUtils'; - -interface NotificationTemplateProps { - item: IWorklenzNotification; - isUnreadNotifications: boolean; - markNotificationAsRead: (id: string) => Promise; - loadersMap: Record; -} - -const NotificationTemplate: React.FC = ({ - item, - isUnreadNotifications, - markNotificationAsRead, - loadersMap, -}) => { - const navigate = useNavigate(); - const dispatch = useAppDispatch(); - - const goToUrl = async (event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - - console.log('goToUrl triggered', { url: item.url, teamId: item.team_id }); - - if (item.url) { - dispatch(toggleDrawer()); - - if (item.team_id) { - await teamsApiService.setActiveTeam(item.team_id); - } - - navigate(item.url, { - state: item.params || null, - }); - } - }; - - const formatDate = (dateString?: string) => { - if (!dateString) return ''; - return formatDistanceToNow(new Date(dateString), { addSuffix: true }); - }; - - const handleMarkAsRead = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - markNotificationAsRead(item.id); - }; - - return ( -
-
-
- - {item.team} - -
- {item.project && item.color && ( - {item.project} - )} -
- -
- {isUnreadNotifications && ( - - )} - - {formatDate(item.created_at)} - -
-
-
- ); -}; - -export default NotificationTemplate; diff --git a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.tsx b/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.tsx deleted file mode 100644 index b0bcc7bb..00000000 --- a/worklenz-frontend/src/components/navbar/notifications/notifications-drawer/notification/push-notification-template.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { notification } from '@/shared/antd-imports'; -import { IWorklenzNotification } from '@/types/notifications/notifications.types'; -import { teamsApiService } from '@/api/teams/teams.api.service'; -import { toQueryString } from '@/utils/toQueryString'; -import { BankOutlined } from '@/shared/antd-imports'; -import './push-notification-template.css'; - -const PushNotificationTemplate = ({ - notification: notificationData, -}: { - notification: IWorklenzNotification; -}) => { - const handleClick = async () => { - if (notificationData.url) { - let url = notificationData.url; - if (notificationData.params && Object.keys(notificationData.params).length) { - const q = toQueryString(notificationData.params); - url += q; - } - - if (notificationData.team_id) { - await teamsApiService.setActiveTeam(notificationData.team_id); - } - - window.location.href = url; - } - }; - - return ( -
-
- {notificationData.team && ( - <> - - {notificationData.team} - - )} - {!notificationData.team && 'Worklenz'} -
-
-
- ); -}; - -let notificationQueue: IWorklenzNotification[] = []; -let isProcessing = false; - -const processNotificationQueue = () => { - if (isProcessing || notificationQueue.length === 0) return; - - isProcessing = true; - const notificationData = notificationQueue.shift(); - - if (notificationData) { - notification.info({ - message: null, - description: , - placement: 'topRight', - duration: 5, - style: { - borderRadius: '8px', - boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', - padding: '12px 16px', - minWidth: '300px', - maxWidth: '400px', - }, - onClose: () => { - isProcessing = false; - processNotificationQueue(); - }, - }); - } else { - isProcessing = false; - } -}; - -export const showNotification = (notificationData: IWorklenzNotification) => { - notificationQueue.push(notificationData); - processNotificationQueue(); -}; diff --git a/worklenz-frontend/src/features/navbar/switchTeam/switchTeam.css b/worklenz-frontend/src/components/navbar/switchTeam/SwitchTeamButton.css similarity index 100% rename from worklenz-frontend/src/features/navbar/switchTeam/switchTeam.css rename to worklenz-frontend/src/components/navbar/switchTeam/SwitchTeamButton.css diff --git a/worklenz-frontend/src/features/navbar/switchTeam/SwitchTeamButton.tsx b/worklenz-frontend/src/components/navbar/switchTeam/SwitchTeamButton.tsx similarity index 75% rename from worklenz-frontend/src/features/navbar/switchTeam/SwitchTeamButton.tsx rename to worklenz-frontend/src/components/navbar/switchTeam/SwitchTeamButton.tsx index b2dddbc4..0ea4e065 100644 --- a/worklenz-frontend/src/features/navbar/switchTeam/SwitchTeamButton.tsx +++ b/worklenz-frontend/src/components/navbar/switchTeam/SwitchTeamButton.tsx @@ -1,33 +1,20 @@ -// Ant Design Icons import { BankOutlined, CaretDownFilled, CheckCircleFilled } from '@/shared/antd-imports'; - -// Ant Design Components import { Card, Divider, Dropdown, Flex, Tooltip, Typography } from '@/shared/antd-imports'; - -// 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'; +import './SwitchTeamButton.css'; +import { useEffect, memo, useCallback, useMemo } from 'react'; -const SwitchTeamButton = () => { +const SwitchTeamButton = memo(() => { const dispatch = useAppDispatch(); const navigate = useNavigate(); const authService = createAuthService(navigate); @@ -43,32 +30,39 @@ const SwitchTeamButton = () => { dispatch(fetchTeams()); }, [dispatch]); - const isActiveTeam = (teamId: string): boolean => { - if (!teamId || !session?.team_id) return false; - return teamId === session.team_id; - }; + const isActiveTeam = useCallback( + (teamId: string): boolean => { + if (!teamId || !session?.team_id) return false; + return teamId === session.team_id; + }, + [session?.team_id] + ); - const handleVerifyAuth = async () => { + const handleVerifyAuth = useCallback(async () => { const result = await dispatch(verifyAuthentication()).unwrap(); if (result.authenticated) { dispatch(setUser(result.user)); authService.setCurrentSession(result.user); } - }; + }, [dispatch, authService]); - const handleTeamSelect = async (id: string) => { - if (!id) return; + const handleTeamSelect = useCallback( + async (id: string) => { + if (!id) return; - await dispatch(setActiveTeam(id)); - await handleVerifyAuth(); - window.location.reload(); - }; + await dispatch(setActiveTeam(id)); + await handleVerifyAuth(); + window.location.reload(); + }, + [dispatch, handleVerifyAuth] + ); - const renderTeamCard = (team: any, index: number) => ( + const renderTeamCard = useCallback( + (team: any, index: number) => ( handleTeamSelect(team.id)} - bordered={false} + variant='borderless' style={{ width: 230 }} > @@ -92,14 +86,19 @@ const SwitchTeamButton = () => { {index < teamsList.length - 1 && } + ), + [handleTeamSelect, isActiveTeam, teamsList.length] ); - const dropdownItems = - teamsList?.map((team, index) => ({ - key: team.id || '', - label: renderTeamCard(team, index), - type: 'item' as const, - })) || []; + const dropdownItems = useMemo( + () => + teamsList?.map((team, index) => ({ + key: team.id || '', + label: renderTeamCard(team, index), + type: 'item' as const, + })) || [], + [teamsList, renderTeamCard] + ); return ( { ); -}; +}); + +SwitchTeamButton.displayName = 'SwitchTeamButton'; export default SwitchTeamButton; diff --git a/worklenz-frontend/src/features/navbar/timers/timer-button.tsx b/worklenz-frontend/src/components/navbar/timers/TimerButton.tsx similarity index 98% rename from worklenz-frontend/src/features/navbar/timers/timer-button.tsx rename to worklenz-frontend/src/components/navbar/timers/TimerButton.tsx index 2735c34a..b3e467b7 100644 --- a/worklenz-frontend/src/features/navbar/timers/timer-button.tsx +++ b/worklenz-frontend/src/components/navbar/timers/TimerButton.tsx @@ -1,6 +1,16 @@ import { ClockCircleOutlined, StopOutlined } from '@/shared/antd-imports'; -import { Badge, Button, Dropdown, List, Tooltip, Typography, Space, Divider, theme } from '@/shared/antd-imports'; -import React, { useEffect, useState, useCallback } from 'react'; +import { + Badge, + Button, + Dropdown, + List, + Tooltip, + Typography, + Space, + Divider, + theme, +} from '@/shared/antd-imports'; +import { useEffect, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { taskTimeLogsApiService, IRunningTimer } from '@/api/tasks/task-time-logs.api.service'; import { useAppDispatch } from '@/hooks/useAppDispatch'; diff --git a/worklenz-frontend/src/features/navbar/upgradePlan/UpgradePlanButton.tsx b/worklenz-frontend/src/components/navbar/upgradePlan/UpgradePlanButton.tsx similarity index 77% rename from worklenz-frontend/src/features/navbar/upgradePlan/UpgradePlanButton.tsx rename to worklenz-frontend/src/components/navbar/upgradePlan/UpgradePlanButton.tsx index 16c1332c..eb583d02 100644 --- a/worklenz-frontend/src/features/navbar/upgradePlan/UpgradePlanButton.tsx +++ b/worklenz-frontend/src/components/navbar/upgradePlan/UpgradePlanButton.tsx @@ -1,11 +1,11 @@ import { Button, Tooltip } from '@/shared/antd-imports'; -import React from 'react'; +import React, { memo, useCallback } 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 = () => { +const UpgradePlanButton = memo(() => { // localization const { t } = useTranslation('navbar'); const navigate = useNavigate(); @@ -22,12 +22,14 @@ const UpgradePlanButton = () => { }} size="small" type="text" - onClick={() => navigate('/worklenz/admin-center/billing')} + onClick={useCallback(() => navigate('/worklenz/admin-center/billing'), [navigate])} > {t('upgradePlan')} ); -}; +}); + +UpgradePlanButton.displayName = 'UpgradePlanButton'; export default UpgradePlanButton; diff --git a/worklenz-frontend/src/features/navbar/user-profile/profile-button.css b/worklenz-frontend/src/components/navbar/user-profile/ProfileButton.css similarity index 100% rename from worklenz-frontend/src/features/navbar/user-profile/profile-button.css rename to worklenz-frontend/src/components/navbar/user-profile/ProfileButton.css diff --git a/worklenz-frontend/src/features/navbar/user-profile/profile-button.tsx b/worklenz-frontend/src/components/navbar/user-profile/ProfileButton.tsx similarity index 86% rename from worklenz-frontend/src/features/navbar/user-profile/profile-button.tsx rename to worklenz-frontend/src/components/navbar/user-profile/ProfileButton.tsx index 3908d385..b8234790 100644 --- a/worklenz-frontend/src/features/navbar/user-profile/profile-button.tsx +++ b/worklenz-frontend/src/components/navbar/user-profile/ProfileButton.tsx @@ -9,17 +9,17 @@ import { RootState } from '@/app/store'; import { getRole } from '@/utils/session-helper'; -import './profile-dropdown.css'; -import './profile-button.css'; +import './ProfileDropdown.css'; +import './ProfileButton.css'; import SingleAvatar from '@/components/common/single-avatar/single-avatar'; import { useAuthService } from '@/hooks/useAuth'; -import { useEffect, useState } from 'react'; +import { memo, useMemo } from 'react'; interface ProfileButtonProps { isOwnerOrAdmin: boolean; } -const ProfileButton = ({ isOwnerOrAdmin }: ProfileButtonProps) => { +const ProfileButton = memo(({ isOwnerOrAdmin }: ProfileButtonProps) => { const { t } = useTranslation('navbar'); const authService = useAuthService(); const currentSession = useAppSelector((state: RootState) => state.userReducer); @@ -27,11 +27,15 @@ const ProfileButton = ({ isOwnerOrAdmin }: ProfileButtonProps) => { const role = getRole(); const themeMode = useAppSelector((state: RootState) => state.themeReducer.mode); - const getLinkStyle = () => ({ - color: themeMode === 'dark' ? '#ffffffd9' : '#181818', - }); + const getLinkStyle = useMemo( + () => ({ + color: themeMode === 'dark' ? '#ffffffd9' : '#181818', + }), + [themeMode] + ); - const profile: MenuProps['items'] = [ + const profile: MenuProps['items'] = useMemo( + () => [ { key: '1', label: ( @@ -81,20 +85,22 @@ const ProfileButton = ({ isOwnerOrAdmin }: ProfileButtonProps) => { style={{ width: 230 }} > {isOwnerOrAdmin && ( - + {t('adminCenter')} )} - + {t('settings')} - + {t('logOut')} ), }, - ]; + ], + [currentSession, role, themeMode, getLinkStyle, isOwnerOrAdmin, t] + ); return ( { ); -}; +}); + +ProfileButton.displayName = 'ProfileButton'; export default ProfileButton; diff --git a/worklenz-frontend/src/features/navbar/user-profile/profile-dropdown.css b/worklenz-frontend/src/components/navbar/user-profile/ProfileDropdown.css similarity index 100% rename from worklenz-frontend/src/features/navbar/user-profile/profile-dropdown.css rename to worklenz-frontend/src/components/navbar/user-profile/ProfileDropdown.css diff --git a/worklenz-frontend/src/features/navbar/mobileMenu/MobileMenuButton.tsx b/worklenz-frontend/src/features/navbar/mobileMenu/MobileMenuButton.tsx deleted file mode 100644 index 4b6c382d..00000000 --- a/worklenz-frontend/src/features/navbar/mobileMenu/MobileMenuButton.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { - ClockCircleOutlined, - HomeOutlined, - MenuOutlined, - ProjectOutlined, - QuestionCircleOutlined, - ReadOutlined, -} from '@/shared/antd-imports'; -import { Button, Card, Dropdown, Flex, MenuProps, Space, Typography } from '@/shared/antd-imports'; -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: ( - - {navLinks.map((navEl, index) => ( - - - - {navEl.icon} - {t(navEl.name)} - - - - ))} - - - - - - - - ), - }, - ]; - - return ( - -