From db27ec5341d854f0c396270fe92319dd8922c9ca Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Mon, 27 Jul 2026 15:52:38 +0300 Subject: [PATCH 1/9] feat: redesign profile page and reset password flow --- .../src/assets/icons/checkmark-icon.svg | 5 +- .../frontend/src/assets/icons/logout.svg | 4 + .../components/profile-bottom-tray.tsx | 53 ++++ .../profile/components/profile-data.tsx | 151 +++++++++--- .../worker/profile/views/profile.page.tsx | 230 ++++++++++++++++-- .../reset-password-success.page.tsx | 94 ++++--- .../reset-password/reset-password.page.tsx | 154 ++++++++---- .../send-reset-link-success.page.tsx | 183 ++++++++------ .../send-reset-link/send-reset-link.page.tsx | 146 +++++++---- .../layout/protected/desktop-aside-bar.tsx | 109 +++++++++ .../layout/protected/drawer-navigation.tsx | 24 +- .../components/layout/protected/layout.tsx | 118 +++++---- .../components/layout/protected/navbar.tsx | 98 ++++---- .../human-app/frontend/src/router/router.tsx | 8 +- .../src/shared/components/ui/icons.tsx | 2 + .../human-app/frontend/src/shared/consts.ts | 1 + .../frontend/src/shared/i18n/en.json | 5 +- 17 files changed, 1023 insertions(+), 362 deletions(-) create mode 100644 packages/apps/human-app/frontend/src/assets/icons/logout.svg create mode 100644 packages/apps/human-app/frontend/src/modules/worker/profile/components/profile-bottom-tray.tsx create mode 100644 packages/apps/human-app/frontend/src/router/components/layout/protected/desktop-aside-bar.tsx diff --git a/packages/apps/human-app/frontend/src/assets/icons/checkmark-icon.svg b/packages/apps/human-app/frontend/src/assets/icons/checkmark-icon.svg index 645514e815..23af691b61 100644 --- a/packages/apps/human-app/frontend/src/assets/icons/checkmark-icon.svg +++ b/packages/apps/human-app/frontend/src/assets/icons/checkmark-icon.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/packages/apps/human-app/frontend/src/assets/icons/logout.svg b/packages/apps/human-app/frontend/src/assets/icons/logout.svg new file mode 100644 index 0000000000..238986cb0f --- /dev/null +++ b/packages/apps/human-app/frontend/src/assets/icons/logout.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/apps/human-app/frontend/src/modules/worker/profile/components/profile-bottom-tray.tsx b/packages/apps/human-app/frontend/src/modules/worker/profile/components/profile-bottom-tray.tsx new file mode 100644 index 0000000000..3ddf16fa59 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/profile/components/profile-bottom-tray.tsx @@ -0,0 +1,53 @@ +import { useColorMode } from '@/shared/contexts/color-mode'; +import { Stack } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { ProfileData } from './profile-data'; +import { Button } from '@/shared/components/ui/button'; +import { HelpIcon } from '@/shared/components/ui/icons'; +import { MOBILE_BOTTOM_TRAY_HEIGHT } from '@/shared/consts'; + +export function ProfileBottomTray() { + const { colorPalette } = useColorMode(); + const { t } = useTranslation(); + + return ( + theme.zIndex.appBar, + }} + > + + } + disableRipple + sx={{ height: { xs: 20, md: 32 }, p: 0, bgcolor: 'transparent' }} + onClick={() => { + // @ts-expect-error -- ... + if ($zoho?.salesiq?.chat?.start) { + // @ts-expect-error -- ... + $zoho.salesiq.chat.start(); + } + }} + > + {t('components.DrawerNavigation.help')} + + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/profile/components/profile-data.tsx b/packages/apps/human-app/frontend/src/modules/worker/profile/components/profile-data.tsx index 79b9d36d82..94cd722c4b 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/profile/components/profile-data.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/profile/components/profile-data.tsx @@ -1,49 +1,128 @@ -import { Stack, Typography } from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; +import { Box, Link, Stack, Typography } from '@mui/material'; +import { Link as RouterLink, useLocation } from 'react-router-dom'; + import { useAuthenticatedUser } from '@/modules/auth/hooks/use-authenticated-user'; -import { Button } from '@/shared/components/ui/button'; -import { routerPaths } from '@/router/router-paths'; import { useColorMode } from '@/shared/contexts/color-mode'; -import { onlyDarkModeColor } from '@/shared/styles/dark-color-palette'; +import { shortenEscrowAddress } from '@/shared/helpers/evm'; +import { routerPaths } from '@/router/router-paths'; + +function Wrapper({ + isCompact, + isProfilePage, + children, +}: { + isCompact: boolean; + isProfilePage: boolean; + children: React.ReactNode; +}) { + if (isCompact && !isProfilePage) { + return ( + + {children} + + ); + } + + return <>{children}; +} -export function ProfileData() { - const { colorPalette, isDarkMode } = useColorMode(); +export function ProfileData({ + variant = 'compact', +}: { + variant?: 'compact' | 'expanded'; +}) { const { user } = useAuthenticatedUser(); - const { t } = useTranslation(); + const { colorPalette } = useColorMode(); + const location = useLocation(); + + const isProfilePage = location.pathname === routerPaths.worker.profile; + const isCompact = variant === 'compact'; + return ( - - - {t('worker.profile.email')} - + + - {user.email} - - - - - {t('worker.profile.password')} - - + + {user.email} + + + {shortenEscrowAddress( + user.wallet_address ?? '', + isCompact ? 9 : 6, + isCompact ? 8 : 5 + )} + + - + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx index 8e3b0fcb21..1099abab4d 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx @@ -1,21 +1,67 @@ -import { Paper, Stack } from '@mui/material'; -import { useEffect } from 'react'; +import { Box, IconButton, Stack, Tooltip, Typography } from '@mui/material'; +import { useEffect, useRef, useState, type MouseEvent } from 'react'; import { t } from 'i18next'; -import { useIsMobile } from '@/shared/hooks/use-is-mobile'; +import { Link, useNavigate } from 'react-router-dom'; + import { useAuthenticatedUser } from '@/modules/auth/hooks/use-authenticated-user'; import { useWalletConnect } from '@/shared/contexts/wallet-connect'; import { TopNotificationType, useNotification, } from '@/shared/hooks/use-notification'; -import { ProfileData } from '../components'; +import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; +import { ProfileData } from '../components/profile-data'; +import { + CheckmarkIcon, + CopyIcon, + LogoutIcon, +} from '@/shared/components/ui/icons'; +import { shortenEscrowAddress } from '@/shared/helpers/evm'; +import { Button } from '@/shared/components/ui/button'; +import { browserAuthProvider } from '@/shared/contexts/browser-auth-provider'; +import { routerPaths } from '@/router/router-paths'; +import { BackButton } from '@/shared/components/ui/page-card/back-button'; +import { useIsMobile } from '@/shared/hooks/use-is-mobile'; export function WorkerProfilePage() { + const [isCopied, setIsCopied] = useState(false); + const timeoutRef = useRef(null); + const { user } = useAuthenticatedUser(); - const isMobile = useIsMobile(); + const { colorPalette } = useColorMode(); const { isConnected, initializing, web3ProviderMutation } = useWalletConnect(); const { showNotification } = useNotification(); + const isMobile = useIsMobile(); + const navigate = useNavigate(); + + const handleBack = () => { + navigate(routerPaths.worker.jobsDiscovery); + }; + + const handleCopyClick = (e: MouseEvent) => { + if (isCopied) return; + + e.stopPropagation(); + navigator.clipboard.writeText(user.wallet_address ?? ''); + setIsCopied(true); + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(() => { + setIsCopied(false); + }, 1500); + }; + + const handleSignOut = () => { + browserAuthProvider.signOut({ + callback: () => { + window.location.reload(); + }, + }); + }; useEffect(() => { if (initializing) return; @@ -43,21 +89,165 @@ export function WorkerProfilePage() { ]); return ( - - - + + + {isMobile && } + + {t('worker.profile.profileHeader')} + + + + + + + + + {t('worker.profile.accountVerified')} + + + + + + + {t('worker.profile.email')} + + + {user.email} + + + + + {t('worker.profile.walletAddress')} + + + {shortenEscrowAddress(user.wallet_address ?? '', 9, 8)} + + + + + + + + + + } + onClick={handleSignOut} + > + {t('worker.profile.logout')} + + + + - + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password-success.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password-success.page.tsx index 4920cfc5dc..696e0230c0 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password-success.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password-success.page.tsx @@ -1,12 +1,11 @@ -import Grid from '@mui/material/Grid'; +import { useEffect } from 'react'; +import { Paper, Stack, Typography } from '@mui/material'; import { t } from 'i18next'; import { Link } from 'react-router-dom'; -import Typography from '@mui/material/Typography'; import CheckCircle from '@mui/icons-material/CheckCircle'; -import { useEffect } from 'react'; + import { Button } from '@/shared/components/ui/button'; import { routerPaths } from '@/router/router-paths'; -import { PageCard } from '@/shared/components/ui/page-card'; import { useAuth } from '@/modules/auth/hooks/use-auth'; import { useColorMode } from '@/shared/contexts/color-mode'; @@ -16,36 +15,71 @@ export function ResetPasswordWorkerSuccessPage() { useEffect(() => { signOut(); - }, [signOut]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( - - {t('worker.resetPasswordSuccess.title')} + + + - - } - > - - - {t('worker.resetPasswordSuccess.description')} - - - - + + {t('worker.resetPasswordSuccess.title')} + + + + + {t('worker.resetPasswordSuccess.description')} + + + + + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx index 2fe4304633..6ee0be01e5 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx @@ -1,15 +1,14 @@ +import { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import Grid from '@mui/material/Grid'; -import Typography from '@mui/material/Typography'; +import { Box, Paper, Stack, Typography } from '@mui/material'; import { t } from 'i18next'; import omit from 'lodash/omit'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import queryString from 'query-string'; + import { Button } from '@/shared/components/ui/button'; import { Password } from '@/shared/components/data-entry/password'; -import { PageCard } from '@/shared/components/ui/page-card'; -import { Alert } from '@/shared/components/ui/alert'; import { getErrorMessageForError } from '@/shared/errors'; import { routerPaths } from '@/router/router-paths'; import { HCaptchaForm } from '@/shared/components/hcaptcha'; @@ -17,10 +16,19 @@ import { useResetMutationErrors } from '@/shared/hooks/use-reset-mutation-errors import { useResetPasswordMutation } from './hooks'; import { resetPasswordDtoSchema } from './schemas'; import { type ResetPasswordDto } from './types'; +import { BackButton } from '@/shared/components/ui/page-card/back-button'; +import { useColorMode } from '@/shared/contexts/color-mode'; +import { + TopNotificationType, + useNotification, +} from '@/shared/hooks/use-notification'; export function ResetPasswordWorkerPage() { const location = useLocation(); + const navigate = useNavigate(); const { token } = queryString.parse(location.search); + const { colorPalette } = useColorMode(); + const { showNotification } = useNotification(); const methods = useForm({ defaultValues: { @@ -47,51 +55,103 @@ export function ResetPasswordWorkerPage() { ); }; + const handleBackButton = () => { + navigate(routerPaths.worker.profile); + }; + + useEffect(() => { + if (isResetPasswordWorkerError) { + showNotification({ + type: TopNotificationType.ERROR, + message: getErrorMessageForError(resetPasswordWorkerError), + }); + } + }, [isResetPasswordWorkerError, resetPasswordWorkerError, showNotification]); + return ( - - {getErrorMessageForError(resetPasswordWorkerError)} - - ) : undefined - } - backNavigation={routerPaths.worker.profile} - title={t('worker.resetPassword.title')} + - -
{ - void methods.handleSubmit(handleWorkerResetPassword)(event); + + - - - {t('worker.resetPassword.description')} - - - - - - -
-
-
+ + + {t('worker.sendResetLinkForm.title')} + + + +
{ + void methods.handleSubmit(handleWorkerResetPassword)(event); + }} + > + + + {t('worker.resetPassword.description')} + + + + + + +
+
+ + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx index 4ab0d39ef7..7d4679e613 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx @@ -1,28 +1,31 @@ -import { Grid, Typography } from '@mui/material'; +import { useEffect } from 'react'; +import { Grid, Paper, Stack, Typography } from '@mui/material'; import { Trans, useTranslation } from 'react-i18next'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { FormProvider, useForm } from 'react-hook-form'; -import { PageCard } from '@/shared/components/ui/page-card'; import { Button } from '@/shared/components/ui/button'; import { useLocationState } from '@/modules/worker/hooks/use-location-state'; import { env } from '@/shared/env'; -import { Alert } from '@/shared/components/ui/alert'; import { getErrorMessageForError } from '@/shared/errors'; import { HCaptchaForm } from '@/shared/components/hcaptcha'; import { MailTo } from '@/shared/components/ui/mail-to'; import { useResetMutationErrors } from '@/shared/hooks/use-reset-mutation-errors'; import { useColorMode } from '@/shared/contexts/color-mode'; -import { onlyDarkModeColor } from '@/shared/styles/dark-color-palette'; import { useSendResetLinkMutation } from './hooks'; import { sendResetLinkHcaptchaDtoSchema, type SendResetLinkHcaptcha, } from './schemas'; +import { + TopNotificationType, + useNotification, +} from '@/shared/hooks/use-notification'; export function SendResetLinkWorkerSuccessPage() { - const { colorPalette, isDarkMode } = useColorMode(); + const { colorPalette } = useColorMode(); const { t } = useTranslation(); + const { showNotification } = useNotification(); const { field: email } = useLocationState({ keyInStorage: 'email', schema: z.email(), @@ -43,79 +46,111 @@ export function SendResetLinkWorkerSuccessPage() { useResetMutationErrors(methods.watch, reset); + useEffect(() => { + if (isError) { + showNotification({ + type: TopNotificationType.ERROR, + message: getErrorMessageForError(error), + }); + } + }, [isError, error, showNotification]); + return ( - - {getErrorMessageForError(error)} - - ) : undefined - } - title={t('worker.sendResetLinkForm.title')} + - -
{ - void methods.handleSubmit(handleWorkerSendResetLink)(event); + + - - - , - }} - i18nKey="worker.sendResetLinkSuccess.paragraph1" - values={{ email }} - /> - - - {t('worker.sendResetLinkSuccess.paragraph2')} - - - , - }} - i18nKey="worker.sendResetLinkSuccess.paragraph3" - values={{ email }} - /> - - - - - - , - 2: , + {t('worker.sendResetLinkForm.title')} + + +
{ + void methods.handleSubmit(handleWorkerSendResetLink)(event); + }} + > + + + , + }} + i18nKey="worker.sendResetLinkSuccess.paragraph1" + values={{ email }} + /> + + -
- -
-
-
+ > + {t('worker.sendResetLinkSuccess.paragraph2')} + + + , + }} + i18nKey="worker.sendResetLinkSuccess.paragraph3" + values={{ email }} + /> + + + + + , + 2: , + }} + i18nKey="worker.sendResetLinkSuccess.paragraph4" + /> + + + + + + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link.page.tsx index be9dab12e1..70ea6c86d5 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link.page.tsx @@ -1,11 +1,10 @@ +import { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import { Grid, Typography } from '@mui/material'; +import { Box, Grid, Paper, Stack, Typography } from '@mui/material'; import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslation } from 'react-i18next'; -import { PageCard } from '@/shared/components/ui/page-card'; import { Input } from '@/shared/components/data-entry/input'; import { Button } from '@/shared/components/ui/button'; -import { Alert } from '@/shared/components/ui/alert'; import { getErrorMessageForError } from '@/shared/errors'; import { useAuth } from '@/modules/auth/hooks/use-auth'; import { HCaptchaForm } from '@/shared/components/hcaptcha'; @@ -13,10 +12,20 @@ import { routerPaths } from '@/router/router-paths'; import { useResetMutationErrors } from '@/shared/hooks/use-reset-mutation-errors'; import { useSendResetLinkMutation } from './hooks'; import { type SendResetLinkDto, sendResetLinkDtoSchema } from './schemas'; +import { useColorMode } from '@/shared/contexts/color-mode'; +import { BackButton } from '@/shared/components/ui/page-card/back-button'; +import { useNavigate } from 'react-router-dom'; +import { + TopNotificationType, + useNotification, +} from '@/shared/hooks/use-notification'; export function SendResetLinkWorkerPage() { const { t } = useTranslation(); const { user } = useAuth(); + const { colorPalette } = useColorMode(); + const { showNotification } = useNotification(); + const navigate = useNavigate(); const methods = useForm({ defaultValues: { @@ -36,52 +45,105 @@ export function SendResetLinkWorkerPage() { useResetMutationErrors(methods.watch, sendResetLinkWorkerMutateReset); + useEffect(() => { + if (isSendResetLinkWorkerError) { + const errorMessage = getErrorMessageForError(sendResetLinkWorkerError); + showNotification({ + type: TopNotificationType.ERROR, + message: errorMessage, + }); + } + }, [isSendResetLinkWorkerError, sendResetLinkWorkerError, showNotification]); + function handleWorkerSendResetLink(data: SendResetLinkDto) { sendResetLinkWorkerMutate(data); } + const handleBackButton = () => { + navigate(routerPaths.worker.profile); + }; + return ( - - {getErrorMessageForError(sendResetLinkWorkerError)} - - ) : undefined - } - backNavigation={routerPaths.worker.profile} - title={t('worker.sendResetLinkForm.title')} + - -
{ - void methods.handleSubmit(handleWorkerSendResetLink)(event); + + - - - {t('worker.sendResetLinkForm.description')} - - - - - -
-
-
+ + + {t('worker.sendResetLinkForm.title')} + + + +
{ + void methods.handleSubmit(handleWorkerSendResetLink)(event); + }} + > + + + {t('worker.sendResetLinkForm.description')} + + + + + +
+
+ + ); } diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/desktop-aside-bar.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/desktop-aside-bar.tsx new file mode 100644 index 0000000000..051f917544 --- /dev/null +++ b/packages/apps/human-app/frontend/src/router/components/layout/protected/desktop-aside-bar.tsx @@ -0,0 +1,109 @@ +import { ProfileData } from '@/modules/worker/profile/components/profile-data'; +import { Button } from '@/shared/components/ui/button'; +import { ColorModeSwitch } from '@/shared/components/ui/dark-mode-switch'; +import { HelpIcon, HumanLogoNavbarIcon } from '@/shared/components/ui/icons'; +import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; +import { useHandleMainNavIconClick } from '@/shared/hooks/use-handle-main-nav-icon-click'; +import { Box, IconButton, Stack } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +export function DesktopAsideBar() { + const { colorPalette } = useColorMode(); + const { t } = useTranslation(); + const handleMainNavIconClick = useHandleMainNavIconClick(); + + return ( + + + + + + + + + + + + + + + } + disableRipple + sx={{ height: 32, p: 0, bgcolor: 'transparent' }} + onClick={() => { + // @ts-expect-error -- ... + if ($zoho?.salesiq?.chat?.start) { + // @ts-expect-error -- ... + $zoho.salesiq.chat.start(); + } + }} + > + {t('components.DrawerNavigation.help')} + + + + + + + + + + ); +} diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/drawer-navigation.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/drawer-navigation.tsx index c199cc2d5a..5dab8d34cc 100644 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/drawer-navigation.tsx +++ b/packages/apps/human-app/frontend/src/router/components/layout/protected/drawer-navigation.tsx @@ -11,8 +11,9 @@ import { useIsMobile } from '@/shared/hooks/use-is-mobile'; import { useHandleMainNavIconClick } from '@/shared/hooks/use-handle-main-nav-icon-click'; import { TopMenuItemsList } from './top-menu-items-list'; import { BottomMenuItemsList } from './bottom-menu-items-list'; +import { useColorMode } from '@/shared/contexts/color-mode'; -const drawerWidth = 240; +const drawerWidth = 330; export interface DrawerItem { label: string; @@ -42,6 +43,7 @@ export function DrawerNavigation({ const navigate = useNavigate(); const isMobile = useIsMobile(); const handleMainNavIconClick = useHandleMainNavIconClick(); + const { colorPalette } = useColorMode(); const handleItemClick = ({ disabled, href, link, onClick }: DrawerItem) => { if (disabled) return; @@ -73,14 +75,24 @@ export function DrawerNavigation({ diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx index 2f3de9ad7c..5040531590 100644 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx +++ b/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx @@ -1,22 +1,24 @@ import { Box, Stack, styled } from '@mui/material'; import type { Dispatch, ReactElement, SetStateAction } from 'react'; import { useEffect, useRef, useState } from 'react'; -import { Outlet } from 'react-router-dom'; +import { Outlet, useLocation } from 'react-router-dom'; import { useIsMobile } from '@/shared/hooks/use-is-mobile'; -import { breakpoints } from '@/shared/styles/breakpoints'; import { useIsHCaptchaLabelingPage } from '@/shared/hooks/use-is-hcaptcha-labeling-page'; import { GovernanceBanner } from '@/modules/governance-banner/components/governance-banner'; import { Footer } from '../../footer'; import { Navbar } from './navbar'; -import { type PageHeaderProps, PageHeader } from './page-header'; +//import { type PageHeaderProps, PageHeader } from './page-header'; import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; +import { DesktopAsideBar } from './desktop-aside-bar'; +import { ProfileBottomTray } from '@/modules/worker/profile/components/profile-bottom-tray'; +import { MOBILE_BOTTOM_TRAY_HEIGHT } from '@/shared/consts'; +import { routerPaths } from '@/router/router-paths'; const Main = styled('main', { - shouldForwardProp: (prop) => prop !== 'open' && prop !== 'isMobile', + shouldForwardProp: (prop) => prop !== 'open', })<{ open?: boolean; - isMobile?: boolean; -}>(({ theme, open, isMobile }) => ({ +}>(({ theme, open }) => ({ width: '100%', display: 'flex', flex: '1', @@ -29,36 +31,39 @@ const Main = styled('main', { easing: theme.transitions.easing.easeOut, duration: theme.transitions.duration.enteringScreen, }), - paddingLeft: isMobile ? 0 : `140px`, }), })); export function ProtectedLayout({ - pageHeaderProps, - renderDrawer, + // pageHeaderProps, + // renderDrawer, renderHCaptchaStatisticsDrawer, renderGovernanceBanner, -}: Readonly<{ - pageHeaderProps: PageHeaderProps; +}: { + //pageHeaderProps: PageHeaderProps; renderDrawer: ( open: boolean, setDrawerOpen: Dispatch> ) => ReactElement; renderHCaptchaStatisticsDrawer?: (isOpen: boolean) => ReactElement; renderGovernanceBanner?: boolean; -}>) { +}) { const layoutElementRef = useRef(null); const isHCaptchaLabelingPage = useIsHCaptchaLabelingPage(); const isMobile = useIsMobile(); const [drawerOpen, setDrawerOpen] = useState(!isMobile); const [hcaptchaDrawerOpen, setHcaptchaDrawerOpen] = useState(false); const { colorPalette } = useColorMode(); + const location = useLocation(); const toggleUserStatsDrawer = isHCaptchaLabelingPage ? () => { setHcaptchaDrawerOpen((state) => !state); } : undefined; + const isProfilePage = location.pathname === routerPaths.worker.profile; + const isBottomTrayVisible = isMobile && !isProfilePage; + useEffect(() => { if (isMobile) { setHcaptchaDrawerOpen(false); @@ -71,54 +76,67 @@ export function ProtectedLayout({ return ( - - {renderDrawer(drawerOpen, setDrawerOpen)} + {isMobile && ( + + )} + + {!isMobile && } + {/* {renderDrawer(drawerOpen, setDrawerOpen)} */} {isHCaptchaLabelingPage && renderHCaptchaStatisticsDrawer ? renderHCaptchaStatisticsDrawer(hcaptchaDrawerOpen) : null} -
- - {renderGovernanceBanner && } - - - - - -
-
+ +
+ + {renderGovernanceBanner && } + {/* */} + + + + + {isBottomTrayVisible && } +
+
+
); } diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/navbar.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/navbar.tsx index 74e98fdd45..1fde484268 100644 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/navbar.tsx +++ b/packages/apps/human-app/frontend/src/router/components/layout/protected/navbar.tsx @@ -1,13 +1,13 @@ -import { Grid, IconButton, Stack } from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; -import CloseIcon from '@mui/icons-material/Close'; +import { Stack } from '@mui/material'; +// import MenuIcon from '@mui/icons-material/Menu'; +// import CloseIcon from '@mui/icons-material/Close'; import { t } from 'i18next'; import { HumanLogoNavbarIcon } from '@/shared/components/ui/icons'; -import { useIsMobile } from '@/shared/hooks/use-is-mobile'; import { Button } from '@/shared/components/ui/button'; -import { useIsHCaptchaLabelingPage } from '@/shared/hooks/use-is-hcaptcha-labeling-page'; +//import { useIsHCaptchaLabelingPage } from '@/shared/hooks/use-is-hcaptcha-labeling-page'; import { useColorMode } from '@/shared/contexts/color-mode'; import { useHandleMainNavIconClick } from '@/shared/hooks/use-handle-main-nav-icon-click'; +import { ColorModeSwitch } from '@/shared/components/ui/dark-mode-switch'; interface NavbarProps { open: boolean; @@ -19,48 +19,47 @@ interface NavbarProps { export function Navbar({ setOpen, open, - userStatsDrawerOpen, - toggleUserStatsDrawer, + // userStatsDrawerOpen, + // toggleUserStatsDrawer, }: NavbarProps) { const handleMainNavIconClick = useHandleMainNavIconClick(); const { colorPalette } = useColorMode(); - const isMobile = useIsMobile(); - const isHCaptchaLabelingPage = useIsHCaptchaLabelingPage(); + //const isHCaptchaLabelingPage = useIsHCaptchaLabelingPage(); - let iconButton = null; - if (open) { - iconButton = ( - { - setOpen(false); - }} - > - - - ); - } else if (userStatsDrawerOpen) { - iconButton = ( - { - if (toggleUserStatsDrawer) { - toggleUserStatsDrawer(); - } - }} - > - - - ); - } else { - iconButton = ( - { - setOpen(true); - }} - > - - - ); - } + // let iconButton = null; + // if (open) { + // iconButton = ( + // { + // setOpen(false); + // }} + // > + // + // + // ); + // } else if (userStatsDrawerOpen) { + // iconButton = ( + // { + // if (toggleUserStatsDrawer) { + // toggleUserStatsDrawer(); + // } + // }} + // > + // + // + // ); + // } else { + // iconButton = ( + // { + // setOpen(true); + // }} + // > + // + // + // ); + // } return ( { - if (isMobile) { - setOpen(false); - } + setOpen(false); handleMainNavIconClick(); }} > - + {/* ) : null} {iconButton} - + */} ); } diff --git a/packages/apps/human-app/frontend/src/router/router.tsx b/packages/apps/human-app/frontend/src/router/router.tsx index b22c8052d8..6efc08fbd3 100644 --- a/packages/apps/human-app/frontend/src/router/router.tsx +++ b/packages/apps/human-app/frontend/src/router/router.tsx @@ -52,13 +52,13 @@ export function Router() { /> ))} - {protectedRoutes.map(({ routerProps, pageHeaderProps }) => { + {protectedRoutes.map(({ routerProps }) => { return ( ( ); })} - {web3ProtectedRoutes.map(({ routerProps, pageHeaderProps }) => ( + {web3ProtectedRoutes.map(({ routerProps }) => ( ( Date: Mon, 27 Jul 2026 17:17:53 +0300 Subject: [PATCH 2/9] chore: remove unused router components and icons --- .../src/assets/icons-dark-mode/hand.svg | 70 --------- .../assets/icons-dark-mode/profile-icon.svg | 26 ---- .../assets/icons-dark-mode/user-outlined.svg | 3 - .../assets/icons-dark-mode/work-header.svg | 30 ---- .../src/assets/icons-dark-mode/work.svg | 12 -- .../frontend/src/assets/icons/hand.svg | 79 ---------- .../src/assets/icons/profile-icon.svg | 32 ---- .../frontend/src/assets/icons/user-filled.svg | 3 - .../src/assets/icons/user-outlined.svg | 3 - .../src/assets/icons/user-secondary.svg | 15 -- .../frontend/src/assets/icons/work-header.svg | 36 ----- .../src/assets/icons/work-secondary.svg | 14 -- .../frontend/src/assets/icons/work.svg | 7 - .../drawer-menu-items-operator.tsx | 25 ---- .../drawer-menu-items-worker.tsx | 30 +--- .../components/drawer-menu-items/index.ts | 1 - .../router/components/layout/helpers/index.ts | 1 - .../layout/helpers/is-drawer-item.ts | 5 - .../protected/bottom-menu-items-list.tsx | 107 -------------- .../layout/protected/drawer-navigation.tsx | 139 ------------------ .../components/layout/protected/index.ts | 1 - .../components/layout/protected/layout.tsx | 15 +- .../layout/protected/page-header.tsx | 70 --------- .../layout/protected/top-menu-items-list.tsx | 103 ------------- .../human-app/frontend/src/router/router.tsx | 43 +----- .../human-app/frontend/src/router/routes.tsx | 46 +----- .../src/shared/components/ui/icons.tsx | 40 ----- 27 files changed, 6 insertions(+), 950 deletions(-) delete mode 100644 packages/apps/human-app/frontend/src/assets/icons-dark-mode/hand.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons-dark-mode/profile-icon.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons-dark-mode/user-outlined.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons-dark-mode/work-header.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons-dark-mode/work.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons/hand.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons/profile-icon.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons/user-filled.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons/user-outlined.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons/user-secondary.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons/work-header.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons/work-secondary.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons/work.svg delete mode 100644 packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-operator.tsx delete mode 100644 packages/apps/human-app/frontend/src/router/components/layout/helpers/index.ts delete mode 100644 packages/apps/human-app/frontend/src/router/components/layout/helpers/is-drawer-item.ts delete mode 100644 packages/apps/human-app/frontend/src/router/components/layout/protected/bottom-menu-items-list.tsx delete mode 100644 packages/apps/human-app/frontend/src/router/components/layout/protected/drawer-navigation.tsx delete mode 100644 packages/apps/human-app/frontend/src/router/components/layout/protected/page-header.tsx delete mode 100644 packages/apps/human-app/frontend/src/router/components/layout/protected/top-menu-items-list.tsx diff --git a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/hand.svg b/packages/apps/human-app/frontend/src/assets/icons-dark-mode/hand.svg deleted file mode 100644 index 03ed5b0ae5..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/hand.svg +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/profile-icon.svg b/packages/apps/human-app/frontend/src/assets/icons-dark-mode/profile-icon.svg deleted file mode 100644 index f6ed123061..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/profile-icon.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/user-outlined.svg b/packages/apps/human-app/frontend/src/assets/icons-dark-mode/user-outlined.svg deleted file mode 100644 index c359fd0e66..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/user-outlined.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/work-header.svg b/packages/apps/human-app/frontend/src/assets/icons-dark-mode/work-header.svg deleted file mode 100644 index e8febf3837..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/work-header.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/work.svg b/packages/apps/human-app/frontend/src/assets/icons-dark-mode/work.svg deleted file mode 100644 index 39ea46187d..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/work.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons/hand.svg b/packages/apps/human-app/frontend/src/assets/icons/hand.svg deleted file mode 100644 index 81169bd335..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons/hand.svg +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons/profile-icon.svg b/packages/apps/human-app/frontend/src/assets/icons/profile-icon.svg deleted file mode 100644 index 811121e23f..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons/profile-icon.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/apps/human-app/frontend/src/assets/icons/user-filled.svg b/packages/apps/human-app/frontend/src/assets/icons/user-filled.svg deleted file mode 100644 index ea37160a77..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons/user-filled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons/user-outlined.svg b/packages/apps/human-app/frontend/src/assets/icons/user-outlined.svg deleted file mode 100644 index 2a59dbb72c..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons/user-outlined.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons/user-secondary.svg b/packages/apps/human-app/frontend/src/assets/icons/user-secondary.svg deleted file mode 100644 index 303b047f37..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons/user-secondary.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons/work-header.svg b/packages/apps/human-app/frontend/src/assets/icons/work-header.svg deleted file mode 100644 index 63e2e5cb89..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons/work-header.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons/work-secondary.svg b/packages/apps/human-app/frontend/src/assets/icons/work-secondary.svg deleted file mode 100644 index 065a03d862..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons/work-secondary.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons/work.svg b/packages/apps/human-app/frontend/src/assets/icons/work.svg deleted file mode 100644 index cd6b13c4de..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons/work.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-operator.tsx b/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-operator.tsx deleted file mode 100644 index 4f15723864..0000000000 --- a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-operator.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { t } from 'i18next'; -import { HelpIcon, UserOutlinedIcon } from '@/shared/components/ui/icons'; -import { routerPaths } from '@/router/router-paths'; -import { ColorModeSwitch } from '@/shared/components/ui/dark-mode-switch'; -import { type MenuItem } from '../layout/protected'; - -export const operatorDrawerBottomMenuItems: MenuItem[] = [ - { - label: t('components.DrawerNavigation.profile'), - link: routerPaths.operator.profile, - icon: , - }, - { - label: t('components.DrawerNavigation.help'), - icon: , - onClick: () => { - // @ts-expect-error -- ... - if ($zoho?.salesiq?.chat?.start) { - // @ts-expect-error -- ... - $zoho.salesiq.chat.start(); - } - }, - }, - , -]; diff --git a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx b/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx index 5278ddb584..1c28472e9b 100644 --- a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx +++ b/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx @@ -1,42 +1,14 @@ import { t } from 'i18next'; -import { - HelpIcon, - UserOutlinedIcon, - WorkIcon, -} from '@/shared/components/ui/icons'; -import { ColorModeSwitch } from '@/shared/components/ui/dark-mode-switch'; import type { UserData } from '@/modules/auth/context/auth-context'; import { routerPaths } from '@/router/router-paths'; import { KycStatus } from '@/modules/worker/profile/types'; -import { type MenuItem } from '../layout/protected'; -export const workerDrawerTopMenuItems = (user: UserData | null): MenuItem[] => { +export const workerDrawerTopMenuItems = (user: UserData | null) => { return [ { label: t('components.DrawerNavigation.jobs'), - icon: , link: routerPaths.worker.jobsDiscovery, disabled: !user?.wallet_address || user.kyc_status !== KycStatus.APPROVED, }, ]; }; - -export const workerDrawerBottomMenuItems: MenuItem[] = [ - { - label: t('components.DrawerNavigation.profile'), - link: routerPaths.worker.profile, - icon: , - }, - { - label: t('components.DrawerNavigation.help'), - icon: , - onClick: () => { - // @ts-expect-error -- ... - if ($zoho?.salesiq?.chat?.start) { - // @ts-expect-error -- ... - $zoho.salesiq.chat.start(); - } - }, - }, - , -]; diff --git a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/index.ts b/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/index.ts index 26df1d0e31..8dd1acfaae 100644 --- a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/index.ts +++ b/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/index.ts @@ -1,2 +1 @@ -export * from './drawer-menu-items-operator'; export * from './drawer-menu-items-worker'; diff --git a/packages/apps/human-app/frontend/src/router/components/layout/helpers/index.ts b/packages/apps/human-app/frontend/src/router/components/layout/helpers/index.ts deleted file mode 100644 index c7582d2d26..0000000000 --- a/packages/apps/human-app/frontend/src/router/components/layout/helpers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './is-drawer-item'; diff --git a/packages/apps/human-app/frontend/src/router/components/layout/helpers/is-drawer-item.ts b/packages/apps/human-app/frontend/src/router/components/layout/helpers/is-drawer-item.ts deleted file mode 100644 index fda999eea3..0000000000 --- a/packages/apps/human-app/frontend/src/router/components/layout/helpers/is-drawer-item.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { type MenuItem, type DrawerItem } from '../protected'; - -export function isDrawerItem(item: MenuItem): item is DrawerItem { - return 'label' in item; -} diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/bottom-menu-items-list.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/bottom-menu-items-list.tsx deleted file mode 100644 index 34d23acb0c..0000000000 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/bottom-menu-items-list.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { - List, - ListItem, - Stack, - ListItemButton, - ListItemText, - Typography, -} from '@mui/material'; -import { useLocation } from 'react-router-dom'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { useIsMobile } from '@/shared/hooks'; -import { colorPalette } from '@/shared/styles/color-palette'; -import { onlyDarkModeColor } from '@/shared/styles/dark-color-palette'; -import { isDrawerItem } from '../helpers'; -import { type MenuItem, type DrawerItem } from './drawer-navigation'; - -interface MenuItemListProps { - handleItemClick: (item: DrawerItem) => void; - items?: MenuItem[]; -} -export function BottomMenuItemsList({ - handleItemClick, - items = [], -}: Readonly) { - const { isDarkMode } = useColorMode(); - const isMobile = useIsMobile(); - const location = useLocation(); - - return ( - - {items.map((item) => { - if (!isDrawerItem(item)) { - return ( - - - {item} - - - ); - } - - const { label, link, icon } = item; - const isActive = location.pathname === link; - - return ( - - { - handleItemClick(item); - }} - selected={isActive} - sx={{ - borderRadius: '4px', - p: 0, - '&.Mui-selected': { - backgroundColor: isDarkMode - ? onlyDarkModeColor.listItemColor - : colorPalette.primary.shades, - }, - }} - > - - {icon} - - {label} - - } - sx={{ - textAlign: 'center', - marginLeft: '10px', - }} - /> - - - - ); - })} - - ); -} diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/drawer-navigation.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/drawer-navigation.tsx deleted file mode 100644 index 5dab8d34cc..0000000000 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/drawer-navigation.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import Box from '@mui/material/Box'; -import Drawer from '@mui/material/Drawer'; -import CssBaseline from '@mui/material/CssBaseline'; -import { Stack } from '@mui/material'; -import { useNavigate } from 'react-router-dom'; -import { t } from 'i18next'; -import type { Dispatch, ReactElement, SetStateAction } from 'react'; -import { HumanLogoNavbarIcon } from '@/shared/components/ui/icons'; -import { Button } from '@/shared/components/ui/button'; -import { useIsMobile } from '@/shared/hooks/use-is-mobile'; -import { useHandleMainNavIconClick } from '@/shared/hooks/use-handle-main-nav-icon-click'; -import { TopMenuItemsList } from './top-menu-items-list'; -import { BottomMenuItemsList } from './bottom-menu-items-list'; -import { useColorMode } from '@/shared/contexts/color-mode'; - -const drawerWidth = 330; - -export interface DrawerItem { - label: string; - link?: string; - href?: string; - icon?: ReactElement; - disabled?: boolean; - onClick?: () => void; -} - -export type MenuItem = DrawerItem | ReactElement; -interface DrawerNavigationProps { - open: boolean; - setDrawerOpen: Dispatch>; - topMenuItems?: MenuItem[]; - bottomMenuItems?: MenuItem[]; - signOut: () => void; -} - -export function DrawerNavigation({ - open, - setDrawerOpen, - topMenuItems, - bottomMenuItems, - signOut, -}: DrawerNavigationProps) { - const navigate = useNavigate(); - const isMobile = useIsMobile(); - const handleMainNavIconClick = useHandleMainNavIconClick(); - const { colorPalette } = useColorMode(); - - const handleItemClick = ({ disabled, href, link, onClick }: DrawerItem) => { - if (disabled) return; - - if (isMobile) setDrawerOpen(false); - - if (onClick) { - onClick(); - return; - } - - if (href) { - window.open(href, '_blank', 'noreferrer'); - return; - } - - if (link) { - navigate(link); - } - }; - - return ( - - - - {!isMobile && ( - - - - )} - - - - - - - - ); -} diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/index.ts b/packages/apps/human-app/frontend/src/router/components/layout/protected/index.ts index 8643262e0f..5d15fe1b3c 100644 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/index.ts +++ b/packages/apps/human-app/frontend/src/router/components/layout/protected/index.ts @@ -1,2 +1 @@ export * from './layout'; -export * from './drawer-navigation'; diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx index 5040531590..5e6449aece 100644 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx +++ b/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx @@ -1,13 +1,12 @@ import { Box, Stack, styled } from '@mui/material'; -import type { Dispatch, ReactElement, SetStateAction } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, type ReactElement } from 'react'; import { Outlet, useLocation } from 'react-router-dom'; + import { useIsMobile } from '@/shared/hooks/use-is-mobile'; import { useIsHCaptchaLabelingPage } from '@/shared/hooks/use-is-hcaptcha-labeling-page'; import { GovernanceBanner } from '@/modules/governance-banner/components/governance-banner'; import { Footer } from '../../footer'; import { Navbar } from './navbar'; -//import { type PageHeaderProps, PageHeader } from './page-header'; import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; import { DesktopAsideBar } from './desktop-aside-bar'; import { ProfileBottomTray } from '@/modules/worker/profile/components/profile-bottom-tray'; @@ -35,16 +34,9 @@ const Main = styled('main', { })); export function ProtectedLayout({ - // pageHeaderProps, - // renderDrawer, renderHCaptchaStatisticsDrawer, renderGovernanceBanner, }: { - //pageHeaderProps: PageHeaderProps; - renderDrawer: ( - open: boolean, - setDrawerOpen: Dispatch> - ) => ReactElement; renderHCaptchaStatisticsDrawer?: (isOpen: boolean) => ReactElement; renderGovernanceBanner?: boolean; }) { @@ -102,7 +94,6 @@ export function ProtectedLayout({ )} {!isMobile && } - {/* {renderDrawer(drawerOpen, setDrawerOpen)} */} {isHCaptchaLabelingPage && renderHCaptchaStatisticsDrawer ? renderHCaptchaStatisticsDrawer(hcaptchaDrawerOpen) : null} @@ -110,7 +101,6 @@ export function ProtectedLayout({ sx={{ flex: 1, py: { xs: 0, md: 4 }, - //px: { xs: 2, md: 4 }, gap: { xs: 0, md: 3 }, bgcolor: colorPalette.background.paper, borderRadius: { xs: '0px', md: '30px' }, @@ -128,7 +118,6 @@ export function ProtectedLayout({ }} > {renderGovernanceBanner && } - {/* */} diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/page-header.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/page-header.tsx deleted file mode 100644 index 9d52ef024e..0000000000 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/page-header.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Box, Grid, Typography } from '@mui/material'; -import React from 'react'; -import { useIsMobile } from '@/shared/hooks/use-is-mobile'; -import { breakpoints } from '@/shared/styles/breakpoints'; - -export interface PageHeaderProps { - headerIcon: React.ReactNode; - headerText: string; - headerItem?: React.ReactNode; -} - -export function PageHeader({ - headerIcon, - headerText, - headerItem, -}: PageHeaderProps) { - const isMobile = useIsMobile(); - return ( - - - - - {headerIcon} - - - - - {headerText} - - - - {!isMobile ? {headerItem} : null} - - ); -} diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/top-menu-items-list.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/top-menu-items-list.tsx deleted file mode 100644 index b8ed1d5933..0000000000 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/top-menu-items-list.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { - List, - ListItem, - Stack, - ListItemButton, - ListItemText, - Typography, -} from '@mui/material'; -import { useLocation } from 'react-router-dom'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { useIsMobile } from '@/shared/hooks'; -import { colorPalette } from '@/shared/styles/color-palette'; -import { onlyDarkModeColor } from '@/shared/styles/dark-color-palette'; -import { isDrawerItem } from '../helpers'; -import { type DrawerItem, type MenuItem } from './drawer-navigation'; - -interface MenuItemListProps { - handleItemClick: (item: DrawerItem) => void; - items?: MenuItem[]; -} - -export function TopMenuItemsList({ - handleItemClick, - items, -}: Readonly) { - const { isDarkMode } = useColorMode(); - const isMobile = useIsMobile(); - const location = useLocation(); - - return ( - - {items?.map((item) => { - if (!isDrawerItem(item)) { - return ( - - - {item} - - - ); - } - - const { link, label, disabled, icon } = item; - const isActive = location.pathname === link; - - return ( - - { - handleItemClick(item); - }} - selected={isActive} - sx={{ - borderRadius: '4px', - p: 0, - '&.Mui-selected': { - backgroundColor: isDarkMode - ? onlyDarkModeColor.listItemColor - : colorPalette.primary.shades, - }, - }} - > - - {icon} - - {label} - - } - /> - - - - ); - })} - - ); -} diff --git a/packages/apps/human-app/frontend/src/router/router.tsx b/packages/apps/human-app/frontend/src/router/router.tsx index 6efc08fbd3..f938b1b921 100644 --- a/packages/apps/human-app/frontend/src/router/router.tsx +++ b/packages/apps/human-app/frontend/src/router/router.tsx @@ -8,30 +8,11 @@ import { import { RequireAuth } from '@/modules/auth/providers/require-auth'; import { RequireWalletConnect } from '@/shared/contexts/wallet-connect'; import { RequireWeb3Auth } from '@/modules/auth-web3/providers/require-web3-auth'; -import { DrawerNavigation } from '@/router/components/layout/protected/drawer-navigation'; -import { operatorDrawerBottomMenuItems } from '@/router/components/drawer-menu-items/drawer-menu-items-operator'; -import { browserAuthProvider } from '@/shared/contexts/browser-auth-provider'; -import { useAuth } from '@/modules/auth/hooks/use-auth'; import { UserStatsDrawer } from '@/modules/worker/hcaptcha-labeling'; import { routerPaths } from './router-paths'; -import { - ProtectedLayout, - UnprotectedLayout, - workerDrawerBottomMenuItems, - workerDrawerTopMenuItems, -} from './components'; +import { ProtectedLayout, UnprotectedLayout } from './components'; export function Router() { - const { user } = useAuth(); - - const handleSignOut = () => { - browserAuthProvider.signOut({ - callback: () => { - window.location.reload(); - }, - }); - }; - return ( }> @@ -58,16 +39,6 @@ export function Router() { element={ ( - - )} renderHCaptchaStatisticsDrawer={(isOpen) => ( )} @@ -87,17 +58,7 @@ export function Router() { element={ - ( - - )} - /> + } diff --git a/packages/apps/human-app/frontend/src/router/routes.tsx b/packages/apps/human-app/frontend/src/router/routes.tsx index 46bff008ab..d6b0fd9fba 100644 --- a/packages/apps/human-app/frontend/src/router/routes.tsx +++ b/packages/apps/human-app/frontend/src/router/routes.tsx @@ -1,17 +1,10 @@ import type { RouteProps } from 'react-router-dom'; -import { t } from 'i18next'; import { routerPaths } from '@/router/router-paths'; import { env } from '@/shared/env'; import { RegistrationPage } from '@/modules/worker/oracle-registration'; -import { - HandIcon, - ProfileIcon, - WorkHeaderIcon, -} from '@/shared/components/ui/icons'; -import type { PageHeaderProps } from '@/router/components/layout/protected/page-header'; import { HcaptchaLabelingPage, - UserStatsAccordion, + //UserStatsAccordion, EnableLabelerPage, } from '@/modules/worker/hcaptcha-labeling'; import { @@ -27,7 +20,6 @@ import { HomePage } from '@/modules/homepage'; import { AddKeysOperatorPage, AddStakeOperatorPage, - ConnectWalletOperatorPage, EditExistingKeysSuccessPage, SetUpOperatorPage, } from '@/modules/signup/operator'; @@ -54,10 +46,6 @@ export const unprotectedRoutes: RouteProps[] = [ path: routerPaths.worker.signUp, element: , }, - { - path: routerPaths.operator.connectWallet, - element: , - }, { path: routerPaths.worker.emailVerification, element: , @@ -86,17 +74,12 @@ export const unprotectedRoutes: RouteProps[] = [ export const protectedRoutes: { routerProps: RouteProps; - pageHeaderProps: PageHeaderProps; }[] = [ { routerProps: { path: routerPaths.worker.jobsDiscovery, element: , }, - pageHeaderProps: { - headerIcon: , - headerText: t('protectedPagesHeaders.jobsDiscovery'), - }, }, ...(env.VITE_FEATURE_FLAG_JOBS_DISCOVERY ? [ @@ -105,20 +88,12 @@ export const protectedRoutes: { path: `${routerPaths.worker.jobs}/:address`, element: , }, - pageHeaderProps: { - headerIcon: , - headerText: t('protectedPagesHeaders.jobs'), - }, }, { routerProps: { path: `${routerPaths.worker.registrationInExchangeOracle}/:address`, element: , }, - pageHeaderProps: { - headerIcon: , - headerText: t('protectedPagesHeaders.registrationInExchangeOracle'), - }, }, ] : []), @@ -127,48 +102,29 @@ export const protectedRoutes: { path: routerPaths.worker.profile, element: , }, - pageHeaderProps: { - headerIcon: , - headerText: t('protectedPagesHeaders.profile'), - }, }, { routerProps: { path: routerPaths.worker.HcaptchaLabeling, element: , }, - pageHeaderProps: { - headerIcon: , - headerText: t('protectedPagesHeaders.hcaptchaLabeling'), - headerItem: , - }, }, { routerProps: { path: routerPaths.worker.enableLabeler, element: , }, - pageHeaderProps: { - headerIcon: , - headerText: t('protectedPagesHeaders.hcaptchaLabeling'), - headerItem: , - }, }, ]; export const web3ProtectedRoutes: { routerProps: RouteProps; - pageHeaderProps: PageHeaderProps; }[] = [ { routerProps: { path: routerPaths.operator.profile, element: , }, - pageHeaderProps: { - headerIcon: , - headerText: t('web3ProtectedPagesHeaders.profile'), - }, }, ]; diff --git a/packages/apps/human-app/frontend/src/shared/components/ui/icons.tsx b/packages/apps/human-app/frontend/src/shared/components/ui/icons.tsx index ae17ef18a6..f05cdb0c89 100644 --- a/packages/apps/human-app/frontend/src/shared/components/ui/icons.tsx +++ b/packages/apps/human-app/frontend/src/shared/components/ui/icons.tsx @@ -2,20 +2,12 @@ import { SvgIcon, SvgIconProps } from '@mui/material'; import HumanLogoIconLight from '@/assets/icons/icons-human-logo/human-logo.svg'; import HumanLogoIconDark from '@/assets/icons-dark-mode/icons-human-logo/human-logo.svg'; -import HandIconLight from '@/assets/icons/hand.svg'; -import HandIconDark from '@/assets/icons-dark-mode/hand.svg'; import RefreshIconLight from '@/assets/icons/refresh.svg'; import RefreshIconDark from '@/assets/icons-dark-mode/refresh.svg'; -import UserOutlinedIconLight from '@/assets/icons/user-outlined.svg'; -import UserOutlinedIconDark from '@/assets/icons-dark-mode/user-outlined.svg'; -import WorkIconLight from '@/assets/icons/work.svg'; -import WorkIconDark from '@/assets/icons-dark-mode/work.svg'; import HumanLogoNavbarIconLight from '@/assets/icons/icons-navbar/human-logo-navbar.svg'; import HumanLogoNavbarIconDark from '@/assets/icons-dark-mode/icons-navbar/human-logo-navbar.svg'; import HelpIconLight from '@/assets/icons/help.svg'; import HelpIconDark from '@/assets/icons-dark-mode/help.svg'; -import ProfileIconLight from '@/assets/icons/profile-icon.svg'; -import ProfileIconDark from '@/assets/icons-dark-mode/profile-icon.svg'; import CheckmarkIcon from '@/assets/icons/checkmark-icon.svg'; import LockerIconLight from '@/assets/icons/locker-icon.svg'; import LockerIconDark from '@/assets/icons-dark-mode/locker-icon.svg'; @@ -30,8 +22,6 @@ import SunIconLight from '@/assets/icons/sun.svg'; import MoonIconDark from '@/assets/icons-dark-mode/moon.svg'; import MoonIconLight from '@/assets/icons/moon.svg'; import { useColorMode } from '@/shared/contexts/color-mode'; -import WorkHeaderDark from '@/assets/icons-dark-mode/work-header.svg'; -import WorkHeaderLight from '@/assets/icons/work-header.svg'; import CopyIconLight from '@/assets/icons/content-copy.svg'; import CopyIconDark from '@/assets/icons-dark-mode/content-copy.svg'; import EditIconLight from '@/assets/icons/edit-icon.svg'; @@ -56,38 +46,14 @@ function HumanLogoNavbarIcon() { ); } -function WorkHeaderIcon() { - const { isDarkMode } = useColorMode(); - return isDarkMode ? : ; -} -function HandIcon() { - const { isDarkMode } = useColorMode(); - return isDarkMode ? : ; -} function RefreshIcon() { const { isDarkMode } = useColorMode(); return isDarkMode ? : ; } -function InverseRefreshIcon() { - const { isDarkMode } = useColorMode(); - return isDarkMode ? : ; -} -function UserOutlinedIcon() { - const { isDarkMode } = useColorMode(); - return isDarkMode ? : ; -} -function WorkIcon() { - const { isDarkMode } = useColorMode(); - return isDarkMode ? : ; -} function HelpIcon() { const { isDarkMode } = useColorMode(); return isDarkMode ? : ; } -function ProfileIcon() { - const { isDarkMode } = useColorMode(); - return isDarkMode ? : ; -} function LockerIcon() { const { isDarkMode } = useColorMode(); return isDarkMode ? : ; @@ -189,13 +155,8 @@ function HourglassIcon() { export { HumanLogoIcon, HumanLogoNavbarIcon, - HandIcon, RefreshIcon, - InverseRefreshIcon, - UserOutlinedIcon, - WorkIcon, HelpIcon, - ProfileIcon, CheckmarkIcon, LockerIcon, FiltersButtonIcon, @@ -203,7 +164,6 @@ export { FiltersIcon, SunIcon, MoonIcon, - WorkHeaderIcon, CopyIcon, EditIcon, DeleteIcon, From 095e114b7b3d38450407869fb2ac9139f9e5c74c Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Mon, 27 Jul 2026 17:54:32 +0300 Subject: [PATCH 3/9] fix: address copilot comments --- .../src/modules/worker/profile/views/profile.page.tsx | 9 +++++++++ .../worker/reset-password/reset-password.page.tsx | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx index 1099abab4d..5db121de0c 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx @@ -63,6 +63,15 @@ export function WorkerProfilePage() { }); }; + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + }, []); + useEffect(() => { if (initializing) return; diff --git a/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx index 6ee0be01e5..e1a153dc75 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx @@ -111,7 +111,7 @@ export function ResetPasswordWorkerPage() { lineHeight: { xs: '150%', md: 'normal' }, }} > - {t('worker.sendResetLinkForm.title')} + {t('worker.resetPassword.title')} From a11a88b76c4fdb47481931a307146d6eb7eb9762 Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Mon, 27 Jul 2026 18:13:00 +0300 Subject: [PATCH 4/9] refactor: expiration modal and auto sign out logic --- .../reset-password-success.page.tsx | 9 +-- .../shared/components/expiration-modal.tsx | 62 ++++++++----------- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password-success.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password-success.page.tsx index 696e0230c0..1fe83805f0 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password-success.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password-success.page.tsx @@ -11,12 +11,13 @@ import { useColorMode } from '@/shared/contexts/color-mode'; export function ResetPasswordWorkerSuccessPage() { const { colorPalette } = useColorMode(); - const { signOut } = useAuth(); + const { signOut, user } = useAuth(); useEffect(() => { - signOut(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (user) { + signOut(); + } + }, [user, signOut]); return ( { + browserAuthProvider.signOut(); + navigate(routerPaths.worker.signIn, { replace: true }); + closeModal(); + }; return ( - - - {t('expirationModal.header')} - - + + {t('expirationModal.header')} + + + {t('expirationModal.description')} - - - + + + ); } From 88d34d81b46832d0d05464d0f77b6e22af7e8dd8 Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Wed, 5 Aug 2026 14:05:47 +0300 Subject: [PATCH 5/9] chore: some typography changes --- .../modules/signin/worker/sign-in.page.tsx | 10 ++----- .../worker/views/sign-up-worker.page.tsx | 10 ++----- .../email-verification-form-container.tsx | 4 +-- .../worker/profile/views/profile.page.tsx | 29 ++++++------------- .../reset-password/reset-password.page.tsx | 10 ++----- .../send-reset-link-success.page.tsx | 6 ++-- .../send-reset-link/send-reset-link.page.tsx | 10 ++----- .../components/ui/page-card/back-button.tsx | 1 + .../src/shared/styles/color-palette.ts | 14 ++++----- .../src/shared/styles/dark-color-palette.ts | 14 ++++----- .../frontend/src/shared/styles/typography.ts | 26 ++++++++--------- 11 files changed, 49 insertions(+), 85 deletions(-) diff --git a/packages/apps/human-app/frontend/src/modules/signin/worker/sign-in.page.tsx b/packages/apps/human-app/frontend/src/modules/signin/worker/sign-in.page.tsx index b480dc0408..ff71994f79 100644 --- a/packages/apps/human-app/frontend/src/modules/signin/worker/sign-in.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/signin/worker/sign-in.page.tsx @@ -141,13 +141,9 @@ export function SignInWorkerPage() { > {t('worker.signInForm.title')} diff --git a/packages/apps/human-app/frontend/src/modules/signup/worker/views/sign-up-worker.page.tsx b/packages/apps/human-app/frontend/src/modules/signup/worker/views/sign-up-worker.page.tsx index db38bc71b2..7cd0698e16 100644 --- a/packages/apps/human-app/frontend/src/modules/signup/worker/views/sign-up-worker.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/signup/worker/views/sign-up-worker.page.tsx @@ -116,13 +116,9 @@ export function SignUpWorkerPage() { > {t('worker.signUpForm.title')} diff --git a/packages/apps/human-app/frontend/src/modules/worker/email-verification/components/email-verification-form-container.tsx b/packages/apps/human-app/frontend/src/modules/worker/email-verification/components/email-verification-form-container.tsx index 7780b70244..6a880b14b2 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/email-verification/components/email-verification-form-container.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/email-verification/components/email-verification-form-container.tsx @@ -95,13 +95,11 @@ export function EmailVerificationFormContainer() { sx={{ mb: 2.5, color: colorPalette.primary.main, fontSize: 54 }} /> diff --git a/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx index 5db121de0c..70285faf85 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx @@ -110,12 +110,9 @@ export function WorkerProfilePage() { {isMobile && } - @@ -163,12 +157,11 @@ export function WorkerProfilePage() { {t('worker.profile.accountVerified')} - {t('worker.profile.email')} @@ -198,20 +189,18 @@ export function WorkerProfilePage() { {t('worker.profile.walletAddress')} @@ -255,7 +244,7 @@ export function WorkerProfilePage() { {t('worker.profile.resetPassword')} - + ); diff --git a/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx index e1a153dc75..c9721bf219 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/reset-password/reset-password.page.tsx @@ -103,13 +103,9 @@ export function ResetPasswordWorkerPage() { > {t('worker.resetPassword.title')} diff --git a/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx index 7d4679e613..1d7f33b368 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx @@ -81,12 +81,10 @@ export function SendResetLinkWorkerSuccessPage() { > diff --git a/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link.page.tsx index 70ea6c86d5..05ebbcf123 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link.page.tsx @@ -98,13 +98,9 @@ export function SendResetLinkWorkerPage() { > {t('worker.sendResetLinkForm.title')} diff --git a/packages/apps/human-app/frontend/src/shared/components/ui/page-card/back-button.tsx b/packages/apps/human-app/frontend/src/shared/components/ui/page-card/back-button.tsx index b62edd931e..1340cad033 100644 --- a/packages/apps/human-app/frontend/src/shared/components/ui/page-card/back-button.tsx +++ b/packages/apps/human-app/frontend/src/shared/components/ui/page-card/back-button.tsx @@ -15,6 +15,7 @@ export function BackButton({ onClick }: { onClick: () => void }) { height: { xs: '32px', md: '40px' }, borderRadius: '50%', fontSize: '24px', + transition: 'none', color: isDarkMode ? colorPalette.text.auxiliary100 : colorPalette.text.primary, diff --git a/packages/apps/human-app/frontend/src/shared/styles/color-palette.ts b/packages/apps/human-app/frontend/src/shared/styles/color-palette.ts index 96dbe6571e..7be38b2795 100644 --- a/packages/apps/human-app/frontend/src/shared/styles/color-palette.ts +++ b/packages/apps/human-app/frontend/src/shared/styles/color-palette.ts @@ -38,16 +38,14 @@ export const colorPalette = { strong: 'rgba(50, 10, 141, 0.30)', }, error: { - main: '#FA2A75', - dark: '#F20D5F', - light: '#FF5995', - contrastText: '#FFFFFF', + main: '#ff6262', + dark: '#ff6262cc', + contrastText: '#ffffff', }, success: { - main: '#0AD397', - dark: '#0E976E', - light: '#00EDA6', - contrastText: '#FFFFFF', + main: '#43ba96', + dark: '#43ba96cc', + contrastText: '#ffffff', }, paper: { main: '#F6F7FE', diff --git a/packages/apps/human-app/frontend/src/shared/styles/dark-color-palette.ts b/packages/apps/human-app/frontend/src/shared/styles/dark-color-palette.ts index 17a8511898..459763d40f 100644 --- a/packages/apps/human-app/frontend/src/shared/styles/dark-color-palette.ts +++ b/packages/apps/human-app/frontend/src/shared/styles/dark-color-palette.ts @@ -40,16 +40,14 @@ export const darkColorPalette = { strong: '#3a2e6f', }, error: { - main: '#F65A93', - dark: '#F3206E', - light: '#F76DA0', - contrastText: '#FFFFFF', + main: '#ff6262', + dark: '#ff6262cc', + contrastText: '#ffffff', }, success: { - main: '#58CAA8', - dark: '#1F916F', - light: '#5FD1AF', - contrastText: 'rgba(255, 255, 255, 0.87)', + main: '#43ba96', + dark: '#43ba96cc', + contrastText: '#ffffff', }, paper: { main: '#1c133f', diff --git a/packages/apps/human-app/frontend/src/shared/styles/typography.ts b/packages/apps/human-app/frontend/src/shared/styles/typography.ts index d14f247a98..fdc40128b4 100644 --- a/packages/apps/human-app/frontend/src/shared/styles/typography.ts +++ b/packages/apps/human-app/frontend/src/shared/styles/typography.ts @@ -77,12 +77,13 @@ export const typography: ThemeOptions['typography'] = { }, h4: { fontSize: 34, - fontWeight: 600, - letterSpacing: 0.25, + fontWeight: 800, + lineHeight: 'normal', + letterSpacing: 0, [breakpoints.mobile]: { - fontSize: 28, - fontWeight: 600, - letterSpacing: 0.15, + fontSize: 20, + fontWeight: 700, + lineHeight: '150%', }, }, mobileHeaderLarge: { @@ -103,10 +104,8 @@ export const typography: ThemeOptions['typography'] = { h6: { fontSize: 20, fontWeight: 500, - letterSpacing: 0.15, - [breakpoints.mobile]: { - fontSize: 20, - }, + lineHeight: 'normal', + letterSpacing: 0.12, }, subtitle1: { fontSize: 16, @@ -127,15 +126,14 @@ export const typography: ThemeOptions['typography'] = { body1: { fontSize: 16, fontWeight: 400, - letterSpacing: 0.15, + lineHeight: 'normal', + letterSpacing: 0.12, }, body2: { fontSize: 14, fontWeight: 400, - letterSpacing: 0.15, - [breakpoints.mobile]: { - fontSize: 14, - }, + lineHeight: 'normal', + letterSpacing: 0.12, }, body4: { fontSize: 24, From 93b7fbe37e85b95918a869fde46501e2d58be834 Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Wed, 5 Aug 2026 14:08:35 +0300 Subject: [PATCH 6/9] chore: cosmetic fix --- .../send-reset-link/send-reset-link-success.page.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx index 1d7f33b368..78caeec0a9 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/send-reset-link/send-reset-link-success.page.tsx @@ -79,14 +79,11 @@ export function SendResetLinkWorkerSuccessPage() { overflow: 'hidden', }} > - + {t('worker.sendResetLinkForm.title')} From a18b0ce106c1ef3be0d5f7f538b78852db780ef8 Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Wed, 5 Aug 2026 15:04:00 +0300 Subject: [PATCH 7/9] fix: remove test case --- .../my-jobs/utils/__tests__/get-chip-status-color.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/__tests__/get-chip-status-color.test.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/__tests__/get-chip-status-color.test.ts index 7c524483e0..900e4ea26a 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/__tests__/get-chip-status-color.test.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/__tests__/get-chip-status-color.test.ts @@ -14,11 +14,6 @@ describe('getChipStatusColor Function', () => { expect(result).toBe(colorPalette.success.main); }); - it('should return the error light color for VALIDATION status', () => { - const result = getChipStatusColor(MyJobStatus.VALIDATION, colorPalette); - expect(result).toBe(colorPalette.error.light); - }); - it('should return the error main color for unknown status', () => { const result = getChipStatusColor(UNKNOWN_JOB_STATUS, colorPalette); expect(result).toBe(colorPalette.error.main); From 61cbc7d8bb0111ea03a6e8799f60ca7382616fb0 Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Wed, 5 Aug 2026 15:07:45 +0300 Subject: [PATCH 8/9] fix: remove unused case --- .../modules/worker/jobs/my-jobs/utils/get-chip-status-color.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/get-chip-status-color.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/get-chip-status-color.ts index c791586ee8..6fc926bbf8 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/get-chip-status-color.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/get-chip-status-color.ts @@ -10,8 +10,6 @@ export function getChipStatusColor( return colorPalette.secondary.main; case MyJobStatus.COMPLETED: return colorPalette.success.main; - case MyJobStatus.VALIDATION: - return colorPalette.error.light; default: return colorPalette.error.main; } From e01a274c5b19f606a1115fba64a92326d0dbb3c4 Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Wed, 5 Aug 2026 16:24:04 +0300 Subject: [PATCH 9/9] [Human App Frontend] Redesign Jobs Page (#3993) --- packages/apps/human-app/frontend/.env.example | 4 - .../assets/icons-dark-mode/content-copy.svg | 3 - .../hcaptcha-disabled-icon.svg | 9 + .../src/assets/icons/content-copy.svg | 3 - .../src/assets/icons/ethereum-icon.svg | 15 + .../assets/icons/hcaptcha-disabled-icon.svg | 9 + .../frontend/src/assets/icons/hcaptcha.svg | 46 +++ .../src/assets/icons/polygon-icon.svg | 14 + .../frontend/src/assets/icons/triangle.svg | 3 + .../signup/operator/views/add-stake.page.tsx | 12 +- .../components/breadcrumbs.tsx | 42 +++ .../components/user-stats-accordion.tsx | 59 ++-- .../components/user-stats-details.tsx | 60 ++-- .../components/user-stats-drawer.tsx | 132 ++++---- .../hcaptcha-labeling/enable-labeler.page.tsx | 113 ++++--- .../hcaptcha-labeling.page.tsx | 281 +++++++++++------ .../hooks/enable-hcaptcha-labeling.ts | 4 +- .../modules/worker/hooks/use-get-oracles.ts | 8 +- .../components/explore-tasks-dialog.tsx | 96 ++++++ .../components/hcaptcha-widget.tsx | 269 ++++++++++++++++ .../worker/jobs-discovery/components/index.ts | 8 +- .../components/oracle-job-card.tsx | 296 ++++++++++++++++++ .../components/oracles-list.tsx | 57 ++++ .../components/oracles-table-desktop.tsx | 55 ---- .../components/oracles-table-item-mobile.tsx | 60 ---- .../oracles-table-job-types-select.tsx | 41 --- .../components/oracles-table.tsx | 69 ---- .../worker/jobs-discovery/helpers/index.ts | 1 - .../helpers/is-hcaptcha-oracle.spec.ts | 39 --- .../helpers/is-hcaptcha-oracle.ts | 4 - .../should-navigate-to-registration.spec.ts | 4 + .../hooks/use-select-oracle-navigation.ts | 24 +- .../jobs-discovery/jobs-discovery.page.tsx | 107 ++++++- .../available-jobs-filter-modal.tsx | 120 ------- .../available-jobs/available-jobs-view.tsx | 17 - .../available-jobs-job-type-filter.tsx | 41 --- .../available-jobs-network-filter.tsx | 36 --- .../available-jobs-reward-amount-sort.tsx | 43 --- .../desktop/available-jobs-table-desktop.tsx | 99 +++--- .../jobs/available-jobs/components/index.ts | 3 - ...vailable-jobs-assign-job-button-mobile.tsx | 39 --- .../components/mobile/available-jobs-card.tsx | 186 +++++++++++ .../mobile/available-jobs-list-mobile.tsx | 113 ++----- ...ailable-jobs-reward-amount-sort-mobile.tsx | 47 --- .../mobile/available-jobs-table-mobile.tsx | 38 --- .../available-jobs/components/mobile/index.ts | 3 +- .../hooks/use-available-jobs-filter-modal.tsx | 21 -- .../hooks/use-get-available-jobs-columns.tsx | 138 ++++---- .../hooks/use-get-available-jobs-data.ts | 25 +- .../worker/jobs/available-jobs/index.ts | 2 - .../components/escrow-address-search-form.tsx | 103 ------ .../worker/jobs/components/evm-address.tsx | 17 +- .../modules/worker/jobs/components/index.ts | 5 +- .../worker/jobs/components/jobs-tab-panel.tsx | 20 -- .../worker/jobs/components/more-button.tsx | 47 +-- .../jobs/components/my-jobs-table-actions.tsx | 11 +- ...buse-modal.tsx => report-abuse-dialog.tsx} | 180 +++++++---- .../worker/jobs/components/reward-amount.tsx | 30 +- .../worker/jobs/components/sorting.tsx | 102 ------ .../src/modules/worker/jobs/hooks/index.ts | 2 - .../worker/jobs/hooks/use-get-all-networks.ts | 15 - .../worker/jobs/hooks/use-get-my-jobs-data.ts | 36 +-- .../hooks/use-job-types-oracles-table.tsx | 17 - .../jobs/hooks/use-jobs-notifications.tsx | 4 +- .../jobs/hooks/use-my-jobs-filter-store.tsx | 69 ++-- .../src/modules/worker/jobs/jobs.page.tsx | 78 ++--- .../my-jobs/components/desktop/columns.tsx | 175 ----------- .../jobs/my-jobs/components/desktop/index.ts | 2 + .../desktop/my-jobs-expires-at-sort.tsx | 43 --- .../components/desktop/my-jobs-filters.tsx | 197 ++++++++++++ .../desktop/my-jobs-job-type-filter.tsx | 36 --- .../desktop/my-jobs-network-filter.tsx | 32 -- .../desktop/my-jobs-reward-amount-sort.tsx | 43 --- .../desktop/my-jobs-status-filter.tsx | 30 -- .../components/desktop/my-jobs-table.tsx | 149 +++++---- .../components/desktop/status-chip.tsx | 27 -- .../components/desktop/status-filter.tsx | 73 +++++ .../jobs/my-jobs/components/mobile/index.ts | 1 - .../components/mobile/my-jobs-card-mobile.tsx | 163 ++++++++++ .../mobile/my-jobs-expires-at-sort-mobile.tsx | 47 --- .../mobile/my-jobs-filter-modal.tsx | 136 -------- .../mobile/my-jobs-job-type-filter-mobile.tsx | 31 -- .../components/mobile/my-jobs-list-mobile.tsx | 230 +++----------- .../mobile/my-jobs-network-filter-mobile.tsx | 32 -- .../my-jobs-reward-amount-sort-mobile.tsx | 47 --- .../mobile/my-jobs-status-filter-mobile.tsx | 30 -- .../jobs/my-jobs/components/time-until.tsx | 71 +++++ .../my-jobs/hooks/use-get-my-jobs-columns.tsx | 141 +++++++++ .../hooks/use-my-jobs-filter-modal.tsx | 21 -- .../worker/jobs/my-jobs/my-jobs-view.tsx | 13 - .../worker/jobs/my-jobs/my-jobs.page.tsx | 88 ++++++ .../__tests__/get-chip-status-color.test.ts | 21 -- .../my-jobs/utils/get-chip-status-color.ts | 16 - .../worker/jobs/my-jobs/utils/index.ts | 1 - .../frontend/src/modules/worker/jobs/types.ts | 2 + .../profile/components/modal-states.tsx | 64 ---- .../worker/profile/views/profile.page.tsx | 64 +--- .../worker/services/oracles.service.ts | 37 +-- .../drawer-menu-items-worker.tsx | 14 - .../components/drawer-menu-items/index.ts | 1 - .../frontend/src/router/components/footer.tsx | 10 +- .../frontend/src/router/components/index.ts | 1 - .../layout/protected/desktop-aside-bar.tsx | 76 ++++- .../layout/protected/jobs-switcher-mobile.tsx | 66 ++++ .../components/layout/protected/layout.tsx | 72 +---- .../components/layout/protected/navbar.tsx | 87 +---- .../frontend/src/router/router-paths.ts | 1 + .../human-app/frontend/src/router/router.tsx | 8 +- .../human-app/frontend/src/router/routes.tsx | 8 +- .../shared/components/expiration-modal.tsx | 6 +- .../src/shared/components/ui/base-drawer.tsx | 12 +- .../src/shared/components/ui/chain-icon.tsx | 33 ++ .../ui/copy-to-clipboard-button.tsx | 60 ++++ .../src/shared/components/ui/icons.tsx | 224 ++++++++++++- .../shared/components/ui/modal/base-modal.tsx | 17 +- .../components/ui/modal/global-modal.tsx | 1 + .../src/shared/components/ui/no-records.tsx | 7 +- .../ui/page-card/page-card-error.tsx | 51 +-- .../ui/page-card/page-card-loader.tsx | 18 +- .../components/ui/page-card/page-card.tsx | 14 +- .../shared/components/ui/page-card/styles.ts | 26 +- .../src/shared/components/ui/table-button.tsx | 2 +- .../components/ui/table/table-header-cell.tsx | 74 ----- .../ui/table/table-header-menu/filtering.tsx | 92 ------ .../ui/table/table-header-menu/sorting.tsx | 55 ---- .../ui/table/table-query-context.tsx | 66 ---- .../human-app/frontend/src/shared/consts.ts | 2 +- .../apps/human-app/frontend/src/shared/env.ts | 7 - .../frontend/src/shared/hooks/index.ts | 1 - .../src/shared/hooks/use-combine-pages.ts | 23 +- .../hooks/use-is-hcaptcha-labeling-page.ts | 7 - .../frontend/src/shared/i18n/en.json | 56 +++- .../src/shared/styles/color-palette.ts | 1 + .../src/shared/styles/dark-color-palette.ts | 9 +- .../frontend/src/shared/styles/dark-theme.ts | 7 +- .../frontend/src/shared/styles/theme.ts | 6 + 136 files changed, 3448 insertions(+), 3490 deletions(-) delete mode 100644 packages/apps/human-app/frontend/src/assets/icons-dark-mode/content-copy.svg create mode 100644 packages/apps/human-app/frontend/src/assets/icons-dark-mode/hcaptcha-disabled-icon.svg delete mode 100644 packages/apps/human-app/frontend/src/assets/icons/content-copy.svg create mode 100644 packages/apps/human-app/frontend/src/assets/icons/ethereum-icon.svg create mode 100644 packages/apps/human-app/frontend/src/assets/icons/hcaptcha-disabled-icon.svg create mode 100644 packages/apps/human-app/frontend/src/assets/icons/hcaptcha.svg create mode 100644 packages/apps/human-app/frontend/src/assets/icons/polygon-icon.svg create mode 100644 packages/apps/human-app/frontend/src/assets/icons/triangle.svg create mode 100644 packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/breadcrumbs.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/explore-tasks-dialog.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/hcaptcha-widget.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracle-job-card.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-list.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-desktop.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-item-mobile.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-job-types-select.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/is-hcaptcha-oracle.spec.ts delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/is-hcaptcha-oracle.ts delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/available-jobs-filter-modal.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/available-jobs-view.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-job-type-filter.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-network-filter.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-reward-amount-sort.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/index.ts delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-assign-job-button-mobile.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-card.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-reward-amount-sort-mobile.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-table-mobile.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-available-jobs-filter-modal.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/index.ts delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/components/escrow-address-search-form.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/components/jobs-tab-panel.tsx rename packages/apps/human-app/frontend/src/modules/worker/jobs/components/{report-abuse-modal.tsx => report-abuse-dialog.tsx} (51%) delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/components/sorting.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-get-all-networks.ts delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-job-types-oracles-table.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/columns.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-expires-at-sort.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-filters.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-job-type-filter.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-network-filter.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-reward-amount-sort.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-status-filter.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/status-chip.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/status-filter.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-card-mobile.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-expires-at-sort-mobile.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-filter-modal.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-job-type-filter-mobile.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-network-filter-mobile.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-reward-amount-sort-mobile.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-status-filter-mobile.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/time-until.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/hooks/use-get-my-jobs-columns.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/hooks/use-my-jobs-filter-modal.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/my-jobs-view.tsx create mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/my-jobs.page.tsx delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/__tests__/get-chip-status-color.test.ts delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/get-chip-status-color.ts delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/index.ts delete mode 100644 packages/apps/human-app/frontend/src/modules/worker/profile/components/modal-states.tsx delete mode 100644 packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx delete mode 100644 packages/apps/human-app/frontend/src/router/components/drawer-menu-items/index.ts create mode 100644 packages/apps/human-app/frontend/src/router/components/layout/protected/jobs-switcher-mobile.tsx create mode 100644 packages/apps/human-app/frontend/src/shared/components/ui/chain-icon.tsx create mode 100644 packages/apps/human-app/frontend/src/shared/components/ui/copy-to-clipboard-button.tsx delete mode 100644 packages/apps/human-app/frontend/src/shared/components/ui/table/table-header-cell.tsx delete mode 100644 packages/apps/human-app/frontend/src/shared/components/ui/table/table-header-menu/filtering.tsx delete mode 100644 packages/apps/human-app/frontend/src/shared/components/ui/table/table-header-menu/sorting.tsx delete mode 100644 packages/apps/human-app/frontend/src/shared/components/ui/table/table-query-context.tsx delete mode 100644 packages/apps/human-app/frontend/src/shared/hooks/use-is-hcaptcha-labeling-page.ts diff --git a/packages/apps/human-app/frontend/.env.example b/packages/apps/human-app/frontend/.env.example index 8a13a7385e..a605cc5028 100644 --- a/packages/apps/human-app/frontend/.env.example +++ b/packages/apps/human-app/frontend/.env.example @@ -17,10 +17,6 @@ VITE_HMT_DAILY_SPENT_LIMIT=0 # hCaptcha annotation tool VITE_H_CAPTCHA_EXCHANGE_URL=replace_me VITE_H_CAPTCHA_LABELING_BASE_URL=replace_me -VITE_H_CAPTCHA_ORACLE_ADDRESS=replace_me -VITE_H_CAPTCHA_ORACLE_ANNOTATION_TOOL=hcaptcha -VITE_H_CAPTCHA_ORACLE_ROLE=hcaptcha -VITE_H_CAPTCHA_ORACLE_TASK_TYPES=image_points,image_boxes # Other VITE_HUMAN_PROTOCOL_HELP_URL=https://docs.humanprotocol.org/ diff --git a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/content-copy.svg b/packages/apps/human-app/frontend/src/assets/icons-dark-mode/content-copy.svg deleted file mode 100644 index c4c4f89b16..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/content-copy.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons-dark-mode/hcaptcha-disabled-icon.svg b/packages/apps/human-app/frontend/src/assets/icons-dark-mode/hcaptcha-disabled-icon.svg new file mode 100644 index 0000000000..c4d02582e3 --- /dev/null +++ b/packages/apps/human-app/frontend/src/assets/icons-dark-mode/hcaptcha-disabled-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/apps/human-app/frontend/src/assets/icons/content-copy.svg b/packages/apps/human-app/frontend/src/assets/icons/content-copy.svg deleted file mode 100644 index 4423c76961..0000000000 --- a/packages/apps/human-app/frontend/src/assets/icons/content-copy.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/apps/human-app/frontend/src/assets/icons/ethereum-icon.svg b/packages/apps/human-app/frontend/src/assets/icons/ethereum-icon.svg new file mode 100644 index 0000000000..a72b032893 --- /dev/null +++ b/packages/apps/human-app/frontend/src/assets/icons/ethereum-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/apps/human-app/frontend/src/assets/icons/hcaptcha-disabled-icon.svg b/packages/apps/human-app/frontend/src/assets/icons/hcaptcha-disabled-icon.svg new file mode 100644 index 0000000000..4307dd8758 --- /dev/null +++ b/packages/apps/human-app/frontend/src/assets/icons/hcaptcha-disabled-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/apps/human-app/frontend/src/assets/icons/hcaptcha.svg b/packages/apps/human-app/frontend/src/assets/icons/hcaptcha.svg new file mode 100644 index 0000000000..65989c188c --- /dev/null +++ b/packages/apps/human-app/frontend/src/assets/icons/hcaptcha.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/apps/human-app/frontend/src/assets/icons/polygon-icon.svg b/packages/apps/human-app/frontend/src/assets/icons/polygon-icon.svg new file mode 100644 index 0000000000..34d6f6b628 --- /dev/null +++ b/packages/apps/human-app/frontend/src/assets/icons/polygon-icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/apps/human-app/frontend/src/assets/icons/triangle.svg b/packages/apps/human-app/frontend/src/assets/icons/triangle.svg new file mode 100644 index 0000000000..917cd60674 --- /dev/null +++ b/packages/apps/human-app/frontend/src/assets/icons/triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/apps/human-app/frontend/src/modules/signup/operator/views/add-stake.page.tsx b/packages/apps/human-app/frontend/src/modules/signup/operator/views/add-stake.page.tsx index 9f3bb5c5b0..cdf8fd8a55 100644 --- a/packages/apps/human-app/frontend/src/modules/signup/operator/views/add-stake.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/signup/operator/views/add-stake.page.tsx @@ -10,7 +10,6 @@ import { import { getErrorMessageForError } from '@/shared/errors'; import { Alert } from '@/shared/components/ui/alert'; import { useColorMode } from '@/shared/contexts/color-mode'; -import { onlyDarkModeColor } from '@/shared/styles/dark-color-palette'; import { useAddStakeMutationState, useGetStakedAmount, @@ -20,7 +19,7 @@ import { StakeForm, Buttons } from '../components/add-stake'; import { stakedAmountFormatter } from '../utils'; export function AddStakeOperatorPage() { - const { colorPalette, isDarkMode } = useColorMode(); + const { colorPalette } = useColorMode(); const [displayForm, setDisplayForm] = useState(false); const { data: stakedAmount, @@ -87,14 +86,7 @@ export function AddStakeOperatorPage() { {t('operator.addStake.label')} - + {stakedAmountFormatter(stakedAmount)} {displayForm ? ( diff --git a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/breadcrumbs.tsx b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/breadcrumbs.tsx new file mode 100644 index 0000000000..15ca2053e8 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/breadcrumbs.tsx @@ -0,0 +1,42 @@ +import { Box, Link, Typography } from '@mui/material'; +import { Link as RouterLink } from 'react-router-dom'; +import { t } from 'i18next'; + +import { routerPaths } from '@/router/router-paths'; +import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; + +export function Breadcrumbs() { + const { colorPalette } = useColorMode(); + + return ( + + + {t('worker.jobs.availableJobs')} + + + {'>'} + + + {t('worker.jobs.hCaptcha')} + + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-accordion.tsx b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-accordion.tsx index 082157d655..f198e6d185 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-accordion.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-accordion.tsx @@ -1,11 +1,14 @@ -import Accordion from '@mui/material/Accordion'; -import AccordionSummary from '@mui/material/AccordionSummary'; -import AccordionDetails from '@mui/material/AccordionDetails'; -import Typography from '@mui/material/Typography'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import Grid from '@mui/material/Grid'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { t } from 'i18next'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Grid, + Typography, +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; + import { TopNotificationType, useNotification, @@ -16,10 +19,14 @@ import { useHCaptchaUserStats } from '../hooks'; import { UserStatsDetails } from './user-stats-details'; import { LoadingOverlay } from './user-stats-loading-overlay'; -const accordionWidth = { width: '284px' }; +const ACCORDION_WIDTH = '284px'; +const ACCORDION_HEIGHT = '48px'; export function UserStatsAccordion() { + const [isExpanded, setIsExpanded] = useState(false); const { colorPalette } = useColorMode(); + const { showNotification } = useNotification(); + const { data: hcaptchaUserStats, isPending: isHcaptchaUserStatsPending, @@ -28,28 +35,32 @@ export function UserStatsAccordion() { refetch: refetchUserStats, isRefetching: isHcaptchaUserStatsRefetching, } = useHCaptchaUserStats(); - const { showNotification } = useNotification(); useEffect(() => { if (isHcaptchaUserStatsError) { showNotification({ - type: TopNotificationType.WARNING, + type: TopNotificationType.ERROR, message: getErrorMessageForError(hcaptchaUserStatsError), }); } - // eslint-disable-next-line react-hooks/exhaustive-deps -- ... - }, [isHcaptchaUserStatsError, hcaptchaUserStatsError]); + }, [isHcaptchaUserStatsError, hcaptchaUserStatsError, showNotification]); return ( - + { + setIsExpanded(expanded); + }} sx={{ - ...accordionWidth, position: 'relative', overflow: 'hidden', + width: ACCORDION_WIDTH, + minHeight: ACCORDION_HEIGHT, + zIndex: 1, }} > - {isHcaptchaUserStatsRefetching && ( + {isExpanded && isHcaptchaUserStatsRefetching && ( } id="panel1-header" - sx={{ ...accordionWidth, height: '76px' }} + sx={{ + width: ACCORDION_WIDTH, + height: ACCORDION_HEIGHT, + py: 1.5, + '& > .MuiAccordionSummary-content': { + m: 0, + }, + '&.Mui-expanded': { minHeight: ACCORDION_HEIGHT }, + }} > {t('worker.hcaptchaLabelingStats.statistics')} {hcaptchaUserStats ? ( - + void refetchUserStats()} stats={hcaptchaUserStats} diff --git a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-details.tsx b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-details.tsx index 6196dfd4be..4bb9a0a58e 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-details.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-details.tsx @@ -1,33 +1,29 @@ import { Divider, IconButton, Stack, Typography } from '@mui/material'; import { t } from 'i18next'; + import { RefreshIcon } from '@/shared/components/ui/icons'; -import { onlyDarkModeColor } from '@/shared/styles/dark-color-palette'; import { useColorMode } from '@/shared/contexts/color-mode'; import { useIsMobile } from '@/shared/hooks'; import { type HCaptchaUserStatsSuccess } from '../types'; -export function UserStatsDetails({ - stats, - refetch, - isRefetching, -}: Readonly<{ +type Props = { stats: HCaptchaUserStatsSuccess; refetch: () => void; isRefetching: boolean; -}>) { - const { colorPalette, isDarkMode } = useColorMode(); - const isMobile = useIsMobile(); +}; - const statsColor = isDarkMode - ? onlyDarkModeColor.additionalTextColor - : colorPalette.primary.light; +export function UserStatsDetails({ stats, refetch, isRefetching }: Props) { + const { colorPalette } = useColorMode(); + const isMobile = useIsMobile(); return ( - - - - + {!isMobile && ( + + )} + + + {t('worker.hcaptchaLabelingStats.allTime')} {t('worker.hcaptchaLabelingStats.jobsServed')} - + {stats.served} @@ -46,7 +42,7 @@ export function UserStatsDetails({ {t('worker.hcaptchaLabelingStats.jobsComplete')} - + {stats.solved} @@ -54,29 +50,41 @@ export function UserStatsDetails({ {t('worker.hcaptchaLabelingStats.hmtEarned')} - + {stats.balance.total}{' '} -
+ {t('inputMasks.humanCurrencySuffix')} - + - + - + {t('worker.hcaptchaLabelingStats.lastHour')} {t('worker.hcaptchaLabelingStats.earnedLastHour')} - + {stats.balance.recent}{' '} - + {t('inputMasks.humanCurrencySuffix')} - + diff --git a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-drawer.tsx b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-drawer.tsx index db65ccd070..dcd304ab0e 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-drawer.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/components/user-stats-drawer.tsx @@ -1,80 +1,98 @@ -import { Box, CssBaseline, Drawer, Stack, Typography } from '@mui/material'; +import { Box, Drawer, IconButton, Stack, Typography } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; import { t } from 'i18next'; + import { Loader } from '@/shared/components/ui/loader'; import { Alert } from '@/shared/components/ui/alert'; import { getErrorMessageForError } from '@/shared/errors'; import { useHCaptchaUserStats } from '../hooks'; import { UserStatsDetails } from './user-stats-details'; import { LoadingOverlay } from './user-stats-loading-overlay'; +import { useColorMode } from '@/shared/contexts/color-mode'; -export interface UserStatsDrawerNavigationProps { +type Props = { isOpen: boolean; -} + onClose: () => void; +}; + +export function UserStatsDrawer({ isOpen, onClose }: Props) { + const { colorPalette } = useColorMode(); -export function UserStatsDrawer({ - isOpen, -}: Readonly) { const { data: hcaptchaUserStats, error: hcaptchaUserStatsError, - status: hcaptchaUserStatsStatus, + isSuccess, + isPending, refetch: hcaptchaUserStatsRefetch, isRefetching: isHcaptchaUserStatsRefetching, } = useHCaptchaUserStats(); return ( - - - theme.zIndex.drawer, + '& .MuiDrawer-paper': { width: '100%', - flexShrink: 0, - '& .MuiDrawer-paper': { - width: '100%', - paddingTop: 11, - }, - }} - > - - {isHcaptchaUserStatsRefetching && ( - + + {isHcaptchaUserStatsRefetching && ( + + )} + + + {t('worker.hcaptchaLabelingStats.hCapchaStatistics')} + + + + + + + {isSuccess && ( + void hcaptchaUserStatsRefetch()} + stats={hcaptchaUserStats} + isRefetching={isHcaptchaUserStatsRefetching} /> )} - - {hcaptchaUserStatsStatus === 'success' ? ( - <> - - {t('worker.hcaptchaLabelingStats.hCapchaStatistics')} - - void hcaptchaUserStatsRefetch()} - stats={hcaptchaUserStats} - isRefetching={isHcaptchaUserStatsRefetching} - /> - - ) : null} - {hcaptchaUserStatsStatus === 'error' ? ( - - {getErrorMessageForError(hcaptchaUserStatsError)} - - ) : null} - {hcaptchaUserStatsStatus === 'pending' ? ( - - ) : null} - - - - + {!!hcaptchaUserStatsError && ( + + {getErrorMessageForError(hcaptchaUserStatsError)} + + )} + {isPending && } + + + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/enable-labeler.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/enable-labeler.page.tsx index 71b64fa101..b8315d7aa4 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/enable-labeler.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/enable-labeler.page.tsx @@ -1,81 +1,100 @@ -import Grid from '@mui/material/Grid'; -import Paper from '@mui/material/Paper'; +import { useEffect } from 'react'; import { t } from 'i18next'; -import Typography from '@mui/material/Typography'; -import { Navigate } from 'react-router-dom'; +import { Box, Paper, Stack, Typography } from '@mui/material'; + import { Button } from '@/shared/components/ui/button'; -import { PageCardError } from '@/shared/components/ui/page-card'; import { getErrorMessageForError } from '@/shared/errors'; -import { breakpoints } from '@/shared/styles/breakpoints'; -import { useAuthenticatedUser } from '@/modules/auth/hooks/use-authenticated-user'; -import { routerPaths } from '@/router/router-paths'; +import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; +import { + TopNotificationType, + useNotification, +} from '@/shared/hooks/use-notification'; import { useEnableHCaptchaLabelingMutation } from './hooks'; +import { Breadcrumbs } from './components/breadcrumbs'; export function EnableLabelerPage() { - const { user } = useAuthenticatedUser(); - const { mutate, error, isError, isPending } = + const { colorPalette } = useColorMode(); + const { showNotification } = useNotification(); + const { mutate, error, isError, isPending, reset } = useEnableHCaptchaLabelingMutation(); - if (!user.wallet_address) { - return ; - } - - if (isError) { - return ; - } + useEffect(() => { + if (isError) { + showNotification({ + message: getErrorMessageForError(error), + type: TopNotificationType.ERROR, + }); + reset(); + } + }, [error, isError, showNotification, reset]); return ( - - + + + - - - + {t('worker.enableHCaptchaLabeling.description')} - - - - - + + + + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/hcaptcha-labeling.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/hcaptcha-labeling.page.tsx index 8ff36d9b51..5429d3c0ff 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/hcaptcha-labeling.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/hcaptcha-labeling.page.tsx @@ -1,17 +1,16 @@ -import HCaptcha from '@hcaptcha/react-hcaptcha'; -import { Grid, Paper, Typography } from '@mui/material'; +import { useRef, useState } from 'react'; import { t } from 'i18next'; -import { useNavigate } from 'react-router-dom'; -import { useRef } from 'react'; +import { useNavigate, Link as RouterLink } from 'react-router-dom'; +import HCaptcha from '@hcaptcha/react-hcaptcha'; +import { Box, Divider, Paper, Stack, Typography } from '@mui/material'; + import { env } from '@/shared/env'; -import { breakpoints } from '@/shared/styles/breakpoints'; import { Counter } from '@/shared/components/ui/counter'; import { getErrorMessageForError } from '@/shared/errors'; import { getTomorrowDate } from '@/shared/helpers/date'; import { useAuthenticatedUser } from '@/modules/auth/hooks/use-authenticated-user'; import { useHCaptchaLabelingNotifications } from '@/modules/worker/hooks/use-hcaptcha-labeling-notifications'; import { useColorMode } from '@/shared/contexts/color-mode'; -import { onlyDarkModeColor } from '@/shared/styles/dark-color-palette'; import { PageCardLoader, PageCardError, @@ -21,21 +20,23 @@ import { useDailyHmtSpent, useSolveHCaptchaMutation, } from './hooks'; +import { Breadcrumbs } from './components/breadcrumbs'; +import { Button } from '@/shared/components/ui/button'; +import { routerPaths } from '@/router/router-paths'; +import { UserStatsAccordion } from './components/user-stats-accordion'; +import { useIsMobile } from '@/shared/hooks/use-is-mobile'; +import { UserStatsDrawer } from './components/user-stats-drawer'; export function HcaptchaLabelingPage() { - const { colorPalette, isDarkMode } = useColorMode(); + const [isStatsDrawerOpen, setIsStatsDrawerOpen] = useState(false); const captchaRef = useRef(null); + + const { colorPalette } = useColorMode(); const { user } = useAuthenticatedUser(); - const { onSuccess, onError } = useHCaptchaLabelingNotifications(); - const statsColor = isDarkMode - ? onlyDarkModeColor.additionalTextColor - : colorPalette.primary.light; + const navigate = useNavigate(); + const isMobile = useIsMobile(); - const resetCaptcha = () => { - if (captchaRef.current) { - captchaRef.current.resetCaptcha(); - } - }; + const { onSuccess, onError } = useHCaptchaLabelingNotifications(); const { mutate: solveHCaptchaMutation } = useSolveHCaptchaMutation({ onSuccess: () => { @@ -62,7 +63,8 @@ export function HcaptchaLabelingPage() { error: dailyHmtSpentError, } = useDailyHmtSpent(); - const navigate = useNavigate(); + const isError = isDailyHmtSpentError || isHcaptchaUserStatsError; + const isPending = isHcaptchaUserStatsPending || isDailyHmtSpentPending; const canSolveCaptcha = dailyHmtSpent && @@ -75,90 +77,189 @@ export function HcaptchaLabelingPage() { solveHCaptchaMutation({ token }); }; - if (isHcaptchaUserStatsPending || isDailyHmtSpentPending) { - return ; - } - - if (isHcaptchaUserStatsError || isDailyHmtSpentError) { - return ( - - ); - } + const resetCaptcha = () => { + if (captchaRef.current) { + captchaRef.current.resetCaptcha(); + } + }; return ( - - - + {isMobile ? ( + + ) : ( + + )} + + + - - - {t('worker.hcaptchaLabeling.description')} - - {canSolveCaptcha ? ( - - - - ) : ( - - - {t('worker.hcaptchaLabeling.noJobs')} - - - { - navigate(0); + {isPending && !isError && } + {isError && ( + + )} + {!isPending && !isError && ( + + {canSolveCaptcha ? ( + <> + + {t('worker.hcaptchaLabeling.description')} + + + + + + ) : ( + + + {t('worker.hcaptchaLabeling.noJobs')} + + - - - )} - - - - + > + + + {t('worker.hcaptchaLabeling.waitFor')} + + + { + navigate(0); + }} + /> + + + + + + {t('worker.hcaptchaLabeling.or')} + + + + + + + + )} + + )} + + + setIsStatsDrawerOpen(false)} + /> + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/hooks/enable-hcaptcha-labeling.ts b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/hooks/enable-hcaptcha-labeling.ts index fcc7c9ccdd..52140a2f4f 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/hooks/enable-hcaptcha-labeling.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/hcaptcha-labeling/hooks/enable-hcaptcha-labeling.ts @@ -1,5 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; + import { routerPaths } from '@/router/router-paths'; import { useAccessTokenRefresh } from '@/api/hooks/use-access-token-refresh'; import * as hCaptchaLabelingService from '../services/hcaptcha-labeling.service'; @@ -7,12 +8,11 @@ import * as hCaptchaLabelingService from '../services/hcaptcha-labeling.service' export function useEnableHCaptchaLabelingMutation() { const navigate = useNavigate(); const { refreshAccessTokenAsync } = useAccessTokenRefresh(); + const mutation = useMutation({ mutationFn: async () => { const result = await hCaptchaLabelingService.enableHCaptchaLabeling(); - await refreshAccessTokenAsync({ authType: 'web2' }); - return result; }, onSuccess: () => { diff --git a/packages/apps/human-app/frontend/src/modules/worker/hooks/use-get-oracles.ts b/packages/apps/human-app/frontend/src/modules/worker/hooks/use-get-oracles.ts index 345439d159..d9b328013c 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/hooks/use-get-oracles.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/hooks/use-get-oracles.ts @@ -1,12 +1,10 @@ import { useQuery } from '@tanstack/react-query'; -import { useJobsTypesOraclesFilterStore } from '../jobs/hooks'; + import * as oraclesService from '../services/oracles.service'; export function useGetOracles() { - const { selectedJobTypes } = useJobsTypesOraclesFilterStore(); - return useQuery({ - queryFn: async () => oraclesService.getOracles(selectedJobTypes), - queryKey: ['oracles', selectedJobTypes], + queryFn: async () => oraclesService.getOracles(), + queryKey: ['oracles'], }); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/explore-tasks-dialog.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/explore-tasks-dialog.tsx new file mode 100644 index 0000000000..204dfef5c1 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/explore-tasks-dialog.tsx @@ -0,0 +1,96 @@ +import { useTranslation } from 'react-i18next'; +import { Stack, Typography } from '@mui/material'; + +import { ResponsiveOverlay } from '@/shared/components/ui/responsive-overlay'; +import { Oracle } from '../../services/oracles.service'; +import { useColorMode } from '@/shared/contexts/color-mode'; +import { shortenEscrowAddress } from '@/shared/helpers/evm'; +import { useIsMobile } from '@/shared/hooks/use-is-mobile'; +import { AvailableJobsTableDesktop } from '../../jobs/available-jobs/components/desktop'; +import { AvailableJobsListMobile } from '../../jobs/available-jobs/components/mobile'; + +type Props = { + open: boolean; + onClose: () => void; + oracle: Oracle; +}; + +export function ExploreTasksDialog({ open, onClose, oracle }: Props) { + const { colorPalette } = useColorMode(); + const { t } = useTranslation(); + const isMobile = useIsMobile(); + + return ( + + + + {t('worker.jobs.jobsFor')}{' '} + + {oracle.name} + + + + {t('worker.jobs.address')}:{' '} + + {shortenEscrowAddress(oracle.address, 4, 4)} + + + + + {isMobile ? ( + + ) : ( + + )} + + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/hcaptcha-widget.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/hcaptcha-widget.tsx new file mode 100644 index 0000000000..985b8a9576 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/hcaptcha-widget.tsx @@ -0,0 +1,269 @@ +import { useMemo } from 'react'; +import { Box, Stack, Typography } from '@mui/material'; +import { Link, useNavigate } from 'react-router-dom'; +import { t } from 'i18next'; + +import { env } from '@/shared/env'; +import { Button } from '@/shared/components/ui/button'; +import { + HcaptchaDisabledIcon, + HcaptchaIcon, +} from '@/shared/components/ui/icons'; +import { useColorMode } from '@/shared/contexts/color-mode'; +import { routerPaths } from '@/router/router-paths'; +import { useAuthenticatedUser } from '@/modules/auth/hooks/use-authenticated-user'; +import { useIsMobile } from '@/shared/hooks/use-is-mobile'; +import { + useHCaptchaUserStats, + useDailyHmtSpent, +} from '../../hcaptcha-labeling/hooks'; +import { getTomorrowDate } from '@/shared/helpers/date'; +import { Counter } from '@/shared/components/ui/counter'; +import { Loader } from '@/shared/components/ui/loader'; + +const getHCaptchaPagePath = (siteKey: string | null | undefined): string => + siteKey + ? routerPaths.worker.HcaptchaLabeling + : routerPaths.worker.enableLabeler; + +const renderDescription = (color: string, isMobile: boolean) => { + let description = ''; + + if (isMobile) { + description = t('worker.hcaptchaWidget.shortDescription'); + } else { + description = t('worker.hcaptchaWidget.description'); + } + + return ( + + {description} + + ); +}; + +export function HCaptchaWidget() { + const { colorPalette } = useColorMode(); + const { user } = useAuthenticatedUser(); + const isMobile = useIsMobile(); + const navigate = useNavigate(); + + const { data: hcaptchaUserStats, isPending: isHcaptchaUserStatsPending } = + useHCaptchaUserStats(); + + const { data: dailyHmtSpent, isPending: isDailyHmtSpentPending } = + useDailyHmtSpent(); + + const hCaptchaPagePath = useMemo( + () => getHCaptchaPagePath(user.site_key), + [user.site_key] + ); + + if (isHcaptchaUserStatsPending || isDailyHmtSpentPending) { + return ( + + + + ); + } + + const isCaptchaLimitReached = + !!dailyHmtSpent && + !!hcaptchaUserStats && + hcaptchaUserStats.currentDateStats.solved >= + env.VITE_DAILY_SOLVED_CAPTCHA_LIMIT && + dailyHmtSpent.spend >= env.VITE_HMT_DAILY_SPENT_LIMIT; + + return ( + + {isCaptchaLimitReached ? ( + <> + + + + + {t('worker.hcaptchaWidget.titleDisabled')} + + {!isMobile && ( + + {t('worker.hcaptchaWidget.descriptionDisabled')} + + )} + + + + + {isMobile + ? t('worker.hcaptchaWidget.nextIn') + : t('worker.hcaptchaWidget.nextAvailable')} + + + { + navigate(0); + }} + /> + + + + ) : ( + <> + + + + + {t('worker.hcaptchaWidget.title')} + + {!isMobile && + renderDescription(colorPalette.text.auxiliary200, isMobile)} + + + + {isMobile && + renderDescription(colorPalette.text.auxiliary200, isMobile)} + + + + )} + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/index.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/index.ts index 9bc965d5af..c520d9ca1c 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/index.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/index.ts @@ -1,4 +1,4 @@ -export * from './oracles-table-job-types-select'; -export * from './oracles-table'; -export * from './oracles-table-item-mobile'; -export * from './oracles-table-desktop'; +export * from './oracles-list'; +export * from './oracle-job-card'; +export * from './hcaptcha-widget'; +export * from './explore-tasks-dialog'; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracle-job-card.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracle-job-card.tsx new file mode 100644 index 0000000000..daddde2d9a --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracle-job-card.tsx @@ -0,0 +1,296 @@ +import { useState } from 'react'; +import { + Box, + Card, + CardContent, + Chip, + Collapse, + Stack, + Tooltip, + Typography, +} from '@mui/material'; +import { t } from 'i18next'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; + +import { Oracle } from '../../services/oracles.service'; +import { + MenuIcon, + OracleAddressIcon, + OracleRewardIcon, +} from '@/shared/components/ui/icons'; +import { useColorMode } from '@/shared/contexts/color-mode'; +import { CopyToClipboardButton } from '@/shared/components/ui/copy-to-clipboard-button'; +import { useIsMobile } from '@/shared/hooks/use-is-mobile'; +import { Button } from '@/shared/components/ui/button'; +import { JOB_TYPES } from '@/shared/consts'; +import { JobType } from '@/modules/smart-contracts/EthKVStore/config'; +import { ExploreTasksDialog } from './explore-tasks-dialog'; +import { EvmAddress, RewardAmount } from '../../jobs/components'; + +function JobTypesTooltipTitle({ jobTypes }: { jobTypes: string[] }) { + return ( + + {jobTypes.map((jobType) => { + const element = JOB_TYPES.find((j) => j === jobType) as JobType; + const label = t(`jobTypeLabels.${element}`); + return ( + + {label} + + ); + })} + + ); +} + +export function OracleJobCard({ oracle }: { oracle: Oracle }) { + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isTaskTypesOpen, setIsTaskTypesOpen] = useState(false); + + const { colorPalette } = useColorMode(); + const isMobile = useIsMobile(); + + const isRewardAmountsEqual = + oracle.minRewardAmount === oracle.maxRewardAmount; + const hasTasks = oracle.nTasks > 0; + + return ( + + + + {oracle.name} + + + + + + {t('worker.oraclesList.address')}: + + + + + + + + {t('worker.oraclesList.reward')}: + + + {isRewardAmountsEqual ? ( + + ) : ( + <> + + -{' '} + + + )} + + + + + + {isMobile ? ( + + ) : ( + } + disableHoverListener={isMobile || !hasTasks} + slotProps={{ + tooltip: { + sx: { + p: 1.5, + bgcolor: colorPalette.background.paper, + boxShadow: 'none', + borderRadius: '15px', + border: `1px solid ${colorPalette.border.main}`, + }, + }, + }} + > + + + {oracle.nTasks}{' '} + {oracle.nTasks === 1 + ? t('worker.oraclesList.task') + : t('worker.oraclesList.tasks')} + + + )} + + + + + {oracle.jobTypes.map((jobType) => { + const element = JOB_TYPES.find((j) => j === jobType) as JobType; + const label = t(`jobTypeLabels.${element}`); + return ( + + ); + })} + + + + + setIsDialogOpen(false)} + oracle={oracle} + /> + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-list.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-list.tsx new file mode 100644 index 0000000000..2713b8b605 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-list.tsx @@ -0,0 +1,57 @@ +import { Grid, Stack } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +import { Loader } from '@/shared/components/ui/loader'; +import { NoRecords } from '@/shared/components/ui/no-records'; +import { PageCardError } from '@/shared/components/ui/page-card'; +import { OracleJobCard } from './oracle-job-card'; +import { Oracle } from '../../services/oracles.service'; + +type OraclesListProps = { + data: Oracle[] | undefined; + isError: boolean; + isPending: boolean; +}; + +export function OraclesList({ data, isError, isPending }: OraclesListProps) { + const { t } = useTranslation(); + + if (isPending) { + return ( + + + + ); + } + + if (isError) { + return ( + + ); + } + + if (!data?.length) { + return ; + } + + return ( + + {data.map((oracle) => ( + + + + ))} + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-desktop.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-desktop.tsx deleted file mode 100644 index 712e80b411..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-desktop.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table'; -import { createTableDarkMode } from '@/shared/styles/create-table-dark-mode'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { useOraclesTableColumns } from '../hooks/use-oracles-table-columns'; -import { type Oracle } from '../../services/oracles.service'; - -interface OraclesTableDesktopProps { - isOraclesDataPending: boolean; - isOraclesDataError: boolean; - oraclesData: Oracle[]; -} - -export function OraclesTableDesktop({ - isOraclesDataPending, - isOraclesDataError, - oraclesData, -}: Readonly) { - const { colorPalette, isDarkMode } = useColorMode(); - const tableColumns = useOraclesTableColumns(); - const table = useMaterialReactTable({ - state: { - isLoading: isOraclesDataPending, - showAlertBanner: isOraclesDataError, - }, - columns: tableColumns, - data: oraclesData, - enableColumnActions: false, - enableColumnFilters: false, - enableSorting: false, - enablePagination: false, - enableTopToolbar: false, - enableBottomToolbar: false, - muiTableHeadCellProps: { - sx: { - borderColor: colorPalette.paper.text, - }, - }, - muiTableBodyCellProps: { - sx: { - borderColor: colorPalette.paper.text, - }, - }, - muiTablePaperProps: { - sx: { - boxShadow: '0px 2px 2px 0px #E9EBFA80', - }, - }, - ...(isDarkMode ? createTableDarkMode(colorPalette) : {}), - }); - - return ; -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-item-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-item-mobile.tsx deleted file mode 100644 index 730f41db79..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-item-mobile.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Stack, Paper, type SxProps, Typography } from '@mui/material'; -import { t } from 'i18next'; -import { Chips } from '@/shared/components/ui/chips'; -import { TableButton } from '@/shared/components/ui/table-button'; -import { ListItem } from '@/shared/components/ui/list-item'; -import type { JobType } from '@/modules/smart-contracts/EthKVStore/config'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { useSelectOracleNavigation } from '../hooks/use-select-oracle-navigation'; -import { EvmAddress } from '../../jobs/components'; -import { type Oracle } from '../../services/oracles.service'; - -interface OraclesTableItemMobileProps { - oracle: Oracle; -} - -const styles: SxProps = { - px: 2, - py: 4, - mb: 2.5, - borderRadius: '20px', - display: 'flex', - flexDirection: 'column', - gap: 2, - boxShadow: 'none', -}; - -export function OraclesTableItemMobile({ - oracle, -}: Readonly) { - const { colorPalette } = useColorMode(); - const { selectOracle } = useSelectOracleNavigation(); - - return ( - - - - - - - {oracle.name} - - - - t(`jobTypeLabels.${jobType as JobType}`) - )} - /> - - - { - selectOracle(oracle); - }} - > - {t('worker.oraclesTable.seeJobs')} - - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-job-types-select.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-job-types-select.tsx deleted file mode 100644 index d41c0a9f36..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table-job-types-select.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { t } from 'i18next'; -import { FormProvider, useForm } from 'react-hook-form'; -import { useEffect } from 'react'; -import { Grid } from '@mui/material'; -import { useIsMobile } from '@/shared/hooks/use-is-mobile'; -import { MultiSelect } from '@/shared/components/data-entry/multi-select'; -import { JOB_TYPES } from '@/shared/consts'; -import { useJobsTypesOraclesFilterStore } from '../../jobs/hooks'; - -export function OraclesTableJobTypesSelect() { - const isMobile = useIsMobile(); - const { selectJobType } = useJobsTypesOraclesFilterStore(); - const methods = useForm<{ jobType: string[] }>({ - defaultValues: { - jobType: [], - }, - }); - - const selectedJobType = methods.watch('jobType'); - - useEffect(() => { - selectJobType(selectedJobType); - }, [selectJobType, selectedJobType]); - - return ( - - -
- ({ - label: t(`jobTypeLabels.${jobType}`), - value: jobType, - }))} - /> - -
-
- ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table.tsx deleted file mode 100644 index 9e19e8af0e..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracles-table.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Stack, Grid } from '@mui/material'; -import { useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useIsMobile } from '@/shared/hooks/use-is-mobile'; -import { Loader } from '@/shared/components/ui/loader'; -import { NoRecords } from '@/shared/components/ui/no-records'; -import { useGetOraclesNotifications } from '@/modules/worker/hooks/use-get-oracles-notifications'; -import { PageCardError } from '@/shared/components/ui/page-card'; -import { useGetOracles } from '../../hooks'; -import { OraclesTableItemMobile } from './oracles-table-item-mobile'; -import { OraclesTableDesktop } from './oracles-table-desktop'; - -export function OraclesTable() { - const isMobile = useIsMobile(); - const { t } = useTranslation(); - const { onError } = useGetOraclesNotifications(); - const { - data: oraclesData, - isError: isOraclesDataError, - isPending: isOraclesDataPending, - error: oraclesDataError, - } = useGetOracles(); - - useEffect(() => { - if (oraclesDataError) { - onError(oraclesDataError); - } - }, [oraclesDataError, onError]); - - if (isOraclesDataPending) { - return ( - - - - ); - } - - if (isOraclesDataError) { - return ( - - ); - } - - if (!oraclesData.length) { - return ; - } - - return isMobile ? ( - - {oraclesData.map((oracle) => ( - - ))} - - ) : ( - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/index.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/index.ts index 7db6869470..c7ea564552 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/index.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/index.ts @@ -1,2 +1 @@ -export * from './is-hcaptcha-oracle'; export * from './should-navigate-to-registration'; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/is-hcaptcha-oracle.spec.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/is-hcaptcha-oracle.spec.ts deleted file mode 100644 index 117e9daaf9..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/is-hcaptcha-oracle.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { faker } from '@faker-js/faker'; -import { env } from '@/shared/env'; -import { isHCaptchaOracle } from './is-hcaptcha-oracle'; - -vi.mock('@/shared/env', () => ({ - env: { - VITE_H_CAPTCHA_ORACLE_ADDRESS: '0x1234567890abcdef1234567890abcdef12345678', - }, -})); - -describe('isHCaptchaOracle Helper', () => { - it('should return true when the address matches the hCaptcha oracle address', () => { - const result = isHCaptchaOracle( - '0x1234567890abcdef1234567890abcdef12345678' - ); - expect(result).toBe(true); - }); - - it('should return false when the address does not match the hCaptcha oracle address', () => { - const result = isHCaptchaOracle(faker.finance.ethereumAddress()); - expect(result).toBe(false); - }); - - it('should handle case sensitivity correctly', () => { - const mixedCaseAddress = env.VITE_H_CAPTCHA_ORACLE_ADDRESS.replace( - 'abcdef', - 'ABCDEF' - ); - - const result = isHCaptchaOracle(mixedCaseAddress); - expect(result).toBe(true); - }); - - it('should handle empty strings', () => { - const result = isHCaptchaOracle(''); - expect(result).toBe(false); - }); -}); diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/is-hcaptcha-oracle.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/is-hcaptcha-oracle.ts deleted file mode 100644 index 9b5cefc8a3..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/is-hcaptcha-oracle.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { env } from '@/shared/env'; - -export const isHCaptchaOracle = (address: string): boolean => - address.toLowerCase() === env.VITE_H_CAPTCHA_ORACLE_ADDRESS.toLowerCase(); diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/should-navigate-to-registration.spec.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/should-navigate-to-registration.spec.ts index 133385f694..a1eb43e1e1 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/should-navigate-to-registration.spec.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/helpers/should-navigate-to-registration.spec.ts @@ -12,6 +12,10 @@ describe('shouldNavigateToRegistration Helper', () => { url: faker.internet.url(), jobTypes: [faker.word.noun()], name: faker.company.name(), + nTasks: faker.number.int({ min: 1, max: 10 }), + minRewardAmount: faker.finance.amount(), + maxRewardAmount: faker.finance.amount(), + rewardToken: faker.finance.currencyCode(), }; const oracleWithNoRegistration: Oracle = { diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/hooks/use-select-oracle-navigation.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/hooks/use-select-oracle-navigation.ts index 1c81650c57..9879955be2 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/hooks/use-select-oracle-navigation.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/hooks/use-select-oracle-navigation.ts @@ -1,26 +1,15 @@ +import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useCallback, useMemo } from 'react'; -import { useAuthenticatedUser } from '@/modules/auth/hooks/use-authenticated-user'; + import { routerPaths } from '@/router/router-paths'; -import { shouldNavigateToRegistration, isHCaptchaOracle } from '../helpers'; +import { shouldNavigateToRegistration } from '../helpers'; import { type Oracle } from '../../services/oracles.service'; import { useGetRegistrationDataInOracles } from './use-get-registration-data-oracles'; -const getHCaptchaPagePath = (siteKey: string | null | undefined): string => - siteKey - ? routerPaths.worker.HcaptchaLabeling - : routerPaths.worker.enableLabeler; - export const useSelectOracleNavigation = () => { const navigate = useNavigate(); - const { user } = useAuthenticatedUser(); const { data } = useGetRegistrationDataInOracles(); - const hCaptchaPagePath = useMemo( - () => getHCaptchaPagePath(user.site_key), - [user.site_key] - ); - const selectOracle = useCallback( (oracle: Oracle) => { if (shouldNavigateToRegistration(oracle, data)) { @@ -30,16 +19,11 @@ export const useSelectOracleNavigation = () => { return; } - if (isHCaptchaOracle(oracle.address)) { - navigate(hCaptchaPagePath); - return; - } - navigate(`${routerPaths.worker.jobs}/${oracle.address}`, { state: { oracle }, }); }, - [data, navigate, hCaptchaPagePath] + [data, navigate] ); return { selectOracle }; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/jobs-discovery.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/jobs-discovery.page.tsx index 6fcb56287a..8645343921 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/jobs-discovery.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/jobs-discovery.page.tsx @@ -1,25 +1,106 @@ -import { Paper, Stack } from '@mui/material'; +import { useEffect } from 'react'; +import { Box, Stack, Typography } from '@mui/material'; +import { t } from 'i18next'; + +import { OraclesList } from './components/oracles-list'; +import { HCaptchaWidget } from './components/hcaptcha-widget'; +import { useColorMode } from '@/shared/contexts/color-mode'; import { useIsMobile } from '@/shared/hooks/use-is-mobile'; -import { OraclesTableJobTypesSelect, OraclesTable } from './components'; +import { useGetOracles } from '../hooks/use-get-oracles'; +import { useGetOraclesNotifications } from '../hooks/use-get-oracles-notifications'; +import { JobsSwitcherMobile } from '@/router/components/layout/protected/jobs-switcher-mobile'; + +const bull = ( + + • + +); export function JobsDiscoveryPage() { + const { colorPalette } = useColorMode(); const isMobile = useIsMobile(); + const { onError } = useGetOraclesNotifications(); + const { + data: oraclesData, + isError, + isPending, + isSuccess, + error, + } = useGetOracles(); + + useEffect(() => { + if (error) { + onError(error); + } + }, [error, onError]); + + const oraclesCount = oraclesData?.length ?? 0; + return ( - - + + + + {isMobile && ( + + + + )} + - - - + + + {t('worker.oraclesList.jobOracles')} + + {isSuccess && ( + <> + {bull} + + {oraclesCount}{' '} + {oraclesCount === 1 + ? t('worker.oraclesList.source') + : t('worker.oraclesList.sources')} + + + )} + + + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/available-jobs-filter-modal.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/available-jobs-filter-modal.tsx deleted file mode 100644 index f66e008a16..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/available-jobs-filter-modal.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import Box from '@mui/material/Box'; -import Drawer from '@mui/material/Drawer'; -import CssBaseline from '@mui/material/CssBaseline'; -import { Divider, IconButton, Stack, Typography } from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import CloseIcon from '@mui/icons-material/Close'; -import { HumanLogoIcon } from '@/shared/components/ui/icons'; -import { useHandleMainNavIconClick } from '@/shared/hooks/use-handle-main-nav-icon-click'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { - AvailableJobsNetworkFilter, - AvailableJobsJobTypeFilter, -} from './components'; -import { AvailableJobsRewardAmountSortMobile } from './components/mobile/available-jobs-reward-amount-sort-mobile'; - -interface AvailableJobsFilterModalProps { - chainIdsEnabled: number[]; - close: () => void; -} -export function AvailableJobsFilterModal({ - chainIdsEnabled, - close, -}: Readonly) { - const handleMainNavIconClick = useHandleMainNavIconClick(); - const { colorPalette } = useColorMode(); - const { t } = useTranslation(); - - return ( - - - - - - - - - - - - - - {t('worker.jobs.mobileFilterDrawer.filters')} - - - - {t('worker.jobs.mobileFilterDrawer.sortBy')} - - - - {t('worker.jobs.mobileFilterDrawer.filters')} - - - - {t('worker.jobs.network')} - - - - - - - - {t('worker.jobs.jobType')} - - - - - - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/available-jobs-view.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/available-jobs-view.tsx deleted file mode 100644 index ac645716d3..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/available-jobs-view.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useIsMobile } from '@/shared/hooks/use-is-mobile'; -import { AvailableJobsTableDesktop } from './components/desktop'; -import { AvailableJobsTableMobile } from './components/mobile'; - -interface AvailableJobsTableView { - chainIdsEnabled: number[]; -} - -export function AvailableJobsView({ chainIdsEnabled }: AvailableJobsTableView) { - const isMobile = useIsMobile(); - - return isMobile ? ( - - ) : ( - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-job-type-filter.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-job-type-filter.tsx deleted file mode 100644 index 893ed6a4a4..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-job-type-filter.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { useMemo } from 'react'; -import { Filtering } from '@/shared/components/ui/table/table-header-menu/filtering'; -import { JOB_TYPES } from '@/shared/consts'; -import { useJobsFilterStore } from '../../hooks'; - -export function AvailableJobsJobTypeFilter({ - showClearButton = false, - showTitle = false, -}) { - const { t } = useTranslation(); - const { setFilterParams, filterParams } = useJobsFilterStore(); - - const filteringOptions = useMemo( - () => - JOB_TYPES.map((jobType) => ({ - name: t(`jobTypeLabels.${jobType}`), - option: jobType, - })), - [t] - ); - - const handleClear = () => { - setFilterParams({ job_type: undefined }); - }; - - const handleFilterChange = (jobType: string) => { - setFilterParams({ job_type: jobType }); - }; - - return ( - option === filterParams.job_type} - setFiltering={handleFilterChange} - showClearButton={showClearButton} - showTitle={showTitle} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-network-filter.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-network-filter.tsx deleted file mode 100644 index 79399bceac..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-network-filter.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Filtering } from '@/shared/components/ui/table/table-header-menu/filtering'; -import { useGetAllNetworks, useJobsFilterStore } from '../../hooks'; - -interface AvailableJobsNetworkFilterProps { - chainIdsEnabled: number[]; - showClearButton?: boolean; - showTitle?: boolean; -} - -export function AvailableJobsNetworkFilter({ - chainIdsEnabled, - showClearButton = false, - showTitle = false, -}: Readonly) { - const { setFilterParams, filterParams } = useJobsFilterStore(); - const { allNetworks } = useGetAllNetworks(chainIdsEnabled); - - const handleClear = () => { - setFilterParams({ chain_id: undefined }); - }; - - const handleFilterChange = (chainId: number) => { - setFilterParams({ chain_id: chainId }); - }; - - return ( - option === filterParams.chain_id} - setFiltering={handleFilterChange} - showClearButton={showClearButton} - showTitle={showTitle} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-reward-amount-sort.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-reward-amount-sort.tsx deleted file mode 100644 index b2825107b6..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/available-jobs-reward-amount-sort.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { t } from 'i18next'; -import { Sorting } from '@/shared/components/ui/table/table-header-menu/sorting'; -import { useJobsFilterStore } from '../../hooks'; -import { SortDirection, SortField } from '../../types'; - -export function AvailableJobsRewardAmountSort() { - const { setFilterParams } = useJobsFilterStore(); - - const sortAscRewardAmount = () => { - setFilterParams({ - sort_field: SortField.REWARD_AMOUNT, - sort: SortDirection.ASC, - }); - }; - - const sortDescRewardAmount = () => { - setFilterParams({ - sort_field: SortField.REWARD_AMOUNT, - sort: SortDirection.DESC, - }); - }; - - return ( - { - setFilterParams({ - sort_field: undefined, - sort: undefined, - }); - }} - sortingOptions={[ - { - label: t('worker.jobs.sortDirection.fromHighest'), - sortCallback: sortDescRewardAmount, - }, - { - label: t('worker.jobs.sortDirection.fromLowest'), - sortCallback: sortAscRewardAmount, - }, - ]} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/desktop/available-jobs-table-desktop.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/desktop/available-jobs-table-desktop.tsx index b20ecf2e28..0a9373e4d1 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/desktop/available-jobs-table-desktop.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/desktop/available-jobs-table-desktop.tsx @@ -1,37 +1,32 @@ +import { useEffect, useMemo } from 'react'; import { MaterialReactTable, useMaterialReactTable, } from 'material-react-table'; -import { t } from 'i18next'; -import { useEffect, useMemo } from 'react'; + import { createTableDarkMode } from '@/shared/styles/create-table-dark-mode'; import { useColorMode } from '@/shared/contexts/color-mode'; import { useJobsFilterStore } from '../../../hooks'; -import { EscrowAddressSearchForm } from '../../../components'; import { useGetAvailableJobsData } from '../../hooks/use-get-available-jobs-data'; import { useGetAvailableJobsColumns } from '../../hooks'; import { useAvailableJobsPagination } from '../../hooks/use-available-jobs-pagination'; -interface AvailableJobsTableProps { - chainIdsEnabled: number[]; -} - export function AvailableJobsTableDesktop({ - chainIdsEnabled, -}: Readonly) { + oracleAddress, +}: { + oracleAddress: string; +}) { const { colorPalette, isDarkMode } = useColorMode(); - const { data: tableData, status: tableStatus } = useGetAvailableJobsData(); - const { - setSearchEscrowAddress, - setPageParams, - filterParams, - resetFilterParams, - } = useJobsFilterStore(); + const { data: tableData, status: tableStatus } = useGetAvailableJobsData({ + oracleAddress, + }); + const { setPageParams, filterParams, resetFilterParams } = + useJobsFilterStore(); const { paginationState, setPaginationState } = useAvailableJobsPagination({ setPageParams, filterParams, }); - const columns = useGetAvailableJobsColumns(chainIdsEnabled); + const columns = useGetAvailableJobsColumns(); const memoizedTableDataResults = useMemo( () => tableData?.results ?? [], @@ -52,51 +47,85 @@ export function AvailableJobsTableDesktop({ showAlertBanner: tableStatus === 'error', pagination: paginationState, }, - enablePagination: Boolean(tableData?.total_pages), + enablePagination: !!tableData?.total_pages, manualPagination: true, onPaginationChange: setPaginationState, muiPaginationProps: { SelectProps: { sx: { + '.MuiSelect-select': { + color: colorPalette.text.auxiliary100, + }, '.MuiSelect-icon': { ':hover': { backgroundColor: 'blue', }, - fill: colorPalette.text.primary, + fill: colorPalette.text.auxiliary100, }, }, }, rowsPerPageOptions: [5, 10], }, + muiBottomToolbarProps: { + sx: { + bgcolor: colorPalette.background.paper, + boxShadow: 'none', + color: colorPalette.text.auxiliary100, + }, + }, pageCount: tableData?.total_pages ?? -1, rowCount: tableData?.total_results, enableColumnActions: false, enableColumnFilters: false, - enableSorting: true, - manualSorting: true, - renderTopToolbar: () => ( - { - setSearchEscrowAddress(address); - }} - /> - ), + enableSorting: false, + manualSorting: false, + renderTopToolbar: false, + muiTablePaperProps: { + sx: { + boxShadow: 'none', + }, + }, + muiTableHeadProps: { + sx: { + backgroundColor: colorPalette.background.default, + }, + }, + muiTableHeadRowProps: { + sx: { + backgroundColor: 'inherit', + boxShadow: 'none', + }, + }, muiTableHeadCellProps: { sx: { - borderColor: colorPalette.paper.text, + paddingTop: '8px', + paddingBottom: '8px', + paddingLeft: '12px', + paddingRight: '12px', + borderColor: colorPalette.background.paper, + color: colorPalette.text.auxiliary200, + typography: 'body1', + fontWeight: 500, + '& .Mui-TableHeadCell-Content': { + justifyContent: 'center', + textAlign: 'center', + }, }, }, muiTableBodyCellProps: { + align: 'center', sx: { - borderColor: colorPalette.paper.text, + textAlign: 'center', + borderBottom: `1px solid ${colorPalette.border.main}`, }, }, - muiTablePaperProps: { + muiTableBodyRowProps: { sx: { - boxShadow: '0px 2px 2px 0px #E9EBFA80', + bgcolor: colorPalette.background.paper, + borderBottom: `1px solid ${colorPalette.border.main}`, + '&:last-of-type': { + borderBottom: 'none', + }, }, }, ...(isDarkMode ? createTableDarkMode(colorPalette) : {}), diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/index.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/index.ts deleted file mode 100644 index 9d9f97fed7..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './available-jobs-job-type-filter'; -export * from './available-jobs-network-filter'; -export * from './available-jobs-reward-amount-sort'; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-assign-job-button-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-assign-job-button-mobile.tsx deleted file mode 100644 index 3acd1102e0..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-assign-job-button-mobile.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { t } from 'i18next'; -import { TableButton } from '@/shared/components/ui/table-button'; -import { useJobsNotifications } from '../../../hooks'; -import { useAssignJobMutation } from '../../hooks/use-assign-job'; -import { type AssignJobBody } from '../../../types'; - -export function AvailableJobsAssignJobButtonMobile({ - assignJobPayload, -}: Readonly<{ - assignJobPayload: AssignJobBody; -}>) { - const { onJobAssignmentError, onJobAssignmentSuccess } = - useJobsNotifications(); - - const { mutate: assignJobMutation, isPending } = useAssignJobMutation( - { - onSuccess: onJobAssignmentSuccess, - onError: onJobAssignmentError, - }, - [`assignJob-${assignJobPayload.escrow_address}`] - ); - - return ( - assignJobMutation(assignJobPayload)} - size="small" - sx={{ - marginTop: '15px', - }} - type="button" - variant="contained" - > - {t('worker.jobs.selectJob')} - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-card.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-card.tsx new file mode 100644 index 0000000000..09a97f8501 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-card.tsx @@ -0,0 +1,186 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Chip, Paper, Stack, Typography } from '@mui/material'; + +import { AvailableJob } from '../../../types'; +import { JobType } from '@/modules/smart-contracts/EthKVStore/config'; +import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; +import { + OracleAddressIcon, + OracleRewardIcon, +} from '@/shared/components/ui/icons'; +import { EvmAddress, RewardAmount } from '../../../components'; +import { Button } from '@/shared/components/ui/button'; +import { useJobsNotifications } from '../../../hooks'; +import { useAssignJobMutation } from '../../hooks'; +import { BaseDrawer } from '@/shared/components/ui/base-drawer'; +import { ChainIcon } from '@/shared/components/ui/chain-icon'; + +function DescriptionDrawer({ + open, + onClose, + description, +}: { + open: boolean; + onClose: () => void; + description: string | undefined; +}) { + const { t } = useTranslation(); + const { colorPalette } = useColorMode(); + + return ( + + + {t('worker.jobs.taskDescriptionTitle')} + + + {description} + + + ); +} + +export function AvailableJobsCard({ job }: { job: AvailableJob }) { + const [showDescriptionDialog, setShowDescriptionDialog] = useState(false); + + const { t } = useTranslation(); + const { colorPalette } = useColorMode(); + + const { onJobAssignmentError, onJobAssignmentSuccess } = + useJobsNotifications(); + + const { mutate: assignJobMutation, isPending } = useAssignJobMutation( + { + onSuccess: onJobAssignmentSuccess, + onError: onJobAssignmentError, + }, + [`assignJob-${job.escrow_address}`] + ); + + return ( + + + + + + + + + + + {t('worker.oraclesList.address')}: + + + + + + + {t('worker.oraclesList.reward')}: + + + + + + + {!!job.job_description && ( + + )} + + + setShowDescriptionDialog(false)} + description={job.job_description} + /> + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-list-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-list-mobile.tsx index da1da55a87..25945747a9 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-list-mobile.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-list-mobile.tsx @@ -1,37 +1,29 @@ -import { Grid, List, Paper, Stack, Typography } from '@mui/material'; -import { useTranslation } from 'react-i18next'; import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Stack } from '@mui/material'; + import { Button } from '@/shared/components/ui/button'; import { Alert } from '@/shared/components/ui/alert'; -import { getNetworkName } from '@/modules/smart-contracts/get-network-name'; import { Loader } from '@/shared/components/ui/loader'; -import { Chip } from '@/shared/components/ui/chip'; -import { ListItem } from '@/shared/components/ui/list-item'; -import { type JobType } from '@/modules/smart-contracts/EthKVStore/config'; -import { useColorMode } from '@/shared/contexts/color-mode'; import { getErrorMessageForError } from '@/shared/errors'; import { useCombinePages } from '@/shared/hooks'; import { useJobsFilterStore } from '../../../hooks'; import { useInifiniteGetAvailableJobsData } from '../../hooks/use-get-available-jobs-data'; -import { EvmAddress, RewardAmount } from '../../../components'; import { type AvailableJob } from '../../../types'; -import { AvailableJobsAssignJobButtonMobile } from './available-jobs-assign-job-button-mobile'; +import { AvailableJobsCard } from './available-jobs-card'; -export function AvailableJobsListMobile() { +export function AvailableJobsListMobile({ + oracleAddress, +}: { + oracleAddress: string; +}) { const { t } = useTranslation(); - const { colorPalette } = useColorMode(); const { filterParams, setPageParams, resetFilterParams } = useJobsFilterStore(); - const { - data: tableData, - status: tableStatus, - isError: isTableError, - error: tableError, - fetchNextPage, - hasNextPage, - } = useInifiniteGetAvailableJobsData(); + const { data, isPending, isError, error, fetchNextPage, hasNextPage } = + useInifiniteGetAvailableJobsData({ oracleAddress }); - const allPages = useCombinePages(tableData, filterParams.page); + const allPages = useCombinePages(data); useEffect(() => { return () => { @@ -40,86 +32,21 @@ export function AvailableJobsListMobile() { }, [resetFilterParams]); return ( - - {isTableError && ( + + {isError && ( - {getErrorMessageForError(tableError)} + {getErrorMessageForError(error)} )} - {tableStatus === 'pending' && ( - + {isPending && ( + )} {allPages.map((d) => ( - - - - - - - {d.job_description} - - - - - - - - - - - - - - - {getNetworkName(d.chain_id)} - - - - - - - - - - - - - - + ))} - {hasNextPage ? ( + {hasNextPage && ( - ) : null} + )} ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-reward-amount-sort-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-reward-amount-sort-mobile.tsx deleted file mode 100644 index 3f78e5fec9..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-reward-amount-sort-mobile.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { t } from 'i18next'; -import Typography from '@mui/material/Typography'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { useJobsFilterStore } from '../../../hooks'; -import { Sorting } from '../../../components'; -import { SortDirection, SortField } from '../../../types'; - -export function AvailableJobsRewardAmountSortMobile() { - const { setFilterParams, filterParams } = useJobsFilterStore(); - const { colorPalette } = useColorMode(); - - return ( - - {t('worker.jobs.rewardAmount')} - - } - fromHighestSelected={ - filterParams.sort_field === SortField.REWARD_AMOUNT && - filterParams.sort === SortDirection.DESC - } - sortFromHighest={() => { - setFilterParams({ - sort: SortDirection.DESC, - sort_field: SortField.REWARD_AMOUNT, - }); - }} - fromLowestSelected={ - filterParams.sort_field === SortField.REWARD_AMOUNT && - filterParams.sort === SortDirection.ASC - } - sortFromLowest={() => { - setFilterParams({ - sort: SortDirection.ASC, - sort_field: SortField.REWARD_AMOUNT, - }); - }} - clear={() => { - setFilterParams({ - sort: undefined, - sort_field: undefined, - }); - }} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-table-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-table-mobile.tsx deleted file mode 100644 index fdefb14217..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/available-jobs-table-mobile.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { Button } from '@/shared/components/ui/button'; -import { FiltersButtonIcon } from '@/shared/components/ui/icons'; -import { useJobsFilterStore } from '../../../hooks'; -import { EscrowAddressSearchForm } from '../../../components'; -import { useAvailableJobsFilterModal } from '../../hooks/use-available-jobs-filter-modal'; -import { AvailableJobsListMobile } from './available-jobs-list-mobile'; - -export function AvailableJobsTableMobile() { - const { t } = useTranslation(); - const { setSearchEscrowAddress } = useJobsFilterStore(); - const { openModal } = useAvailableJobsFilterModal(); - - return ( - <> - - - - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/index.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/index.ts index 04265ea0ed..f6d103b75d 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/index.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/components/mobile/index.ts @@ -1,2 +1 @@ -export * from './available-jobs-reward-amount-sort-mobile'; -export * from './available-jobs-table-mobile'; +export * from './available-jobs-list-mobile'; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-available-jobs-filter-modal.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-available-jobs-filter-modal.tsx deleted file mode 100644 index f90e919961..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-available-jobs-filter-modal.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useModal } from '@/shared/contexts/modal-context'; -import { useGetUiConfig } from '@/shared/hooks'; -import { AvailableJobsFilterModal } from '../available-jobs-filter-modal'; - -export function useAvailableJobsFilterModal() { - const { openModal, closeModal } = useModal(); - const { data: uiConfigData } = useGetUiConfig(); - - return { - openModal: () => { - openModal({ - content: ( - - ), - }); - }, - }; -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-get-available-jobs-columns.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-get-available-jobs-columns.tsx index 753ad47200..9be4db2351 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-get-available-jobs-columns.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-get-available-jobs-columns.tsx @@ -1,39 +1,33 @@ +import { useMemo } from 'react'; import type { MRT_ColumnDef } from 'material-react-table'; import { t } from 'i18next'; -import { Grid } from '@mui/material'; -import { useMemo } from 'react'; -import { getNetworkName } from '@/modules/smart-contracts/get-network-name'; -import { Chip } from '@/shared/components/ui/chip'; +import { Box, Chip, Stack, Typography } from '@mui/material'; + import { TableButton } from '@/shared/components/ui/table-button'; -import { TableHeaderCell } from '@/shared/components/ui/table/table-header-cell'; import type { JobType } from '@/modules/smart-contracts/EthKVStore/config'; import { useJobsNotifications } from '../../hooks'; import { EvmAddress, RewardAmount } from '../../components'; -import { - AvailableJobsNetworkFilter, - AvailableJobsRewardAmountSort, - AvailableJobsJobTypeFilter, -} from '../components'; import { type AvailableJob } from '../../types'; import { useAssignJobMutation } from './use-assign-job'; +import { Button } from '@/shared/components/ui/button'; +import { useColorMode } from '@/shared/contexts/color-mode'; +import { useModal } from '@/shared/contexts/modal-context'; +import { ChainIcon } from '@/shared/components/ui/chain-icon'; + +const COL_SIZE = 50; +const COL_SIZE_MD = 100; +const COL_SIZE_LG = 150; +const COL_SIZE_XL = 250; -const COL_SIZE = 100; -const COL_SIZE_LG = 200; +export const useGetAvailableJobsColumns = (): MRT_ColumnDef[] => { + const { colorPalette } = useColorMode(); + const { openModal } = useModal(); -export const useGetAvailableJobsColumns = ( - chainIdsEnabled: number[] -): MRT_ColumnDef[] => { return useMemo( () => [ - { - accessorKey: 'job_description', - header: t('worker.jobs.jobDescription'), - size: COL_SIZE, - enableSorting: false, - }, { accessorKey: 'escrow_address', - header: t('worker.jobs.escrowAddress'), + header: t('worker.jobs.address'), size: COL_SIZE, enableSorting: false, Cell: (props) => { @@ -46,26 +40,13 @@ export const useGetAvailableJobsColumns = ( size: COL_SIZE, enableSorting: false, Cell: (props) => { - return getNetworkName(props.row.original.chain_id); + return ; }, - Header: ( - - } - /> - ), }, { accessorKey: 'reward_amount', - header: t('worker.jobs.rewardAmount'), - size: COL_SIZE, + header: t('worker.jobs.reward'), + size: COL_SIZE_MD, enableSorting: false, Cell: (props) => { const { reward_amount, reward_token } = props.row.original; @@ -73,16 +54,10 @@ export const useGetAvailableJobsColumns = ( ); }, - Header: ( - } - /> - ), }, { accessorKey: 'job_type', @@ -91,26 +66,31 @@ export const useGetAvailableJobsColumns = ( enableSorting: false, Cell: ({ row }) => { const label = t(`jobTypeLabels.${row.original.job_type as JobType}`); - return ; + return ( + + ); }, - Header: ( - - } - /> - ), }, { accessorKey: 'escrow_address', id: 'selectJobAction', - header: '', - size: COL_SIZE, + header: t('worker.jobs.action'), + size: COL_SIZE_XL, enableSorting: false, Cell: (props) => { - const { escrow_address, chain_id } = props.row.original; + const { escrow_address, chain_id, job_description } = + props.row.original; const { onJobAssignmentError, onJobAssignmentSuccess } = useJobsNotifications(); const { mutate: assignJobMutation, isPending } = useAssignJobMutation( @@ -120,21 +100,53 @@ export const useGetAvailableJobsColumns = ( }, [`assignJob-${escrow_address}`] ); + const description = job_description?.trim() || ''; + + const handleOpenTaskDescription = () => { + openModal({ + content: ( + + + {t('worker.jobs.jobDescription')} + + + {description} + + + ), + }); + }; return ( - + + {description && ( + + )} assignJobMutation({ escrow_address, chain_id })} > - {t('worker.jobs.selectJob')} + {t('worker.jobs.claimTask')} - + ); }, }, ], - [chainIdsEnabled] + [colorPalette, openModal] ); }; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-get-available-jobs-data.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-get-available-jobs-data.ts index 077ffc0408..643044b2e1 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-get-available-jobs-data.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/hooks/use-get-available-jobs-data.ts @@ -1,34 +1,31 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; -import { useParams } from 'react-router-dom'; -import { useJobsFilterStore } from '../../hooks'; + import * as jobsService from '../../services/jobs.service'; import { type AvailableJobsSuccessResponse } from '../../types'; -export function useGetAvailableJobsData() { - const { filterParams } = useJobsFilterStore(); - const { address: oracle_address } = useParams<{ address: string }>(); - const queryParams = { ...filterParams, oracle_address: oracle_address ?? '' }; +type Props = { + oracleAddress?: string; +}; + +export function useGetAvailableJobsData({ oracleAddress }: Props) { + const queryParams = { oracle_address: oracleAddress }; return useQuery({ queryKey: ['availableJobs', queryParams], queryFn: async ({ signal }) => jobsService.fetchAvailableJobs({ queryParams, signal }), + enabled: !!oracleAddress, }); } -export function useInifiniteGetAvailableJobsData() { - const { filterParams } = useJobsFilterStore(); - const { address: oracleAddress } = useParams<{ address: string }>(); - - const queryParams = { - ...filterParams, - oracle_address: oracleAddress ?? '', - }; +export function useInifiniteGetAvailableJobsData({ oracleAddress }: Props) { + const queryParams = { oracle_address: oracleAddress }; return useInfiniteQuery({ queryKey: ['availableJobsInfinite', queryParams], queryFn: async ({ signal }) => jobsService.fetchAvailableJobs({ queryParams, signal }), + enabled: !!oracleAddress, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.total_pages - 1 <= lastPage.page ? undefined : lastPage.page, diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/index.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/index.ts deleted file mode 100644 index 2d3fbc8ac2..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/available-jobs/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './available-jobs-view'; -export * from './available-jobs-filter-modal'; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/escrow-address-search-form.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/escrow-address-search-form.tsx deleted file mode 100644 index 472dbc17ca..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/escrow-address-search-form.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import Search from '@mui/icons-material/Search'; -import InputAdornment from '@mui/material/InputAdornment'; -import { FormProvider, useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { z } from 'zod'; -import { useEffect, useMemo } from 'react'; -import Grid from '@mui/material/Grid'; -import debounce from 'lodash/debounce'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { Input } from '@/shared/components/data-entry/input'; -import { addressSchemaOrEmptyString } from '@/shared/schemas'; -import { useIsMobile } from '@/shared/hooks/use-is-mobile'; - -interface SearchFormProps { - label: string; - placeholder: string; - columnId: string; - fullWidth?: boolean; - updater: (fieldValue: string) => void; -} - -export function EscrowAddressSearchForm({ - label, - placeholder, - updater, - fullWidth = false, -}: Readonly) { - const isMobile = useIsMobile(); - const { colorPalette } = useColorMode(); - const methods = useForm({ - defaultValues: { - searchValue: '', - }, - resolver: zodResolver( - z.object({ searchValue: addressSchemaOrEmptyString }) - ), - }); - - const debouncedUpdater = useMemo( - () => - debounce(async (value: string) => { - const isValid = await methods.trigger('searchValue'); - if (isValid) { - updater(value); - } - }, 500), - [updater, methods] - ); - - useEffect(() => { - return () => { - debouncedUpdater.cancel(); - }; - }, [debouncedUpdater]); - - useEffect(() => { - const subscription = methods.watch((value, { name }) => { - if (name === 'searchValue') { - void debouncedUpdater(value.searchValue ?? ''); - } - }); - return () => { - subscription.unsubscribe(); - }; - }, [debouncedUpdater, methods]); - - return ( - - - - - - ), - }, - }} - onChange={(e) => { - methods.setValue('searchValue', e.target.value); - }} - placeholder={placeholder} - sx={{ - width: fullWidth ? '100%' : '362px', - margin: fullWidth ? '0' : '1rem', - }} - onBlur={() => { - void methods.trigger('searchValue'); - }} - /> - - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/evm-address.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/evm-address.tsx index 1370dd854a..6d2e704c0c 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/evm-address.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/evm-address.tsx @@ -1,7 +1,6 @@ -import Tooltip from '@mui/material/Tooltip'; -import Typography from '@mui/material/Typography'; +import { Tooltip, Typography } from '@mui/material'; + import { shortenEscrowAddress } from '@/shared/helpers/evm'; -import { breakpoints } from '@/shared/styles/breakpoints'; import { useIsMobile } from '@/shared/hooks/use-is-mobile'; import { useColorMode } from '@/shared/contexts/color-mode'; @@ -9,19 +8,13 @@ export function EvmAddress({ address }: { address: string }) { const { colorPalette } = useColorMode(); const isMobile = useIsMobile(); - const shortAddress = isMobile - ? shortenEscrowAddress(address, 4, 4) - : shortenEscrowAddress(address); + const shortAddress = shortenEscrowAddress(address, 4, 4); return ( {shortAddress} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/index.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/index.ts index ca383549de..4cffc4caf3 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/index.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/index.ts @@ -1,8 +1,5 @@ export * from './evm-address'; -export * from './jobs-tab-panel'; export * from './my-jobs-table-actions'; -export * from './escrow-address-search-form'; export * from './reward-amount'; -export * from './sorting'; export * from './more-button'; -export * from './report-abuse-modal'; +export * from './report-abuse-dialog'; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/jobs-tab-panel.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/jobs-tab-panel.tsx deleted file mode 100644 index b87990ffbf..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/jobs-tab-panel.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Box } from '@mui/material'; - -interface TabPanelProps { - children?: React.ReactNode; - index: number; - activeTab: number; -} - -export function TabPanel({ children, index, activeTab }: TabPanelProps) { - return ( - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/more-button.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/more-button.tsx index a79920ed10..d8674df036 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/more-button.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/more-button.tsx @@ -1,14 +1,15 @@ import { useState } from 'react'; import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import { Button, MenuList, ListItemButton, Popover } from '@mui/material'; -import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useModal } from '@/shared/contexts/modal-context'; + import { useIsMobile } from '@/shared/hooks/use-is-mobile'; import { TopNotificationType, useNotification } from '@/shared/hooks'; import { useResignJobMutation } from '../my-jobs/hooks'; import { type MyJob } from '../schemas'; -import { ReportAbuseModal } from './report-abuse-modal'; +import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; +import { useMyJobsFilterStore } from '../hooks'; +import { ReportAbuseDialog } from './report-abuse-dialog'; interface MoreButtonProps { job: MyJob; @@ -17,20 +18,23 @@ interface MoreButtonProps { export function MoreButton({ job, isDisabled }: MoreButtonProps) { const [anchorEl, setAnchorEl] = useState(null); - const { address: oracleAddress } = useParams<{ address: string }>(); - const { mutateAsync: rejectTaskMutation } = useResignJobMutation(); - const { openModal, closeModal } = useModal(); - const isMobile = useIsMobile(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + const { colorPalette } = useColorMode(); const { t } = useTranslation(); + const isMobile = useIsMobile(); const { showNotification } = useNotification(); + const { filterParams } = useMyJobsFilterStore(); + const { mutateAsync: rejectTaskMutation } = useResignJobMutation(); + const isOpen = Boolean(anchorEl); const handleCancelTask = async () => { setAnchorEl(null); try { await rejectTaskMutation({ - oracle_address: oracleAddress ?? '', + oracle_address: filterParams.oracle_address ?? '', assignment_id: job.assignment_id, }); } catch { @@ -44,16 +48,7 @@ export function MoreButton({ job, isDisabled }: MoreButtonProps) { const handleOpenReportAbuseModal = () => { setAnchorEl(null); - openModal({ - content: ( - - ), - showCloseButton: false, - }); + setIsDialogOpen(true); }; return ( @@ -62,12 +57,12 @@ export function MoreButton({ job, isDisabled }: MoreButtonProps) { disabled={isDisabled} sx={{ minWidth: 'unset', - width: { xs: '48px', md: '30px' }, - height: { xs: '48px', md: '30px' }, + width: { xs: '44px', md: '30px' }, + height: { xs: '44px', md: '30px' }, p: 1, - border: isMobile ? '1px solid #858ec6' : 'none', + border: { xs: `1px solid ${colorPalette.border.main}`, md: 'none' }, borderRadius: '4px', - color: '#858ec6', + color: colorPalette.text.auxiliary100, }} onClick={(e) => { if (!isDisabled) { @@ -95,7 +90,7 @@ export function MoreButton({ job, isDisabled }: MoreButtonProps) { paper: { elevation: 8, sx: { - mt: isMobile ? -1 : 1, + mt: { xs: -1, md: 1 }, }, }, }} @@ -109,6 +104,12 @@ export function MoreButton({ job, isDisabled }: MoreButtonProps) { + setIsDialogOpen(false)} + escrowAddress={job.escrow_address} + chainId={job.chain_id} + /> ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/my-jobs-table-actions.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/my-jobs-table-actions.tsx index 886dc72875..7501dcb0b4 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/my-jobs-table-actions.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/my-jobs-table-actions.tsx @@ -1,17 +1,16 @@ import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; + import { TableButton } from '@/shared/components/ui/table-button'; import { MyJobStatus } from '../types'; import { type MyJob } from '../schemas'; import { MoreButton } from './more-button'; -interface MyJobsTableRejectActionProps { +type Props = { job: MyJob; -} +}; -export function MyJobsTableActions({ - job, -}: Readonly) { +export function MyJobsTableActions({ job }: Props) { const { t } = useTranslation(); const isDisabled = job.status !== MyJobStatus.ACTIVE; @@ -28,7 +27,7 @@ export function MyJobsTableActions({ target="_blank" to={job.url} sx={{ - height: { xs: '48px', md: '30px' }, + height: { xs: '44px', md: '30px' }, maxWidth: { xs: 'unset', sm: '160px' }, flex: { xs: 1, md: 'unset' }, }} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/report-abuse-modal.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/report-abuse-dialog.tsx similarity index 51% rename from packages/apps/human-app/frontend/src/modules/worker/jobs/components/report-abuse-modal.tsx rename to packages/apps/human-app/frontend/src/modules/worker/jobs/components/report-abuse-dialog.tsx index 5cbfa52622..9e2bcfd6f3 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/report-abuse-modal.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/report-abuse-dialog.tsx @@ -1,26 +1,29 @@ import { useState } from 'react'; import { Box, - Button, - CircularProgress, FormControl, MenuItem, - Select as MuiSelect, + Select, Stack, Typography, } from '@mui/material'; import ErrorIcon from '@mui/icons-material/Error'; import SuccessIcon from '@mui/icons-material/CheckCircle'; import { useTranslation } from 'react-i18next'; -import { useIsMobile } from '@/shared/hooks/use-is-mobile'; -import { colorPalette } from '@/shared/styles/color-palette'; + +import { ResponsiveOverlay } from '@/shared/components/ui/responsive-overlay'; import { useReportAbuseMutation } from '../available-jobs/hooks/use-report-abuse'; +import { useIsMobile } from '@/shared/hooks/use-is-mobile'; +import { Button } from '@/shared/components/ui/button'; +import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; +import { Loader } from '@/shared/components/ui/loader'; -interface ReportAbuseModalProps { +type Props = { + open: boolean; + onClose: () => void; escrowAddress: string; chainId: number; - close: () => void; -} +}; const ABUSE_ERROR = 'Abuse has already been reported'; @@ -40,12 +43,21 @@ const REASON_OPTIONS = [ function ErrorState({ error }: { error: string }) { const { t } = useTranslation(); + const { colorPalette } = useColorMode(); const isAbuseError = error === ABUSE_ERROR; const errorColor = colorPalette.error.main; return ( - + {isAbuseError ? ( <> @@ -56,7 +68,12 @@ function ErrorState({ error }: { error: string }) { > {t('worker.reportAbuse.modalHeaderAlreadyReportedError')} - + {t('worker.reportAbuse.modalParagraphAlreadyReportedError')} @@ -71,30 +88,49 @@ function ErrorState({ error }: { error: string }) { function SuccessState() { const { t } = useTranslation(); + const { colorPalette } = useColorMode(); + return ( - + - + {t('worker.reportAbuse.modalSuccessHeader')} - + {t('worker.reportAbuse.modalSuccessParagraph')} ); } -export function ReportAbuseModal({ +export function ReportAbuseDialog({ + open, + onClose, escrowAddress, chainId, - close, -}: ReportAbuseModalProps) { +}: Props) { const [reason, setReason] = useState(''); const [error, setError] = useState(''); - const isMobile = useIsMobile(); + const { t } = useTranslation(); + const { colorPalette } = useColorMode(); + const isMobile = useIsMobile(); const { mutate: reportAbuseMutation, @@ -127,68 +163,98 @@ export function ReportAbuseModal({ }; return ( - - - {t('worker.reportAbuse.modalHeader')} - - {isIdleOrLoading && ( - <> - - {t('worker.reportAbuse.modalParagraph')} - - - { - setReason(e.target.value); - }} + + + {t('worker.reportAbuse.modalHeader')} + + {isIdleOrLoading && ( + <> + - {REASON_OPTIONS.map((value) => ( - - {t(`worker.reportAbuse.reasons.${value}`)} - - ))} - - - + {t('worker.reportAbuse.modalParagraph')} + + + + + + )} + + {isPending && ( + + + )} - {isPending && } {isError && } {isSuccess && } - + - + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/reward-amount.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/reward-amount.tsx index f4af594c6c..ddfde6c4fb 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/reward-amount.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/reward-amount.tsx @@ -1,5 +1,6 @@ -import Tooltip from '@mui/material/Tooltip'; -import Typography from '@mui/material/Typography'; +import { Tooltip, Typography } from '@mui/material'; + +import { useIsMobile } from '@/shared/hooks/use-is-mobile'; export function RewardAmount({ reward_amount, @@ -10,30 +11,49 @@ export function RewardAmount({ reward_token?: string; color?: string; }) { + const isMobile = useIsMobile(); + const variant = isMobile ? 'body2' : 'body1'; + if (!(reward_amount !== undefined && reward_token)) { return ''; } + const parsedReward = Number(reward_amount); const isNumeric = Number.isFinite(parsedReward); + if (!isNumeric) { return ( - + {`${reward_amount} ${reward_token}`} ); } + const hasDecimals = parsedReward - Math.floor(parsedReward) !== 0; if (hasDecimals) { return ( - + {`${parsedReward.toFixed(2)} ${reward_token}`} ); } + return ( - + {`${reward_amount} ${reward_token}`} ); diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/sorting.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/sorting.tsx deleted file mode 100644 index 4126e5fc53..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/sorting.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { t } from 'i18next'; -import Typography from '@mui/material/Typography'; -import Box from '@mui/material/Box'; -import ListItemButton from '@mui/material/ListItemButton'; -import List from '@mui/material/List'; -import { Grid } from '@mui/material'; -import { SortArrow } from '@/shared/components/ui/icons'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { Button } from '@/shared/components/ui/button'; - -interface SortingProps { - fromHighestSelected: boolean; - fromLowestSelected: boolean; - sortFromHighest: () => void; - sortFromLowest: () => void; - clear: () => void; - label: React.ReactElement; -} - -export function Sorting({ - fromHighestSelected, - fromLowestSelected, - sortFromHighest, - sortFromLowest, - clear, - label, -}: SortingProps) { - const { colorPalette } = useColorMode(); - - return ( - <> - {label} - - - - - {t('worker.jobs.sortDirection.fromHighest')} - - - - - - - - From lowest - - - - - - - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/index.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/index.ts index b977266dbe..8cb316adfb 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/index.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/index.ts @@ -1,6 +1,4 @@ -export * from './use-get-all-networks'; export * from './use-jobs-notifications'; -export * from './use-job-types-oracles-table'; export * from './use-jobs-filter-store'; export * from './use-my-jobs-filter-store'; export * from './use-get-my-jobs-data'; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-get-all-networks.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-get-all-networks.ts deleted file mode 100644 index 61daf258dd..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-get-all-networks.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useMemo } from 'react'; -import { getEnabledChainsByUiConfig } from '@/modules/smart-contracts/chains'; - -export const useGetAllNetworks = (chainIdsEnabled: number[]) => { - const allNetworks = useMemo(() => { - const chains = getEnabledChainsByUiConfig(chainIdsEnabled); - - return chains.map(({ chainId, name }) => ({ - option: chainId, - name, - })); - }, [chainIdsEnabled]); - - return { allNetworks }; -}; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-get-my-jobs-data.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-get-my-jobs-data.ts index 2576e22e10..d055d59693 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-get-my-jobs-data.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-get-my-jobs-data.ts @@ -1,48 +1,38 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; -import { useParams } from 'react-router-dom'; + import * as jobsService from '../services/jobs.service'; import { type MyJobPaginationResponse } from '../schemas'; -import { - useMyJobsFilterStore, - type MyJobsFilterStoreProps, -} from './use-my-jobs-filter-store'; - -type OracleParams = MyJobsFilterStoreProps['filterParams'] & { - oracle_address: string; -}; +import { useMyJobsFilterStore } from './use-my-jobs-filter-store'; export function useGetMyJobsData() { const { filterParams } = useMyJobsFilterStore(); - const { address } = useParams<{ address: string }>(); - const queryParams: OracleParams = { - ...filterParams, - oracle_address: address ?? '', - }; + const queryParams = { ...filterParams }; return useQuery({ queryKey: ['fetchMyJobs', queryParams], queryFn: async ({ signal }) => - jobsService.fetchMyJobs({ queryParams, signal }), + jobsService.fetchMyJobs({ queryParams: queryParams, signal }), + enabled: !!filterParams.oracle_address, }); } export function useInfiniteGetMyJobsData() { const { filterParams } = useMyJobsFilterStore(); - const { address } = useParams<{ address: string }>(); - const queryParams: OracleParams = { - ...filterParams, - oracle_address: address ?? '', - }; + const { page: _page, ...queryParams } = filterParams; return useInfiniteQuery({ initialPageParam: 0, queryKey: ['myJobsInfinite', queryParams], - queryFn: async ({ signal }) => - jobsService.fetchMyJobs({ queryParams, signal }), + queryFn: async ({ pageParam, signal }) => + jobsService.fetchMyJobs({ + queryParams: { ...queryParams, page: pageParam }, + signal, + }), + enabled: !!filterParams.oracle_address, getNextPageParam: (pageParams: MyJobPaginationResponse) => { return pageParams.total_pages - 1 <= pageParams.page ? undefined - : pageParams.page; + : pageParams.page + 1; }, }); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-job-types-oracles-table.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-job-types-oracles-table.tsx deleted file mode 100644 index 7dadc09e9f..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-job-types-oracles-table.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { create } from 'zustand'; - -export interface JobsTypesOraclesFilterStore { - selectedJobTypes: string[]; - selectJobType: (jobType: string[]) => void; -} - -export const useJobsTypesOraclesFilterStore = - create((set) => ({ - selectedJobTypes: [], - selectJobType: (jobTypes: string[]) => { - set((state) => ({ - ...state, - selectedJobTypes: jobTypes, - })); - }, - })); diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-jobs-notifications.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-jobs-notifications.tsx index 474a8294f8..97322d8d96 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-jobs-notifications.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-jobs-notifications.tsx @@ -10,7 +10,7 @@ export const useJobsNotifications = () => { const onJobAssignmentSuccess = () => { showNotification({ - message: t('worker.jobs.successFullyAssignedJob'), + message: t('worker.jobs.successfullyAssignedJob'), type: TopNotificationType.SUCCESS, durationMs: 5000, }); @@ -19,7 +19,7 @@ export const useJobsNotifications = () => { const onJobAssignmentError = (error: Error) => { showNotification({ message: getErrorMessageForError(error), - type: TopNotificationType.WARNING, + type: TopNotificationType.ERROR, durationMs: 5000, }); }; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-my-jobs-filter-store.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-my-jobs-filter-store.tsx index 8138fd4027..4547d82a14 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-my-jobs-filter-store.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/hooks/use-my-jobs-filter-store.tsx @@ -1,40 +1,32 @@ import { create } from 'zustand'; import type { PageSize } from '@/shared/types/entity.type'; -import { SortDirection, SortField, type MyJobStatus } from '../types'; +import { SortDirection, SortField, type StatusFilterType } from '../types'; export interface MyJobsFilterStoreProps { filterParams: { - sort?: SortDirection; - sort_field?: SortField; - job_type?: string; - status?: MyJobStatus; - escrow_address?: string; + status?: StatusFilterType; + oracle_address?: string; page: number; page_size: PageSize; - chain_id?: number; }; - availableJobTypes: string[]; setFilterParams: ( partialParams: Partial ) => void; resetFilterParams: () => void; - setSearchEscrowAddress: (escrow_address: string) => void; - setOracleAddress: (oracleAddress: string) => void; - setAvailableJobTypes: (jobTypes: string[]) => void; setPageParams: (pageIndex: number, pageSize: PageSize) => void; } const initialFiltersState = { - escrow_address: '', + oracle_address: undefined, + status: '', page: 0, page_size: 5, sort_field: SortField.CREATED_AT, - sort: SortDirection.DESC, + sort_direction: SortDirection.DESC, } as const; export const useMyJobsFilterStore = create((set) => ({ filterParams: initialFiltersState, - availableJobTypes: [], setFilterParams: ( partialParams: Partial ) => { @@ -48,40 +40,25 @@ export const useMyJobsFilterStore = create((set) => ({ })); }, setPageParams: (pageIndex: number, pageSize: PageSize) => { - set((state) => ({ - ...state, - filterParams: { - ...state.filterParams, - page: pageIndex, - page_size: pageSize, - }, - })); + set((state) => { + if ( + state.filterParams.page === pageIndex && + state.filterParams.page_size === pageSize + ) { + return state; + } + + return { + ...state, + filterParams: { + ...state.filterParams, + page: pageIndex, + page_size: pageSize, + }, + }; + }); }, resetFilterParams: () => { set({ filterParams: initialFiltersState }); }, - setSearchEscrowAddress: (escrow_address: string) => { - set((state) => ({ - ...state, - filterParams: { - ...state.filterParams, - escrow_address, - }, - })); - }, - setOracleAddress: (oracleAddress: string) => { - set((state) => ({ - ...state, - filterParams: { - ...state.filterParams, - address: oracleAddress, - }, - })); - }, - setAvailableJobTypes: (jobTypes: string[]) => { - set((state) => ({ - ...state, - jobTypes, - })); - }, })); diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/jobs.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/jobs.page.tsx index a4e79bd62c..0c4f80e810 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/jobs.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/jobs.page.tsx @@ -2,17 +2,11 @@ import React, { useEffect, useState } from 'react'; import { Box, Grid, Paper, Stack, Tab, Tabs, Typography } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import { TableQueryContextProvider } from '@/shared/components/ui/table/table-query-context'; import { useIsMobile } from '@/shared/hooks/use-is-mobile'; import { useColorMode } from '@/shared/contexts/color-mode'; -import { NoRecords } from '@/shared/components/ui/no-records'; import { PageCardLoader } from '@/shared/components/ui/page-card'; -import { useGetUiConfig } from '@/shared/hooks'; import { useGetOracles } from '../hooks'; import { useGetOraclesNotifications } from '../hooks/use-get-oracles-notifications'; -import { TabPanel } from './components'; -import { AvailableJobsView } from './available-jobs'; -import { MyJobsView } from './my-jobs/my-jobs-view'; function generateTabA11yProps(index: number) { return { @@ -30,19 +24,13 @@ export function JobsPage() { error, } = useGetOracles(); - const { - data: uiConfigData, - isPending: isPendingUiConfig, - isError: isErrorUiConfig, - } = useGetUiConfig(); - const { address: oracle_address } = useParams<{ address: string }>(); const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(0); const isMobile = useIsMobile(); - const isError = isErrorGetOracles || isErrorUiConfig; - const isPending = isPendingGetOracles || isPendingUiConfig; + const isError = isErrorGetOracles; + const isPending = isPendingGetOracles; const { onError } = useGetOraclesNotifications(); const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { @@ -91,49 +79,29 @@ export function JobsPage() { )} - - - + + - - - - - - - {isError ? ( - - ) : ( - - )} - - - {isError ? ( - - ) : ( - - )} - + + + - + diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/columns.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/columns.tsx deleted file mode 100644 index 5e4594d32d..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/columns.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { t } from 'i18next'; -import Grid from '@mui/material/Grid'; -import { type MRT_ColumnDef } from 'material-react-table'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import { TableHeaderCell } from '@/shared/components/ui/table/table-header-cell'; -import { getNetworkName } from '@/modules/smart-contracts/get-network-name'; -import { Button } from '@/shared/components/ui/button'; -import { Chip } from '@/shared/components/ui/chip'; -import type { JobType } from '@/modules/smart-contracts/EthKVStore/config'; -import { formatDate } from '@/shared/helpers/date'; -import { - EvmAddress, - RewardAmount, - MyJobsTableActions, -} from '../../../components'; -import { type MyJob } from '../../../schemas'; -import { StatusChip } from './status-chip'; -import { MyJobsExpiresAtSort } from './my-jobs-expires-at-sort'; -import { MyJobsJobTypeFilter } from './my-jobs-job-type-filter'; -import { MyJobsNetworkFilter } from './my-jobs-network-filter'; -import { MyJobsRewardAmountSort } from './my-jobs-reward-amount-sort'; -import { MyJobsStatusFilter } from './my-jobs-status-filter'; - -export const getColumnsDefinition = ({ - refreshData, - isRefreshTasksPending, - chainIdsEnabled, -}: { - refreshData: () => void; - isRefreshTasksPending: boolean; - chainIdsEnabled: number[]; -}): MRT_ColumnDef[] => [ - { - accessorKey: 'escrow_address', - header: t('worker.jobs.escrowAddress'), - size: 100, - enableSorting: true, - Cell: (props) => { - return ; - }, - }, - { - accessorKey: 'network', - header: t('worker.jobs.network'), - size: 100, - Cell: (props) => { - return getNetworkName(props.row.original.chain_id); - }, - Header: ( - - } - /> - ), - }, - { - accessorKey: 'reward_amount', - header: t('worker.jobs.rewardAmount'), - size: 100, - enableSorting: true, - Cell: (props) => { - const { reward_amount, reward_token } = props.row.original; - return ( - - ); - }, - Header: ( - } - /> - ), - }, - { - accessorKey: 'job_type', - header: t('worker.jobs.jobType'), - size: 100, - enableSorting: true, - Cell: ({ row }) => { - const label = t(`jobTypeLabels.${row.original.job_type as JobType}`); - return ; - }, - Header: ( - } - /> - ), - }, - { - accessorKey: 'expires_at', - header: t('worker.jobs.expiresAt'), - size: 100, - enableSorting: true, - Cell: (props) => { - return formatDate(props.row.original.expires_at); - }, - Header: ( - } - /> - ), - }, - { - accessorKey: 'status', - header: t('worker.jobs.status'), - size: 100, - enableSorting: true, - Cell: (props) => { - const status = props.row.original.status; - return ; - }, - Header: ( - } - /> - ), - }, - { - accessorKey: 'assignment_id', - header: t('worker.jobs.refresh'), - size: 100, - enableSorting: true, - Cell: (props) => ( - - - - ), - Header: ( - - - - ), - }, -]; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/index.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/index.ts index 677305df30..ed7a437325 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/index.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/index.ts @@ -1 +1,3 @@ export * from './my-jobs-table'; +export * from './my-jobs-filters'; +export * from './status-filter'; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-expires-at-sort.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-expires-at-sort.tsx deleted file mode 100644 index 608edc3452..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-expires-at-sort.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { t } from 'i18next'; -import { Sorting } from '@/shared/components/ui/table/table-header-menu/sorting'; -import { useMyJobsFilterStore } from '../../../hooks'; -import { SortDirection, SortField } from '../../../types'; - -export function MyJobsExpiresAtSort() { - const { setFilterParams } = useMyJobsFilterStore(); - - const sortAscExpiresAt = () => { - setFilterParams({ - sort_field: SortField.EXPIRES_AT, - sort: SortDirection.ASC, - }); - }; - - const sortDescExpiresAt = () => { - setFilterParams({ - sort_field: SortField.EXPIRES_AT, - sort: SortDirection.DESC, - }); - }; - - return ( - { - setFilterParams({ - sort_field: undefined, - sort: undefined, - }); - }} - sortingOptions={[ - { - label: t('worker.jobs.sortDirection.closestToNow'), - sortCallback: sortAscExpiresAt, - }, - { - label: t('worker.jobs.sortDirection.furthestToNow'), - sortCallback: sortDescExpiresAt, - }, - ]} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-filters.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-filters.tsx new file mode 100644 index 0000000000..c957fed112 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-filters.tsx @@ -0,0 +1,197 @@ +import { useEffect, useState, type SubmitEvent } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + FormControlLabel, + IconButton, + Radio, + RadioGroup, + Stack, + Typography, +} from '@mui/material'; + +import { FilterIcon } from '@/shared/components/ui/icons'; +import { useColorMode } from '@/shared/contexts/color-mode'; +import { ResponsiveOverlay } from '@/shared/components/ui/responsive-overlay'; +import { useGetOracles } from '@/modules/worker/hooks/use-get-oracles'; +import { Alert } from '@/shared/components/ui/alert'; +import { Button } from '@/shared/components/ui/button'; +import { useIsMobile } from '@/shared/hooks/use-is-mobile'; +import { useMyJobsFilterStore } from '../../../hooks'; +import { type StatusFilterType } from '../../../types'; +import { STATUS_FILTER_OPTIONS } from './status-filter'; + +const CVAT_ORACLE_NAME = 'cvat'; + +const getDefaultOracleAddress = ( + oracles: { address: string; name: string }[] | undefined +) => { + if (!oracles?.length) { + return undefined; + } + + return ( + oracles.find((oracle) => + oracle.name.toLowerCase().includes(CVAT_ORACLE_NAME) + )?.address ?? oracles[0].address + ); +}; + +export function MyJobsFilters() { + const [isOpen, setIsOpen] = useState(false); + const [draftOracleAddress, setDraftOracleAddress] = useState(''); + const [draftStatus, setDraftStatus] = useState(''); + + const { colorPalette } = useColorMode(); + const { t } = useTranslation(); + const isMobile = useIsMobile(); + const { filterParams, setFilterParams } = useMyJobsFilterStore(); + + const { data: oraclesData, isError, isPending } = useGetOracles(); + + const defaultOracleAddress = getDefaultOracleAddress(oraclesData); + const selectedOracleAddress = + filterParams.oracle_address ?? defaultOracleAddress ?? ''; + const selectedStatus = filterParams.status ?? ''; + + useEffect(() => { + if (!filterParams.oracle_address && defaultOracleAddress) { + setFilterParams({ oracle_address: defaultOracleAddress }); + } + }, [defaultOracleAddress, filterParams.oracle_address, setFilterParams]); + + useEffect(() => { + if (isOpen) { + setDraftOracleAddress(selectedOracleAddress); + setDraftStatus(selectedStatus); + } + }, [isOpen, selectedOracleAddress, selectedStatus]); + + const handleSubmit = (e: SubmitEvent) => { + e.preventDefault(); + setFilterParams({ + oracle_address: draftOracleAddress, + status: draftStatus, + }); + setIsOpen(false); + }; + + return ( + <> + setIsOpen(!isOpen)} + > + + + setIsOpen(false)} + desktopSx={{ p: 0 }} + mobileSx={{ p: 0 }} + > + + + + {t('worker.jobs.jobsFilter')} + + + + {t('worker.jobs.oracles')} + + {isError && ( + + {t('worker.oraclesTable.error.gettingOracles')} + + )} + { + setDraftOracleAddress(oracleAddress); + }} + > + {(oraclesData ?? []).map((oracle) => ( + } + label={ + + {oracle.name} + + } + value={oracle.address} + /> + ))} + + + {isMobile && ( + + + {t('worker.jobs.status')} + + { + setDraftStatus(status as StatusFilterType); + }} + > + {STATUS_FILTER_OPTIONS.map((option) => ( + } + label={ + + {t(option.labelKey)} + + } + value={option.value} + /> + ))} + + + )} + + + + + + + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-job-type-filter.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-job-type-filter.tsx deleted file mode 100644 index 7b9fd2e428..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-job-type-filter.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { useMemo } from 'react'; -import { Filtering } from '@/shared/components/ui/table/table-header-menu/filtering'; -import { JOB_TYPES } from '@/shared/consts'; -import { useMyJobsFilterStore } from '../../../hooks'; - -export function MyJobsJobTypeFilter() { - const { t } = useTranslation(); - const { setFilterParams, filterParams } = useMyJobsFilterStore(); - const filteringOptions = useMemo( - () => - JOB_TYPES.map((jobType) => ({ - name: t(`jobTypeLabels.${jobType}`), - option: jobType, - })), - [t] - ); - return ( - { - setFilterParams({ - job_type: undefined, - }); - }} - filteringOptions={filteringOptions} - isChecked={(option) => option === filterParams.job_type} - setFiltering={(jobType) => { - setFilterParams({ - job_type: jobType, - }); - }} - showClearButton - showTitle - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-network-filter.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-network-filter.tsx deleted file mode 100644 index b404ffada7..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-network-filter.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Filtering } from '@/shared/components/ui/table/table-header-menu/filtering'; -import { useMyJobsFilterStore, useGetAllNetworks } from '../../../hooks'; - -interface MyJobsNetworkFilterProps { - chainIdsEnabled: number[]; -} - -export function MyJobsNetworkFilter({ - chainIdsEnabled, -}: Readonly) { - const { setFilterParams, filterParams } = useMyJobsFilterStore(); - const { allNetworks } = useGetAllNetworks(chainIdsEnabled); - - return ( - { - setFilterParams({ - chain_id: undefined, - }); - }} - filteringOptions={allNetworks} - isChecked={(option) => option === filterParams.chain_id} - setFiltering={(chainId) => { - setFilterParams({ - chain_id: chainId, - }); - }} - showClearButton - showTitle - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-reward-amount-sort.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-reward-amount-sort.tsx deleted file mode 100644 index 2e1a335540..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-reward-amount-sort.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { t } from 'i18next'; -import { Sorting } from '@/shared/components/ui/table/table-header-menu/sorting'; -import { useMyJobsFilterStore } from '../../../hooks'; -import { SortDirection, SortField } from '../../../types'; - -export function MyJobsRewardAmountSort() { - const { setFilterParams } = useMyJobsFilterStore(); - - const sortAscRewardAmount = () => { - setFilterParams({ - sort_field: SortField.REWARD_AMOUNT, - sort: SortDirection.ASC, - }); - }; - - const sortDescRewardAmount = () => { - setFilterParams({ - sort_field: SortField.REWARD_AMOUNT, - sort: SortDirection.DESC, - }); - }; - - return ( - { - setFilterParams({ - sort_field: undefined, - sort: undefined, - }); - }} - sortingOptions={[ - { - label: t('worker.jobs.sortDirection.fromHighest'), - sortCallback: sortDescRewardAmount, - }, - { - label: t('worker.jobs.sortDirection.fromLowest'), - sortCallback: sortAscRewardAmount, - }, - ]} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-status-filter.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-status-filter.tsx deleted file mode 100644 index 4c17f3f157..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-status-filter.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import capitalize from 'lodash/capitalize'; -import { Filtering } from '@/shared/components/ui/table/table-header-menu/filtering'; -import { useMyJobsFilterStore } from '../../../hooks'; -import { MyJobStatus } from '../../../types'; - -export function MyJobsStatusFilter() { - const { setFilterParams, filterParams } = useMyJobsFilterStore(); - - return ( - { - setFilterParams({ - status: undefined, - }); - }} - filteringOptions={Object.values(MyJobStatus).map((status) => ({ - name: capitalize(status), - option: status, - }))} - isChecked={(status) => status === filterParams.status} - setFiltering={(status) => { - setFilterParams({ - status, - }); - }} - showClearButton - showTitle - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-table.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-table.tsx index 0fd5c61c70..2eb57540a0 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-table.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/my-jobs-table.tsx @@ -1,126 +1,141 @@ -import { t } from 'i18next'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { useEffect, useMemo, useState } from 'react'; import { MaterialReactTable, + type MRT_PaginationState, useMaterialReactTable, } from 'material-react-table'; + import { useColorMode } from '@/shared/contexts/color-mode'; import { createTableDarkMode } from '@/shared/styles/create-table-dark-mode'; -import { EscrowAddressSearchForm } from '../../../components'; import { useGetMyJobsData, useMyJobsFilterStore } from '../../../hooks'; -import { useRefreshJobsMutation } from '../../hooks'; -import { getColumnsDefinition } from './columns'; - -interface MyJobsTableProps { - chainIdsEnabled: number[]; -} +import { useGetMyJobsColumns } from '../../hooks/use-get-my-jobs-columns'; -export function MyJobsTable({ chainIdsEnabled }: Readonly) { +export function MyJobsTable() { const { colorPalette, isDarkMode } = useColorMode(); - const { - setSearchEscrowAddress, - setPageParams, - filterParams, - resetFilterParams, - } = useMyJobsFilterStore(); - const { data: tableData, status: tableStatus } = useGetMyJobsData(); + const { data: tableData, isPending, isError } = useGetMyJobsData(); + const { setPageParams, filterParams } = useMyJobsFilterStore(); + const [paginationState, setPaginationState] = useState( + () => ({ + pageIndex: filterParams.page, + pageSize: filterParams.page_size, + }) + ); const memoizedTableDataResults = useMemo( () => tableData?.results ?? [], [tableData?.results] ); - const { mutate: refreshTasksMutation, isPending: isRefreshTasksPending } = - useRefreshJobsMutation(); - const { address: oracle_address } = useParams<{ address: string }>(); - - const [paginationState, setPaginationState] = useState({ - pageIndex: 0, - pageSize: 5, - }); - - const refreshData = useCallback(() => { - refreshTasksMutation({ oracle_address: oracle_address ?? '' }); - }, [refreshTasksMutation, oracle_address]); + const columns = useGetMyJobsColumns(); useEffect(() => { if (paginationState.pageSize === 5 || paginationState.pageSize === 10) { setPageParams(paginationState.pageIndex, paginationState.pageSize); } - }, [paginationState, setPageParams]); + }, [paginationState.pageIndex, paginationState.pageSize, setPageParams]); useEffect(() => { - setPaginationState({ - pageIndex: filterParams.page, - pageSize: filterParams.page_size, + setPaginationState((currentPaginationState) => { + if ( + currentPaginationState.pageIndex === filterParams.page && + currentPaginationState.pageSize === filterParams.page_size + ) { + return currentPaginationState; + } + + return { + pageIndex: filterParams.page, + pageSize: filterParams.page_size, + }; }); }, [filterParams.page, filterParams.page_size]); - useEffect(() => { - return () => { - resetFilterParams(); - }; - }, [resetFilterParams]); - const table = useMaterialReactTable({ - columns: getColumnsDefinition({ - refreshData, - isRefreshTasksPending, - chainIdsEnabled, - }), + columns, data: memoizedTableDataResults, state: { - isLoading: tableStatus === 'pending', - showAlertBanner: tableStatus === 'error', + isLoading: isPending, + showAlertBanner: isError, pagination: paginationState, }, - enablePagination: Boolean(tableData?.total_pages), + enablePagination: !!tableData?.total_pages, manualPagination: true, - onPaginationChange: (updater) => { - setPaginationState(updater); - }, + onPaginationChange: setPaginationState, muiPaginationProps: { + rowsPerPageOptions: [5, 10], SelectProps: { sx: { + '.MuiSelect-select': { + color: colorPalette.text.auxiliary100, + }, '.MuiSelect-icon': { ':hover': { backgroundColor: 'blue', }, - fill: colorPalette.text.primary, + fill: colorPalette.text.auxiliary100, }, }, }, - rowsPerPageOptions: [5, 10], + }, + muiBottomToolbarProps: { + sx: { + bgcolor: colorPalette.background.paper, + boxShadow: 'none', + color: colorPalette.text.auxiliary100, + }, }, pageCount: tableData?.total_pages ?? -1, rowCount: tableData?.total_results, enableColumnActions: false, enableColumnFilters: false, enableSorting: false, - renderTopToolbar: () => ( - { - setSearchEscrowAddress(address); - }} - /> - ), + renderTopToolbar: false, + muiTablePaperProps: { + sx: { + boxShadow: 'none', + }, + }, + muiTableHeadProps: { + sx: { + backgroundColor: colorPalette.background.default, + }, + }, + muiTableHeadRowProps: { + sx: { + backgroundColor: 'inherit', + boxShadow: 'none', + }, + }, muiTableHeadCellProps: { sx: { - borderColor: colorPalette.paper.text, + paddingTop: '8px', + paddingBottom: '8px', + paddingLeft: '12px', + paddingRight: '12px', + borderColor: colorPalette.background.paper, + color: colorPalette.text.auxiliary200, + typography: 'body1', + fontWeight: 500, + '& .Mui-TableHeadCell-Content': { + justifyContent: 'center', + textAlign: 'center', + }, }, }, muiTableBodyCellProps: { + align: 'center', sx: { - borderColor: colorPalette.paper.text, + textAlign: 'center', + borderBottom: `1px solid ${colorPalette.border.main}`, }, }, - muiTablePaperProps: { + muiTableBodyRowProps: { sx: { - boxShadow: '0px 2px 2px 0px #E9EBFA80', + bgcolor: colorPalette.background.paper, + borderBottom: `1px solid ${colorPalette.border.main}`, + '&:last-of-type': { + borderBottom: 'none', + }, }, }, ...(isDarkMode ? createTableDarkMode(colorPalette) : {}), diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/status-chip.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/status-chip.tsx deleted file mode 100644 index 0d7523530e..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/status-chip.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { getChipStatusColor } from '../../utils'; -import { type MyJob } from '../../../schemas'; - -export function StatusChip({ status }: Readonly<{ status: MyJob['status'] }>) { - const { colorPalette } = useColorMode(); - - return ( - - - {status} - - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/status-filter.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/status-filter.tsx new file mode 100644 index 0000000000..cd3fcc93c7 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/desktop/status-filter.tsx @@ -0,0 +1,73 @@ +import { useTranslation } from 'react-i18next'; +import { Button } from '@/shared/components/ui/button'; +import { Stack } from '@mui/material'; +import { MyJobStatus, type StatusFilterType } from '../../../types'; +import { useColorMode } from '@/shared/contexts/color-mode'; +import { useMyJobsFilterStore } from '../../../hooks'; + +export const STATUS_FILTER_OPTIONS = [ + { + labelKey: 'worker.jobs.statusFilter.all', + value: '', + }, + { + labelKey: 'worker.jobs.statusFilter.inProgress', + value: MyJobStatus.ACTIVE, + }, + { + labelKey: 'worker.jobs.statusFilter.completed', + value: MyJobStatus.COMPLETED, + }, +] as const satisfies { + labelKey: string; + value: StatusFilterType; +}[]; + +export function StatusFilter() { + const { t } = useTranslation(); + const { colorPalette } = useColorMode(); + const { filterParams, setFilterParams } = useMyJobsFilterStore(); + const activeStatus = filterParams.status ?? ''; + + return ( + + {STATUS_FILTER_OPTIONS.map((option) => { + const isActive = option.value === activeStatus; + return ( + + ); + })} + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/index.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/index.ts index cb17920d1b..083493610a 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/index.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/index.ts @@ -1,2 +1 @@ -export * from './my-jobs-filter-modal'; export * from './my-jobs-list-mobile'; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-card-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-card-mobile.tsx new file mode 100644 index 0000000000..49fae4ae2f --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-card-mobile.tsx @@ -0,0 +1,163 @@ +import { Chip, Paper, Stack, Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +import { + EvmAddress, + MyJobsTableActions, + RewardAmount, +} from '../../../components'; +import { type MyJob } from '../../../schemas'; +import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; +import { JobType } from '@/modules/smart-contracts/EthKVStore/config'; +import { TimeUntil } from '../time-until'; +import { ChainIcon } from '@/shared/components/ui/chain-icon'; +import { + JobExpiryTimeIcon, + JobStatusIcon, + OracleAddressIcon, + OracleRewardIcon, +} from '@/shared/components/ui/icons'; + +const Row = ({ + label, + icon, + children, +}: { + label: string; + icon: React.ReactNode; + children: React.ReactNode; +}) => { + const { colorPalette } = useColorMode(); + return ( + + {icon} + + {label}:{' '} + + {children} + + ); +}; + +export function MyJobsCardMobile({ job }: { job: MyJob }) { + const { colorPalette } = useColorMode(); + const { t } = useTranslation(); + + const jobTypeLabel = t(`jobTypeLabels.${job.job_type as JobType}`); + + const hasUrl = !!job.url; + + return ( + + + + + + + + } + > + + + + } + > + + + + + } + > + + {job.status.toLowerCase()} + + + + } + > + + + + {hasUrl && ( + + + + )} + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-expires-at-sort-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-expires-at-sort-mobile.tsx deleted file mode 100644 index 3ae5d921aa..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-expires-at-sort-mobile.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Typography from '@mui/material/Typography'; -import { t } from 'i18next'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { Sorting } from '../../../components'; -import { useMyJobsFilterStore } from '../../../hooks'; -import { SortDirection, SortField } from '../../../types'; - -export function MyJobsExpiresAtSortMobile() { - const { setFilterParams, filterParams } = useMyJobsFilterStore(); - const { colorPalette } = useColorMode(); - - return ( - - {t('worker.jobs.expiresAt')} - - } - fromHighestSelected={ - filterParams.sort_field === SortField.EXPIRES_AT && - filterParams.sort === SortDirection.DESC - } - sortFromHighest={() => { - setFilterParams({ - sort: SortDirection.DESC, - sort_field: SortField.EXPIRES_AT, - }); - }} - fromLowestSelected={ - filterParams.sort_field === SortField.EXPIRES_AT && - filterParams.sort === SortDirection.ASC - } - sortFromLowest={() => { - setFilterParams({ - sort: SortDirection.ASC, - sort_field: SortField.EXPIRES_AT, - }); - }} - clear={() => { - setFilterParams({ - sort: undefined, - sort_field: undefined, - }); - }} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-filter-modal.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-filter-modal.tsx deleted file mode 100644 index ef40385c0f..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-filter-modal.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import Box from '@mui/material/Box'; -import Drawer from '@mui/material/Drawer'; -import CssBaseline from '@mui/material/CssBaseline'; -import { Divider, IconButton, Stack, Typography } from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import CloseIcon from '@mui/icons-material/Close'; -import { HumanLogoIcon } from '@/shared/components/ui/icons'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { useHandleMainNavIconClick } from '@/shared/hooks/use-handle-main-nav-icon-click'; -import { MyJobsNetworkFilterMobile } from './my-jobs-network-filter-mobile'; -import { MyJobsJobTypeFilterMobile } from './my-jobs-job-type-filter-mobile'; -import { MyJobsStatusFilterMobile } from './my-jobs-status-filter-mobile'; -import { MyJobsExpiresAtSortMobile } from './my-jobs-expires-at-sort-mobile'; -import { MyJobsRewardAmountSortMobile } from './my-jobs-reward-amount-sort-mobile'; - -interface MyJobsFilterModalProps { - chainIdsEnabled: number[]; - close: () => void; -} -export function MyJobsFilterModal({ - chainIdsEnabled, - close, -}: Readonly) { - const handleMainNavIconClick = useHandleMainNavIconClick(); - const { colorPalette } = useColorMode(); - const { t } = useTranslation(); - - return ( - - - - - { - handleMainNavIconClick(); - }} - > - - - - - - - - - {t('worker.jobs.mobileFilterDrawer.filters')} - - - - {t('worker.jobs.mobileFilterDrawer.sortBy')} - - - - - {t('worker.jobs.mobileFilterDrawer.filters')} - - - - {t('worker.jobs.network')} - - - - - - - - {t('worker.jobs.jobType')} - - - - - - - {t('worker.jobs.status')} - - - - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-job-type-filter-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-job-type-filter-mobile.tsx deleted file mode 100644 index 7e9ba0eb96..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-job-type-filter-mobile.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { Filtering } from '@/shared/components/ui/table/table-header-menu/filtering'; -import { JOB_TYPES } from '@/shared/consts'; -import { useMyJobsFilterStore } from '../../../hooks'; - -export function MyJobsJobTypeFilterMobile() { - const { t } = useTranslation(); - const { setFilterParams, filterParams } = useMyJobsFilterStore(); - - return ( - { - setFilterParams({ - job_type: undefined, - page: 0, - }); - }} - filteringOptions={JOB_TYPES.map((jobType) => ({ - name: t(`jobTypeLabels.${jobType}`), - option: jobType, - }))} - isChecked={(option) => option === filterParams.job_type} - setFiltering={(jobType) => { - setFilterParams({ - job_type: jobType, - page: 0, - }); - }} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-list-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-list-mobile.tsx index fa38a01fca..a38d313de6 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-list-mobile.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-list-mobile.tsx @@ -1,211 +1,51 @@ -import { Grid, List, Paper, Stack, Typography } from '@mui/material'; +import { Stack } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import { useEffect } from 'react'; -import { useParams } from 'react-router-dom'; + import { Button } from '@/shared/components/ui/button'; -import { FiltersButtonIcon, RefreshIcon } from '@/shared/components/ui/icons'; import { Loader } from '@/shared/components/ui/loader'; import { Alert } from '@/shared/components/ui/alert'; -import { getNetworkName } from '@/modules/smart-contracts/get-network-name'; import { getErrorMessageForError } from '@/shared/errors'; -import { ListItem } from '@/shared/components/ui/list-item'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { Chip } from '@/shared/components/ui/chip'; -import type { JobType } from '@/modules/smart-contracts/EthKVStore/config'; -import { formatDate } from '@/shared/helpers/date'; import { useCombinePages } from '@/shared/hooks'; -import { - EscrowAddressSearchForm, - EvmAddress, - RewardAmount, - MyJobsTableActions, -} from '../../../components'; -import { - useMyJobsFilterStore, - useJobsFilterStore, - useInfiniteGetMyJobsData, -} from '../../../hooks'; -import { useRefreshJobsMutation } from '../../hooks'; -import { getChipStatusColor } from '../../utils'; +import { useMyJobsFilterStore, useInfiniteGetMyJobsData } from '../../../hooks'; import { type MyJob } from '../../../schemas'; -import { useMyJobFilterModal } from '../../hooks/use-my-jobs-filter-modal'; +import { MyJobsCardMobile } from './my-jobs-card-mobile'; export function MyJobsListMobile() { - const { colorPalette } = useColorMode(); - const { filterParams, setPageParams, resetFilterParams } = - useMyJobsFilterStore(); - const { t } = useTranslation(); - const { - data: tableData, - status: tableStatus, - isError: isTableError, - error: tableError, - fetchNextPage, - hasNextPage, - } = useInfiniteGetMyJobsData(); - - const { mutate: refreshTasksMutation, isPending: isRefreshTasksPending } = - useRefreshJobsMutation(); - const { setSearchEscrowAddress } = useJobsFilterStore(); - const { address: oracle_address } = useParams<{ address: string }>(); + const { filterParams, setPageParams } = useMyJobsFilterStore(); + const { data, isPending, isError, error, fetchNextPage, hasNextPage } = + useInfiniteGetMyJobsData(); - const allPages = useCombinePages(tableData, filterParams.page); - const { openModal } = useMyJobFilterModal(); - - useEffect(() => { - return () => { - resetFilterParams(); - }; - }, [resetFilterParams]); + const allPages = useCombinePages(data); return ( - <> - - - - - - - - - - - - {isTableError ? ( - - {getErrorMessageForError(tableError)} - - ) : null} - {tableStatus === 'pending' ? ( - - - - ) : null} - {allPages.map((d) => { - return ( - - - - - - - - - - {d.expires_at ? formatDate(d.expires_at) : ''} - - - - - - - - - - {getNetworkName(d.chain_id)} - - - - - {d.status} - - } - /> - - - - - - - - - - - - ); + + {isError && ( + + {getErrorMessageForError(error)} + + )} + {isPending && ( + + + + )} + {!isPending && + !isError && + allPages.map((d) => { + return ; })} - {hasNextPage ? ( - - ) : null} - - + {hasNextPage && ( + + )} + ); } diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-network-filter-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-network-filter-mobile.tsx deleted file mode 100644 index 550df051f4..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-network-filter-mobile.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Filtering } from '@/shared/components/ui/table/table-header-menu/filtering'; -import { useMyJobsFilterStore, useGetAllNetworks } from '../../../hooks'; - -interface MyJobsNetworkFilterMobileProps { - chainIdsEnabled: number[]; -} - -export function MyJobsNetworkFilterMobile({ - chainIdsEnabled, -}: Readonly) { - const { setFilterParams, filterParams } = useMyJobsFilterStore(); - const { allNetworks } = useGetAllNetworks(chainIdsEnabled); - - return ( - { - setFilterParams({ - chain_id: undefined, - page: 0, - }); - }} - filteringOptions={allNetworks} - isChecked={(option) => option === filterParams.chain_id} - setFiltering={(chainId) => { - setFilterParams({ - chain_id: chainId, - page: 0, - }); - }} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-reward-amount-sort-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-reward-amount-sort-mobile.tsx deleted file mode 100644 index 3362987213..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-reward-amount-sort-mobile.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Typography from '@mui/material/Typography'; -import { t } from 'i18next'; -import { useColorMode } from '@/shared/contexts/color-mode'; -import { Sorting } from '../../../components'; -import { useMyJobsFilterStore } from '../../../hooks'; -import { SortDirection, SortField } from '../../../types'; - -export function MyJobsRewardAmountSortMobile() { - const { setFilterParams, filterParams } = useMyJobsFilterStore(); - const { colorPalette } = useColorMode(); - const isRewardAmountSortSelected = - filterParams.sort_field === SortField.REWARD_AMOUNT; - - return ( - - {t('worker.jobs.rewardAmount')} - - } - fromHighestSelected={ - isRewardAmountSortSelected && filterParams.sort === SortDirection.DESC - } - sortFromHighest={() => { - setFilterParams({ - sort: SortDirection.DESC, - sort_field: SortField.REWARD_AMOUNT, - }); - }} - fromLowestSelected={ - isRewardAmountSortSelected && filterParams.sort === SortDirection.ASC - } - sortFromLowest={() => { - setFilterParams({ - sort: SortDirection.ASC, - sort_field: SortField.REWARD_AMOUNT, - }); - }} - clear={() => { - setFilterParams({ - sort: undefined, - sort_field: undefined, - }); - }} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-status-filter-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-status-filter-mobile.tsx deleted file mode 100644 index d1199971d8..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-status-filter-mobile.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import capitalize from 'lodash/capitalize'; -import { Filtering } from '@/shared/components/ui/table/table-header-menu/filtering'; -import { useMyJobsFilterStore } from '../../../hooks'; -import { MyJobStatus } from '../../../types'; - -export function MyJobsStatusFilterMobile() { - const { setFilterParams, filterParams } = useMyJobsFilterStore(); - - return ( - { - setFilterParams({ - status: undefined, - page: 0, - }); - }} - filteringOptions={Object.values(MyJobStatus).map((status) => ({ - name: capitalize(status), - option: status, - }))} - isChecked={(status) => status === filterParams.status} - setFiltering={(status) => { - setFilterParams({ - status, - page: 0, - }); - }} - /> - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/time-until.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/time-until.tsx new file mode 100644 index 0000000000..d2edfe0aa9 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/time-until.tsx @@ -0,0 +1,71 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { formatDistanceToNow } from 'date-fns'; +import { enUS } from 'date-fns/locale'; + +const SECOND_IN_MS = 1000; + +const DATE_FNS_LOCALES = { + en: enUS, +} as const; + +export function TimeUntil({ date }: { date?: string | null }) { + const { t, i18n } = useTranslation(); + const [now, setNow] = useState(Date.now()); + + const dateFnsLocale = useMemo(() => { + const language = i18n.resolvedLanguage ?? i18n.language; + const baseLanguage = language.split( + '-' + )[0] as keyof typeof DATE_FNS_LOCALES; + + return DATE_FNS_LOCALES[baseLanguage] ?? enUS; + }, [i18n.language, i18n.resolvedLanguage]); + + const targetDate = useMemo(() => { + if (!date) { + return null; + } + + const parsedDate = new Date(date); + + return Number.isNaN(parsedDate.getTime()) ? null : parsedDate; + }, [date]); + + useEffect(() => { + const targetTime = targetDate?.getTime(); + + if (targetTime == null || targetTime <= Date.now()) { + return; + } + + setNow(Date.now()); + + const intervalId = setInterval(() => { + const nextNow = Date.now(); + + setNow(nextNow); + + if (nextNow >= targetTime) { + clearInterval(intervalId); + } + }, SECOND_IN_MS); + + return () => { + clearInterval(intervalId); + }; + }, [targetDate]); + + if (targetDate === null || targetDate.getTime() <= now) { + return t('worker.jobs.expired'); + } + + return ( + <> + {formatDistanceToNow(targetDate, { + includeSeconds: true, + locale: dateFnsLocale, + })} + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/hooks/use-get-my-jobs-columns.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/hooks/use-get-my-jobs-columns.tsx new file mode 100644 index 0000000000..9ae2d16ff7 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/hooks/use-get-my-jobs-columns.tsx @@ -0,0 +1,141 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Chip, Grid, Typography } from '@mui/material'; +import { type MRT_ColumnDef } from 'material-react-table'; + +import type { JobType } from '@/modules/smart-contracts/EthKVStore/config'; +import { EvmAddress, RewardAmount, MyJobsTableActions } from '../../components'; +import { type MyJob } from '../../schemas'; +import { useColorMode } from '@/shared/contexts/color-mode'; +import { useIsMobile } from '@/shared/hooks/use-is-mobile'; +import { ChainIcon } from '@/shared/components/ui/chain-icon'; +import { TimeUntil } from '../components/time-until'; + +const COL_SIZE_SM = 50; +const COL_SIZE = 100; +const COL_SIZE_MD = 150; + +export const useGetMyJobsColumns = (): MRT_ColumnDef[] => { + const { colorPalette } = useColorMode(); + const isMobile = useIsMobile(); + const { t } = useTranslation(); + + return useMemo( + () => [ + { + accessorKey: 'escrow_address', + header: t('worker.jobs.address'), + size: COL_SIZE, + enableSorting: true, + Cell: (props) => { + return ; + }, + }, + { + accessorKey: 'network', + header: t('worker.jobs.network'), + size: COL_SIZE_SM, + Cell: (props) => { + return ; + }, + }, + { + accessorKey: 'reward_amount', + header: t('worker.jobs.reward'), + size: COL_SIZE_MD, + enableSorting: true, + Cell: (props) => { + const { reward_amount, reward_token } = props.row.original; + return ( + + ); + }, + }, + { + accessorKey: 'job_type', + header: t('worker.jobs.jobType'), + size: COL_SIZE, + enableSorting: true, + Cell: ({ row }) => { + const label = t(`jobTypeLabels.${row.original.job_type as JobType}`); + return ( + + ); + }, + }, + { + accessorKey: 'expires_at', + header: t('worker.jobs.expiryTime'), + size: COL_SIZE_MD, + enableSorting: true, + Cell: (props) => { + return ( + + + + ); + }, + }, + { + accessorKey: 'status', + header: t('worker.jobs.status'), + size: COL_SIZE_SM, + enableSorting: true, + Cell: (props) => { + return ( + + {props.row.original.status.toLowerCase()} + + ); + }, + }, + { + accessorKey: 'assignment_id', + header: t('worker.jobs.action'), + size: COL_SIZE_MD, + enableSorting: true, + Cell: (props) => ( + + + + ), + }, + ], + [colorPalette, t, isMobile] + ); +}; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/hooks/use-my-jobs-filter-modal.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/hooks/use-my-jobs-filter-modal.tsx deleted file mode 100644 index 40a0c9c1be..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/hooks/use-my-jobs-filter-modal.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useModal } from '@/shared/contexts/modal-context'; -import { useGetUiConfig } from '@/shared/hooks'; -import { MyJobsFilterModal } from '../components/mobile/my-jobs-filter-modal'; - -export function useMyJobFilterModal() { - const { openModal, closeModal } = useModal(); - const { data: uiConfigData } = useGetUiConfig(); - - return { - openModal: () => { - openModal({ - content: ( - - ), - }); - }, - }; -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/my-jobs-view.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/my-jobs-view.tsx deleted file mode 100644 index 8e2500625b..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/my-jobs-view.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useIsMobile } from '@/shared/hooks'; -import { MyJobsTable } from './components/desktop'; -import { MyJobsListMobile } from './components/mobile'; - -export function MyJobsView({ chainIdsEnabled }: { chainIdsEnabled: number[] }) { - const isMobile = useIsMobile(); - - return isMobile ? ( - - ) : ( - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/my-jobs.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/my-jobs.page.tsx new file mode 100644 index 0000000000..cb814e3e61 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/my-jobs.page.tsx @@ -0,0 +1,88 @@ +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Stack, Typography } from '@mui/material'; +import RefreshIcon from '@mui/icons-material/Refresh'; + +import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; +import { useIsMobile } from '@/shared/hooks/use-is-mobile'; +import { Button } from '@/shared/components/ui/button'; +import { MyJobsListMobile } from './components/mobile/my-jobs-list-mobile'; +import { MyJobsTable } from './components/desktop/my-jobs-table'; +import { useRefreshJobsMutation } from './hooks'; +import { StatusFilter } from './components/desktop/status-filter'; +import { MyJobsFilters } from './components/desktop/my-jobs-filters'; +import { useMyJobsFilterStore } from '../hooks'; +import { JobsSwitcherMobile } from '@/router/components/layout/protected/jobs-switcher-mobile'; + +export function MyJobsPage() { + const { colorPalette } = useColorMode(); + const isMobile = useIsMobile(); + const { t } = useTranslation(); + + const { + filterParams: { oracle_address }, + resetFilterParams, + } = useMyJobsFilterStore(); + + useEffect(() => { + return () => { + resetFilterParams(); + }; + }, [resetFilterParams]); + + const { mutate: refreshTasksMutation, isPending: isRefreshPending } = + useRefreshJobsMutation(); + + return ( + + {isMobile && ( + + + + )} + + + {t('worker.jobs.myJobs')} + + + + + + {!isMobile && } + + + + + {isMobile ? : } + + + ); +} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/__tests__/get-chip-status-color.test.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/__tests__/get-chip-status-color.test.ts deleted file mode 100644 index 900e4ea26a..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/__tests__/get-chip-status-color.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { colorPalette } from '@/shared/styles/color-palette'; -import { getChipStatusColor } from '../get-chip-status-color'; -import { MyJobStatus, UNKNOWN_JOB_STATUS } from '../../../types'; - -describe('getChipStatusColor Function', () => { - it('should return the secondary main color for ACTIVE status', () => { - const result = getChipStatusColor(MyJobStatus.ACTIVE, colorPalette); - expect(result).toBe(colorPalette.secondary.main); - }); - - it('should return the success main color for COMPLETED status', () => { - const result = getChipStatusColor(MyJobStatus.COMPLETED, colorPalette); - expect(result).toBe(colorPalette.success.main); - }); - - it('should return the error main color for unknown status', () => { - const result = getChipStatusColor(UNKNOWN_JOB_STATUS, colorPalette); - expect(result).toBe(colorPalette.error.main); - }); -}); diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/get-chip-status-color.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/get-chip-status-color.ts deleted file mode 100644 index 6fc926bbf8..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/get-chip-status-color.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type ColorPalette } from '@/shared/styles/color-palette'; -import { MyJobStatus, type UNKNOWN_JOB_STATUS } from '../../types'; - -export function getChipStatusColor( - status: MyJobStatus | typeof UNKNOWN_JOB_STATUS, - colorPalette: ColorPalette -) { - switch (status) { - case MyJobStatus.ACTIVE: - return colorPalette.secondary.main; - case MyJobStatus.COMPLETED: - return colorPalette.success.main; - default: - return colorPalette.error.main; - } -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/index.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/index.ts deleted file mode 100644 index bfddc6caac..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './get-chip-status-color'; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/types.ts b/packages/apps/human-app/frontend/src/modules/worker/jobs/types.ts index d2317b7cf0..977e6b9e3a 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/types.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/types.ts @@ -13,6 +13,8 @@ export enum MyJobStatus { REJECTED = 'REJECTED', } +export type StatusFilterType = '' | MyJobStatus.ACTIVE | MyJobStatus.COMPLETED; + export const UNKNOWN_JOB_STATUS = 'UNKNOWN'; export enum SortField { diff --git a/packages/apps/human-app/frontend/src/modules/worker/profile/components/modal-states.tsx b/packages/apps/human-app/frontend/src/modules/worker/profile/components/modal-states.tsx deleted file mode 100644 index 929d1219c2..0000000000 --- a/packages/apps/human-app/frontend/src/modules/worker/profile/components/modal-states.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import CheckIcon from '@mui/icons-material/CheckCircle'; -import { Loader } from '@/shared/components/ui/loader'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import CloseIcon from '@mui/icons-material/Cancel'; - -export function ModalLoading() { - return ( - - - - ); -} - -export function ModalSuccess({ children }: { children: React.ReactNode }) { - return ( - <> - - - - {children} - - ); -} - -export function ModalError({ message }: { message: string }) { - return ( - <> - - - - - {message} - - - ); -} diff --git a/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx index 70285faf85..d705d0edbd 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/profile/views/profile.page.tsx @@ -1,5 +1,5 @@ -import { Box, IconButton, Stack, Tooltip, Typography } from '@mui/material'; -import { useEffect, useRef, useState, type MouseEvent } from 'react'; +import { useEffect } from 'react'; +import { Box, Stack, Typography } from '@mui/material'; import { t } from 'i18next'; import { Link, useNavigate } from 'react-router-dom'; @@ -11,22 +11,16 @@ import { } from '@/shared/hooks/use-notification'; import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; import { ProfileData } from '../components/profile-data'; -import { - CheckmarkIcon, - CopyIcon, - LogoutIcon, -} from '@/shared/components/ui/icons'; +import { CheckmarkIcon, LogoutIcon } from '@/shared/components/ui/icons'; import { shortenEscrowAddress } from '@/shared/helpers/evm'; import { Button } from '@/shared/components/ui/button'; import { browserAuthProvider } from '@/shared/contexts/browser-auth-provider'; import { routerPaths } from '@/router/router-paths'; import { BackButton } from '@/shared/components/ui/page-card/back-button'; import { useIsMobile } from '@/shared/hooks/use-is-mobile'; +import { CopyToClipboardButton } from '@/shared/components/ui/copy-to-clipboard-button'; export function WorkerProfilePage() { - const [isCopied, setIsCopied] = useState(false); - const timeoutRef = useRef(null); - const { user } = useAuthenticatedUser(); const { colorPalette } = useColorMode(); const { isConnected, initializing, web3ProviderMutation } = @@ -39,22 +33,6 @@ export function WorkerProfilePage() { navigate(routerPaths.worker.jobsDiscovery); }; - const handleCopyClick = (e: MouseEvent) => { - if (isCopied) return; - - e.stopPropagation(); - navigator.clipboard.writeText(user.wallet_address ?? ''); - setIsCopied(true); - - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - - timeoutRef.current = setTimeout(() => { - setIsCopied(false); - }, 1500); - }; - const handleSignOut = () => { browserAuthProvider.signOut({ callback: () => { @@ -63,15 +41,6 @@ export function WorkerProfilePage() { }); }; - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - }; - }, []); - useEffect(() => { if (initializing) return; @@ -98,7 +67,7 @@ export function WorkerProfilePage() { ]); return ( - + {shortenEscrowAddress(user.wallet_address ?? '', 9, 8)} - - - - - + diff --git a/packages/apps/human-app/frontend/src/modules/worker/services/oracles.service.ts b/packages/apps/human-app/frontend/src/modules/worker/services/oracles.service.ts index 518006885e..bd4f5fd2b2 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/services/oracles.service.ts +++ b/packages/apps/human-app/frontend/src/modules/worker/services/oracles.service.ts @@ -1,7 +1,6 @@ import { z } from 'zod'; import { ApiClientError, authorizedHumanAppApiClient } from '@/api'; import { env } from '@/shared/env'; -import { MainnetChains, TestnetChains } from '@/modules/smart-contracts/chains'; const apiPaths = { oracles: '/oracles', @@ -13,6 +12,10 @@ const OracleSchema = z.object({ role: z.string(), name: z.string(), url: z.string(), + nTasks: z.number(), + minRewardAmount: z.string(), + maxRewardAmount: z.string(), + rewardToken: z.string(), jobTypes: z.array(z.string()), registrationNeeded: z.boolean().optional().nullable(), registrationInstructions: z.string().optional().nullable(), @@ -26,41 +29,13 @@ export type Oracle = OracleBase & { name: string; }; -const isTestnet = env.VITE_NETWORK === 'testnet'; - -const H_CAPTCHA_ORACLE: Oracle = { - address: env.VITE_H_CAPTCHA_ORACLE_ADDRESS, - chainId: isTestnet ? TestnetChains[0].chainId : MainnetChains[0].chainId, - jobTypes: env.VITE_H_CAPTCHA_ORACLE_TASK_TYPES, - role: env.VITE_H_CAPTCHA_ORACLE_ROLE, - url: env.VITE_H_CAPTCHA_ORACLE_ANNOTATION_TOOL, - name: 'hCaptcha', - registrationNeeded: false, -}; - -async function getOracles(selectedJobTypes: string[]) { +async function getOracles() { try { - const params = selectedJobTypes.length - ? { selected_job_types: selectedJobTypes } - : undefined; - - const queryParams = params ?? {}; - let oracles: Oracle[] = []; - if ( - selectedJobTypes.length === 0 || - selectedJobTypes.some((t) => H_CAPTCHA_ORACLE.jobTypes.includes(t)) - ) { - oracles.push(H_CAPTCHA_ORACLE); - } - if (env.VITE_FEATURE_FLAG_JOBS_DISCOVERY) { const results = await authorizedHumanAppApiClient.get( - apiPaths.oracles, - { - queryParams, - } + apiPaths.oracles ); if (Array.isArray(results)) { diff --git a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx b/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx deleted file mode 100644 index 1c28472e9b..0000000000 --- a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { t } from 'i18next'; -import type { UserData } from '@/modules/auth/context/auth-context'; -import { routerPaths } from '@/router/router-paths'; -import { KycStatus } from '@/modules/worker/profile/types'; - -export const workerDrawerTopMenuItems = (user: UserData | null) => { - return [ - { - label: t('components.DrawerNavigation.jobs'), - link: routerPaths.worker.jobsDiscovery, - disabled: !user?.wallet_address || user.kyc_status !== KycStatus.APPROVED, - }, - ]; -}; diff --git a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/index.ts b/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/index.ts deleted file mode 100644 index 8dd1acfaae..0000000000 --- a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './drawer-menu-items-worker'; diff --git a/packages/apps/human-app/frontend/src/router/components/footer.tsx b/packages/apps/human-app/frontend/src/router/components/footer.tsx index 8eb703de07..79ae8ef89e 100644 --- a/packages/apps/human-app/frontend/src/router/components/footer.tsx +++ b/packages/apps/human-app/frontend/src/router/components/footer.tsx @@ -6,13 +6,9 @@ import { useColorMode } from '@/shared/contexts/color-mode'; interface FooterProps { displayChatIcon?: boolean; - isProtected?: boolean; } -export function Footer({ - displayChatIcon = true, - isProtected = false, -}: FooterProps) { +export function Footer({ displayChatIcon = true }: FooterProps) { const { colorPalette, isDarkMode } = useColorMode(); const { t } = useTranslation(); @@ -24,8 +20,8 @@ export function Footer({ container sx={{ width: '100%', - px: { xs: 3, md: 0 }, - py: { xs: 2, md: isProtected ? 0 : 3 }, + px: { xs: 3, md: 2 }, + py: { xs: 2, md: 3 }, bgcolor: { xs: colorPalette.background.paper, md: 'transparent' }, borderTop: { xs: `1px solid ${colorPalette.border.main}`, diff --git a/packages/apps/human-app/frontend/src/router/components/index.ts b/packages/apps/human-app/frontend/src/router/components/index.ts index 0a514e542c..c78a32e4dc 100644 --- a/packages/apps/human-app/frontend/src/router/components/index.ts +++ b/packages/apps/human-app/frontend/src/router/components/index.ts @@ -1,3 +1,2 @@ -export * from './drawer-menu-items'; export * from './layout/protected'; export * from './layout/unprotected'; diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/desktop-aside-bar.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/desktop-aside-bar.tsx index 051f917544..363945c19d 100644 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/desktop-aside-bar.tsx +++ b/packages/apps/human-app/frontend/src/router/components/layout/protected/desktop-aside-bar.tsx @@ -1,17 +1,47 @@ +import { + Box, + IconButton, + List, + ListItem, + ListItemButton, + Stack, +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { Link, useLocation } from 'react-router-dom'; + import { ProfileData } from '@/modules/worker/profile/components/profile-data'; +import { routerPaths } from '@/router/router-paths'; import { Button } from '@/shared/components/ui/button'; import { ColorModeSwitch } from '@/shared/components/ui/dark-mode-switch'; -import { HelpIcon, HumanLogoNavbarIcon } from '@/shared/components/ui/icons'; +import { + HelpIcon, + HumanLogoNavbarIcon, + TriangleIcon, +} from '@/shared/components/ui/icons'; import { useColorMode } from '@/shared/contexts/color-mode/use-color-mode'; import { useHandleMainNavIconClick } from '@/shared/hooks/use-handle-main-nav-icon-click'; -import { Box, IconButton, Stack } from '@mui/material'; -import { useTranslation } from 'react-i18next'; + +const menuItems = [ + { + labelKey: 'components.DrawerNavigation.availableJobs', + href: routerPaths.worker.jobsDiscovery, + }, + { + labelKey: 'components.DrawerNavigation.myJobs', + href: routerPaths.worker.myJobs, + }, +] as const; export function DesktopAsideBar() { - const { colorPalette } = useColorMode(); + const { colorPalette, isDarkMode } = useColorMode(); const { t } = useTranslation(); + const { pathname } = useLocation(); const handleMainNavIconClick = useHandleMainNavIconClick(); + const activeLinkGradient = isDarkMode + ? 'linear-gradient(90deg, rgba(212, 207, 255, 0.10) 0%, rgba(37, 29, 71, 0.10) 90%)' + : 'linear-gradient(90deg, rgba(50, 10, 141, 0.10) 0%, rgba(255, 255, 255, 0.10) 90%)'; + return ( - + + + {menuItems.map((item) => { + const { href, labelKey } = item; + const isActive = pathname === href; + return ( + + + {isActive && } + {t(labelKey)} + + + ); + })} + + + {BUTTONS.map((button) => { + const isActive = pathname === button.path; + return ( + + ); + })} + + ); +} diff --git a/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx b/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx index 5e6449aece..213a11e448 100644 --- a/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx +++ b/packages/apps/human-app/frontend/src/router/components/layout/protected/layout.tsx @@ -1,9 +1,8 @@ -import { Box, Stack, styled } from '@mui/material'; -import { useEffect, useRef, useState, type ReactElement } from 'react'; +import { useRef } from 'react'; import { Outlet, useLocation } from 'react-router-dom'; +import { Box, Stack, styled } from '@mui/material'; import { useIsMobile } from '@/shared/hooks/use-is-mobile'; -import { useIsHCaptchaLabelingPage } from '@/shared/hooks/use-is-hcaptcha-labeling-page'; import { GovernanceBanner } from '@/modules/governance-banner/components/governance-banner'; import { Footer } from '../../footer'; import { Navbar } from './navbar'; @@ -13,59 +12,22 @@ import { ProfileBottomTray } from '@/modules/worker/profile/components/profile-b import { MOBILE_BOTTOM_TRAY_HEIGHT } from '@/shared/consts'; import { routerPaths } from '@/router/router-paths'; -const Main = styled('main', { - shouldForwardProp: (prop) => prop !== 'open', -})<{ - open?: boolean; -}>(({ theme, open }) => ({ - width: '100%', +const Main = styled('main')({ display: 'flex', flex: '1', - transition: theme.transitions.create('margin', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - ...(open && { - transition: theme.transitions.create('margin', { - easing: theme.transitions.easing.easeOut, - duration: theme.transitions.duration.enteringScreen, - }), - }), -})); + width: '100%', +}); -export function ProtectedLayout({ - renderHCaptchaStatisticsDrawer, - renderGovernanceBanner, -}: { - renderHCaptchaStatisticsDrawer?: (isOpen: boolean) => ReactElement; - renderGovernanceBanner?: boolean; -}) { +export function ProtectedLayout() { const layoutElementRef = useRef(null); - const isHCaptchaLabelingPage = useIsHCaptchaLabelingPage(); + const isMobile = useIsMobile(); - const [drawerOpen, setDrawerOpen] = useState(!isMobile); - const [hcaptchaDrawerOpen, setHcaptchaDrawerOpen] = useState(false); const { colorPalette } = useColorMode(); const location = useLocation(); - const toggleUserStatsDrawer = isHCaptchaLabelingPage - ? () => { - setHcaptchaDrawerOpen((state) => !state); - } - : undefined; const isProfilePage = location.pathname === routerPaths.worker.profile; const isBottomTrayVisible = isMobile && !isProfilePage; - useEffect(() => { - if (isMobile) { - setHcaptchaDrawerOpen(false); - setDrawerOpen(false); - } else { - setHcaptchaDrawerOpen(false); - setDrawerOpen(true); - } - }, [isMobile]); - return ( - {isMobile && ( - - )} - + {isMobile && } {!isMobile && } - {isHCaptchaLabelingPage && renderHCaptchaStatisticsDrawer - ? renderHCaptchaStatisticsDrawer(hcaptchaDrawerOpen) - : null} -
+
- {renderGovernanceBanner && } + {isBottomTrayVisible && }
-