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/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/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/jobs-discovery/components/hcaptha-widget.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/hcaptha-widget.tsx new file mode 100644 index 0000000000..985b8a9576 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/hcaptha-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..e8eb2de22d 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-list'; export * from './oracles-table-item-mobile'; export * from './oracles-table-desktop'; 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..3cb2cd4af5 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs-discovery/components/oracle-job-card.tsx @@ -0,0 +1,283 @@ +import { useState } from 'react'; +import { + Box, + Card, + CardContent, + 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 { shortenEscrowAddress } from '@/shared/helpers/evm'; +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 { Chip } from '@/shared/components/ui/chip'; + +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 [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')}: + + + {shortenEscrowAddress(oracle.address, 4, 4)} + + + + + + + {t('worker.oraclesList.reward')}: + + + {isRewardAmountsEqual ? ( + <> + {oracle.minRewardAmount} {oracle.rewardToken} + + ) : ( + <> + {oracle.minRewardAmount} {oracle.rewardToken} -{' '} + {oracle.maxRewardAmount} {oracle.rewardToken} + + )} + + + + + + {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 ( + + ); + })} + + + + + + ); +} 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.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..5280eb2f72 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,94 @@ -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/hcaptha-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'; + +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 ( - - + - - - + + + + + + {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-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/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..edc417100b 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 @@ -6,3 +6,4 @@ export * from './reward-amount'; export * from './sorting'; export * from './more-button'; export * from './report-abuse-modal'; +export * from './network-name'; 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..dd48dec93a 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; diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/components/network-name.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/network-name.tsx new file mode 100644 index 0000000000..5db2e7c854 --- /dev/null +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/components/network-name.tsx @@ -0,0 +1,22 @@ +import { Typography } from '@mui/material'; + +import { getNetworkName } from '@/modules/smart-contracts/get-network-name'; +import { useColorMode } from '@/shared/contexts/color-mode'; +import { useIsMobile } from '@/shared/hooks'; + +export function NetworkName({ chainId }: { chainId: number }) { + const { colorPalette } = useColorMode(); + const isMobile = useIsMobile(); + + return ( + + {getNetworkName(chainId)} + + ); +} 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..534b4bdcd9 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,37 @@ 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/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/jobs.page.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/jobs.page.tsx index a4e79bd62c..4d6724549f 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 @@ -7,12 +7,9 @@ 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 +27,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) => { @@ -115,22 +106,10 @@ export function JobsPage() { - {isError ? ( - - ) : ( - - )} + {isError ? : null} - {isError ? ( - - ) : ( - - )} + {isError ? : null} 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..9d573f20e6 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 ( - + } @@ -209,6 +175,9 @@ export function WorkerProfilePage() { {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..944d4b927e 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,7 @@ import { z } from 'zod'; import { ApiClientError, authorizedHumanAppApiClient } from '@/api'; import { env } from '@/shared/env'; -import { MainnetChains, TestnetChains } from '@/modules/smart-contracts/chains'; +import { TestnetChains } from '@/modules/smart-contracts/chains'; const apiPaths = { oracles: '/oracles', @@ -13,6 +13,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,17 +30,76 @@ 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, -}; +// 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, +// }; + +//TODO: Remove before merging +const MOCKED_ORACLES: Oracle[] = [ + { + address: '0x1111111111111111111111111111111111111111', + chainId: TestnetChains[0].chainId, + jobTypes: ['image_points'], + role: 'Recording Oracle', + url: 'https://example.com/oracle-1', + name: 'Mock Oracle 1', + nTasks: 1, + minRewardAmount: '0.001', + maxRewardAmount: '0.002', + rewardToken: 'HMT', + registrationNeeded: false, + }, + { + address: '0x2222222222222222222222222222222222222222', + chainId: TestnetChains[0].chainId, + jobTypes: ['image_points', 'image_boxes'], + role: 'Recording Oracle', + url: 'https://example.com/oracle-2', + name: 'Mock Oracle 2', + nTasks: 2, + minRewardAmount: '0.1', + maxRewardAmount: '0.2', + rewardToken: 'HMT', + registrationNeeded: false, + }, + { + address: '0x3333333333333333333333333333333333333333', + chainId: TestnetChains[0].chainId, + jobTypes: ['image_points', 'image_boxes'], + role: 'Recording Oracle', + url: 'https://example.com/oracle-3', + name: 'Mock Oracle 3', + nTasks: 3, + minRewardAmount: '0.1', + maxRewardAmount: '0.2', + rewardToken: 'HMT', + registrationNeeded: false, + }, + { + address: '0x4444444444444444444444444444444444444444', + chainId: TestnetChains[0].chainId, + jobTypes: [ + 'image_points', + 'image_boxes', + 'image_polygons', + 'social_media_engagement', + ], + role: 'Recording Oracle', + url: 'https://example.com/oracle-4', + name: 'Mock Oracle 4', + nTasks: 8, + minRewardAmount: '1', + maxRewardAmount: '2', + rewardToken: 'HMT', + registrationNeeded: false, + }, +]; async function getOracles(selectedJobTypes: string[]) { try { @@ -48,11 +111,8 @@ async function getOracles(selectedJobTypes: string[]) { let oracles: Oracle[] = []; - if ( - selectedJobTypes.length === 0 || - selectedJobTypes.some((t) => H_CAPTCHA_ORACLE.jobTypes.includes(t)) - ) { - oracles.push(H_CAPTCHA_ORACLE); + if (selectedJobTypes.length === 0) { + oracles.push(...MOCKED_ORACLES); } if (env.VITE_FEATURE_FLAG_JOBS_DISCOVERY) { 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)} + + + ); + })} + + 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 && }
-