diff --git a/package.json b/package.json index c9f9c2bf94..84a2b15b3a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "react-split-pane": "^0.1.92", "react-use": "^17.4.0", "react-virtualized": "^9.22.3", + "react-zoom-pan-pinch": "^3.2.0", "redux": "^4.0.4", "redux-saga": "^1.1.3", "reselect": "^4.0.0", diff --git a/public/assets/images/apps-qr.svg b/public/assets/images/apps-qr.svg deleted file mode 100644 index ebd37a7520..0000000000 --- a/public/assets/images/apps-qr.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/assets/images/iphone-full-1.png b/public/assets/images/iphone-full-1.png deleted file mode 100644 index 2a0de2caef..0000000000 Binary files a/public/assets/images/iphone-full-1.png and /dev/null differ diff --git a/public/assets/images/iphone-full-2.png b/public/assets/images/iphone-full-2.png deleted file mode 100644 index 12cea8bfb1..0000000000 Binary files a/public/assets/images/iphone-full-2.png and /dev/null differ diff --git a/public/assets/images/iphone-full-3.png b/public/assets/images/iphone-full-3.png deleted file mode 100644 index 7ef7660f76..0000000000 Binary files a/public/assets/images/iphone-full-3.png and /dev/null differ diff --git a/public/assets/images/iphone-half-1.png b/public/assets/images/iphone-half-1.png deleted file mode 100644 index ebbd48fd8b..0000000000 Binary files a/public/assets/images/iphone-half-1.png and /dev/null differ diff --git a/public/assets/images/iphone-half-2.png b/public/assets/images/iphone-half-2.png deleted file mode 100644 index 8ef9694099..0000000000 Binary files a/public/assets/images/iphone-half-2.png and /dev/null differ diff --git a/public/assets/images/iphone-half-3.png b/public/assets/images/iphone-half-3.png deleted file mode 100644 index 8e6eac8056..0000000000 Binary files a/public/assets/images/iphone-half-3.png and /dev/null differ diff --git a/public/assets/images/iphone-half-desktop.png b/public/assets/images/iphone-half-desktop.png deleted file mode 100644 index ee432e0044..0000000000 Binary files a/public/assets/images/iphone-half-desktop.png and /dev/null differ diff --git a/public/assets/images/iphone-half-mobile.png b/public/assets/images/iphone-half-mobile.png deleted file mode 100644 index 177786a72e..0000000000 Binary files a/public/assets/images/iphone-half-mobile.png and /dev/null differ diff --git a/public/assets/images/join-mobile.jpg b/public/assets/images/join-mobile.jpg deleted file mode 100644 index 32554d1c41..0000000000 Binary files a/public/assets/images/join-mobile.jpg and /dev/null differ diff --git a/public/assets/images/join.jpg b/public/assets/images/join.jpg deleted file mode 100644 index d9ac3d0fcd..0000000000 Binary files a/public/assets/images/join.jpg and /dev/null differ diff --git a/public/assets/images/landing-mobile-poster.jpg b/public/assets/images/landing-mobile-poster.jpg new file mode 100644 index 0000000000..4ff0d06e68 Binary files /dev/null and b/public/assets/images/landing-mobile-poster.jpg differ diff --git a/public/assets/images/main-landing-image-1.jpg b/public/assets/images/main-landing-image-1.jpg deleted file mode 100644 index 8381909e27..0000000000 Binary files a/public/assets/images/main-landing-image-1.jpg and /dev/null differ diff --git a/public/assets/images/main-landing-image-2.jpg b/public/assets/images/main-landing-image-2.jpg deleted file mode 100644 index 5bc5a55ae9..0000000000 Binary files a/public/assets/images/main-landing-image-2.jpg and /dev/null differ diff --git a/public/assets/images/main-landing-image-3.jpg b/public/assets/images/main-landing-image-3.jpg deleted file mode 100644 index c0330c3283..0000000000 Binary files a/public/assets/images/main-landing-image-3.jpg and /dev/null differ diff --git a/src/constants.scss b/src/constants.scss index dd8fbd33f1..2afb37a95a 100644 --- a/src/constants.scss +++ b/src/constants.scss @@ -149,6 +149,7 @@ $c-gray-30: #b7bcd2; $c-gray-40: #8d91a9; $c-gray-50: #7a819c; $c-gray-60: #6b718e; +$c-gray-80: #2e3452; $c-gray-90: #1f2535; $c-gray-100: #131b23; $c-gray-800: #27292c; diff --git a/src/pages/Landing/components/LandingContainer/VideoSection/VideoSection.tsx b/src/pages/Landing/components/LandingContainer/VideoSection/VideoSection.tsx index 00b6a0f053..4852e714b1 100644 --- a/src/pages/Landing/components/LandingContainer/VideoSection/VideoSection.tsx +++ b/src/pages/Landing/components/LandingContainer/VideoSection/VideoSection.tsx @@ -1,7 +1,8 @@ import React, { FC } from "react"; import { useTranslation } from "react-i18next"; -import landingVideoPosterSrc from "@/shared/assets/images/landing-video-poster.jpeg"; +import landingVideoPosterSrc from "@/shared/assets/images/landing-video-poster.jpg"; import landingVideoSrc from "@/shared/assets/videos/landing-video.mp4"; +import { useIsBigPhoneView } from "@/shared/hooks/viewport"; import { Button, ButtonVariant } from "@/shared/ui-kit"; import "./index.scss"; @@ -13,21 +14,24 @@ const VideoSection: FC = ({ onLaunchClick }) => { const { t } = useTranslation("translation", { keyPrefix: "landing", }); + const isBigPhoneView = useIsBigPhoneView(); return (
- + {!isBigPhoneView && ( + + )}

