diff --git a/src/components/app-button-menu/AppButtonMenu.tsx b/src/components/app-button-menu/AppButtonMenu.tsx index d672cfd40..33dcea531 100644 --- a/src/components/app-button-menu/AppButtonMenu.tsx +++ b/src/components/app-button-menu/AppButtonMenu.tsx @@ -73,7 +73,7 @@ const AppButtonMenu = >({ const filteredItems = useMemo(() => { const noneItem = { _id: 'null', - [valueField as string]: 'None' + [valueField as string]: 'No category' } const filtered = response.filter((item) => diff --git a/src/components/enhanced-table/date-filter/DateFilter.constants.ts b/src/components/enhanced-table/date-filter/DateFilter.constants.ts new file mode 100644 index 000000000..6cc5574a0 --- /dev/null +++ b/src/components/enhanced-table/date-filter/DateFilter.constants.ts @@ -0,0 +1,35 @@ +export type Placement = + | 'auto-end' + | 'auto-start' + | 'auto' + | 'bottom-end' + | 'bottom-start' + | 'bottom' + | 'left-end' + | 'left-start' + | 'left' + | 'right-end' + | 'right-start' + | 'right' + | 'top-end' + | 'top-start' + | 'top' + +export const datePickersOptions: { + placement: Placement + direction: 'from' | 'to' +}[] = [ + { + placement: 'bottom-end', + direction: 'from' + }, + { + placement: 'bottom-start', + direction: 'to' + } +] + +export const initialState: { from: boolean; to: boolean } = { + from: false, + to: false +} diff --git a/src/components/enhanced-table/date-filter/DateFilter.styles.js b/src/components/enhanced-table/date-filter/DateFilter.styles.ts similarity index 100% rename from src/components/enhanced-table/date-filter/DateFilter.styles.js rename to src/components/enhanced-table/date-filter/DateFilter.styles.ts diff --git a/src/components/enhanced-table/date-filter/DateFilter.jsx b/src/components/enhanced-table/date-filter/DateFilter.tsx similarity index 74% rename from src/components/enhanced-table/date-filter/DateFilter.jsx rename to src/components/enhanced-table/date-filter/DateFilter.tsx index 5ef534455..913caf890 100644 --- a/src/components/enhanced-table/date-filter/DateFilter.jsx +++ b/src/components/enhanced-table/date-filter/DateFilter.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import React, { useState } from 'react' import { format } from 'date-fns' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { DesktopDatePicker } from '@mui/x-date-pickers/DesktopDatePicker' @@ -9,21 +9,43 @@ import IconButton from '@mui/material/IconButton' import ClearIcon from '@mui/icons-material/Clear' import CalendarMonthIcon from '@mui/icons-material/CalendarMonth' -import { initialState, datePickersOptions } from './constants' +import { initialState, datePickersOptions } from './DateFilter.constants' import { styles } from './DateFilter.styles' -const DateFilter = ({ filter, setFilter, clearFilter }) => { - const [open, setOpen] = useState(initialState) +interface Filter { + from: string | null + to: string | null +} + +interface OpenState { + from: boolean + to: boolean +} + +export interface DateFilterProps { + filter: Filter + setFilter: (filter: Filter) => void + clearFilter: () => void +} + +const DateFilter: React.FC = ({ + filter, + setFilter, + clearFilter +}) => { + const [open, setOpen] = useState(initialState) - const handleChange = (direction) => (date) => { - setFilter({ ...filter, [direction]: format(date, 'yyyy-MM-dd') }) + const handleChange = (direction: string) => (date: Date | null) => { + if (date) { + setFilter({ ...filter, [direction]: format(date, 'yyyy-MM-dd') }) + } } - const handleClose = (direction) => { + const handleClose = (direction: string) => { setOpen((prev) => ({ ...prev, [direction]: false })) } - const handleOpen = (direction) => { + const handleOpen = (direction: string) => { setOpen((prev) => ({ ...prev, [direction]: true })) } diff --git a/src/components/enhanced-table/date-filter/constants.js b/src/components/enhanced-table/date-filter/constants.js deleted file mode 100644 index 9109b6d79..000000000 --- a/src/components/enhanced-table/date-filter/constants.js +++ /dev/null @@ -1,15 +0,0 @@ -export const datePickersOptions = [ - { - placement: 'bottom-end', - direction: 'from' - }, - { - placement: 'bottom-start', - direction: 'to' - } -] - -export const initialState = { - from: false, - to: false -} diff --git a/src/components/find-block/FindBlock.jsx b/src/components/find-block/FindBlock.tsx similarity index 84% rename from src/components/find-block/FindBlock.jsx rename to src/components/find-block/FindBlock.tsx index ec3151ca0..f419678a8 100644 --- a/src/components/find-block/FindBlock.jsx +++ b/src/components/find-block/FindBlock.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react' +import { ChangeEvent, KeyboardEvent, useCallback, useState } from 'react' import { useNavigate } from 'react-router' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' @@ -13,7 +13,11 @@ import bag from '~/assets/img/student-home/bag.png' import { authRoutes } from '~/router/constants/authRoutes' import { styles } from '~/components/find-block/find-block.styles' -const FindBlock = ({ translationKey }) => { +interface FindBlockProps { + translationKey: string +} + +const FindBlock = ({ translationKey }: FindBlockProps) => { const [inputValue, setInputValue] = useState('') const { t } = useTranslation() const navigate = useNavigate() @@ -21,12 +25,12 @@ const FindBlock = ({ translationKey }) => { const encodedInputValue = encodeURIComponent(inputValue) const findOffers = `${authRoutes.findOffers.path}?search=${encodedInputValue}` - const onChange = (e) => { + const onChange = (e: ChangeEvent) => { setInputValue(e.target.value) } const handleEnterPress = useCallback( - (e) => { + (e: KeyboardEvent) => { if (e.key === 'Enter' && inputValue) { navigate(findOffers) } @@ -45,7 +49,7 @@ const FindBlock = ({ translationKey }) => { fullWidth={isMobile} onChange={onChange} onClear={onClear} - onKeyPress={handleEnterPress} + onKeyDown={handleEnterPress} placeholder={t(`${translationKey}.label`)} startIcon={} sx={styles.input} diff --git a/src/components/find-block/find-block.styles.js b/src/components/find-block/find-block.styles.ts similarity index 100% rename from src/components/find-block/find-block.styles.js rename to src/components/find-block/find-block.styles.ts diff --git a/src/components/find-block/find-student-constants.js b/src/components/find-block/find-student-constants.ts similarity index 100% rename from src/components/find-block/find-student-constants.js rename to src/components/find-block/find-student-constants.ts diff --git a/src/components/find-block/find-tutor-constants.js b/src/components/find-block/find-tutor-constants.ts similarity index 100% rename from src/components/find-block/find-tutor-constants.js rename to src/components/find-block/find-tutor-constants.ts diff --git a/src/components/icon-with-text-list/ProfileDoneItemsList.style.js b/src/components/icon-with-text-list/ProfileDoneItemsList.style.ts similarity index 100% rename from src/components/icon-with-text-list/ProfileDoneItemsList.style.js rename to src/components/icon-with-text-list/ProfileDoneItemsList.style.ts diff --git a/src/components/icon-with-text-list/ProfileDoneItemsList.jsx b/src/components/icon-with-text-list/ProfileDoneItemsList.tsx similarity index 72% rename from src/components/icon-with-text-list/ProfileDoneItemsList.jsx rename to src/components/icon-with-text-list/ProfileDoneItemsList.tsx index 3281229ca..399fbd06f 100644 --- a/src/components/icon-with-text-list/ProfileDoneItemsList.jsx +++ b/src/components/icon-with-text-list/ProfileDoneItemsList.tsx @@ -1,14 +1,27 @@ +import { FC } from 'react' import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' import Typography from '@mui/material/Typography' +import { PopoverOrigin } from '@mui/material' import AppPopover from '../app-popover/AppPopover' import TitleWithDescription from '~/components/title-with-description/TitleWithDescription' import { styles } from '~/components/icon-with-text-list/ProfileDoneItemsList.style' +import { ProfileDoneItem } from '~/types' -const ProfileDoneItemsList = ({ items, icon, defaultQuantity }) => { +interface ProfileDoneItemsListProps { + items: ProfileDoneItem[] + icon: JSX.Element + defaultQuantity: number +} + +const ProfileDoneItemsList: FC = ({ + items, + icon, + defaultQuantity +}) => { const { t } = useTranslation() const shouldShowMore = items.length > defaultQuantity @@ -36,12 +49,13 @@ const ProfileDoneItemsList = ({ items, icon, defaultQuantity }) => { return ( {itemsList} diff --git a/src/components/message/Message.tsx b/src/components/message/Message.tsx index 2b5570a31..b1bad0d9e 100644 --- a/src/components/message/Message.tsx +++ b/src/components/message/Message.tsx @@ -76,7 +76,9 @@ const Message: FC = ({ const avatar = !isMyMessage && isAvatarVisible && ( diff --git a/src/components/offer-card/offer-details/OfferDetails.jsx b/src/components/offer-card/offer-details/OfferDetails.tsx similarity index 76% rename from src/components/offer-card/offer-details/OfferDetails.jsx rename to src/components/offer-card/offer-details/OfferDetails.tsx index 6870baabe..48502290b 100644 --- a/src/components/offer-card/offer-details/OfferDetails.jsx +++ b/src/components/offer-card/offer-details/OfferDetails.tsx @@ -5,8 +5,18 @@ import LanguagesListWithIcon from '~/components/languages-list-with-icon/Languag import SubjectLevelChips from '~/components/subject-level-chips/SubjectLevelChips' import { styles } from '~/components/offer-card/offer-details/OfferDetails.styles' +import { LanguagesEnum, ProficiencyLevelEnum } from '~/types' -const OfferDetails = ({ +interface OfferDetailsProps { + subject: string + chipsColor: string + level: ProficiencyLevelEnum + title: string + description: string + languages: LanguagesEnum +} + +const OfferDetails: React.FC = ({ subject, chipsColor, level, diff --git a/src/components/profile-item/ProfileItem.styles.js b/src/components/profile-item/ProfileItem.styles.ts similarity index 100% rename from src/components/profile-item/ProfileItem.styles.js rename to src/components/profile-item/ProfileItem.styles.ts diff --git a/src/components/profile-item/ProfileItem.jsx b/src/components/profile-item/ProfileItem.tsx similarity index 73% rename from src/components/profile-item/ProfileItem.jsx rename to src/components/profile-item/ProfileItem.tsx index a05c5da3a..39e7cb15f 100644 --- a/src/components/profile-item/ProfileItem.jsx +++ b/src/components/profile-item/ProfileItem.tsx @@ -5,8 +5,14 @@ import { useTranslation } from 'react-i18next' import { styles } from '~/components/profile-item/ProfileItem.styles' import useBreakpoints from '~/hooks/use-breakpoints' +import { ProfileItemType } from '~/components/profile-item/complete-profile.constants' -const ProfileItem = ({ item, isFilled = false }) => { +interface ProfileItemProps { + item: ProfileItemType + isFilled?: boolean +} + +const ProfileItem = ({ item, isFilled = false }: ProfileItemProps) => { const { t } = useTranslation() const { isMobile } = useBreakpoints() const { id, icon } = item @@ -17,16 +23,10 @@ const ProfileItem = ({ item, isFilled = false }) => { {!isMobile && {icon}} - + {t(`completeProfile.${id}.title`)} - + {t(`completeProfile.${id}.subtitle`)} diff --git a/src/components/search-input/SearchInput.jsx b/src/components/search-input/SearchInput.tsx similarity index 86% rename from src/components/search-input/SearchInput.jsx rename to src/components/search-input/SearchInput.tsx index 337765099..3b989cca8 100644 --- a/src/components/search-input/SearchInput.jsx +++ b/src/components/search-input/SearchInput.tsx @@ -7,7 +7,12 @@ import SearchIcon from '@mui/icons-material/Search' import { styles } from './SearchInput.styles' -const SearchInput = ({ search, setSearch }) => { +interface SearchInputProps { + search: string + setSearch: React.Dispatch> +} + +const SearchInput = ({ search, setSearch }: SearchInputProps) => { const [searchInput, setSearchInput] = useState(search) return ( @@ -37,7 +42,7 @@ const SearchInput = ({ search, setSearch }) => { autoComplete: 'off' }} onChange={(e) => setSearchInput(e.target.value)} - onKeyPress={(e) => { + onKeyDown={(e) => { if (e.key === 'Enter') { setSearch(searchInput) } diff --git a/src/components/sidebar-menu/SidebarMenu.styles.ts b/src/components/sidebar-menu/SidebarMenu.styles.ts index d475eb1f3..93409b8a7 100644 --- a/src/components/sidebar-menu/SidebarMenu.styles.ts +++ b/src/components/sidebar-menu/SidebarMenu.styles.ts @@ -37,5 +37,14 @@ export const styles = { minWidth: 'unset' } } + }, + listItemContent: { + display: 'flex', + alignItems: 'center', + gap: 1.25 + }, + errorIcon: { + color: `${palette.error[800]} !important`, + fontSize: '1.25rem' } } diff --git a/src/components/sidebar-menu/SidebarMenu.tsx b/src/components/sidebar-menu/SidebarMenu.tsx index 84064ee56..84aa5afce 100644 --- a/src/components/sidebar-menu/SidebarMenu.tsx +++ b/src/components/sidebar-menu/SidebarMenu.tsx @@ -3,6 +3,8 @@ import ListItemButton from '@mui/material/ListItemButton' import ListItemText from '@mui/material/ListItemText' import List from '@mui/material/List' import ListItemIcon from '@mui/material/ListItemIcon' +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' +import { Box, Tooltip } from '@mui/material' import { FC } from 'react' import { useTranslation } from 'react-i18next' @@ -16,9 +18,15 @@ interface SidebarMenu { tabsData: UserProfileProps handleClick: (tab: UserProfileTabsEnum) => void activeTab: UserProfileTabsEnum + errorTooltipHolders: Partial> } -const SidebarMenu: FC = ({ handleClick, tabsData, activeTab }) => { +const SidebarMenu: FC = ({ + handleClick, + tabsData, + activeTab, + errorTooltipHolders +}) => { const { t } = useTranslation() const list = Object.keys(tabsData).map((key) => { @@ -27,11 +35,28 @@ const SidebarMenu: FC = ({ handleClick, tabsData, activeTab }) => { const isTabActive = tabKey === activeTab + const enableTooltipError = !isTabActive && errorTooltipHolders?.[tabKey] + + const toolTip = enableTooltipError && ( + + + + ) + return ( handleClick(tabKey)}> {item.icon} - + + + {t(item.title)} + {toolTip} + + ) diff --git a/src/components/user-profile-info/UserProfileInfo.tsx b/src/components/user-profile-info/UserProfileInfo.tsx index 1b05b2d9f..d4777b3f7 100644 --- a/src/components/user-profile-info/UserProfileInfo.tsx +++ b/src/components/user-profile-info/UserProfileInfo.tsx @@ -67,7 +67,7 @@ const UserProfileInfo: FC = ({ const avatar = ( ) diff --git a/src/constants/translations/en/edit-profile.json b/src/constants/translations/en/edit-profile.json index f1d42b1fa..c49262ed0 100644 --- a/src/constants/translations/en/edit-profile.json +++ b/src/constants/translations/en/edit-profile.json @@ -19,7 +19,8 @@ "professionalStatus": "Describe in short your professional summary. Maximum 200 characters.", "languagesTitle": "Language", "languagesDesc": "Select the language in which you would like to study and cooperate.", - "videoPresentationDesc": "Add the link to your presentational video. Your video needs to be public and uploaded to YouTube." + "videoPresentationDesc": "Add the link to your presentational video. Your video needs to be public and uploaded to YouTube.", + "errorTooltip": "There is a problem with your changes" }, "professionalTab": { "tabTitle": "Professional info", diff --git a/src/constants/translations/en/signup.json b/src/constants/translations/en/signup.json index 9178fb7d6..a38b2dbb0 100644 --- a/src/constants/translations/en/signup.json +++ b/src/constants/translations/en/signup.json @@ -11,5 +11,7 @@ "joinUs": "Login!", "confirmEmailTitle": "Your email address needs to be verified", "confirmEmailMessage": "We sent a confirmation email to: ", - "confirmEmailDesc": " Check your email and click on the confirmation button to continue." + "confirmEmailDesc": " Check your email and click on the confirmation button to continue.", + "terms": "Terms", + "privacyPolicy": "Privacy policy" } diff --git a/src/constants/translations/uk/edit-profile.json b/src/constants/translations/uk/edit-profile.json index 15775a79c..d2b67d7ac 100644 --- a/src/constants/translations/uk/edit-profile.json +++ b/src/constants/translations/uk/edit-profile.json @@ -19,7 +19,8 @@ "professionalStatus": "Коротко опишіть своє професійне резюме. Максимум 200 символів.", "languagesTitle": "Мова", "languagesDesc": "Виберіть мову, якою ви б хотіли навчатися та співпрацювати.", - "videoPresentationDesc": "Додайте посилання на вашу відеопрезентацію. Відео має бути публічним та розміщеним на YouTube." + "videoPresentationDesc": "Додайте посилання на вашу відеопрезентацію. Відео має бути публічним та розміщеним на YouTube.", + "errorTooltip": "Виникла проблема з вашими змінами" }, "professionalTab": { "tabTitle": "Професійні відомості", diff --git a/src/constants/translations/uk/signup.json b/src/constants/translations/uk/signup.json index ad5729ed0..50e81ce5d 100644 --- a/src/constants/translations/uk/signup.json +++ b/src/constants/translations/uk/signup.json @@ -11,5 +11,7 @@ "joinUs": "Увійти!", "confirmEmailTitle": "Вашу електронну адресу потрібно підтвердити", "confirmEmailMessage": "Ми відправили лист з підтвердженням на електронну пошту: ", - "confirmEmailDesc": " Перевірте свою електронну пошту та натисніть на кнопку підтвердження для продовження." + "confirmEmailDesc": " Перевірте свою електронну пошту та натисніть на кнопку підтвердження для продовження.", + "terms": "Умовами", + "privacyPolicy": "Політикою конфіденційності" } diff --git a/src/containers/about-chat-sidebar/AboutChatSidebar.tsx b/src/containers/about-chat-sidebar/AboutChatSidebar.tsx index 929a8c26e..decf4b0d2 100644 --- a/src/containers/about-chat-sidebar/AboutChatSidebar.tsx +++ b/src/containers/about-chat-sidebar/AboutChatSidebar.tsx @@ -91,7 +91,8 @@ const AboutChatSidebar: FC = ({ diff --git a/src/containers/chat/chat-item/ChatItem.tsx b/src/containers/chat/chat-item/ChatItem.tsx index bf26b6104..5a2976511 100644 --- a/src/containers/chat/chat-item/ChatItem.tsx +++ b/src/containers/chat/chat-item/ChatItem.tsx @@ -15,7 +15,7 @@ import { OverlapEnum, PositionEnum } from '~/types' -import { getFormattedDate } from '~/utils/helper-functions' +import { createUrlPath, getFormattedDate } from '~/utils/helper-functions' interface ItemOfChatProps { isActiveChat: boolean @@ -84,7 +84,10 @@ const ChatItem: FC = ({ overlap={OverlapEnum.Circular} > diff --git a/src/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.tsx b/src/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.tsx index 23e11ad66..5f5ffe7af 100644 --- a/src/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.tsx +++ b/src/containers/edit-profile/professional-info-tab/add-professional-category-modal/AddProfessionalCategoryModal.tsx @@ -162,6 +162,13 @@ const AddProfessionalCategoryModal: FC = ({ _: SyntheticEvent, value: CategoryNameInterface | null ) => { + if (!value) { + handleDataChange({ + category: { _id: '', name: '' }, + subjects: [{ _id: '', name: '' }] + }) + return + } handleDataChange({ category: value, professionalSubjectTemplate }) } diff --git a/src/containers/edit-profile/profile-tab/profile-tab-form/ProfileTabForm.tsx b/src/containers/edit-profile/profile-tab/profile-tab-form/ProfileTabForm.tsx index 402950e54..d4b809fac 100644 --- a/src/containers/edit-profile/profile-tab/profile-tab-form/ProfileTabForm.tsx +++ b/src/containers/edit-profile/profile-tab/profile-tab-form/ProfileTabForm.tsx @@ -32,6 +32,7 @@ import { snackbarVariants } from '~/constants' import { imageResize } from '~/utils/image-resize' import { styles } from '~/containers/edit-profile/profile-tab/profile-tab-form/ProfileTabForm.styles' import { openAlert } from '~/redux/features/snackbarSlice' +import { createUrlPath } from '~/utils/helper-functions' export interface ProfileTabFormProps { data: EditProfileForm @@ -106,7 +107,7 @@ const ProfileTabForm: FC = ({ const { photo } = data const photoToDisplay = typeof photo === 'string' - ? photo && `${import.meta.env.VITE_APP_IMG_USER_URL}${photo}` + ? photo && createUrlPath(import.meta.env.VITE_APP_IMG_USER_URL, photo) : photo?.src return ( diff --git a/src/containers/email-confirm-modal/EmailConfirmModal.jsx b/src/containers/email-confirm-modal/EmailConfirmModal.tsx similarity index 90% rename from src/containers/email-confirm-modal/EmailConfirmModal.jsx rename to src/containers/email-confirm-modal/EmailConfirmModal.tsx index bf2c8f947..4a86d0329 100644 --- a/src/containers/email-confirm-modal/EmailConfirmModal.jsx +++ b/src/containers/email-confirm-modal/EmailConfirmModal.tsx @@ -16,9 +16,17 @@ import imgReject from '~/assets/img/email-confirmation-modals/not-success-icon.s import { AuthService } from '~/services/auth-service' import { ButtonVariantEnum } from '~/types' import useAxios from '~/hooks/use-axios' -import { useModalContext } from '~/context/modal-context' +import { Component, useModalContext } from '~/context/modal-context' -const EmailConfirmModal = ({ confirmToken, openModal }) => { +interface EmailConfirmModalProps { + confirmToken: string + openModal: (component: Component, delayToClose?: number) => void +} + +const EmailConfirmModal = ({ + confirmToken, + openModal +}: EmailConfirmModalProps) => { const { t } = useTranslation() const { closeModal } = useModalContext() diff --git a/src/containers/guest-home-page/google-login/GoogleLogin.styles.js b/src/containers/guest-home-page/google-login/GoogleLogin.styles.ts similarity index 95% rename from src/containers/guest-home-page/google-login/GoogleLogin.styles.js rename to src/containers/guest-home-page/google-login/GoogleLogin.styles.ts index ba0270263..a62e0fa0b 100644 --- a/src/containers/guest-home-page/google-login/GoogleLogin.styles.js +++ b/src/containers/guest-home-page/google-login/GoogleLogin.styles.ts @@ -34,6 +34,6 @@ export const styles = { googleForm: { display: 'flex', flexDirection: 'column', - minWidth: { sm: '330px' } + minWidth: '330px' } } diff --git a/src/containers/guest-home-page/google-login/GoogleLogin.jsx b/src/containers/guest-home-page/google-login/GoogleLogin.tsx similarity index 90% rename from src/containers/guest-home-page/google-login/GoogleLogin.jsx rename to src/containers/guest-home-page/google-login/GoogleLogin.tsx index b20117f82..1bb46ba66 100644 --- a/src/containers/guest-home-page/google-login/GoogleLogin.jsx +++ b/src/containers/guest-home-page/google-login/GoogleLogin.tsx @@ -8,8 +8,19 @@ import LoginDialog from '~/containers/guest-home-page/login-dialog/LoginDialog' import GoogleButton from '~/containers/guest-home-page/google-button/GoogleButton' import { styles } from '~/containers/guest-home-page/google-login/GoogleLogin.styles' +import { UserRole } from '~/types' -const GoogleLogin = ({ type, buttonWidth, role }) => { +interface GoogleLoginProps { + type: 'signup' | 'login' + buttonWidth: string + role: UserRole +} + +const GoogleLogin: React.FC = ({ + type, + buttonWidth, + role +}) => { const { t, i18n } = useTranslation() const { whatCanYouDo } = guestRoutes.navBar const { openModal, closeModal } = useModalContext() diff --git a/src/containers/guest-home-page/login-form/LoginForm.jsx b/src/containers/guest-home-page/login-form/LoginForm.tsx similarity index 73% rename from src/containers/guest-home-page/login-form/LoginForm.jsx rename to src/containers/guest-home-page/login-form/LoginForm.tsx index d02aea5a8..09f4f89c1 100644 --- a/src/containers/guest-home-page/login-form/LoginForm.jsx +++ b/src/containers/guest-home-page/login-form/LoginForm.tsx @@ -1,6 +1,5 @@ import { useTranslation } from 'react-i18next' import useInputVisibility from '~/hooks/use-input-visibility' -import { useSelector } from 'react-redux' import Box from '@mui/material/Box' import ButtonBase from '@mui/material/ButtonBase' @@ -14,8 +13,28 @@ import AppTextField from '~/components/app-text-field/AppTextField' import AppButton from '~/components/app-button/AppButton' import { styles } from '~/containers/guest-home-page/login-form/LoginForm.styles' +import { useAppSelector } from '~/hooks/use-redux' -const LoginForm = ({ +interface LoginFormProps { + handleSubmit: (event: React.FormEvent) => void + handleChange: ( + field: string + ) => (event: React.SyntheticEvent) => void + handleBlur: ( + field: string + ) => (event: React.FocusEvent) => void + data: { + email: string + password: string + rememberMe: boolean + } + errors: { + email?: string + password?: string + } +} + +const LoginForm: React.FC = ({ handleSubmit, handleChange, handleBlur, @@ -23,9 +42,9 @@ const LoginForm = ({ errors }) => { const { inputVisibility: passwordVisibility, showInputText: showPassword } = - useInputVisibility(errors.password) + useInputVisibility(errors.password ?? '') - const { authLoading } = useSelector((state) => state.appMain) + const { authLoading } = useAppSelector((state) => state.appMain) const { openModal } = useModalContext() @@ -40,13 +59,13 @@ const LoginForm = ({ } label={t('login.rememberMe')} labelPlacement='end' - onChange={handleChange('rememberMe')} + onChange={(event) => + handleChange('rememberMe')( + event as React.ChangeEvent + ) + } sx={styles.checkboxLabel} value={data.rememberMe} /> diff --git a/src/containers/guest-home-page/signup-form/SignupForm.jsx b/src/containers/guest-home-page/signup-form/SignupForm.tsx similarity index 87% rename from src/containers/guest-home-page/signup-form/SignupForm.jsx rename to src/containers/guest-home-page/signup-form/SignupForm.tsx index 83a5118cd..d0cacd0f6 100644 --- a/src/containers/guest-home-page/signup-form/SignupForm.jsx +++ b/src/containers/guest-home-page/signup-form/SignupForm.tsx @@ -1,7 +1,12 @@ -import { useState, useMemo } from 'react' +import { + useState, + useMemo, + FormEventHandler, + FocusEvent, + ChangeEvent +} from 'react' import { useTranslation } from 'react-i18next' import HashLink from '~/components/hash-link/HashLink' -import { useSelector } from 'react-redux' import Box from '@mui/material/Box' import FormControlLabel from '@mui/material/FormControlLabel' @@ -13,6 +18,16 @@ import { guestRoutes } from '~/router/constants/guestRoutes' import AppButton from '~/components/app-button/AppButton' import { styles } from '~/containers/guest-home-page/signup-form/SignupForm.styles' +import { SignupParams, UseFormErrors, UseFormEventHandler } from '~/types' +import { useAppSelector } from '~/hooks/use-redux' + +interface SignupFormProps { + handleSubmit: FormEventHandler + handleChange: UseFormEventHandler> + handleBlur: UseFormEventHandler> + data: SignupParams + errors: UseFormErrors +} const SignupForm = ({ handleSubmit, @@ -20,7 +35,7 @@ const SignupForm = ({ handleBlur, data, errors -}) => { +}: SignupFormProps) => { const { t } = useTranslation() const { privacyPolicy, termOfUse } = guestRoutes const [isAgreementChecked, setIsAgreementChecked] = useState(false) @@ -30,7 +45,7 @@ const SignupForm = ({ inputVisibility: confirmPasswordVisibility, showInputText: showConfirmPassword } = useInputVisibility(errors.confirmPassword) - const { authLoading } = useSelector((state) => state.appMain) + const { authLoading } = useAppSelector((state) => state.appMain) const handleOnAgreementChange = () => { setIsAgreementChecked((prev) => !prev) @@ -54,7 +69,7 @@ const SignupForm = ({ to={termOfUse.path} variant='subtitle2' > - {t('common.labels.terms')} + {t('signup.terms')} {t('signup.and')} @@ -67,7 +82,7 @@ const SignupForm = ({ to={privacyPolicy.path} variant='subtitle2' > - {t('common.labels.privacyPolicy')} + {t('signup.privacyPolicy')} ) diff --git a/src/containers/layout/admin-portal/admin-nav-bar-item/AdminNavBarItem.jsx b/src/containers/layout/admin-portal/admin-nav-bar-item/AdminNavBarItem.tsx similarity index 84% rename from src/containers/layout/admin-portal/admin-nav-bar-item/AdminNavBarItem.jsx rename to src/containers/layout/admin-portal/admin-nav-bar-item/AdminNavBarItem.tsx index 318527974..f29c4d87f 100644 --- a/src/containers/layout/admin-portal/admin-nav-bar-item/AdminNavBarItem.jsx +++ b/src/containers/layout/admin-portal/admin-nav-bar-item/AdminNavBarItem.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { ReactNode, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' @@ -12,6 +12,18 @@ import ListItemText from '@mui/material/ListItemText' import Box from '@mui/material/Box' import { styles } from './AdminNavBarItem.styles' +interface AdminNavBarItemProps { + label: string + icon: ReactNode + expanded: boolean + children: { subLabel: string; path: string }[] + path: string + active: boolean + showSubItems: boolean + handleShowSubItems: (label: string) => void + handleActive: () => void +} + const AdminNavBarItem = ({ label, icon, @@ -22,8 +34,8 @@ const AdminNavBarItem = ({ showSubItems, handleShowSubItems, handleActive -}) => { - const [activeSubItem, setActiveSubItem] = useState(null) +}: AdminNavBarItemProps) => { + const [activeSubItem, setActiveSubItem] = useState(null) const { t } = useTranslation() diff --git a/src/containers/my-cooperations/my-cooperations-details/MyCooperationsDetails.tsx b/src/containers/my-cooperations/my-cooperations-details/MyCooperationsDetails.tsx index 9c17c2857..0e2b2dc86 100644 --- a/src/containers/my-cooperations/my-cooperations-details/MyCooperationsDetails.tsx +++ b/src/containers/my-cooperations/my-cooperations-details/MyCooperationsDetails.tsx @@ -104,7 +104,10 @@ const MyCooperationsDetails = () => { diff --git a/src/containers/navigation-icons/AccountIcon.tsx b/src/containers/navigation-icons/AccountIcon.tsx index 178265c39..dba1aad21 100644 --- a/src/containers/navigation-icons/AccountIcon.tsx +++ b/src/containers/navigation-icons/AccountIcon.tsx @@ -13,6 +13,7 @@ import { defaultResponses } from '~/constants' import { styles } from '~/containers/navigation-icons/NavigationIcons.styles' import { UserResponse, UserRole } from '~/types' +import { createUrlPath } from '~/utils/helper-functions' interface AccountIconProps { openMenu: (event: MouseEvent) => void @@ -45,7 +46,10 @@ const AccountIcon: FC = ({ openMenu }) => { {!loading && firstName && lastName && `${firstName[0]}${lastName[0]}`} diff --git a/src/containers/tutor-home-page/subjects-step/SubjectsStep.styles.js b/src/containers/tutor-home-page/subjects-step/SubjectsStep.styles.ts similarity index 100% rename from src/containers/tutor-home-page/subjects-step/SubjectsStep.styles.js rename to src/containers/tutor-home-page/subjects-step/SubjectsStep.styles.ts diff --git a/src/containers/tutor-home-page/subjects-step/SubjectsStep.jsx b/src/containers/tutor-home-page/subjects-step/SubjectsStep.tsx similarity index 80% rename from src/containers/tutor-home-page/subjects-step/SubjectsStep.jsx rename to src/containers/tutor-home-page/subjects-step/SubjectsStep.tsx index 6c20cb24c..61dc0440c 100644 --- a/src/containers/tutor-home-page/subjects-step/SubjectsStep.jsx +++ b/src/containers/tutor-home-page/subjects-step/SubjectsStep.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react' +import { useState, useCallback, SyntheticEvent } from 'react' import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' @@ -16,25 +16,35 @@ import img from '~/assets/img/tutor-home-page/become-tutor/study-category.svg' import { useStepContext } from '~/context/step-context' import { categoryService } from '~/services/category-service' import { subjectService } from '~/services/subject-service' +import { CategoryNameInterface, SubjectNameInterface } from '~/types' -const SubjectsStep = ({ btnsBox }) => { +interface SubjectsStepProps { + btnsBox: JSX.Element +} + +interface SubjectsType { + category: CategoryNameInterface | null + subject: SubjectNameInterface | null +} + +const SubjectsStep = ({ btnsBox }: SubjectsStepProps) => { const { t } = useTranslation() const { isLaptopAndAbove, isMobile } = useBreakpoints() const { stepData, handleSubjects } = useStepContext() const subjectData = stepData.subjects - const [subjects, setSubjects] = useState({ + const [subjects, setSubjects] = useState({ category: null, subject: null }) const [subjectError, setSubjectError] = useState('') - const [subjectFetched, setSubjectIsFetched] = useState(false) + const [subjectIsFetched, setSubjectIsFetched] = useState(false) const fetchSubjectHandler = () => setSubjectIsFetched(true) const getSubjectsNames = useCallback( - () => subjectService.getSubjectsNames(subjects.category._id), + () => subjectService.getSubjectsNames(subjects.category?._id ?? null), [subjects.category] ) @@ -44,18 +54,25 @@ const SubjectsStep = ({ btnsBox }) => { ) - const onChangeCategory = (_, value) => { - setSubjects( - (prev) => - prev.category?._id !== value?._id && { - category: value, - subject: null - } + const onChangeCategory = ( + _: SyntheticEvent, + value: CategoryNameInterface | null + ) => { + setSubjects((prev) => + prev.category?._id !== value?._id + ? { + category: value, + subject: null + } + : prev ) setSubjectIsFetched(false) } - const onChangeSubject = (_, value) => { + const onChangeSubject = ( + _: SyntheticEvent, + value: SubjectNameInterface | null + ) => { setSubjects((prev) => ({ category: prev.category, subject: value })) subjectError && setSubjectError('') } @@ -89,7 +106,7 @@ const SubjectsStep = ({ btnsBox }) => { }) } - const handleChipDelete = (item) => { + const handleChipDelete = (item: string) => { const newItems = subjectData.filter(({ name }) => name !== item) handleSubjects(newItems) } @@ -117,7 +134,7 @@ const SubjectsStep = ({ btnsBox }) => { { +}: ProfileContainerDesktopProps) => { return ( diff --git a/src/containers/user-profile/profile-info/ProfileContainerMobile.jsx b/src/containers/user-profile/profile-info/ProfileContainerMobile.tsx similarity index 76% rename from src/containers/user-profile/profile-info/ProfileContainerMobile.jsx rename to src/containers/user-profile/profile-info/ProfileContainerMobile.tsx index 7870fb68d..82e21b279 100644 --- a/src/containers/user-profile/profile-info/ProfileContainerMobile.jsx +++ b/src/containers/user-profile/profile-info/ProfileContainerMobile.tsx @@ -1,3 +1,5 @@ +import { ReactNode } from 'react' + import Box from '@mui/material/Box' import SchoolIcon from '@mui/icons-material/School' import DoneIcon from '@mui/icons-material/Done' @@ -8,6 +10,18 @@ import AppChipList from '~/components/app-chips-list/AppChipList' import ProfileDoneItemsList from '~/components/icon-with-text-list/ProfileDoneItemsList' import { styles } from '~/containers/user-profile/profile-info/ProfileInfo.styles' +import { UserResponse } from '~/types' +import { createUrlPath } from '~/utils/helper-functions' + +interface ProfileContainerMobileProps { + actionIcon: ReactNode + accInfo: ReactNode + buttonGroup: ReactNode + defaultQuantity: number + doneItems: { title: string; description: string }[] + userData: UserResponse + chipItems: string[] +} const ProfileContainerMobile = ({ actionIcon, @@ -17,7 +31,7 @@ const ProfileContainerMobile = ({ doneItems, userData, chipItems -}) => { +}: ProfileContainerMobileProps) => { return ( @@ -25,7 +39,10 @@ const ProfileContainerMobile = ({ diff --git a/src/containers/user-profile/video-presentation/VideoPresentation.jsx b/src/containers/user-profile/video-presentation/VideoPresentation.tsx similarity index 77% rename from src/containers/user-profile/video-presentation/VideoPresentation.jsx rename to src/containers/user-profile/video-presentation/VideoPresentation.tsx index 890eda16d..ad8057c3d 100644 --- a/src/containers/user-profile/video-presentation/VideoPresentation.jsx +++ b/src/containers/user-profile/video-presentation/VideoPresentation.tsx @@ -4,7 +4,17 @@ import Typography from '@mui/material/Typography' import VideoBox from '~/components/video-box/VideoBox' import { ComponentEnum } from '~/types' -const VideoPresentation = ({ video, videoPreview, videoMock }) => { +interface VideoPresentationProps { + video: string + videoPreview: boolean + videoMock: string +} + +const VideoPresentation = ({ + video, + videoPreview, + videoMock +}: VideoPresentationProps) => { const { t } = useTranslation() return ( diff --git a/src/context/modal-context.tsx b/src/context/modal-context.tsx index 56472a704..6199e33ac 100644 --- a/src/context/modal-context.tsx +++ b/src/context/modal-context.tsx @@ -11,7 +11,7 @@ import { import PopupDialog from '~/components/popup-dialog/PopupDialog' import { PaperProps } from '@mui/material/Paper' -interface Component { +export interface Component { component: ReactElement paperProps?: PaperProps customCloseModal?: () => void diff --git a/src/pages/edit-profile/EditProfile.tsx b/src/pages/edit-profile/EditProfile.tsx index 7972062f3..65b53ac9a 100644 --- a/src/pages/edit-profile/EditProfile.tsx +++ b/src/pages/edit-profile/EditProfile.tsx @@ -29,7 +29,9 @@ import { LoadingStatusEnum } from '~/redux/redux.constants' const EditProfile = () => { const { t } = useTranslation() const dispatch = useAppDispatch() - const { loading } = useAppSelector((state) => state.editProfile) + const { loading, tabValidityStatus } = useAppSelector( + (state) => state.editProfile + ) const [searchParams, setSearchParams] = useSearchParams({ tab: UserProfileTabsEnum.Profile @@ -66,6 +68,11 @@ const EditProfile = () => { } const cooperationContent = activeTab && tabsData[activeTab]?.content + const errorTooltipHolders = { + [UserProfileTabsEnum.Profile]: !tabValidityStatus.profileTab, + [UserProfileTabsEnum.ProfessionalInfo]: + !tabValidityStatus.professionalInfoTab + } return ( @@ -92,6 +99,7 @@ const EditProfile = () => { void handleClick(tab)} tabsData={tabsData} /> diff --git a/src/types/common/interfaces/common.interfaces.ts b/src/types/common/interfaces/common.interfaces.ts index cd47dd90f..8552be23e 100644 --- a/src/types/common/interfaces/common.interfaces.ts +++ b/src/types/common/interfaces/common.interfaces.ts @@ -119,7 +119,7 @@ export interface StepData { errors: Record } photo: string[] - subjects: SubjectInterface[] + subjects: Array language: UserResponse['nativeLanguage'] } diff --git a/src/types/components/components.index.ts b/src/types/components/components.index.ts index 5d3923d89..d874ea631 100644 --- a/src/types/components/components.index.ts +++ b/src/types/components/components.index.ts @@ -12,3 +12,4 @@ export * from '~/types/components/user-profile-info/userProfileInfo.interface' export * from '~/types/components/app-breadcrumbs/appBreadcrumbs.interfaces' export * from '~/types/components/app-breadcrumbs/appBreadcrumbs.types' export * from '~/types/components/subject-level-chips/subjectLevelChips.interface' +export * from '~/types/components/icon-with-text-list/profileDoneItemsList.interfaces' diff --git a/src/types/components/icon-with-text-list/profileDoneItemsList.interfaces.ts b/src/types/components/icon-with-text-list/profileDoneItemsList.interfaces.ts new file mode 100644 index 000000000..77182a373 --- /dev/null +++ b/src/types/components/icon-with-text-list/profileDoneItemsList.interfaces.ts @@ -0,0 +1,4 @@ +export interface ProfileDoneItem { + title: string + description: string +} diff --git a/src/types/mui/customTypes.index.ts b/src/types/mui/customTypes.index.ts new file mode 100644 index 000000000..99f4c6045 --- /dev/null +++ b/src/types/mui/customTypes.index.ts @@ -0,0 +1 @@ +export * from '~/types/mui/customTypes/customTypes' diff --git a/src/types/mui/customTypes/customTypes.d.ts b/src/types/mui/customTypes/customTypes.d.ts new file mode 100644 index 000000000..467358a3c --- /dev/null +++ b/src/types/mui/customTypes/customTypes.d.ts @@ -0,0 +1,7 @@ +import { TextFieldProps } from '@mui/material/TextField' + +declare module '@mui/x-date-pickers/DesktopDatePicker' { + interface DesktopDatePickerProps { + inputProps?: TextFieldProps['inputProps'] + } +} diff --git a/src/utils/helper-functions.tsx b/src/utils/helper-functions.tsx index cd4c6a238..90dd12253 100644 --- a/src/utils/helper-functions.tsx +++ b/src/utils/helper-functions.tsx @@ -206,7 +206,7 @@ export const createUrlPath = ( query = {} ) => { let trimmedUrl = URL - while (trimmedUrl.endsWith('/')) { + while (trimmedUrl?.endsWith('/')) { trimmedUrl = trimmedUrl.slice(0, -1) } diff --git a/tests/unit/components/date-filter/DateFilter.spec.jsx b/tests/unit/components/date-filter/DateFilter.spec.jsx index 67eed0dc5..39621bfcd 100644 --- a/tests/unit/components/date-filter/DateFilter.spec.jsx +++ b/tests/unit/components/date-filter/DateFilter.spec.jsx @@ -6,21 +6,18 @@ import DateFilter from '~/components/enhanced-table/date-filter/DateFilter' const props = { filter: { from: '', to: '' }, setFilter: vi.fn(), - clearFilter: vi.fn(), + clearFilter: vi.fn() } const dateMock = new Date('2023-01-01') describe('DateFilter test', () => { beforeEach(() => { - render( - - - ) + render() }) it('Should open, and change value in calendar', () => { - const calendarIcon = screen.getByTestId('calendar-icon') + const calendarIcon = screen.getByTestId('calendar-icon') fireEvent.click(calendarIcon) @@ -38,12 +35,11 @@ describe('DateFilter test', () => { }) it('Should clear value in calendar', async () => { - const dateToInput = screen.getByLabelText('date-filter-to') fireEvent.change(dateToInput, { target: { value: dateMock } }) - const clearIcon = screen.getByTestId('clear-icon') + const clearIcon = screen.getByTestId('clear-icon') fireEvent.click(clearIcon) diff --git a/tests/unit/components/find-block/FindBlock.spec.jsx b/tests/unit/components/find-block/FindBlock.spec.jsx index 167c3645e..45cf7503b 100644 --- a/tests/unit/components/find-block/FindBlock.spec.jsx +++ b/tests/unit/components/find-block/FindBlock.spec.jsx @@ -42,7 +42,7 @@ describe('FindBlock test', () => { const input = screen.getByRole('textbox') fireEvent.change(input, { target: { value: 'test' } }) - fireEvent.keyPress(input, { key: 'Enter', code: 'Enter', charCode: 13 }) + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 }) expect(mockNavigate).toHaveBeenCalled() }) diff --git a/tests/unit/utils/count-active-filters.spec.js b/tests/unit/utils/count-active-filters.spec.js new file mode 100644 index 000000000..bd3dff071 --- /dev/null +++ b/tests/unit/utils/count-active-filters.spec.js @@ -0,0 +1,99 @@ +import { + countActiveOfferFilters, + countActiveCourseFilters +} from '~/utils/count-active-filters' + +const mockedDefaultFilters = { + category: '', + subject: '', + title: '', + page: 1, + search: 'search', + price: [1, 2] +} + +const activeOfferIgnoredFields = { + sort: '', + authorRole: '', + categoryId: '', + subjectId: '', + page: '' +} + +const activeCourseIgnoredFields = { + sort: '', + page: '' +} + +describe('countActiveOfferFilters', () => { + it("Should return 0 if there aren't search params", () => { + const mockedSearchParams = new URLSearchParams({}) + const count = countActiveCourseFilters( + mockedSearchParams, + mockedDefaultFilters + ) + expect(count).toBe(0) + }) + + it('Should not count ignored fields', () => { + const mockedSearchParams = new URLSearchParams({ + ...activeOfferIgnoredFields, + title: 'title counts' + }) + const count = countActiveOfferFilters( + mockedSearchParams, + mockedDefaultFilters + ) + expect(count).toBe(1) + }) + + it('Should not count default price', () => { + const mockedSearchParams = new URLSearchParams({ + price: [1, 2], + title: 'title counts' + }) + const count = countActiveOfferFilters( + mockedSearchParams, + mockedDefaultFilters + ) + expect(count).toBe(1) + }) + + it('Should count search params that are not equal to default filter params', () => { + const mockedSearchParams = new URLSearchParams({ + title: 'title counts', + page: 1, + search: 'search' + }) + const count = countActiveOfferFilters( + mockedSearchParams, + mockedDefaultFilters + ) + expect(count).toBe(1) + }) +}) + +describe('countActiveCourseFilters', () => { + it('Should not count ignored fields', () => { + const mockedSearchParams = new URLSearchParams(activeCourseIgnoredFields) + const count = countActiveOfferFilters( + mockedSearchParams, + mockedDefaultFilters + ) + expect(count).toBe(0) + }) + + it('Should return correct count of filters', () => { + const mockedSearchParams = new URLSearchParams({ + foo: 'bar', + title: 'title counts', + subject: 'subject counts' + }) + + const count = countActiveCourseFilters( + mockedSearchParams, + mockedDefaultFilters + ) + expect(count).toBe(2) + }) +})