diff --git a/src/pages/Landing/components/LandingContainer/VideoSection/index.scss b/src/pages/Landing/components/LandingContainer/VideoSection/index.scss index 1488912fa1..fead75a6ac 100644 --- a/src/pages/Landing/components/LandingContainer/VideoSection/index.scss +++ b/src/pages/Landing/components/LandingContainer/VideoSection/index.scss @@ -6,6 +6,13 @@ $viewport-breakpoint: 1000px; .landing-video-section { position: relative; + + @include big-phone { + background-image: url("/assets/images/landing-mobile-poster.jpg"); + background-size: cover; + background-position: center; + height: 37.125rem; + } } .landing-video-section__video-wrapper { diff --git a/src/pages/MyAccount/components/Billing/Billing.module.scss b/src/pages/MyAccount/components/Billing/Billing.module.scss new file mode 100644 index 0000000000..12459bba7c --- /dev/null +++ b/src/pages/MyAccount/components/Billing/Billing.module.scss @@ -0,0 +1,14 @@ +@import "../../../../constants"; +@import "../../../../styles/sizes"; + +.container { + width: 100%; +} + +.header { + margin-bottom: 2.25rem; + + @include tablet { + margin-bottom: 0; + } +} diff --git a/src/pages/MyAccount/components/Billing/Billing.tsx b/src/pages/MyAccount/components/Billing/Billing.tsx index 1d3864f8fe..f459747378 100644 --- a/src/pages/MyAccount/components/Billing/Billing.tsx +++ b/src/pages/MyAccount/components/Billing/Billing.tsx @@ -1,20 +1,22 @@ import React, { useEffect, useState, FC } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { getBankDetails, loadUserCards } from "@/pages/OldCommon/store/actions"; -import { ScreenSize } from "@/shared/constants"; import { usePaymentMethodChange, useUserContributions, } from "@/shared/hooks/useCases"; +import { useIsTabletView } from "@/shared/hooks/viewport"; import { BankAccountDetails, Payment, Subscription } from "@/shared/models"; -import { getScreenSize } from "@/shared/store/selectors"; import { DesktopBilling } from "./DesktopBilling"; import { MobileBilling } from "./MobileBilling"; +import { Header } from "./components"; import { BankAccountState, BillingProps, CardsState } from "./types"; +import styles from "./Billing.module.scss"; import "./index.scss"; const Billing: FC = () => { const dispatch = useDispatch(); + const isMobileView = useIsTabletView(); const [cardsState, setCardsState] = useState({ loading: false, fetched: false, @@ -28,8 +30,6 @@ const Billing: FC = () => { const [activeContribution, setActiveContribution] = useState< Payment | Subscription | null >(null); - const screenSize = useSelector(getScreenSize()); - const isMobileView = screenSize === ScreenSize.Mobile; const { changePaymentMethodState, onPaymentMethodChange, @@ -138,13 +138,11 @@ const Billing: FC = () => { }; return ( -
- {(!isMobileView || !activeContribution) && ( -
-

Billing

-
- )} - +
+
+
+ +
); }; diff --git a/src/pages/MyAccount/components/Billing/MobileBilling/index.scss b/src/pages/MyAccount/components/Billing/MobileBilling/index.scss index b5a2f47de0..39f43b8bb8 100644 --- a/src/pages/MyAccount/components/Billing/MobileBilling/index.scss +++ b/src/pages/MyAccount/components/Billing/MobileBilling/index.scss @@ -1,7 +1,7 @@ @import "../../../../../constants"; .my-account-mobile-billing { - --content-ph: var(--container-pl, #{$content-padding-mobile}); + --content-ph: 1rem; flex: 1; display: flex; @@ -19,13 +19,13 @@ .my-account-mobile-billing__tabs-wrapper { padding: 0 var(--content-ph); - margin: 0 calc(var(--content-ph) * -1); white-space: nowrap; border-bottom: 1px solid $light-gray-1; } .my-account-mobile-billing__tab-panels { height: 100%; + padding: 0 var(--content-ph); } .my-account-mobile-billing__tab-panel { diff --git a/src/pages/MyAccount/components/Billing/components/Header/Header.module.scss b/src/pages/MyAccount/components/Billing/components/Header/Header.module.scss new file mode 100644 index 0000000000..21247df7e5 --- /dev/null +++ b/src/pages/MyAccount/components/Billing/components/Header/Header.module.scss @@ -0,0 +1,37 @@ +@import "../../../../../../constants"; + +.desktopContainer { + display: flex; + align-items: center; + justify-content: space-between; +} + +.desktopTitle { + margin: 0; + font-family: Lexend, sans-serif; + font-weight: normal; + font-size: 2.25rem; + line-height: 3rem; +} + +.topNavigationWithBlocks { + z-index: 10; +} + +.backIcon { + flex-shrink: 0; + width: 0.875rem; + height: 0.875rem; +} + +.mobileTitle { + margin: 0; + font-family: PoppinsSans, sans-serif; + font-weight: 500; + font-size: 1rem; + text-align: center; + color: $c-gray-100; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/src/pages/MyAccount/components/Billing/components/Header/Header.tsx b/src/pages/MyAccount/components/Billing/components/Header/Header.tsx new file mode 100644 index 0000000000..e68aff2d6f --- /dev/null +++ b/src/pages/MyAccount/components/Billing/components/Header/Header.tsx @@ -0,0 +1,54 @@ +import React, { FC } from "react"; +import { useHistory } from "react-router-dom"; +import classNames from "classnames"; +import { useRoutesContext } from "@/shared/contexts"; +import { useGoBack } from "@/shared/hooks"; +import { LongLeftArrowIcon } from "@/shared/icons"; +import { + TopNavigationBackButton, + TopNavigationWithBlocks, +} from "@/shared/ui-kit"; +import styles from "./Header.module.scss"; + +interface HeaderProps { + className?: string; + isMobileVersion?: boolean; +} + +const Header: FC = (props) => { + const { className, isMobileVersion = false } = props; + const history = useHistory(); + const { canGoBack, goBack } = useGoBack(); + const { getProfilePagePath } = useRoutesContext(); + + if (!isMobileVersion) { + return ( +
+

Billing

+
+ ); + } + + const handleBackButtonClick = () => { + if (canGoBack) { + goBack(); + } else { + history.push(getProfilePagePath()); + } + }; + + return ( + } + onClick={handleBackButtonClick} + /> + } + centralElement={

Billing

} + /> + ); +}; + +export default Header; diff --git a/src/pages/MyAccount/components/Billing/components/Header/index.ts b/src/pages/MyAccount/components/Billing/components/Header/index.ts new file mode 100644 index 0000000000..d88c989bbd --- /dev/null +++ b/src/pages/MyAccount/components/Billing/components/Header/index.ts @@ -0,0 +1 @@ +export { default as Header } from "./Header"; diff --git a/src/pages/MyAccount/components/Billing/components/index.ts b/src/pages/MyAccount/components/Billing/components/index.ts new file mode 100644 index 0000000000..9e08a64dbf --- /dev/null +++ b/src/pages/MyAccount/components/Billing/components/index.ts @@ -0,0 +1 @@ +export * from "./Header"; diff --git a/src/pages/MyAccount/components/Billing/index.scss b/src/pages/MyAccount/components/Billing/index.scss index c919e50329..d2db0f7536 100644 --- a/src/pages/MyAccount/components/Billing/index.scss +++ b/src/pages/MyAccount/components/Billing/index.scss @@ -2,15 +2,17 @@ @import "../../../../styles/sizes"; .my-account-billing { + max-width: 36.25rem; width: 100%; + margin: 0 auto; + padding-top: 3.375rem; + display: flex; + flex-direction: column; font-family: PoppinsSans, sans-serif; color: $secondary-blue; - overflow: hidden; - @include big-phone { - display: flex; - flex-direction: column; - overflow: initial; + @include tablet { + padding-top: 0; } .my-account-billing__header { diff --git a/src/pages/MyAccount/components/Profile/Profile.module.scss b/src/pages/MyAccount/components/Profile/Profile.module.scss index 20ec36bfc2..dd4e764f0d 100644 --- a/src/pages/MyAccount/components/Profile/Profile.module.scss +++ b/src/pages/MyAccount/components/Profile/Profile.module.scss @@ -1,4 +1,5 @@ @import "../../../../constants"; +@import "../../../../styles/mixins"; @import "../../../../styles/sizes"; .container { @@ -6,6 +7,17 @@ width: 100%; } +.content { + max-width: 36.25rem; + width: 100%; + margin: 0 auto; + padding-top: 3.375rem; + + @include tablet { + padding-top: 0; + } +} + .header { margin-bottom: 2.25rem; @@ -37,6 +49,14 @@ } } +.userDetails { + margin-bottom: 2.25rem; + + @include tablet { + margin-bottom: 0; + } +} + .menuButtonsWrapper { border-top: 0.0625rem solid $c-gray-20; } @@ -48,3 +68,18 @@ .logoutMenuButton { color: $c-pink-mention; } + +.buttonsWrapper { + @include flex-list-with-gap(1rem); + + @include tablet { + width: 100%; + margin: 1.5rem 0 0; + flex-direction: column-reverse; + box-sizing: border-box; + + & > * { + margin: 0 0 1rem; + } + } +} diff --git a/src/pages/MyAccount/components/Profile/Profile.tsx b/src/pages/MyAccount/components/Profile/Profile.tsx index 3d548994fa..d0ffbcafb6 100644 --- a/src/pages/MyAccount/components/Profile/Profile.tsx +++ b/src/pages/MyAccount/components/Profile/Profile.tsx @@ -2,19 +2,13 @@ import React, { FC, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { logOut } from "@/pages/Auth/store/actions"; import { selectUser } from "@/pages/Auth/store/selectors"; -import { - UserDetails, - UserDetailsRef, -} from "@/pages/Login/components/LoginContainer/UserDetails"; import { ButtonIcon, Loader } from "@/shared/components"; import { useRoutesContext } from "@/shared/contexts"; import { useIsTabletView } from "@/shared/hooks/viewport"; -import { LogoutIcon } from "@/shared/icons"; -import EditIcon from "@/shared/icons/edit.icon"; +import { Edit3Icon as EditIcon, LogoutIcon } from "@/shared/icons"; import { Button, ButtonVariant } from "@/shared/ui-kit"; -import { Header, MenuButton } from "./components"; +import { Header, MenuButton, UserDetails, UserDetailsRef } from "./components"; import styles from "./Profile.module.scss"; -import "./index.scss"; interface ProfileProps { onEditingChange?: (isEditing: boolean) => void; @@ -60,7 +54,7 @@ const Profile: FC = (props) => { }; const buttonsWrapperEl = ( -
+
); @@ -86,7 +80,7 @@ const Profile: FC = (props) => { return (
{!isMobileView && !isEditing && editButtonEl} -
+
= (props) => {
{isEditing && buttonsWrapperEl}
diff --git a/src/pages/MyAccount/components/Profile/components/UserDetails/UserDetails.module.scss b/src/pages/MyAccount/components/Profile/components/UserDetails/UserDetails.module.scss new file mode 100644 index 0000000000..9af7df8f38 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/UserDetails/UserDetails.module.scss @@ -0,0 +1,194 @@ +@import "../../../../../../constants"; +@import "../../../../../../styles/sizes"; +@import "../../../../../../styles/typography"; + +.container { + width: 100%; + max-width: 36.5rem; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + color: $secondary-blue; +} + +.form { + width: 100%; + display: flex; + flex-direction: column; +} + +.previewWrapper { + margin-bottom: 2.25rem; + display: flex; + align-items: flex-start; + + @include tablet { + margin-bottom: 2.625rem; + flex-direction: column; + align-items: center; + } +} + +.introWrapper { + margin-bottom: 2.25rem; + + @include tablet { + margin-bottom: 1rem; + } +} + +.introTitle { + @include h5; + + margin: 0 0 0.5rem; + text-align: left; +} + +.introDescription { + @include body-sm-regular; + + max-width: 24.875rem; + margin: 0; + text-align: left; + word-break: break-word; + + @include tablet { + max-width: unset; + } +} + +.avatar { + position: relative; + text-align: center; + + @include tablet { + margin-bottom: 0.625rem; + display: flex; + flex-direction: column; + align-items: center; + } +} + +.avatarInputFile { + display: none; +} + +.userDetailsPreview { + margin-left: 1rem; + + @include tablet { + margin-left: 0; + } +} + +.userPhoto { + width: 6.25rem; + height: 6.25rem; + border-radius: 50%; + object-fit: cover; +} + +.editAvatarButton { + position: absolute; + right: 0; + bottom: 0; + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + background-color: $c-pink-mention; + border: 0.125rem solid $white; + border-radius: 50%; + cursor: pointer; +} + +.editAvatarButtonMobile { + --btn-h: 1.5rem; + --btn-pl: 0.25rem; + --btn-pr: 0.25rem; + + margin-top: 0.625rem; + font-weight: 500; +} + +.editAvatarImage { + width: 0.875rem; + height: 0.875rem; + color: $c-pink-light; +} + +.editAvatarLoader { + width: 1.25rem; + height: 1.25rem; +} + +.textFieldContainer { + max-width: 100%; + display: grid; + gap: 2rem 2.5rem; + grid-auto-columns: minmax(0, 1fr); + grid-template-areas: + "firstName lastName" + "email country" + "intro intro"; + + @include tablet { + grid-template-areas: + "firstName" + "lastName" + "email" + "country" + "intro"; + } +} + +.textField, +.textareaTextField { + width: 100%; + text-align: left; + z-index: 1; + + .textFieldLabelWrapper { + @include body-sm-regular; + + color: $c-gray-80; + } + + .textFieldInput { + padding-left: 0.75rem; + padding-right: 0.875rem; + } + + .introInputWrapper { + height: 100%; + + @include tablet { + height: 9rem; + } + } +} + +.firstNameTextField { + grid-area: firstName; +} + +.lastNameTextField { + grid-area: lastName; +} + +.emailTextField { + grid-area: email; +} + +.countryTextField { + z-index: 2; + grid-area: country; +} + +.textareaTextField { + grid-area: intro; + width: 100%; +} diff --git a/src/pages/MyAccount/components/Profile/components/UserDetails/UserDetails.tsx b/src/pages/MyAccount/components/Profile/components/UserDetails/UserDetails.tsx new file mode 100644 index 0000000000..a2f8c92335 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/UserDetails/UserDetails.tsx @@ -0,0 +1,367 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, + useImperativeHandle, + forwardRef, + ForwardRefRenderFunction, +} from "react"; +import { useDispatch } from "react-redux"; +import classNames from "classnames"; +import { Formik, FormikConfig } from "formik"; +import { FormikProps } from "formik/dist/types"; +import { updateUserDetails } from "@/pages/Auth/store/actions"; +import { uploadImage } from "@/pages/Auth/store/saga"; +import { countryList } from "@/shared/assets/countries"; +import { ButtonIcon, DropdownOption, UserAvatar } from "@/shared/components"; +import { Form, Dropdown, TextField } from "@/shared/components/Form/Formik"; +import { + ANONYMOUS_USER_FIRST_NAME, + ANONYMOUS_USER_LAST_NAME, +} from "@/shared/constants"; +import { useImageSizeCheck } from "@/shared/hooks"; +import { Edit3Icon as EditIcon } from "@/shared/icons"; +import { User } from "@/shared/models"; +import { + Button, + ButtonSize, + ButtonVariant, + Loader, + LoaderColor, +} from "@/shared/ui-kit"; +import { getUserName } from "@/shared/utils"; +import { UserDetailsPreview } from "./components"; +import { validationSchema } from "./validationSchema"; +import styles from "./UserDetails.module.scss"; + +interface Styles { + previewWrapper?: string; + avatar?: string; + userAvatar?: string; + editAvatar?: string; + fieldContainer?: string; + introInputWrapper?: string; +} + +export interface UserDetailsRef { + submit: () => void; +} + +interface UserDetailsProps { + className?: string; + user: User; + isNewUser?: boolean; + closeModal?: () => void; + isCountryDropdownFixed?: boolean; + isEditing?: boolean; + isMobileView?: boolean; + onEdit: () => void; + onLoading?: (isLoading: boolean) => void; + onSubmitting?: (isSubmitting: boolean) => void; + styles?: Styles; +} + +interface FormValues { + firstName: string; + lastName: string; + email: string; + country: string; + photo: string; + intro: string; +} + +const getInitialValues = (user: User, isNewUser: boolean): FormValues => { + const names = (user.displayName || "").split(" "); + const isAnonymousUser = + isNewUser && + user.firstName === ANONYMOUS_USER_FIRST_NAME && + user.lastName === ANONYMOUS_USER_LAST_NAME; + + return { + firstName: (!isAnonymousUser && (user.firstName || names[0])) || "", + lastName: (!isAnonymousUser && (user.lastName || names[1])) || "", + email: user.email || "", + country: user.country || "", + photo: user.photoURL || "", + intro: user.intro || "", + }; +}; + +const UserDetails: ForwardRefRenderFunction< + UserDetailsRef, + UserDetailsProps +> = (props, userDetailsRef) => { + const { + className, + user, + isNewUser = false, + closeModal, + isCountryDropdownFixed = true, + isEditing = true, + isMobileView = false, + styles: outerStyles, + onEdit, + onLoading, + onSubmitting, + } = props; + const dispatch = useDispatch(); + const formRef = useRef>(null); + const { checkImageSize } = useImageSizeCheck(); + const [loading, setLoading] = useState(false); + const [uploadedPhoto, setUploadedPhoto] = useState(""); + const inputFile = useRef(null); + const options = useMemo( + () => + countryList.map((item) => ({ + text: item.name, + value: item.value, + })), + [], + ); + + const uploadAvatar = ( + files: FileList | null, + setFieldValue: ( + field: string, + value: any, + shouldValidate?: boolean, + ) => void, + ) => { + const file = files && files[0]; + + if (!file) { + return; + } + + if (!checkImageSize(file.name, file.size)) { + return; + } + + setLoading(true); + + if (onLoading) { + onLoading(true); + } + + const fileReader = new FileReader(); + fileReader.readAsDataURL(file); + fileReader.addEventListener("load", async function () { + const imageUrl = await uploadImage(this.result); + setUploadedPhoto(URL.createObjectURL(file)); + setLoading(false); + setFieldValue("photo", imageUrl); + + if (onLoading) { + onLoading(false); + } + }); + }; + + const handleSubmit = useCallback["onSubmit"]>( + (values, { setSubmitting }) => { + setSubmitting(true); + + if (onSubmitting) { + onSubmitting(true); + } + + dispatch( + updateUserDetails.request({ + user: { ...user, ...values }, + callback: () => { + if (closeModal) { + closeModal(); + } + if (onSubmitting) { + onSubmitting(false); + } + + setSubmitting(false); + }, + }), + ); + }, + [closeModal, dispatch, user, onSubmitting], + ); + + const handleAvatarEdit = () => { + if (!loading) { + inputFile?.current?.click(); + } + }; + + useImperativeHandle( + userDetailsRef, + () => ({ + submit: () => { + formRef.current?.submitForm(); + }, + }), + [formRef], + ); + + const fieldStyles = { + labelWrapper: styles.textFieldLabelWrapper, + input: { + default: styles.textFieldInput, + }, + }; + + useEffect(() => { + if (!isEditing) { + formRef.current?.setFieldValue("photo", user.photoURL || ""); + setUploadedPhoto(""); + } + }, [isEditing, user]); + + return ( +
+ + {({ values, setFieldValue, isValid, isSubmitting }) => ( + <> +
+
+
+ + {isEditing && !isSubmitting && !isMobileView && ( + + {loading ? ( + + ) : ( + + )} + + )} + {isEditing && + !isSubmitting && + isMobileView && + (!loading ? ( + + ) : ( + + ))} + + uploadAvatar(value.target.files, setFieldValue) + } + /> +
+ {!isEditing && ( + + )} +
+ {!isEditing && user.intro && ( +
+

About

+

{user.intro}

+
+ )} + {isEditing && ( +
+ + + + + +
+ )} +
+ + )} +
+
+ ); +}; + +export default forwardRef(UserDetails); diff --git a/src/pages/MyAccount/components/Profile/components/UserDetails/components/UserDetailsPreview/UserDetailsPreview.module.scss b/src/pages/MyAccount/components/Profile/components/UserDetails/components/UserDetailsPreview/UserDetailsPreview.module.scss new file mode 100644 index 0000000000..6daf9b9e98 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/UserDetails/components/UserDetailsPreview/UserDetailsPreview.module.scss @@ -0,0 +1,38 @@ +@import "../../../../../../../../constants"; +@import "../../../../../../../../styles/sizes"; +@import "../../../../../../../../styles/typography"; + +.container { + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; + word-break: break-word; + + @include tablet { + align-items: center; + } +} + +.name { + @include h6; + + margin: 0 0 0.25rem; + text-align: left; +} + +.info { + @include body-sm-regular; + + margin: 0 0 0.25rem; + text-align: left; + color: $c-gray-40; + + &:last-child { + margin: 0; + } + + @include tablet { + text-align: center; + } +} diff --git a/src/pages/MyAccount/components/Profile/components/UserDetails/components/UserDetailsPreview/UserDetailsPreview.tsx b/src/pages/MyAccount/components/Profile/components/UserDetails/components/UserDetailsPreview/UserDetailsPreview.tsx new file mode 100644 index 0000000000..2c05009047 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/UserDetails/components/UserDetailsPreview/UserDetailsPreview.tsx @@ -0,0 +1,40 @@ +import React, { FC } from "react"; +import classNames from "classnames"; +import { countryList } from "@/shared/assets/countries"; +import { DateFormat, User } from "@/shared/models"; +import { formatDate, getUserName } from "@/shared/utils"; +import styles from "./UserDetailsPreview.module.scss"; + +interface UserDetailsPreviewProps { + className?: string; + user: User; + isMobileView?: boolean; +} + +const UserDetailsPreview: FC = (props) => { + const { className, user, isMobileView = false } = props; + const country = countryList.find((item) => item.value === user.country); + const countryName = + country && country.name.slice(0, country.name.lastIndexOf(" ")); + + return ( +
+

{getUserName(user)}

+ {user.email &&

{user.email}

} + {!isMobileView && ( + <> +

+ Join at{" "} + {formatDate( + new Date(user.createdAt.seconds * 1000), + DateFormat.SuperShortSecondary, + )} +

+ {countryName &&

{countryName}

} + + )} +
+ ); +}; + +export default UserDetailsPreview; diff --git a/src/pages/MyAccount/components/Profile/components/UserDetails/components/UserDetailsPreview/index.ts b/src/pages/MyAccount/components/Profile/components/UserDetails/components/UserDetailsPreview/index.ts new file mode 100644 index 0000000000..1cc006214b --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/UserDetails/components/UserDetailsPreview/index.ts @@ -0,0 +1 @@ +export { default as UserDetailsPreview } from "./UserDetailsPreview"; diff --git a/src/pages/MyAccount/components/Profile/components/UserDetails/components/index.ts b/src/pages/MyAccount/components/Profile/components/UserDetails/components/index.ts new file mode 100644 index 0000000000..06c4870654 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/UserDetails/components/index.ts @@ -0,0 +1 @@ +export * from "./UserDetailsPreview"; diff --git a/src/pages/MyAccount/components/Profile/components/UserDetails/index.ts b/src/pages/MyAccount/components/Profile/components/UserDetails/index.ts new file mode 100644 index 0000000000..c507ba11ad --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/UserDetails/index.ts @@ -0,0 +1,2 @@ +export { default as UserDetails } from "./UserDetails"; +export type { UserDetailsRef } from "./UserDetails"; diff --git a/src/pages/MyAccount/components/Profile/components/UserDetails/validationSchema.ts b/src/pages/MyAccount/components/Profile/components/UserDetails/validationSchema.ts new file mode 100644 index 0000000000..7bffd24fd5 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/UserDetails/validationSchema.ts @@ -0,0 +1,10 @@ +import * as Yup from "yup"; +import { FORM_ERROR_MESSAGES } from "@/shared/constants"; + +export const validationSchema = Yup.object({ + firstName: Yup.string().required(FORM_ERROR_MESSAGES.REQUIRED), + lastName: Yup.string().required(FORM_ERROR_MESSAGES.REQUIRED), + email: Yup.string() + .required(FORM_ERROR_MESSAGES.EMAIL) + .email(FORM_ERROR_MESSAGES.EMAIL), +}); diff --git a/src/pages/MyAccount/components/Profile/components/index.ts b/src/pages/MyAccount/components/Profile/components/index.ts index 6733922ac9..5a9a72ed74 100644 --- a/src/pages/MyAccount/components/Profile/components/index.ts +++ b/src/pages/MyAccount/components/Profile/components/index.ts @@ -1,2 +1,3 @@ export * from "./Header"; export * from "./MenuButton"; +export * from "./UserDetails"; diff --git a/src/pages/MyAccount/components/Profile/index.scss b/src/pages/MyAccount/components/Profile/index.scss deleted file mode 100644 index 8b12f92037..0000000000 --- a/src/pages/MyAccount/components/Profile/index.scss +++ /dev/null @@ -1,100 +0,0 @@ -@import "../../../../constants"; -@import "../../../../styles/mixins"; -@import "../../../../styles/sizes"; - -.profile-wrapper { - max-width: 36.25rem; - width: 100%; - margin: 0 auto; - padding-top: 3.375rem; - - @include tablet { - padding-top: 0; - } - - .profile-wrapper__header { - height: 4.5rem; - margin-bottom: 2.5rem; - display: flex; - align-items: center; - justify-content: space-between; - - @include tablet { - margin-bottom: 0; - } - } - - .profile-wrapper__buttons-wrapper { - @include flex-list-with-gap(1rem); - } - - .profile-wrapper__user-details { - margin-bottom: 2.125rem; - - @include tablet { - margin-bottom: 1.375rem; - } - } - - .profile-wrapper__avatar-wrapper { - margin-bottom: 2.5rem; - display: flex; - flex-direction: column; - align-items: flex-start; - - @include tablet { - margin-bottom: 1.5rem; - justify-content: center; - align-items: center; - } - } - - .profile-wrapper__avatar { - margin: 0; - } - - .profile-wrapper__user-avatar { - width: 6.5rem; - height: 6.5rem; - margin-right: auto; - } - - .profile-wrapper__edit-avatar { - top: unset; - right: 0; - bottom: 0; - left: unset; - width: 2rem; - height: 2rem; - border: 0.125rem solid $white; - } - - .profile-wrapper__field-container { - gap: 2rem 2.5rem; - grid-template-areas: - "firstName country" - "lastName intro" - "email intro"; - - @include tablet { - grid-template-areas: - "firstName" - "lastName" - "email" - "country" - "intro"; - } - } - - .profile-wrapper__form-intro-input-wrapper { - height: 100%; - - @include tablet { - height: 9rem; - } - } -} - -.profile-wrapper__delete-account-button { - margin-top: 1.5rem; -} diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestIntroduce.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestIntroduce.tsx index 56e4cc4274..9c06712769 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestIntroduce.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestIntroduce.tsx @@ -1,11 +1,9 @@ import React, { useCallback } from "react"; -import { useSelector } from "react-redux"; import { Formik, FormikConfig } from "formik"; -import { Button } from "@/shared/components"; import { Form, TextField, LinksArray } from "@/shared/components/Form/Formik"; -import { ScreenSize, MAX_LINK_TITLE_LENGTH } from "@/shared/constants"; +import { MAX_LINK_TITLE_LENGTH } from "@/shared/constants"; import { CommonLink } from "@/shared/models"; -import { getScreenSize } from "@/shared/store/selectors"; +import { Button, ButtonVariant } from "@/shared/ui-kit"; import { parseLinksForSubmission } from "@/shared/utils"; import { IStageProps } from "./MembershipRequestModal"; import { MembershipRequestStage } from "./constants"; @@ -25,8 +23,6 @@ const getInitialValues = (data: IStageProps["userData"]): FormValues => ({ export default function MembershipRequestIntroduce(props: IStageProps) { const { userData, setUserData, governance } = props; - const screenSize = useSelector(getScreenSize()); - const isMobileView = screenSize === ScreenSize.Mobile; const handleSubmit = useCallback["onSubmit"]>( (values) => { @@ -95,7 +91,7 @@ export default function MembershipRequestIntroduce(props: IStageProps) { className="membership-request-introduce__submit-button" type="submit" disabled={!isValid} - shouldUseFullWidth={isMobileView} + variant={ButtonVariant.PrimaryPink} > Continue diff --git a/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreateCommonModal.tsx b/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreateCommonModal.tsx index 7548b94acb..2167e9e57a 100644 --- a/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreateCommonModal.tsx +++ b/src/pages/OldCommon/components/CommonListContainer/CreateCommonModal/CreateCommonModal.tsx @@ -284,6 +284,7 @@ export default function CreateCommonModal(props: CreateCommonModalProps) { image: createdCommonData.common.image, name: createdCommonData.common.name, directParent: createdCommonData.common.directParent, + rootCommonId: createdCommonData.common.rootCommonId, hasMembership: true, hasPermissionToAddProject, notificationsAmount: 0, diff --git a/src/pages/billing/Billing.module.scss b/src/pages/billing/Billing.module.scss index 0069aa000a..779c11b95f 100644 --- a/src/pages/billing/Billing.module.scss +++ b/src/pages/billing/Billing.module.scss @@ -1,15 +1,17 @@ +@import "../../constants"; @import "../../styles/sizes"; -.topNavigation { - z-index: 2; -} - .container { - padding-top: 3rem; + flex: 1; + max-width: 49.5rem; + padding-top: var(--header-h); padding-bottom: 3rem; + border-left: 0.0625rem solid $c-gray-10; + border-right: 0.0625rem solid $c-gray-10; @include tablet { - padding-top: 1rem; + padding-top: 0; padding-bottom: 1rem; + border: 0; } } diff --git a/src/pages/billing/Billing.tsx b/src/pages/billing/Billing.tsx index 32fca7e0f0..38f63931d3 100644 --- a/src/pages/billing/Billing.tsx +++ b/src/pages/billing/Billing.tsx @@ -1,14 +1,20 @@ import React, { FC } from "react"; import { Billing } from "@/pages/MyAccount/components/Billing"; +import { ViewportBreakpointVariant } from "@/shared/constants"; import { MainRoutesProvider } from "@/shared/contexts"; -import { Container, PureCommonTopNavigation } from "@/shared/ui-kit"; +import { Container } from "@/shared/ui-kit"; import styles from "./Billing.module.scss"; const BillingPage: FC = () => { return ( - - + diff --git a/src/pages/billing/Billing_v04.tsx b/src/pages/billing/Billing_v04.tsx index b1d36f6fad..399f7237df 100644 --- a/src/pages/billing/Billing_v04.tsx +++ b/src/pages/billing/Billing_v04.tsx @@ -1,14 +1,20 @@ import React, { FC } from "react"; import { Billing } from "@/pages/MyAccount/components/Billing"; +import { ViewportBreakpointVariant } from "@/shared/constants"; import { RoutesV04Provider } from "@/shared/contexts"; -import { Container, PureCommonTopNavigation } from "@/shared/ui-kit"; +import { Container } from "@/shared/ui-kit"; import styles from "./Billing.module.scss"; const BillingPage_v04: FC = () => { return ( - - + diff --git a/src/pages/common/components/ChatComponent/ChatComponent.module.scss b/src/pages/common/components/ChatComponent/ChatComponent.module.scss index 73a2fb1550..32838bd7c3 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.module.scss +++ b/src/pages/common/components/ChatComponent/ChatComponent.module.scss @@ -76,7 +76,7 @@ flex-direction: column; justify-content: center; word-break: break-word; - padding: 0.125rem 1.75rem 0.125rem 0.25rem; + padding: 0.125rem 1.75rem 0.125rem 0.75rem; margin: 0.3rem 0; } @@ -86,7 +86,8 @@ .messageInputEmpty { line-height: 2.25rem; - padding-left: 0; + padding-left: 0.75rem; + overflow-x: hidden; } .addFilesIcon { diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 4dd9064852..0abf8d3382 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -11,7 +11,7 @@ import { useDispatch, useSelector } from "react-redux"; import { useDebounce, useMeasure } from "react-use"; import classNames from "classnames"; import isHotkey from "is-hotkey"; -import { delay, omit } from "lodash"; +import { debounce, delay, omit } from "lodash"; import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { ChatService, DiscussionMessageService, FileService } from "@/services"; @@ -59,6 +59,7 @@ import { selectFilesPreview, FileInfo, } from "@/store/states"; +import { ChatContentContext, ChatContentData } from "../CommonContent/context"; import { ChatContent, ChatContentRef, @@ -181,6 +182,15 @@ export default function ChatComponent({ const chatContentRef = useRef(null); const chatWrapperId = useMemo(() => `chat-wrapper-${uuidv4()}`, []); const chatInputWrapperRef = useRef(null); + const chatContainerRef = useRef(null); + const [isScrolling, setScrolling] = useState(false); + const chatContentContextValue: ChatContentData = useMemo( + () => ({ + isScrolling, + chatContentRect: chatContainerRef.current?.getBoundingClientRect(), + }), + [isScrolling], + ); const [message, setMessage] = useState( parseStringToTextEditorValue(), @@ -591,6 +601,23 @@ export default function ChatComponent({ }; }, []); + useEffect(() => { + const deactivateScrollingFlag = debounce(() => { + setScrolling(false); + }, 300); + + function handleScroll() { + setScrolling(true); + deactivateScrollingFlag(); + } + + chatContainerRef.current?.addEventListener("scroll", handleScroll); + + return () => { + chatContainerRef.current?.removeEventListener("scroll", handleScroll); + }; + }, []); + const renderChatInput = (): ReactNode => { const shouldHideChatInput = !isChatChannel && (!hasAccess || isHidden); @@ -642,7 +669,7 @@ export default function ChatComponent({ }} value={message} onChange={setMessage} - placeholder="What do you think?" + placeholder="Message" onKeyDown={onEnterKeyDown} users={users} shouldReinitializeEditor={shouldReinitializeEditor} @@ -666,32 +693,35 @@ export default function ChatComponent({ [styles.emptyChat]: !dateList.length, })} id={chatWrapperId} + ref={chatContainerRef} > - + + +
diff --git a/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx b/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx index f8775030d4..e047d3c936 100644 --- a/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx +++ b/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx @@ -17,7 +17,7 @@ import { LOADER_APPEARANCE_DELAY, QueryParamKey, } from "@/shared/constants"; -import { useQueryParams } from "@/shared/hooks"; +import { useForceUpdate, useQueryParams } from "@/shared/hooks"; import { checkIsUserDiscussionMessage, CommonFeedObjectUserUnique, @@ -97,6 +97,13 @@ const ChatContent: ForwardRefRenderFunction< const userId = user?.uid; const queryParams = useQueryParams(); const messageIdParam = queryParams[QueryParamKey.Message]; + const forceUpdate = useForceUpdate(); + + useEffect(() => { + if (messages) { + forceUpdate(); + } + }, [messages]); const [highlightedMessageId, setHighlightedMessageId] = useState( () => (typeof messageIdParam === "string" && messageIdParam) || null, @@ -120,20 +127,6 @@ const ChatContent: ForwardRefRenderFunction< [chatWrapperId], ); - const scrollMore = useCallback( - (toY: number) => - setTimeout( - () => - animateScroll.scrollMore(toY, { - containerId: chatWrapperId, - smooth: true, - delay: 0, - }), - 0, - ), - [chatWrapperId], - ); - const dateListReverse = useMemo(() => [...dateList].reverse(), [dateList]); useEffect(() => { @@ -234,17 +227,6 @@ const ChatContent: ForwardRefRenderFunction< scrollToRepliedMessage={scrollToRepliedMessage} highlighted={message.id === highlightedMessageId} hasPermissionToHide={hasPermissionToHide} - onMessageDropdownOpen={(isOpen, messageTopPosition = 0) => { - const dropdownHeight = 240; - const visibleDropdownHeight = - window.innerHeight - messageTopPosition; - const hasEnoughSpaceForMenu = - visibleDropdownHeight >= dropdownHeight; - - if (isOpen && !hasEnoughSpaceForMenu) { - scrollMore(dropdownHeight - visibleDropdownHeight + 20); - } - }} users={users} feedItemId={feedItemId} commonMember={commonMember} diff --git a/src/pages/common/components/CommonContent/context.ts b/src/pages/common/components/CommonContent/context.ts new file mode 100644 index 0000000000..8d8716534c --- /dev/null +++ b/src/pages/common/components/CommonContent/context.ts @@ -0,0 +1,13 @@ +import React, { useContext } from "react"; + +export interface ChatContentData { + isScrolling: boolean; + chatContentRect?: DOMRect; +} + +export const ChatContentContext = React.createContext({ + isScrolling: false, +}); + +export const useChatContentContext = (): ChatContentData => + useContext(ChatContentContext); diff --git a/src/pages/common/components/CommonTabPanels/components/AboutTab/components/CommonEntranceInfo/components/CommonEntranceJoin/CommonEntranceJoin.module.scss b/src/pages/common/components/CommonTabPanels/components/AboutTab/components/CommonEntranceInfo/components/CommonEntranceJoin/CommonEntranceJoin.module.scss index a7bc347d2c..4ce189cea1 100644 --- a/src/pages/common/components/CommonTabPanels/components/AboutTab/components/CommonEntranceInfo/components/CommonEntranceJoin/CommonEntranceJoin.module.scss +++ b/src/pages/common/components/CommonTabPanels/components/AboutTab/components/CommonEntranceInfo/components/CommonEntranceJoin/CommonEntranceJoin.module.scss @@ -2,9 +2,9 @@ @import "../../../../../../../../../../styles/sizes"; .joinButton { - max-width: 8.5rem; margin-top: 1.75rem; padding: 0 0.875rem; + width: 100%; @include tablet { margin-top: 1.625rem; diff --git a/src/pages/common/components/CommonTabPanels/components/AboutTab/components/CommonEntranceInfo/components/CommonEntranceJoin/CommonEntranceJoin.tsx b/src/pages/common/components/CommonTabPanels/components/AboutTab/components/CommonEntranceInfo/components/CommonEntranceJoin/CommonEntranceJoin.tsx index db5c6179d9..95d06595b2 100644 --- a/src/pages/common/components/CommonTabPanels/components/AboutTab/components/CommonEntranceInfo/components/CommonEntranceJoin/CommonEntranceJoin.tsx +++ b/src/pages/common/components/CommonTabPanels/components/AboutTab/components/CommonEntranceInfo/components/CommonEntranceJoin/CommonEntranceJoin.tsx @@ -1,5 +1,5 @@ import React, { FC } from "react"; -import { NavLink } from "react-router-dom"; +import { NavLink, useHistory } from "react-router-dom"; import { useJoinProjectAutomatically } from "@/pages/common/hooks"; import { useCommonDataContext } from "@/pages/common/providers"; import { useRoutesContext } from "@/shared/contexts"; @@ -14,7 +14,8 @@ interface CommonEntranceJoinProps { } const CommonEntranceJoin: FC = (props) => { - const { withJoinRequest = false, common, isProject } = props; + const { withJoinRequest, common, isProject } = props; + const history = useHistory(); const { parentCommon, parentCommonMember, @@ -47,13 +48,13 @@ const CommonEntranceJoin: FC = (props) => {

)} {!commonMember && rootCommon && !rootCommonMember && ( -

- Join via{" "} - - {rootCommon.name} - {" "} - page -

+ )} {!commonMember && rootCommonMember && @@ -67,7 +68,7 @@ const CommonEntranceJoin: FC = (props) => { page

)} - {withJoinRequest && (isJoinAllowed || isJoinPending) && ( + {withJoinRequest && !isProject && (isJoinAllowed || isJoinPending) && (
); }; diff --git a/src/services/Common.ts b/src/services/Common.ts index 4a131c415a..7bc06fc7ea 100644 --- a/src/services/Common.ts +++ b/src/services/Common.ts @@ -119,6 +119,38 @@ class CommonService { .reduce((acc, items) => [...acc, ...items], []); }; + public subscribeToCommonsByDirectParentId = ( + parentCommonId: string, + callback: ( + data: { + common: Common; + statuses: { + isAdded: boolean; + isRemoved: boolean; + }; + }[], + ) => void, + ): UnsubscribeFunction => { + const query = firebase + .firestore() + .collection(Collection.Daos) + .where("state", "==", CommonState.ACTIVE) + .where("directParent.commonId", "==", parentCommonId) + .withConverter(converter); + + return query.onSnapshot((snapshot) => { + callback( + snapshot.docChanges().map((docChange) => ({ + common: docChange.doc.data(), + statuses: { + isAdded: docChange.type === DocChange.Added, + isRemoved: docChange.type === DocChange.Removed, + }, + })), + ); + }); + }; + public getUserCommonIds = async (userId: string): Promise => ( await firebase @@ -144,6 +176,39 @@ class CommonService { })); }; + public subscribeToAllUserCommonMemberInfo = ( + userId: string, + callback: ( + data: { + commonId: string; + commonMember: CommonMember; + statuses: { + isAdded: boolean; + isRemoved: boolean; + }; + }[], + ) => void, + ): UnsubscribeFunction => { + const query = firebase + .firestore() + .collectionGroup(SubCollections.Members) + .where("userId", "==", userId) + .withConverter(commonMemberConverter); + + return query.onSnapshot((snapshot) => { + callback( + snapshot.docChanges().map((docChange) => ({ + commonId: docChange.doc.ref.path.split("/")[1], + commonMember: docChange.doc.data(), + statuses: { + isAdded: docChange.type === DocChange.Added, + isRemoved: docChange.type === DocChange.Removed, + }, + })), + ); + }); + }; + public getCommonsWithSubCommons = async ( commonIds: string[], ): Promise => { diff --git a/src/services/Governance.ts b/src/services/Governance.ts index 003b8b824c..3a1784db95 100644 --- a/src/services/Governance.ts +++ b/src/services/Governance.ts @@ -1,4 +1,5 @@ import { governanceCollection } from "@/pages/OldCommon/store/api"; +import { DocChange } from "@/shared/constants"; import { UnsubscribeFunction } from "@/shared/interfaces"; import { Collection, Governance } from "@/shared/models"; import { @@ -64,6 +65,37 @@ class GovernanceService { } }); }; + + public subscribeToGovernanceListByCommonIds = ( + commonIds: string[], + callback: ( + data: { + governance: Governance; + statuses: { + isAdded: boolean; + isRemoved: boolean; + }; + }[], + ) => void, + ): UnsubscribeFunction => { + const query = firebase + .firestore() + .collection(Collection.Governance) + .where("commonId", "in", commonIds) + .withConverter(converter); + + return query.onSnapshot((snapshot) => { + callback( + snapshot.docChanges().map((docChange) => ({ + governance: docChange.doc.data(), + statuses: { + isAdded: docChange.type === DocChange.Added, + isRemoved: docChange.type === DocChange.Removed, + }, + })), + ); + }); + }; } export default new GovernanceService(); diff --git a/src/shared/assets/images/landing-video-poster.jpeg b/src/shared/assets/images/landing-video-poster.jpeg deleted file mode 100644 index 72a4f41aa1..0000000000 Binary files a/src/shared/assets/images/landing-video-poster.jpeg and /dev/null differ diff --git a/src/shared/assets/images/landing-video-poster.jpg b/src/shared/assets/images/landing-video-poster.jpg new file mode 100644 index 0000000000..9ec203d64f Binary files /dev/null and b/src/shared/assets/images/landing-video-poster.jpg differ diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss b/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss index 9da9e2e916..065ea21b61 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss @@ -58,6 +58,8 @@ box-sizing: border-box; position: relative; text-align: start; + font-size: $small; + color: $secondary-blue; &:hover { .menuArrowButton { @@ -85,10 +87,10 @@ } .systemMessage { - width: 100%; - max-width: unset; - min-width: unset; - background-color: $c-pink-light; + width: fit-content; + text-align: center; + font-size: $xxsmall-2; + color: $c-gray-60; } .messageName { @@ -102,8 +104,6 @@ } .messageContent { - font-size: $small; - color: $secondary-blue; word-wrap: break-word; white-space: pre-line; margin-right: 1rem; diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx index 343a2e74c9..76fe7c0841 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx @@ -3,10 +3,10 @@ import React, { useCallback, useEffect, useState, - useRef, useMemo, } from "react"; import classNames from "classnames"; +import { useLongPress } from "use-long-press"; import { ElementDropdown, UserAvatar, @@ -52,10 +52,6 @@ interface ChatMessageProps { chatType: ChatType; highlighted?: boolean; className?: string; - onMessageDropdownOpen?: ( - isOpen: boolean, - messageTopPosition?: number, - ) => void; user: User | null; scrollToRepliedMessage: (messageId: string) => void; hasPermissionToHide: boolean; @@ -86,7 +82,6 @@ export default function ChatMessage({ chatType, highlighted = false, className, - onMessageDropdownOpen, user, scrollToRepliedMessage, hasPermissionToHide, @@ -100,7 +95,6 @@ export default function ChatMessage({ onFeedItemClick, onInternalLinkClick, }: ChatMessageProps) { - const messageRef = useRef(null); const { getCommonPagePath, getCommonPageAboutTabPath } = useRoutesContext(); const [isEditMode, setEditMode] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -140,15 +134,6 @@ export default function ChatMessage({ } }; - const handleMessageDropdownOpen = - onMessageDropdownOpen && - ((isOpen: boolean) => { - onMessageDropdownOpen( - isOpen, - messageRef.current?.getBoundingClientRect().top, - ); - }); - useEffect(() => { (async () => { if (!discussionMessage.text) { @@ -216,19 +201,17 @@ export default function ChatMessage({ onUserClick, ]); - const handleMenuToggle = (isOpen: boolean) => { - setIsMenuOpen(isOpen); - - if (handleMessageDropdownOpen) { - handleMessageDropdownOpen(isOpen); - } + const handleLongPress = () => { + setIsMenuOpen(true); }; - const handleMessageClick: MouseEventHandler = () => { - if (isTabletView) { - setIsMenuOpen(true); - } - }; + const getLongPressProps = useLongPress( + isTabletView ? handleLongPress : null, + { + threshold: 400, + cancelOnMovement: true, + }, + ); const handleContextMenu: MouseEventHandler = (event) => { if (!isTabletView) { @@ -376,7 +359,6 @@ export default function ChatMessage({ ) : ( <>
{isNotCurrentUserMessage && !isSystemMessage && (
@@ -418,7 +400,11 @@ export default function ChatMessage({ - {messageText.map((text) => text)} + {!messageText.length ? ( + Loading... + ) : ( + messageText.map((text) => text) + )} {!isSystemMessage && (
- {isSystemMessage && ( -
diff --git a/src/shared/components/Dropdown/Dropdown.tsx b/src/shared/components/Dropdown/Dropdown.tsx index 019f1f1303..8614194041 100644 --- a/src/shared/components/Dropdown/Dropdown.tsx +++ b/src/shared/components/Dropdown/Dropdown.tsx @@ -7,7 +7,6 @@ import React, { CSSProperties, ForwardRefRenderFunction, ReactNode, - RefObject, useEffect, } from "react"; import { @@ -21,12 +20,15 @@ import { } from "react-aria-menubutton"; import classNames from "classnames"; import { v4 as uuidv4 } from "uuid"; +import { useChatContentContext } from "@/pages/common/components/CommonContent/context"; import { Loader } from "@/shared/components"; import RightArrowIcon from "../../icons/rightArrow.icon"; import { GlobalOverlay } from "../GlobalOverlay"; +import { getMenuStyles } from "./helpers"; import "./index.scss"; export interface Styles { + labelWrapper?: string; menuButton?: string; value?: string; placeholder?: string; @@ -78,53 +80,6 @@ export interface DropdownProps { disabled?: boolean; } -const getFixedMenuStyles = ( - ref: RefObject, - menuRef: HTMLUListElement | null, -): CSSProperties | undefined => { - if (!ref.current || !menuRef) { - return; - } - - const { top, left, height } = ref.current.getBoundingClientRect(); - const menuRect = menuRef.getBoundingClientRect(); - const bottom = top + height + menuRect.height; - const styles: CSSProperties = { - left, - top: top + height, - }; - - if (window.innerHeight < bottom) { - styles.top = top - menuRect.height; - } - if (styles.top && styles.top < 0) { - styles.top = 0; - styles.bottom = 0; - styles.maxHeight = "100%"; - } - - return styles; -}; - -const getMenuStyles = ( - ref: RefObject, - menuRef: HTMLUListElement | null, - shouldBeFixed?: boolean, -): CSSProperties | undefined => { - if (!menuRef) { - return; - } - if (shouldBeFixed) { - return getFixedMenuStyles(ref, menuRef); - } - - const { right } = menuRef.getBoundingClientRect(); - - if (window.innerWidth < right) { - return { right: 0 }; - } -}; - const Dropdown: ForwardRefRenderFunction = ( props, dropdownRef, @@ -152,6 +107,8 @@ const Dropdown: ForwardRefRenderFunction = ( const [isOpen, setIsOpen] = useState(false); const selectedOption = options.find((option) => option.value === value); const dropdownId = useMemo(() => `dropdown-${uuidv4()}`, []); + const { isScrolling: isChatScrolling, chatContentRect } = + useChatContentContext(); const handleSelection: MenuWrapperProps["onSelection"] = ( value, @@ -178,9 +135,16 @@ const Dropdown: ForwardRefRenderFunction = ( } }; + useEffect(() => { + if (isMenuOpen && isChatScrolling) { + handleMenuToggle({ isOpen: false }); + closeMenu(dropdownId); + } + }, [isMenuOpen, isChatScrolling]); + const menuStyles = useMemo( - () => getMenuStyles(menuButtonRef, menuRef, shouldBeFixed), - [menuRef, shouldBeFixed], + () => getMenuStyles(menuButtonRef, menuRef, chatContentRect, shouldBeFixed), + [menuRef, shouldBeFixed, chatContentRect], ); useImperativeHandle( @@ -205,7 +169,12 @@ const Dropdown: ForwardRefRenderFunction = ( onMenuToggle={handleMenuToggle} > {label && ( -
+
{label}
)} diff --git a/src/shared/components/Dropdown/helpers.ts b/src/shared/components/Dropdown/helpers.ts new file mode 100644 index 0000000000..a0fe50ee04 --- /dev/null +++ b/src/shared/components/Dropdown/helpers.ts @@ -0,0 +1,62 @@ +import { CSSProperties, RefObject } from "react"; + +const getFixedMenuStyles = ( + ref: RefObject, + menuRef: HTMLUListElement | null, +): CSSProperties | undefined => { + if (!ref.current || !menuRef) { + return; + } + + const { top, left, height } = ref.current.getBoundingClientRect(); + const menuRect = menuRef.getBoundingClientRect(); + const bottom = top + height + menuRect.height; + const styles: CSSProperties = { + left, + top: top + height, + }; + + if (window.innerHeight < bottom) { + styles.top = top - menuRect.height; + } + if (styles.top && Number(styles.top) < 0) { + styles.top = 0; + styles.bottom = 0; + styles.maxHeight = "100%"; + } + + return styles; +}; + +export const getMenuStyles = ( + ref: RefObject, + menuRef: HTMLUListElement | null, + chatContentRect?: DOMRect, + shouldBeFixed?: boolean, +): CSSProperties | undefined => { + if (!menuRef) { + return; + } + if (shouldBeFixed) { + return getFixedMenuStyles(ref, menuRef); + } + + const styles: CSSProperties = {}; + const { right, height } = menuRef.getBoundingClientRect(); + + if (window.innerWidth < right) { + styles.right = 0; + } + + if (ref.current && chatContentRect) { + const { bottom: menuButtonBottom, height: menuButtonHeight } = + ref.current.getBoundingClientRect(); + const menuBottom = menuButtonBottom + height; + + if (chatContentRect.bottom < menuBottom) { + styles.bottom = menuButtonHeight; + } + } + + return styles; +}; diff --git a/src/shared/components/Image/Image.tsx b/src/shared/components/Image/Image.tsx index a8609da1f9..3e32f9c523 100644 --- a/src/shared/components/Image/Image.tsx +++ b/src/shared/components/Image/Image.tsx @@ -8,6 +8,7 @@ import React, { ReactEventHandler, } from "react"; import classNames from "classnames"; +import { ImageWithZoom } from "../ImageWithZoom"; import styles from "./Image.module.scss"; interface CustomImageProps extends ImgHTMLAttributes { @@ -16,6 +17,7 @@ interface CustomImageProps extends ImgHTMLAttributes { placeholderElement?: ReactNode; imageOverlayClassName?: string; imageContainerClassName?: string; + hasZoom?: boolean; } const CustomImage: FC = (props) => { @@ -28,6 +30,7 @@ const CustomImage: FC = (props) => { imageOverlayClassName, imageContainerClassName, onClick, + hasZoom = false, ...restProps } = props; const [isLoaded, setIsLoaded] = useState(false); @@ -70,13 +73,24 @@ const CustomImage: FC = (props) => { return hasError && (placeholderElement || placeholderElement === null) ? ( <>{placeholderElement} ) : ( -
- {alt} -
-
+ <> + {hasZoom ? ( + + ) : ( +
+ {alt} +
+
+ )} + ); }; diff --git a/src/shared/components/ImageWithZoom/ImageWithZoom.module.scss b/src/shared/components/ImageWithZoom/ImageWithZoom.module.scss new file mode 100644 index 0000000000..c87c46f308 --- /dev/null +++ b/src/shared/components/ImageWithZoom/ImageWithZoom.module.scss @@ -0,0 +1,4 @@ +.imageWithZoomContainer { + width: 100%; + height: 100%; +} diff --git a/src/shared/components/ImageWithZoom/ImageWithZoom.tsx b/src/shared/components/ImageWithZoom/ImageWithZoom.tsx new file mode 100644 index 0000000000..2c8b2b7dbc --- /dev/null +++ b/src/shared/components/ImageWithZoom/ImageWithZoom.tsx @@ -0,0 +1,23 @@ +import React, { ImgHTMLAttributes } from "react"; +import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; +import styles from "./ImageWithZoom.module.scss"; + +const ImageWthZoom = ({ + src, + alt, + className, + ...restProps +}: ImgHTMLAttributes) => { + return ( + + + {alt} + + + ); +}; + +export default ImageWthZoom; diff --git a/src/shared/components/ImageWithZoom/index.ts b/src/shared/components/ImageWithZoom/index.ts new file mode 100644 index 0000000000..a40fbbecb6 --- /dev/null +++ b/src/shared/components/ImageWithZoom/index.ts @@ -0,0 +1 @@ +export { default as ImageWithZoom } from "./ImageWithZoom"; \ No newline at end of file diff --git a/src/shared/components/index.tsx b/src/shared/components/index.tsx index 522ef5eae2..0e8ec5fdbf 100644 --- a/src/shared/components/index.tsx +++ b/src/shared/components/index.tsx @@ -41,3 +41,4 @@ export * from "./BackgroundNotificationModal"; export * from "./Chat"; export * from "./ReportModal"; export * from "./UserInfoPopup"; +export * from "./ImageWithZoom"; diff --git a/src/shared/constants/viewportBreakpoint.ts b/src/shared/constants/viewportBreakpoint.ts index 6b693b5a54..b6b3d8b8d6 100644 --- a/src/shared/constants/viewportBreakpoint.ts +++ b/src/shared/constants/viewportBreakpoint.ts @@ -3,6 +3,7 @@ export enum ViewportBreakpoint { Desktop = 1560, Laptop = 1152, Tablet = 768, + BigPhone = 450, Phone = 390, } diff --git a/src/shared/hooks/useCases/index.ts b/src/shared/hooks/useCases/index.ts index 41dc1342fb..8be24f937c 100644 --- a/src/shared/hooks/useCases/index.ts +++ b/src/shared/hooks/useCases/index.ts @@ -1,9 +1,11 @@ +export { useAllUserCommonMemberInfo } from "./useAllUserCommonMemberInfo"; export { useChatChannelUserStatus } from "./useChatChannelUserStatus"; export { useChatMessages } from "./useChatMessages"; export { useCommon } from "./useCommon"; export { useCommonFeedItems } from "./useCommonFeedItems"; export { useCommonMembersWithCircleIdsAmount } from "./useCommonMembersWithCircleIdsAmount"; export { useCommonRulesAcceptance } from "./useCommonRulesAcceptance"; +export { useCommonsByDirectParentId } from "./useCommonsByDirectParentId"; export { useDiscussionById } from "./useDiscussionById"; export { useDMUserChatChannel } from "./useDMUserChatChannel"; export { useFeedItemUserMetadata } from "./useFeedItemUserMetadata"; @@ -28,6 +30,7 @@ export { useUserFeedItemFollowData } from "./useUserFeedItemFollowData"; export { useFeedItemFollow } from "./useFeedItemFollow"; export type { FeedItemFollowState } from "./useFeedItemFollow"; export { useGovernance } from "./useGovernance"; +export { useGovernanceListByCommonIds } from "./useGovernanceListByCommonIds"; export { useGovernanceByCommonId } from "./useGovernanceByCommonId"; export { useUserInfoAboutMemberships } from "./useUserInfoAboutMemberships"; export { useBankAccountDetails } from "./useBankAccountDetails"; diff --git a/src/shared/hooks/useCases/useAllUserCommonMemberInfo.ts b/src/shared/hooks/useCases/useAllUserCommonMemberInfo.ts new file mode 100644 index 0000000000..73007a6343 --- /dev/null +++ b/src/shared/hooks/useCases/useAllUserCommonMemberInfo.ts @@ -0,0 +1,87 @@ +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { selectUser } from "@/pages/Auth/store/selectors"; +import { CommonService } from "@/services"; +import { Awaited, LoadingState } from "@/shared/interfaces"; + +type State = LoadingState +> | null>; + +type Return = State; + +export const useAllUserCommonMemberInfo = (): Return => { + const user = useSelector(selectUser()); + const userId = user?.uid; + const [state, setState] = useState({ + loading: true, + fetched: false, + data: null, + }); + + useEffect(() => { + if (!userId) { + return; + } + + const unsubscribe = CommonService.subscribeToAllUserCommonMemberInfo( + userId, + (data) => { + setState((currentState) => { + if (!currentState.data || currentState.data.length === 0) { + return { + loading: false, + fetched: true, + data: data.map((item) => ({ + ...item.commonMember, + commonId: item.commonId, + })), + }; + } + + const nextData = [...currentState.data]; + + data.forEach((item) => { + const itemIndex = nextData.findIndex( + ({ commonId }) => commonId === item.commonId, + ); + + if (itemIndex === -1) { + nextData.push({ + ...item.commonMember, + commonId: item.commonId, + }); + return; + } + + if (item.statuses.isRemoved) { + nextData.splice(itemIndex, 1); + } else { + nextData[itemIndex] = { + ...item.commonMember, + commonId: item.commonId, + }; + } + }); + + return { + loading: false, + fetched: true, + data: nextData, + }; + }); + }, + ); + + return () => { + unsubscribe(); + setState({ + loading: true, + fetched: false, + data: null, + }); + }; + }, [userId]); + + return state; +}; diff --git a/src/shared/hooks/useCases/useCommonsByDirectParentId.ts b/src/shared/hooks/useCases/useCommonsByDirectParentId.ts new file mode 100644 index 0000000000..1be4c6028d --- /dev/null +++ b/src/shared/hooks/useCases/useCommonsByDirectParentId.ts @@ -0,0 +1,73 @@ +import { useEffect, useState } from "react"; +import { CommonService } from "@/services"; +import { LoadingState } from "@/shared/interfaces"; +import { Common } from "@/shared/models"; + +type State = LoadingState; + +type Return = State; + +export const useCommonsByDirectParentId = (parentCommonId?: string): Return => { + const [state, setState] = useState({ + loading: true, + fetched: false, + data: null, + }); + + useEffect(() => { + if (!parentCommonId) { + return; + } + + const unsubscribe = CommonService.subscribeToCommonsByDirectParentId( + parentCommonId, + (data) => { + setState((currentState) => { + if (!currentState.data || currentState.data.length === 0) { + return { + loading: false, + fetched: true, + data: data.map((item) => item.common), + }; + } + + const nextData = [...currentState.data]; + + data.forEach((item) => { + const itemIndex = nextData.findIndex( + ({ id }) => id === item.common.id, + ); + + if (itemIndex === -1) { + nextData.push(item.common); + return; + } + + if (item.statuses.isRemoved) { + nextData.splice(itemIndex, 1); + } else { + nextData[itemIndex] = item.common; + } + }); + + return { + loading: false, + fetched: true, + data: nextData, + }; + }); + }, + ); + + return () => { + unsubscribe(); + setState({ + loading: true, + fetched: false, + data: null, + }); + }; + }, [parentCommonId]); + + return state; +}; diff --git a/src/shared/hooks/useCases/useGovernanceListByCommonIds.ts b/src/shared/hooks/useCases/useGovernanceListByCommonIds.ts new file mode 100644 index 0000000000..5d8ddba5df --- /dev/null +++ b/src/shared/hooks/useCases/useGovernanceListByCommonIds.ts @@ -0,0 +1,66 @@ +import { useEffect, useState } from "react"; +import { GovernanceService } from "@/services"; +import { LoadingState } from "@/shared/interfaces"; +import { Governance } from "@/shared/models"; + +type State = LoadingState; + +type Return = State; + +export const useGovernanceListByCommonIds = (commonIds: string[]): Return => { + const [state, setState] = useState({ + loading: true, + fetched: false, + data: null, + }); + + useEffect(() => { + if (commonIds.length === 0) { + return; + } + + const unsubscribe = GovernanceService.subscribeToGovernanceListByCommonIds( + commonIds, + (data) => { + setState((currentState) => { + if (!currentState.data || currentState.data.length === 0) { + return { + loading: false, + fetched: true, + data: data.map((item) => item.governance), + }; + } + + const nextData = [...currentState.data]; + + data.forEach((item) => { + const itemIndex = nextData.findIndex( + ({ id }) => id === item.governance.id, + ); + + if (itemIndex === -1) { + nextData.push(item.governance); + return; + } + + if (item.statuses.isRemoved) { + nextData.splice(itemIndex, 1); + } else { + nextData[itemIndex] = item.governance; + } + }); + + return { + loading: false, + fetched: true, + data: nextData, + }; + }); + }, + ); + + return unsubscribe; + }, [commonIds]); + + return state; +}; diff --git a/src/shared/hooks/viewport/index.ts b/src/shared/hooks/viewport/index.ts index 642e70bf13..e16fad16b9 100644 --- a/src/shared/hooks/viewport/index.ts +++ b/src/shared/hooks/viewport/index.ts @@ -4,3 +4,4 @@ export * from "./useIsLaptopView"; export * from "./useIsPhoneOrientedView"; export * from "./useIsPhoneView"; export * from "./useIsTabletView"; +export * from "./useIsBigPhoneView"; diff --git a/src/shared/hooks/viewport/useIsBigPhoneView.ts b/src/shared/hooks/viewport/useIsBigPhoneView.ts new file mode 100644 index 0000000000..f97d7d9ee4 --- /dev/null +++ b/src/shared/hooks/viewport/useIsBigPhoneView.ts @@ -0,0 +1,5 @@ +import { ViewportBreakpoint } from "@/shared/constants"; +import useScreenSize from "../useScreenSize"; + +export const useIsBigPhoneView = (): boolean => + useScreenSize(`max-width: ${ViewportBreakpoint.BigPhone}px`); diff --git a/src/shared/icons/avatar3.icon.tsx b/src/shared/icons/avatar3.icon.tsx new file mode 100644 index 0000000000..6cd1eb5693 --- /dev/null +++ b/src/shared/icons/avatar3.icon.tsx @@ -0,0 +1,44 @@ +import React, { FC } from "react"; + +interface Avatar3IconProps { + className?: string; +} + +const Avatar3Icon: FC = ({ className }) => { + const color = "currentColor"; + + return ( + + + + + + ); +}; + +export default Avatar3Icon; diff --git a/src/shared/icons/billing.icon.tsx b/src/shared/icons/billing.icon.tsx new file mode 100644 index 0000000000..f2a36eae6b --- /dev/null +++ b/src/shared/icons/billing.icon.tsx @@ -0,0 +1,44 @@ +import React, { FC } from "react"; + +interface BillingIconProps { + className?: string; +} + +const BillingIcon: FC = ({ className }) => { + const color = "currentColor"; + + return ( + + + + + + ); +}; + +export default BillingIcon; diff --git a/src/shared/icons/copy.icon.tsx b/src/shared/icons/copy.icon.tsx index 4db0c41a84..6d07a03261 100644 --- a/src/shared/icons/copy.icon.tsx +++ b/src/shared/icons/copy.icon.tsx @@ -17,16 +17,16 @@ export default function CopyIcon({ className }: CopyIconProps): ReactElement { ); diff --git a/src/shared/icons/hide.icon.tsx b/src/shared/icons/hide.icon.tsx index 32d65942b9..d9078dd53a 100644 --- a/src/shared/icons/hide.icon.tsx +++ b/src/shared/icons/hide.icon.tsx @@ -17,23 +17,23 @@ export default function HideIcon({ className }: HideIconProps): ReactElement { ); diff --git a/src/shared/icons/index.ts b/src/shared/icons/index.ts index 230f33abbb..9f9a8fb7b3 100644 --- a/src/shared/icons/index.ts +++ b/src/shared/icons/index.ts @@ -1,6 +1,8 @@ export * from "./socials"; export { default as AttachIcon } from "./attach.icon"; export { default as Avatar2Icon } from "./avatar2.icon"; +export { default as Avatar3Icon } from "./avatar3.icon"; +export { default as BillingIcon } from "./billing.icon"; export { default as BlocksIcon } from "./blocks.icon"; export { default as BoldMarkIcon } from "./boldMark.icon"; export { default as BoldPlusIcon } from "./boldPlus.icon"; @@ -58,6 +60,7 @@ export { default as ShareIcon } from "./share.icon"; export { default as Share2Icon } from "./share2.icon"; export { default as Share3Icon } from "./share3.icon"; export { default as SendIcon } from "./send.icon"; +export { default as SettingsIcon } from "./settings.icon"; export { default as MinusIcon } from "./minus.icon"; export { default as FileIcon } from "./file.icon"; export { default as EmojiIcon } from "./emoji.icon"; diff --git a/src/shared/icons/settings.icon.tsx b/src/shared/icons/settings.icon.tsx new file mode 100644 index 0000000000..136299a67b --- /dev/null +++ b/src/shared/icons/settings.icon.tsx @@ -0,0 +1,37 @@ +import React, { FC } from "react"; + +interface SettingsIconProps { + className?: string; +} + +const SettingsIcon: FC = ({ className }) => { + const color = "currentColor"; + + return ( + + + + + ); +}; + +export default SettingsIcon; diff --git a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsSubscription.ts b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsSubscription.ts index 54797df4b4..0cb9d5fbca 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsSubscription.ts +++ b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsSubscription.ts @@ -28,6 +28,7 @@ const getProjectItemFromCommon = async ( image: common.image, name: common.name, directParent: common.directParent, + rootCommonId: common.rootCommonId, }; if (initialItem) { diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/ActiveBreadcrumbsItem/ActiveBreadcrumbsItem.tsx b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/ActiveBreadcrumbsItem/ActiveBreadcrumbsItem.tsx index bc5e9104d9..d2e3235c9b 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/ActiveBreadcrumbsItem/ActiveBreadcrumbsItem.tsx +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/ActiveBreadcrumbsItem/ActiveBreadcrumbsItem.tsx @@ -6,12 +6,13 @@ import { ProjectsStateItem } from "@/store/states"; import { BreadcrumbsMenu } from "../BreadcrumbsMenu"; import styles from "./ActiveBreadcrumbsItem.module.scss"; -interface ActiveBreadcrumbsItemProps { +export interface ActiveBreadcrumbsItemProps { name: string; image?: string; items?: ProjectsStateItem[]; commonIdToAddProject?: string | null; withMenu?: boolean; + isLoading?: boolean; } const ActiveBreadcrumbsItem: FC = (props) => { @@ -21,6 +22,7 @@ const ActiveBreadcrumbsItem: FC = (props) => { items = [], commonIdToAddProject, withMenu = true, + isLoading = false, } = props; const itemsButtonRef = useRef(null); const contextMenuRef = useRef(null); @@ -57,6 +59,7 @@ const ActiveBreadcrumbsItem: FC = (props) => { ref={contextMenuRef} items={items} commonIdToAddProject={commonIdToAddProject} + isLoading={isLoading} /> )} diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/ActiveBreadcrumbsItem/index.ts b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/ActiveBreadcrumbsItem/index.ts index 7df7cf9a08..4159264f72 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/ActiveBreadcrumbsItem/index.ts +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/ActiveBreadcrumbsItem/index.ts @@ -1 +1,2 @@ export { default as ActiveBreadcrumbsItem } from "./ActiveBreadcrumbsItem"; +export type { ActiveBreadcrumbsItemProps } from "./ActiveBreadcrumbsItem"; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.tsx b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.tsx index cf31772150..373ea8c6ac 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.tsx +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.tsx @@ -6,35 +6,35 @@ import { ProjectsStateItem } from "@/store/states"; import { BreadcrumbsMenu } from "../BreadcrumbsMenu"; import styles from "./BreadcrumbsItem.module.scss"; -interface BreadcrumbsItemProps { - activeItemId: string; +export interface BreadcrumbsItemProps { + activeItem: ProjectsStateItem; items: ProjectsStateItem[]; commonIdToAddProject?: string | null; onCommonCreate?: () => void; withMenu?: boolean; + isLoading?: boolean; + onClick?: () => void; } const BreadcrumbsItem: FC = (props) => { const { - activeItemId, + activeItem, items, commonIdToAddProject, onCommonCreate, withMenu = true, + isLoading = false, + onClick, } = props; const history = useHistory(); const { getCommonPagePath } = useRoutesContext(); const containerRef = useRef(null); const contextMenuRef = useRef(null); - const activeItem = items.find((item) => item.commonId === activeItemId); - - if (!activeItem) { - return null; - } const handleButtonClick = () => { if (!withMenu) { - history.push(getCommonPagePath(activeItemId)); + history.push(getCommonPagePath(activeItem.commonId)); + onClick?.(); return; } if (containerRef.current) { @@ -52,8 +52,9 @@ const BreadcrumbsItem: FC = (props) => { )} diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/index.ts b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/index.ts index 7382a210fb..4e733600f6 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/index.ts +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/index.ts @@ -1 +1,2 @@ export { default as BreadcrumbsItem } from "./BreadcrumbsItem"; +export type { BreadcrumbsItemProps } from "./BreadcrumbsItem"; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsMenu/BreadcrumbsMenu.tsx b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsMenu/BreadcrumbsMenu.tsx index 96cf4f263e..686527c30b 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsMenu/BreadcrumbsMenu.tsx +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsMenu/BreadcrumbsMenu.tsx @@ -8,12 +8,19 @@ interface BreadcrumbsMenuProps { items: ProjectsStateItem[]; activeItemId?: string; commonIdToAddProject?: string | null; + isLoading?: boolean; onCommonCreate?: () => void; } const BreadcrumbsMenu = forwardRef( (props, ref) => { - const { items, activeItemId, commonIdToAddProject, onCommonCreate } = props; + const { + items, + activeItemId, + commonIdToAddProject, + isLoading = false, + onCommonCreate, + } = props; const menuItems = useMenuItems({ items, activeItemId, @@ -21,7 +28,7 @@ const BreadcrumbsMenu = forwardRef( onCommonCreate, }); - if (menuItems.length === 0) { + if (menuItems.length === 0 && !isLoading) { return null; } @@ -30,6 +37,7 @@ const BreadcrumbsMenu = forwardRef( ref={ref} menuItems={menuItems} listClassName={styles.contextMenuList} + isLoading={isLoading} /> ); }, diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/FeedItemBreadcrumbs.tsx b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/FeedItemBreadcrumbs.tsx index cbc5852d49..dd86ccc171 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/FeedItemBreadcrumbs.tsx +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/FeedItemBreadcrumbs.tsx @@ -1,11 +1,15 @@ -import React, { FC, useMemo } from "react"; -import { MultipleSpacesLayoutFeedItemBreadcrumbs } from "@/store/states"; +import React, { FC } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + commonLayoutActions, + MultipleSpacesLayoutFeedItemBreadcrumbs, + ProjectsStateItem, + selectCommonLayoutCommonId, +} from "@/store/states"; import { useGoToCreateCommon } from "../../../../../../hooks"; -import { ActiveBreadcrumbsItem } from "../ActiveBreadcrumbsItem"; -import { BreadcrumbsItem } from "../BreadcrumbsItem"; import { LoadingBreadcrumbsItem } from "../LoadingBreadcrumbsItem"; import { Separator } from "../Separator"; -import { getBreadcrumbsData } from "./utils"; +import { ActiveFeedBreadcrumbsItem, FeedBreadcrumbsItem } from "./components"; import styles from "./FeedItemBreadcrumbs.module.scss"; interface FeedItemBreadcrumbsProps { @@ -15,40 +19,45 @@ interface FeedItemBreadcrumbsProps { const FeedItemBreadcrumbs: FC = (props) => { const { breadcrumbs, itemsWithMenus } = props; + const dispatch = useDispatch(); + const currentLayoutCommonId = useSelector(selectCommonLayoutCommonId); const goToCreateCommon = useGoToCreateCommon(); - const { data, projects, hasPermissionToAddProjectInActiveCommon } = useMemo( - () => getBreadcrumbsData(breadcrumbs.items, breadcrumbs.activeCommonId), - [breadcrumbs.items, breadcrumbs.activeCommonId], - ); + + const handleItemClick = (item: ProjectsStateItem) => { + if ( + currentLayoutCommonId && + item.rootCommonId && + item.rootCommonId !== currentLayoutCommonId + ) { + dispatch(commonLayoutActions.setCurrentCommonId(item.rootCommonId)); + dispatch(commonLayoutActions.clearProjects()); + } + }; return (
    {breadcrumbs.areItemsLoading && } {!breadcrumbs.areItemsLoading && - data.map((item, index) => ( - + breadcrumbs.items.map((item, index) => ( + {index > 0 && } - handleItemClick(item)} /> ))} {breadcrumbs.activeItem && ( <> - {(breadcrumbs.areItemsLoading || data.length > 0) && } - 0) && ( + + )} + diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/ActiveFeedBreadcrumbsItem/ActiveFeedBreadcrumbsItem.tsx b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/ActiveFeedBreadcrumbsItem/ActiveFeedBreadcrumbsItem.tsx new file mode 100644 index 0000000000..59095f3c9d --- /dev/null +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/ActiveFeedBreadcrumbsItem/ActiveFeedBreadcrumbsItem.tsx @@ -0,0 +1,61 @@ +import React, { FC, useMemo } from "react"; +import { useSelector } from "react-redux"; +import { + selectCommonLayoutCommonsState, + selectCommonLayoutProjectsState, +} from "@/store/states"; +import { + ActiveBreadcrumbsItem, + ActiveBreadcrumbsItemProps, +} from "../../../ActiveBreadcrumbsItem"; + +interface ActiveFeedBreadcrumbsItemProps + extends Pick { + activeItemId: string; +} + +const ActiveFeedBreadcrumbsItem: FC = ( + props, +) => { + const { activeItemId, ...restProps } = props; + const { commons, areCommonsFetched } = useSelector( + selectCommonLayoutCommonsState, + ); + const { projects, areProjectsFetched } = useSelector( + selectCommonLayoutProjectsState, + ); + const baseItems = useMemo( + () => + projects.filter( + (project) => project.directParent?.commonId === activeItemId, + ), + [projects, activeItemId], + ); + const areItemsLoading = !areCommonsFetched || !areProjectsFetched; + const hasPermissionToAddProject = useMemo( + () => + ( + commons.find((item) => item.commonId === activeItemId) || + projects.find((item) => item.commonId === activeItemId) + )?.hasPermissionToAddProject ?? false, + [commons, projects, activeItemId], + ); + const items = useMemo( + () => + [...baseItems].sort((prevItem) => + prevItem.commonId === activeItemId ? -1 : 1, + ), + [baseItems, activeItemId], + ); + + return ( + + ); +}; + +export default ActiveFeedBreadcrumbsItem; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/ActiveFeedBreadcrumbsItem/index.ts b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/ActiveFeedBreadcrumbsItem/index.ts new file mode 100644 index 0000000000..32c0207ed6 --- /dev/null +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/ActiveFeedBreadcrumbsItem/index.ts @@ -0,0 +1 @@ +export { default as ActiveFeedBreadcrumbsItem } from "./ActiveFeedBreadcrumbsItem"; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/FeedBreadcrumbsItem/FeedBreadcrumbsItem.tsx b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/FeedBreadcrumbsItem/FeedBreadcrumbsItem.tsx new file mode 100644 index 0000000000..50b81a54e4 --- /dev/null +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/FeedBreadcrumbsItem/FeedBreadcrumbsItem.tsx @@ -0,0 +1,74 @@ +import React, { FC, useMemo } from "react"; +import { useSelector } from "react-redux"; +import { + ProjectsStateItem, + selectCommonLayoutCommonsState, + selectCommonLayoutProjectsState, +} from "@/store/states"; +import { + BreadcrumbsItem, + BreadcrumbsItemProps, +} from "../../../BreadcrumbsItem"; + +type FeedBreadcrumbsItemProps = Pick< + BreadcrumbsItemProps, + "activeItem" | "onCommonCreate" | "withMenu" | "onClick" +>; + +const getItemsByParentId = ( + parentId: string, + data: ProjectsStateItem[], +): ProjectsStateItem[] => + data.filter((item) => item.directParent?.commonId === parentId); + +const FeedBreadcrumbsItem: FC = (props) => { + const { activeItem, ...restProps } = props; + const { commons, areCommonsFetched } = useSelector( + selectCommonLayoutCommonsState, + ); + const { projects, areProjectsFetched } = useSelector( + selectCommonLayoutProjectsState, + ); + const parentCommonId = activeItem.directParent?.commonId; + const baseItems = useMemo( + () => + parentCommonId ? getItemsByParentId(parentCommonId, projects) : commons, + [parentCommonId, projects, commons], + ); + const areItemsLoading = parentCommonId + ? !areProjectsFetched + : !areCommonsFetched; + const hasParentPermissionToAddProject = useMemo( + () => + (parentCommonId && + ( + commons.find((item) => item.commonId === parentCommonId) || + projects.find((item) => item.commonId === parentCommonId) + )?.hasPermissionToAddProject) ?? + false, + [commons, projects, parentCommonId], + ); + const items = useMemo( + () => + baseItems.length === 0 + ? [activeItem] + : [...baseItems].sort((prevItem) => + prevItem.commonId === activeItem.commonId ? -1 : 1, + ), + [baseItems, activeItem], + ); + + return ( + + ); +}; + +export default FeedBreadcrumbsItem; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/FeedBreadcrumbsItem/index.ts b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/FeedBreadcrumbsItem/index.ts new file mode 100644 index 0000000000..d3fae5bea5 --- /dev/null +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/FeedBreadcrumbsItem/index.ts @@ -0,0 +1 @@ +export { default as FeedBreadcrumbsItem } from "./FeedBreadcrumbsItem"; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/index.ts b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/index.ts new file mode 100644 index 0000000000..091fdeaee7 --- /dev/null +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/index.ts @@ -0,0 +1,2 @@ +export * from "./ActiveFeedBreadcrumbsItem"; +export * from "./FeedBreadcrumbsItem"; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/utils/getBreadcrumbsData.ts b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/utils/getBreadcrumbsData.ts deleted file mode 100644 index 3e74178a3f..0000000000 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/utils/getBreadcrumbsData.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { ProjectsStateItem } from "@/store/states"; - -interface Return { - data: { - activeCommonId: string; - items: ProjectsStateItem[]; - commonIdToAddProject?: string | null; - }[]; - projects: ProjectsStateItem[]; - hasPermissionToAddProjectInActiveCommon?: boolean; -} - -const getSortFn = ( - activeCommonId: string, -): ((item: ProjectsStateItem) => number) => { - return (item) => (item.commonId === activeCommonId ? -1 : 1); -}; - -export const getBreadcrumbsData = ( - items: ProjectsStateItem[], - activeCommonId: string, -): Return => { - const activeCommon = items.find((item) => item.commonId === activeCommonId); - - if (!activeCommon) { - return { - data: [], - projects: [], - hasPermissionToAddProjectInActiveCommon: false, - }; - } - - const mainLevelCommons = items - .filter((item) => !item.directParent) - .sort(getSortFn(activeCommonId)); - const activeCommonProjects = items.filter( - (item) => item.directParent?.commonId === activeCommonId, - ); - - if (!activeCommon.directParent) { - return { - data: [ - { - activeCommonId, - items: mainLevelCommons, - }, - ], - projects: activeCommonProjects, - hasPermissionToAddProjectInActiveCommon: - activeCommon.hasPermissionToAddProject, - }; - } - - let parentCommon = items.find( - (item) => item.commonId === activeCommon.directParent?.commonId, - ); - - if (!parentCommon) { - return { - data: [], - projects: [], - hasPermissionToAddProjectInActiveCommon: false, - }; - } - - const data: Return["data"] = []; - let activeCommonIdInParentCommonProjects = activeCommonId; - - while (parentCommon) { - const parentCommonProjects = items - .filter((item) => item.directParent?.commonId === parentCommon?.commonId) - .sort(getSortFn(activeCommonIdInParentCommonProjects)); - - data.unshift({ - activeCommonId: activeCommonIdInParentCommonProjects, - items: parentCommonProjects, - commonIdToAddProject: parentCommon.hasPermissionToAddProject - ? parentCommon.commonId - : null, - }); - - activeCommonIdInParentCommonProjects = parentCommon.commonId; - parentCommon = items.find( - (item) => item.commonId === parentCommon?.directParent?.commonId, - ); - } - - data.unshift({ - activeCommonId: activeCommonIdInParentCommonProjects, - items: mainLevelCommons.sort( - getSortFn(activeCommonIdInParentCommonProjects), - ), - }); - - return { - data, - projects: activeCommonProjects, - hasPermissionToAddProjectInActiveCommon: - activeCommon.hasPermissionToAddProject, - }; -}; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/utils/index.ts b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/utils/index.ts deleted file mode 100644 index 8c7b2a20dd..0000000000 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./getBreadcrumbsData"; diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.module.scss b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.module.scss index c37fa11e7d..dbeec0bb42 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.module.scss +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.module.scss @@ -22,7 +22,3 @@ .itemsWrapperPlacementBottom { top: 100%; } - -.logoutItem { - color: $c-error-300; -} diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx index 383dfece6f..78b6884971 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx @@ -4,6 +4,12 @@ import classNames from "classnames"; import { Menu } from "@headlessui/react"; import { logOut } from "@/pages/Auth/store/actions"; import { useRoutesContext } from "@/shared/contexts"; +import { + Avatar3Icon, + BillingIcon, + LogoutIcon, + SettingsIcon, +} from "@/shared/icons"; import { MenuItem } from "./components"; import { Item, ItemType } from "./types"; import styles from "./MenuItems.module.scss"; @@ -31,23 +37,26 @@ const MenuItems: FC = (props) => { { key: "my-profile", text: "My profile", + icon: , to: getProfilePagePath(), }, { key: "settings", text: "Settings", + icon: , to: getSettingsPagePath(), }, { key: "billing", text: "Billing", + icon: , to: getBillingPagePath(), }, { key: "log-out", - className: styles.logoutItem, type: ItemType.Button, text: "Log out", + icon: , onClick: () => { dispatch(logOut()); }, diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/components/MenuItem/MenuItem.module.scss b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/components/MenuItem/MenuItem.module.scss index 664877803e..17721a6e55 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/components/MenuItem/MenuItem.module.scss +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/components/MenuItem/MenuItem.module.scss @@ -5,13 +5,16 @@ --item-border: 0.0625rem solid #{$c-neutrals-100}; padding: 1.125rem 1.5rem; + display: flex; + align-items: center; text-decoration: none; color: inherit; background-color: var(--item-bg-color); border: var(--item-border); border-bottom: 0; font-family: PoppinsSans, sans-serif; - font-size: $small; + font-size: 1rem; + font-weight: 500; text-align: left; cursor: pointer; box-sizing: border-box; @@ -30,3 +33,10 @@ .itemActive { --item-bg-color: var(--hover-fill); } + +.icon { + width: 1.5rem; + height: 1.5rem; + margin-right: 1rem; + color: inherit; +} diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/components/MenuItem/MenuItem.tsx b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/components/MenuItem/MenuItem.tsx index d569f8e4bf..16cadcc424 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/components/MenuItem/MenuItem.tsx +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/components/MenuItem/MenuItem.tsx @@ -1,7 +1,10 @@ import React, { + cloneElement, forwardRef, ForwardRefRenderFunction, + isValidElement, MouseEventHandler, + ReactNode, RefObject, } from "react"; import { NavLink } from "react-router-dom"; @@ -20,10 +23,24 @@ const MenuItem: ForwardRefRenderFunction = ( ref, ) => { const { item, active, ...restProps } = props; - const content = item.text; + let iconEl: ReactNode | null = null; + + if (isValidElement(item.icon)) { + iconEl = cloneElement(item.icon, { + ...item.icon.props, + className: classNames(styles.icon, item.icon.props.className), + }); + } + const className = classNames(styles.item, item.className, { [styles.itemActive]: active, }); + const content = ( + <> + {iconEl} + {item.text} + + ); switch (item.type) { case ItemType.Button: diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/types.ts b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/types.ts index 69a3858abc..ed180e01bb 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/types.ts +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/types.ts @@ -10,6 +10,7 @@ interface GeneralItem { key: string; className?: string; text: ReactNode; + icon?: ReactNode; } interface LinkItem extends GeneralItem { diff --git a/src/shared/models/Common.tsx b/src/shared/models/Common.tsx index c03ad4ca19..31989caa75 100644 --- a/src/shared/models/Common.tsx +++ b/src/shared/models/Common.tsx @@ -4,6 +4,7 @@ import { Discussion } from "./Discussion"; import { DiscussionMessage } from "./DiscussionMessage"; import { PaymentAmount } from "./Payment"; import { Proposal } from "./Proposals"; +import { Timestamp } from "./Timestamp"; import { User } from "./User"; import { AllowedActions, @@ -119,6 +120,8 @@ export interface Common extends BaseEntity { hasPublicItems: boolean; rootCommonId?: string; + + lastActivity?: Timestamp; } export interface Project extends Common { diff --git a/src/shared/models/User.tsx b/src/shared/models/User.tsx index 060933f486..fa6afdad06 100644 --- a/src/shared/models/User.tsx +++ b/src/shared/models/User.tsx @@ -1,4 +1,5 @@ -import { Proposal } from "."; +import { BaseEntity } from "./BaseEntity"; +import { Proposal } from "./Proposals"; export enum UserRole { Trustee = "trustee", @@ -17,7 +18,7 @@ export enum UserEmailNotificationPreference { AllInbox = "allInbox", } -export interface User { +export interface User extends Omit { displayName?: string; country: string; firstName: string; @@ -27,8 +28,6 @@ export interface User { photo?: string; photoURL?: string; intro?: string; - createdAt?: Date; - updatedAt?: Date; proposals?: Proposal[]; uid: string; roles?: UserRole[]; diff --git a/src/shared/ui-kit/Button/Button.module.scss b/src/shared/ui-kit/Button/Button.module.scss index e77542eb27..cfc8e566a7 100644 --- a/src/shared/ui-kit/Button/Button.module.scss +++ b/src/shared/ui-kit/Button/Button.module.scss @@ -161,6 +161,25 @@ } } +.buttonLightPinkVariant { + --btn-color: #{$c-pink-mention}; + --btn-bg-color: #{$c-pink-active-feed-cards-light}; + --btn-border-color: none; + --btn-border: 0; + + &:hover { + --btn-bg-color: #{$c-pink-hover-feed-cards}; + } + + &:active { + --btn-bg-color: #{$c-pink-active-btn}; + } + + &.buttonDisabled { + --btn-bg-color: #{$c-gray-10}; + } +} + .buttonOutlineBlueVariant { --btn-color: #{$c-primary-400}; --btn-bg-color: #{$c-primary-100}; diff --git a/src/shared/ui-kit/Button/Button.tsx b/src/shared/ui-kit/Button/Button.tsx index 554a4a0d61..846e7132c7 100644 --- a/src/shared/ui-kit/Button/Button.tsx +++ b/src/shared/ui-kit/Button/Button.tsx @@ -17,6 +17,7 @@ export enum ButtonVariant { PrimaryPurple = "primary-purple", PrimaryPink = "primary-pink", LightPurple = "light-purple", + LightPink = "light-pink", OutlineBlue = "outline-blue", OutlinePink = "outline-pink", OutlineDarkPink = "outline-dark-pink", @@ -60,6 +61,7 @@ const Button: ForwardRefRenderFunction = ( variant === ButtonVariant.PrimaryPurple, [styles.buttonPrimaryPinkVariant]: variant === ButtonVariant.PrimaryPink, [styles.buttonLightPurpleVariant]: variant === ButtonVariant.LightPurple, + [styles.buttonLightPinkVariant]: variant === ButtonVariant.LightPink, [styles.buttonOutlineBlueVariant]: variant === ButtonVariant.OutlineBlue, [styles.buttonOutlinePinkVariant]: variant === ButtonVariant.OutlinePink, [styles.buttonOutlineDarkPinkVariant]: diff --git a/src/shared/ui-kit/ContextMenu/ContextMenu.module.scss b/src/shared/ui-kit/ContextMenu/ContextMenu.module.scss index f58d193abe..ea1ebedf7b 100644 --- a/src/shared/ui-kit/ContextMenu/ContextMenu.module.scss +++ b/src/shared/ui-kit/ContextMenu/ContextMenu.module.scss @@ -20,3 +20,7 @@ border-radius: var(--items-br); box-shadow: 0 0.25rem 0.9375rem #{$c-sidebar-user-menu-shadow}; } + +.loader { + margin: 0 auto; +} diff --git a/src/shared/ui-kit/ContextMenu/ContextMenu.tsx b/src/shared/ui-kit/ContextMenu/ContextMenu.tsx index d8774250bb..3b89afa047 100644 --- a/src/shared/ui-kit/ContextMenu/ContextMenu.tsx +++ b/src/shared/ui-kit/ContextMenu/ContextMenu.tsx @@ -23,6 +23,7 @@ import { } from "@floating-ui/react"; import { useLockedBody } from "@/shared/hooks"; import { ContextMenuItem as Item } from "@/shared/interfaces"; +import { Loader } from "../Loader"; import { ContextMenuItem } from "./components"; import styles from "./ContextMenu.module.scss"; @@ -34,11 +35,12 @@ interface ContextMenuProps { menuItems: Item[]; onOpenChange?: (open: boolean) => void; listClassName?: string; + isLoading?: boolean; } export const ContextMenu = forwardRef( (props, forwardedRef) => { - const { menuItems, onOpenChange, listClassName } = props; + const { menuItems, onOpenChange, listClassName, isLoading = false } = props; const [activeIndex, setActiveIndex] = useState(null); const [isOpen, setIsOpen] = useState(false); const listItemsRef = useRef<(HTMLElement | null)[]>([]); @@ -156,6 +158,7 @@ export const ContextMenu = forwardRef( })} /> ))} + {isLoading && }
diff --git a/src/shared/ui-kit/ImageGallery/components/ImageGalleryMobileModal/ImageGalleryMobileModal.tsx b/src/shared/ui-kit/ImageGallery/components/ImageGalleryMobileModal/ImageGalleryMobileModal.tsx index 741688fd47..28d97d352e 100644 --- a/src/shared/ui-kit/ImageGallery/components/ImageGalleryMobileModal/ImageGalleryMobileModal.tsx +++ b/src/shared/ui-kit/ImageGallery/components/ImageGalleryMobileModal/ImageGalleryMobileModal.tsx @@ -73,6 +73,7 @@ const ImageGalleryMobileModal: FC = (props) => { )} {images.map((imageURL, index) => ( = (props) => { loop={true} pagination initialSlide={initialSlide} + allowTouchMove={false} > {videoSrc && ( @@ -61,6 +62,7 @@ const ImageGalleryModal: FC = (props) => { {images.map((imageURL, index) => ( {`Common { + if (!nextCommon.lastActivity) { + return -1; + } + if (!prevCommon.lastActivity) { + return 1; + } + + return nextCommon.lastActivity.seconds - prevCommon.lastActivity.seconds; +}; diff --git a/src/shared/utils/generateStaticShareLink.ts b/src/shared/utils/generateStaticShareLink.ts index 51a2df05fc..c68737288f 100644 --- a/src/shared/utils/generateStaticShareLink.ts +++ b/src/shared/utils/generateStaticShareLink.ts @@ -1,5 +1,6 @@ -import { Environment, REACT_APP_ENV } from "../constants"; +import { Environment, REACT_APP_ENV, ROUTE_PATHS } from "../constants"; import { Common, Discussion, DiscussionMessage, Proposal } from "../models"; +import { matchRoute } from "./matchRoute"; const staticLinkPrefix = () => { if (window.location.hostname === "localhost") { @@ -15,6 +16,24 @@ const staticLinkPrefix = () => { } }; +const getStaticLinkBasePath = (): string => { + const pathname: string = window.location.pathname; + + if (matchRoute(pathname, ROUTE_PATHS.COMMON)) { + return "commons"; + } + + if (matchRoute(pathname, ROUTE_PATHS.V04_COMMON)) { + return "commons-v04"; + } + + if (matchRoute(pathname, ROUTE_PATHS.V03_COMMON)) { + return "commons-v03"; + } + + return "commons"; +}; + export const enum StaticLinkType { DiscussionMessage, ProposalComment, @@ -28,22 +47,24 @@ export const generateStaticShareLink = ( elem: Common | Proposal | Discussion | DiscussionMessage, feedItemId?: string, ): string => { + const basePath: string = getStaticLinkBasePath(); + if (!feedItemId && linkType === StaticLinkType.Common) { elem = elem as Common; - return `${staticLinkPrefix()}/commons/${elem.id}`; + return `${staticLinkPrefix()}/${basePath}/${elem.id}`; } switch (linkType) { case StaticLinkType.Proposal: case StaticLinkType.Discussion: elem = elem as Discussion; - return `${staticLinkPrefix()}/commons/${ + return `${staticLinkPrefix()}/${basePath}/${ elem.commonId }?item=${feedItemId}`; case StaticLinkType.DiscussionMessage: case StaticLinkType.ProposalComment: elem = elem as DiscussionMessage; - return `${staticLinkPrefix()}/commons/${ + return `${staticLinkPrefix()}/${basePath}/${ elem.commonId }?item=${feedItemId}&message=${elem.id}`; default: diff --git a/src/shared/utils/index.tsx b/src/shared/utils/index.tsx index dbf698daac..74e159b1d9 100755 --- a/src/shared/utils/index.tsx +++ b/src/shared/utils/index.tsx @@ -15,6 +15,7 @@ export * from "./parseLinksForSubmission"; export * from "./proposals"; export * from "./queryParams"; export { default as request } from "./request"; +export * from "./compareCommonsByLastActivity"; export * from "./convertDatesToFirestoreTimestamps"; export * from "./convertLinkToUploadFile"; export * from "./timeAgo"; diff --git a/src/store/states/common/reducer.ts b/src/store/states/common/reducer.ts index 27c63fa277..150dd9154e 100644 --- a/src/store/states/common/reducer.ts +++ b/src/store/states/common/reducer.ts @@ -43,6 +43,14 @@ const initialState: CommonState = { recentAssignedCircle: null, }; +const sortFeedItems = (data: FeedItemFollowLayoutItem[]): void => { + data.sort( + (prevItem, nextItem) => + nextItem.feedItem.updatedAt.toMillis() - + prevItem.feedItem.updatedAt.toMillis(), + ); +}; + const updateFeedItemInList = ( state: WritableDraft, payload: { @@ -76,10 +84,17 @@ const updateFeedItemInList = ( ...updatedItem, }, }; + sortFeedItems(nextData); } + const firstDocTimestamp = nextData[0]?.feedItem.updatedAt || null; + const lastDocTimestamp = + nextData[nextData.length - 1]?.feedItem.updatedAt || null; + state.feedItems = { ...state.feedItems, + firstDocTimestamp, + lastDocTimestamp, data: nextData, }; }; @@ -95,8 +110,6 @@ const addNewFeedItems = ( }[], shouldSortNewItems = false, ) => { - let firstDocTimestamp = state.feedItems.firstDocTimestamp; - const data = payload.reduceRight((acc, { commonFeedItem, statuses }) => { const nextData = [...acc]; const itemIndex = nextData.findIndex( @@ -117,7 +130,6 @@ const addNewFeedItems = ( itemId: commonFeedItem.id, feedItem: commonFeedItem, }; - firstDocTimestamp = commonFeedItem.updatedAt; if (itemIndex >= 0) { nextData[itemIndex] = finalItem; @@ -139,11 +151,16 @@ const addNewFeedItems = ( return nextData; }, state.feedItems.data || []); + sortFeedItems(data); + + const firstDocTimestamp = data[0]?.feedItem.updatedAt || null; + const lastDocTimestamp = data[data.length - 1]?.feedItem.updatedAt || null; state.feedItems = { ...state.feedItems, data, firstDocTimestamp, + lastDocTimestamp, }; }; diff --git a/src/store/states/commonLayout/saga/getCommons.ts b/src/store/states/commonLayout/saga/getCommons.ts index 08d44bd956..72ed1e171b 100644 --- a/src/store/states/commonLayout/saga/getCommons.ts +++ b/src/store/states/commonLayout/saga/getCommons.ts @@ -3,7 +3,7 @@ import { selectUser } from "@/pages/Auth/store/selectors"; import { CommonService, GovernanceService, ProjectService } from "@/services"; import { Awaited } from "@/shared/interfaces"; import { User } from "@/shared/models"; -import { isError } from "@/shared/utils"; +import { compareCommonsByLastActivity, isError } from "@/shared/utils"; import { ProjectsStateItem } from "../../projects"; import * as actions from "../actions"; import { getPermissionsDataByAllUserCommonMemberInfo } from "./utils"; @@ -80,17 +80,20 @@ export function* getCommons( commonId, userId, )) as Awaited>; - const projectsData: ProjectsStateItem[] = data.map( - ({ common, hasMembership, hasPermissionToAddProject }) => ({ + const projectsData: ProjectsStateItem[] = [...data] + .sort((prevItem, nextItem) => + compareCommonsByLastActivity(prevItem.common, nextItem.common), + ) + .map(({ common, hasMembership, hasPermissionToAddProject }) => ({ commonId: common.id, image: common.image, name: common.name, directParent: common.directParent, + rootCommonId: common.rootCommonId, hasMembership, hasPermissionToAddProject, notificationsAmount: 0, - }), - ); + })); yield put( actions.getCommons.success({ diff --git a/src/store/states/commonLayout/saga/getProjects.ts b/src/store/states/commonLayout/saga/getProjects.ts index 978dfe64bb..cada89f37e 100644 --- a/src/store/states/commonLayout/saga/getProjects.ts +++ b/src/store/states/commonLayout/saga/getProjects.ts @@ -54,6 +54,7 @@ export function* getProjects( image: common.image, name: common.name, directParent: common.directParent, + rootCommonId: common.rootCommonId, hasMembership, hasPermissionToAddProject, notificationsAmount: 0, diff --git a/src/store/states/inbox/reducer.ts b/src/store/states/inbox/reducer.ts index af17b96832..ef82ac6c60 100644 --- a/src/store/states/inbox/reducer.ts +++ b/src/store/states/inbox/reducer.ts @@ -7,7 +7,7 @@ import { checkIsFeedItemFollowLayoutItem, FeedLayoutItemWithFollowData, } from "@/shared/interfaces"; -import { ChatChannel, CommonFeed } from "@/shared/models"; +import { ChatChannel, CommonFeed, Timestamp } from "@/shared/models"; import * as actions from "./actions"; import { InboxItems, InboxState } from "./types"; import { getFeedLayoutItemDateForSorting } from "./utils"; @@ -30,6 +30,26 @@ const initialState: InboxState = { nextChatChannelItemId: null, }; +const sortInboxItems = (data: FeedLayoutItemWithFollowData[]): void => { + data.sort( + (prevItem, nextItem) => + getFeedLayoutItemDateForSorting(nextItem).toMillis() - + getFeedLayoutItemDateForSorting(prevItem).toMillis(), + ); +}; + +const getDocTimestamps = ( + data: FeedLayoutItemWithFollowData[], +): { + firstDocTimestamp: Timestamp | null; + lastDocTimestamp: Timestamp | null; +} => ({ + firstDocTimestamp: data[0] ? getFeedLayoutItemDateForSorting(data[0]) : null, + lastDocTimestamp: data[data.length - 1] + ? getFeedLayoutItemDateForSorting(data[data.length - 1]) + : null, +}); + const updateInboxItemInList = ( state: WritableDraft, payload: { @@ -64,10 +84,14 @@ const updateInboxItemInList = ( ...nextData[itemIndex], ...updatedItem, }; + sortInboxItems(nextData); } + const { firstDocTimestamp, lastDocTimestamp } = getDocTimestamps(nextData); state.items = { ...state.items, + firstDocTimestamp, + lastDocTimestamp, data: nextData, }; }; @@ -162,9 +186,13 @@ const updateFeedItemInInboxItem = ( feedItem: { ...newFeedItem }, }, }; + sortInboxItems(nextData); + const { firstDocTimestamp, lastDocTimestamp } = getDocTimestamps(nextData); state.items = { ...state.items, + firstDocTimestamp, + lastDocTimestamp, data: nextData, }; }; @@ -285,9 +313,13 @@ const updateChatChannelItemInInboxItem = ( lastMessage: updatedChatChannelItem.lastMessage || undefined, }, }; + sortInboxItems(nextData); + const { firstDocTimestamp, lastDocTimestamp } = getDocTimestamps(nextData); state.items = { ...state.items, + firstDocTimestamp, + lastDocTimestamp, data: nextData, }; }; @@ -421,8 +453,6 @@ export const reducer = createReducer(initialState) (chatChannelItem) => chatChannelItem.itemId === item.item.itemId, ), ); - let firstDocTimestamp = nextState.items.firstDocTimestamp; - const data = payload.reduceRight((acc, { item, statuses }) => { const nextData = [...acc]; const itemIndex = nextData.findIndex( @@ -443,7 +473,6 @@ export const reducer = createReducer(initialState) } const finalItem: FeedLayoutItemWithFollowData = { ...item }; - firstDocTimestamp = getFeedLayoutItemDateForSorting(item); if (itemIndex < 0) { return [finalItem, ...nextData]; @@ -453,11 +482,14 @@ export const reducer = createReducer(initialState) return nextData; }, nextState.items.data || []); + sortInboxItems(data); + const { firstDocTimestamp, lastDocTimestamp } = getDocTimestamps(data); nextState.items = { ...nextState.items, data, firstDocTimestamp, + lastDocTimestamp, }; }), ) diff --git a/src/store/states/multipleSpacesLayout/reducer.ts b/src/store/states/multipleSpacesLayout/reducer.ts index 347ae0914f..25fd6537ec 100644 --- a/src/store/states/multipleSpacesLayout/reducer.ts +++ b/src/store/states/multipleSpacesLayout/reducer.ts @@ -28,9 +28,12 @@ const updateProjectInBreadcrumbs = ( ); if (itemIndex > -1) { + const item = state.breadcrumbs.items[itemIndex]; + state.breadcrumbs.items[itemIndex] = { - ...state.breadcrumbs.items[itemIndex], - ...payload, + ...item, + name: payload.name ?? item.name, + image: payload.image ?? item.image, }; } }; @@ -56,20 +59,8 @@ export const reducer = createReducer( ) .handleAction(actions.addOrUpdateProjectInBreadcrumbs, (state, { payload }) => produce(state, (nextState) => { - if (nextState.breadcrumbs?.type !== InboxItemType.FeedItemFollow) { - return; - } - - const isItemFound = nextState.breadcrumbs.items.some( - (item) => item.commonId === payload.commonId, - ); - - if (isItemFound) { - updateProjectInBreadcrumbs(nextState, payload); - } else { - nextState.breadcrumbs.items = - nextState.breadcrumbs.items.concat(payload); - } + // Intentionally removed adding logic, because now we do not need any new items in the list + updateProjectInBreadcrumbs(nextState, payload); }), ) .handleAction(actions.updateProjectInBreadcrumbs, (state, { payload }) => diff --git a/src/store/states/multipleSpacesLayout/saga/configureBreadcrumbsData.ts b/src/store/states/multipleSpacesLayout/saga/configureBreadcrumbsData.ts index 45025fea08..9a189226e1 100644 --- a/src/store/states/multipleSpacesLayout/saga/configureBreadcrumbsData.ts +++ b/src/store/states/multipleSpacesLayout/saga/configureBreadcrumbsData.ts @@ -1,8 +1,43 @@ import { put, select } from "redux-saga/effects"; import { InboxItemType } from "@/shared/constants"; +import { + selectCommonLayoutCommonsState, + selectCommonLayoutProjectsState, +} from "@/store/states"; import * as actions from "../actions"; import { selectMultipleSpacesLayoutBreadcrumbs } from "../selectors"; -import { MultipleSpacesLayoutState } from "../types"; +import { MultipleSpacesLayoutState, ProjectsStateItem } from "../types"; + +const getItemsByExistingData = ( + activeCommonId: string, + existingItems: ProjectsStateItem[], +): ProjectsStateItem[] | null => { + const currentItem = existingItems.find( + (item) => item.commonId === activeCommonId, + ); + + if (!currentItem) { + return null; + } + + const items: ProjectsStateItem[] = [currentItem]; + let parentCommonId = currentItem.directParent?.commonId; + + while (parentCommonId) { + const parentItem = existingItems.find( + (item) => item.commonId === parentCommonId, + ); + + if (!parentItem) { + return null; + } + + items.unshift(parentItem); + parentCommonId = parentItem.directParent?.commonId; + } + + return items; +}; export function* configureBreadcrumbsData( action: ReturnType, @@ -41,27 +76,44 @@ export function* configureBreadcrumbsData( return; } + const { commons, areCommonsFetched } = (yield select( + selectCommonLayoutCommonsState, + )) as { commons: ProjectsStateItem[]; areCommonsFetched: boolean }; + const { projects } = (yield select(selectCommonLayoutProjectsState)) as { + projects: ProjectsStateItem[]; + }; + const items = areCommonsFetched + ? getItemsByExistingData(payload.activeCommonId, [...commons, ...projects]) + : null; + yield put( actions.setBreadcrumbsData({ - ...(currentBreadcrumbs || { - items: [], - areItemsLoading: true, - areItemsFetched: false, - }), + ...(items + ? { + items, + areItemsLoading: false, + areItemsFetched: true, + } + : currentBreadcrumbs || { + items: [], + areItemsLoading: true, + areItemsFetched: false, + }), type: InboxItemType.FeedItemFollow, activeItem: payload.activeItem ? { ...payload.activeItem } : null, activeCommonId: payload.activeCommonId, }), ); - if ( - currentBreadcrumbs?.activeCommonId !== payload.activeCommonId || - !currentBreadcrumbs.items.some( - (item) => item.commonId === payload.activeCommonId, - ) - ) { + if (!items) { yield put( actions.fetchBreadcrumbsItemsByCommonId.request(payload.activeCommonId), ); + } else { + yield put( + actions.fetchBreadcrumbsItemsByCommonId.cancel( + "Stop current breadcrumbs items fetch", + ), + ); } } diff --git a/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts b/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts index b505658868..c7e27de108 100644 --- a/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts +++ b/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts @@ -1,73 +1,32 @@ import { call, put, select } from "redux-saga/effects"; -import { selectUser } from "@/pages/Auth/store/selectors"; -import { CommonService, GovernanceService, ProjectService } from "@/services"; +import { CommonService } from "@/services"; import { InboxItemType } from "@/shared/constants"; import { Awaited } from "@/shared/interfaces"; -import { Common, User } from "@/shared/models"; -import { getPermissionsDataByAllUserCommonMemberInfo } from "../../commonLayout/saga/utils"; import * as actions from "../actions"; import { selectMultipleSpacesLayoutBreadcrumbs } from "../selectors"; import { MultipleSpacesLayoutState, ProjectsStateItem } from "../types"; const fetchProjectsInfoByActiveCommonId = async ( commonId: string, - userId?: string, -): Promise> => { +): Promise => { const activeCommon = await CommonService.getCommonById(commonId); if (!activeCommon) { return []; } - const finalCommons: Common[] = []; - let commonForSiblings: Common | null = activeCommon; - let lastParentCommon: Common | null = !commonForSiblings.directParent - ? commonForSiblings - : null; - - while (commonForSiblings?.directParent?.commonId) { - const commonIdForProjects = commonForSiblings.directParent.commonId; - const commonProjects = await CommonService.getCommonsByDirectParentIds([ - commonIdForProjects, - ]); - commonForSiblings = await CommonService.getCommonById(commonIdForProjects); - finalCommons.push(...commonProjects); - - if (!commonForSiblings?.directParent) { - lastParentCommon = commonForSiblings; - } - } - - const allUserCommonMemberInfo = userId - ? await CommonService.getAllUserCommonMemberInfo(userId) - : []; - const userCommonIds = allUserCommonMemberInfo.map((item) => item.commonId); - const [userCommons, activeCommonProjects, governanceList] = await Promise.all( - [ - CommonService.getParentCommonsByIds(userCommonIds), - CommonService.getCommonsByDirectParentIds([commonId]), - GovernanceService.getGovernanceListByCommonIds(userCommonIds), - ], - ); - const permissionsData = getPermissionsDataByAllUserCommonMemberInfo( - allUserCommonMemberInfo, - governanceList, + const commons = await CommonService.getAllParentCommonsForCommon( + activeCommon, ); - if ( - lastParentCommon && - !userCommons.some((common) => common.id === lastParentCommon?.id) - ) { - userCommons.push(lastParentCommon); - } - - finalCommons.push(...userCommons, ...activeCommonProjects); - - return ProjectService.parseDataToProjectsInfo( - finalCommons, - userCommonIds, - permissionsData, - ); + return [...commons, activeCommon].map((common) => ({ + commonId: common.id, + image: common.image, + name: common.name, + directParent: common.directParent, + rootCommonId: common.rootCommonId, + hasMembership: true, + })); }; export function* fetchBreadcrumbsItemsByCommonId( @@ -87,6 +46,14 @@ export function* fetchBreadcrumbsItemsByCommonId( return; } + const commonIndex = currentBreadcrumbs.items.findIndex( + (item) => item.commonId === commonId, + ); + + if (commonIndex > -1) { + return; + } + yield put( actions.setBreadcrumbsData({ ...currentBreadcrumbs, @@ -96,23 +63,10 @@ export function* fetchBreadcrumbsItemsByCommonId( ); try { - const user = (yield select(selectUser())) as User | null; - const projectsInfo = (yield call( + const projectsStateItems = (yield call( fetchProjectsInfoByActiveCommonId, commonId, - user?.uid, )) as Awaited>; - const projectsData: ProjectsStateItem[] = projectsInfo.map( - ({ common, hasMembership, hasPermissionToAddProject }) => ({ - commonId: common.id, - image: common.image, - name: common.name, - directParent: common.directParent, - hasMembership, - hasPermissionToAddProject, - }), - ); - const currentBreadcrumbs = (yield select( selectMultipleSpacesLayoutBreadcrumbs, )) as MultipleSpacesLayoutState["breadcrumbs"]; @@ -121,7 +75,7 @@ export function* fetchBreadcrumbsItemsByCommonId( yield put( actions.setBreadcrumbsData({ ...currentBreadcrumbs, - items: projectsData, + items: projectsStateItems, areItemsLoading: false, areItemsFetched: true, }), diff --git a/src/store/states/projects/saga.ts b/src/store/states/projects/saga.ts index f65febe742..19b11aa1e8 100644 --- a/src/store/states/projects/saga.ts +++ b/src/store/states/projects/saga.ts @@ -25,6 +25,7 @@ function* getProjects(action: ReturnType) { image: common.image, name: common.name, directParent: common.directParent, + rootCommonId: common.rootCommonId, hasMembership, notificationsAmount: 0, }), diff --git a/src/store/states/projects/types.ts b/src/store/states/projects/types.ts index 02497d91dd..2926b9ac13 100644 --- a/src/store/states/projects/types.ts +++ b/src/store/states/projects/types.ts @@ -5,6 +5,7 @@ export interface ProjectsStateItem { image: string; name: string; directParent: Common["directParent"]; + rootCommonId?: string; hasMembership?: boolean; hasPermissionToAddProject?: boolean; notificationsAmount?: number; diff --git a/src/styles/typography.scss b/src/styles/typography.scss index f9ea03f775..74a1321b9a 100644 --- a/src/styles/typography.scss +++ b/src/styles/typography.scss @@ -3,7 +3,7 @@ @mixin h5 { font-family: PoppinsSans, sans-serif; font-weight: normal; - font-size: $moderate-small-2; + font-size: 1.125rem; line-height: 1.25rem; color: $c-gray-90; text-align: center; @@ -12,7 +12,7 @@ @mixin h6 { font-family: PoppinsSans, sans-serif; font-weight: 500; - font-size: $moderate-xsmall; + font-size: 1rem; line-height: 1.5rem; color: $c-gray-100; text-align: center; @@ -21,7 +21,7 @@ @mixin body-sm-regular { font-family: PoppinsSans, sans-serif; font-weight: normal; - font-size: $mobile-title; + font-size: 0.875rem; line-height: 1.25rem; color: $c-gray-50; text-align: center; diff --git a/yarn.lock b/yarn.lock index 11e925ebf6..e4e8ed095d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17005,6 +17005,11 @@ react-virtualized@^9.22.3: prop-types "^15.7.2" react-lifecycles-compat "^3.0.4" +react-zoom-pan-pinch@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.2.0.tgz#6ce7d014a8dc4aa62ce83ca57f85e76cf2e934b8" + integrity sha512-7MS0wYWoXjr6PrmpgHOVpVyNQr9gj7LEr4xIvq6lBy62nuNwjdI1r+XxahQ0SDHhWrLuSF11e2PTL/YLengYyg== + react@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"