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/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/App/handlers/WebViewLoginHandler/WebViewLoginHandler.tsx b/src/pages/App/handlers/WebViewLoginHandler/WebViewLoginHandler.tsx index d3f1aac2e3..3a293f7004 100644 --- a/src/pages/App/handlers/WebViewLoginHandler/WebViewLoginHandler.tsx +++ b/src/pages/App/handlers/WebViewLoginHandler/WebViewLoginHandler.tsx @@ -4,7 +4,7 @@ import { webviewLogin } from "@/pages/Auth/store/actions"; import { history } from "@/shared/appConfig"; import { WebviewActions } from "@/shared/constants"; import { FirebaseCredentials } from "@/shared/interfaces/FirebaseCredentials"; -import { getInboxPagePath_v04 } from "@/shared/utils"; +import { getInboxPagePath } from "@/shared/utils"; import { parseJson } from "@/shared/utils/json"; const WebViewLoginHandler: FC = () => { @@ -25,7 +25,7 @@ const WebViewLoginHandler: FC = () => { window.ReactNativeWebView.postMessage( WebviewActions.loginSuccess, ); - history.push(getInboxPagePath_v04()); + history.push(getInboxPagePath()); } else { window.ReactNativeWebView.postMessage(WebviewActions.loginError); } 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..da7141e309 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/UserDetails/UserDetails.module.scss @@ -0,0 +1,203 @@ +@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; + } +} + +.editProfileButton { + --btn-h: 1.5rem; + --btn-pl: 0.25rem; + --btn-pr: 0.25rem; + + margin-top: 0.25rem; + font-weight: 500; +} + +.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..57dc568219 --- /dev/null +++ b/src/pages/MyAccount/components/Profile/components/UserDetails/UserDetails.tsx @@ -0,0 +1,379 @@ +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 && ( + <> + + {isMobileView && ( + + )} + + )} +
+ {!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/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..6b086e0edf 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -642,7 +642,7 @@ export default function ChatComponent({ }} value={message} onChange={setMessage} - placeholder="What do you think?" + placeholder="Message" onKeyDown={onEnterKeyDown} users={users} shouldReinitializeEditor={shouldReinitializeEditor} diff --git a/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx b/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx index f8775030d4..17c3d6c60a 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, diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index bafa9ab5eb..96b79f19fb 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -15,7 +15,6 @@ import { DiscussionFeedCard } from "../DiscussionFeedCard"; import { ProposalFeedCard } from "../ProposalFeedCard"; import { ProjectFeedItem } from "./components"; import { useFeedItemContext } from "./context"; -import { useFeedItemCounters } from "./hooks"; import { FeedItemRef } from "./types"; interface FeedItemProps { @@ -68,8 +67,6 @@ const FeedItem = forwardRef((props, ref) => { const { onFeedItemUpdate, getLastMessage, getNonAllowedItems, onUserSelect } = useFeedItemContext(); useFeedItemSubscription(item.id, commonId, onFeedItemUpdate); - const { projectUnreadStreamsCount, projectUnreadMessages } = - useFeedItemCounters(item.id, commonId); if ( shouldCheckItemVisibility && @@ -118,14 +115,7 @@ const FeedItem = forwardRef((props, ref) => { } if (item.data.type === CommonFeedType.Project) { - return ( - - ); + return ; } return null; diff --git a/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx b/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx index fdb107b579..ae282dc417 100644 --- a/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx +++ b/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx @@ -8,28 +8,29 @@ import { OpenIcon } from "@/shared/icons"; import { CommonFeed } from "@/shared/models"; import { CommonAvatar, parseStringToTextEditorValue } from "@/shared/ui-kit"; import { checkIsProject } from "@/shared/utils"; +import { useFeedItemCounters } from "../../hooks"; import styles from "./ProjectFeedItem.module.scss"; interface ProjectFeedItemProps { item: CommonFeed; isMobileVersion: boolean; - unreadStreamsCount?: number; - unreadMessages?: number; } export const ProjectFeedItem: FC = (props) => { - const { item, isMobileVersion, unreadStreamsCount, unreadMessages } = props; + const { item, isMobileVersion } = props; const history = useHistory(); const { getCommonPagePath } = useRoutesContext(); const { renderFeedItemBaseContent } = useFeedItemContext(); const { data: common, fetched: isCommonFetched, fetchCommon } = useCommon(); + const { + projectUnreadStreamsCount: unreadStreamsCount, + projectUnreadMessages: unreadMessages, + } = useFeedItemCounters(item.id, common?.directParent?.commonId); const commonId = item.data.id; const lastMessage = parseStringToTextEditorValue( - Number.isInteger(unreadStreamsCount) - ? `${unreadStreamsCount} updated stream${ - unreadStreamsCount === 1 ? "" : "s" - }` - : undefined, + `${unreadStreamsCount ?? 0} updated stream${ + unreadStreamsCount === 1 ? "" : "s" + }`, ); const isProject = checkIsProject(common); const titleEl = ( diff --git a/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/ProjectCreationForm.tsx b/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/ProjectCreationForm.tsx index 79c91d8e78..ebca1317b8 100644 --- a/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/ProjectCreationForm.tsx +++ b/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/ProjectCreationForm.tsx @@ -178,8 +178,12 @@ const ProjectCreationForm: FC = (props) => { ref={formRef} initialValues={initialValues} onSubmit={isEditing ? handleProjectUpdate : handleProjectCreate} - items={getConfiguration(true, roles, { - existingNames: existingProjectsNames, + items={getConfiguration({ + isProject: true, + roles, + shouldBeUnique: { + existingNames: existingProjectsNames, + }, })} submitButtonText={isEditing ? "Save changes" : "Create Space"} disabled={isLoading} diff --git a/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/configuration.ts b/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/configuration.ts index 118bedcf33..85064ac204 100644 --- a/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/configuration.ts +++ b/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/configuration.ts @@ -9,11 +9,20 @@ import { } from "../../constants"; import styles from "./ProjectCreationForm.module.scss"; -export const getConfiguration = ( - isProject = true, - roles?: Roles, - shouldBeUnique?: { existingNames: string[] }, -): CreationFormItem[] => { +interface Options { + isProject: boolean; + roles?: Roles; + shouldBeUnique?: { existingNames: string[] }; + isImageRequired?: boolean; +} + +export const getConfiguration = (options: Options): CreationFormItem[] => { + const { + isProject = true, + roles, + shouldBeUnique, + isImageRequired = false, + } = options; const type = isProject ? "Space" : "Common"; const items: CreationFormItem[] = [ @@ -22,9 +31,17 @@ export const getConfiguration = ( className: styles.projectImages, props: { name: "projectImages", - label: `${type} picture`, + label: `${type} picture${isImageRequired ? " (required)" : ""}`, maxImagesAmount: 1, }, + validation: isImageRequired + ? { + min: { + value: 1, + message: `${type} picture is required`, + }, + } + : undefined, }, { type: CreationFormItemType.TextField, diff --git a/src/pages/commonCreation/hooks/useCommonForm.ts b/src/pages/commonCreation/hooks/useCommonForm.ts index 18ed453a27..c614e73d1f 100644 --- a/src/pages/commonCreation/hooks/useCommonForm.ts +++ b/src/pages/commonCreation/hooks/useCommonForm.ts @@ -76,6 +76,10 @@ export const useCommonForm = ( return { initialValues, onSubmit, - formItems: getConfiguration(false, roles), + formItems: getConfiguration({ + isProject: false, + roles, + isImageRequired: true, + }), }; }; diff --git a/src/pages/commonFeed/CommonFeed.tsx b/src/pages/commonFeed/CommonFeed.tsx index 0da50b96fa..5f3c58f20d 100644 --- a/src/pages/commonFeed/CommonFeed.tsx +++ b/src/pages/commonFeed/CommonFeed.tsx @@ -29,7 +29,6 @@ import { useRoutesContext } from "@/shared/contexts"; import { useAuthorizedModal, useQueryParams } from "@/shared/hooks"; import { useCommonFeedItems, useUserCommonIds } from "@/shared/hooks/useCases"; import { useCommonPinnedFeedItems } from "@/shared/hooks/useCases/useCommonPinnedFeedItems"; -import { useIsTabletView } from "@/shared/hooks/viewport"; import { RightArrowThinIcon } from "@/shared/icons"; import { checkIsFeedItemFollowLayoutItem, @@ -92,7 +91,6 @@ const CommonFeedComponent: FC = (props) => { onActiveItemDataChange, } = props; const { getCommonPagePath, getProfilePagePath } = useRoutesContext(); - const isTabletView = useIsTabletView(); const queryParams = useQueryParams(); const dispatch = useDispatch(); const history = useHistory(); @@ -437,7 +435,7 @@ const CommonFeedComponent: FC = (props) => { <> {headerEl}
- +
diff --git a/src/pages/commonFeed/CommonFeedPage_v04.tsx b/src/pages/commonFeed/CommonFeedPage_v04.tsx index 4f4e0373ca..b0fc1ac089 100644 --- a/src/pages/commonFeed/CommonFeedPage_v04.tsx +++ b/src/pages/commonFeed/CommonFeedPage_v04.tsx @@ -1,7 +1,6 @@ import React, { FC } from "react"; import { RoutesV04Provider } from "@/shared/contexts"; import { CommonSidenavLayoutPageContent } from "@/shared/layouts"; -import { checkIsProject } from "@/shared/utils"; import BaseCommonFeedPage from "./BaseCommonFeedPage"; import { RenderCommonFeedContentWrapper } from "./CommonFeed"; import HeaderContent_v04 from "./components/HeaderContent_v04/HeaderContent_v04"; @@ -19,13 +18,9 @@ const renderContentWrapper: RenderCommonFeedContentWrapper = ({ headerClassName={styles.layoutHeader} headerContent={ } isGlobalLoading={!isGlobalDataFetched} diff --git a/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.module.scss b/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.module.scss index 90e0145540..7adfd958c3 100644 --- a/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.module.scss +++ b/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.module.scss @@ -12,6 +12,13 @@ } } +.actionsContainer { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; +} + .openSidenavButton { display: none; @@ -78,6 +85,12 @@ overflow: hidden; } +.commonMainInfoWrapper { + display: flex; + align-items: center; + column-gap: 8px; +} + .commonName { margin: 0; font-family: PoppinsSans, sans-serif; diff --git a/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.tsx b/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.tsx index adc058837a..bf4d0ea019 100644 --- a/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.tsx +++ b/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.tsx @@ -3,37 +3,36 @@ import { NavLink } from "react-router-dom"; import classNames from "classnames"; import { NewStreamButton } from "@/pages/common/components/CommonTabPanels/components/FeedTab/components"; import { useRoutesContext } from "@/shared/contexts"; +import { useCommonFollow } from "@/shared/hooks/useCases"; import { useIsTabletView } from "@/shared/hooks/viewport"; -import { RightArrowThinIcon } from "@/shared/icons"; -import { CirclesPermissions, CommonMember, Governance } from "@/shared/models"; +import { RightArrowThinIcon, StarIcon } from "@/shared/icons"; +import { + CirclesPermissions, + Common, + CommonMember, + Governance, +} from "@/shared/models"; import { CommonAvatar, TopNavigationOpenSidenavButton } from "@/shared/ui-kit"; -import { getPluralEnding } from "@/shared/utils"; +import { checkIsProject, getPluralEnding } from "@/shared/utils"; +import { ActionsButton } from "../HeaderContent/components"; import styles from "./HeaderContent_v04.module.scss"; interface HeaderContentProps { className?: string; - commonId: string; - commonName: string; - commonImage: string; - commonMembersAmount: number; - isProject?: boolean; + common: Common; commonMember: (CommonMember & CirclesPermissions) | null; governance: Governance; } const HeaderContent_v04: FC = (props) => { - const { - className, - commonId, - commonName, - commonImage, - commonMembersAmount, - isProject = false, - commonMember, - governance, - } = props; + const { className, common, commonMember, governance } = props; const { getCommonPageAboutTabPath } = useRoutesContext(); const isMobileVersion = useIsTabletView(); + const isProject = checkIsProject(common); + const commonFollow = useCommonFollow(common.id, commonMember); + const showFollowIcon = commonFollow.isFollowInProgress + ? !commonMember?.isFollowing + : commonMember?.isFollowing; return (
@@ -44,11 +43,11 @@ const HeaderContent_v04: FC = (props) => { /> = (props) => { />
-

{commonName}

+
+

{common.name}

+ {showFollowIcon && } +

- {commonMembersAmount} member{getPluralEnding(commonMembersAmount)} + {common.memberCount} member{getPluralEnding(common.memberCount)}

- +
+ + +
); }; diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx index 343a2e74c9..c7a7274694 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx @@ -7,6 +7,7 @@ import React, { useMemo, } from "react"; import classNames from "classnames"; +import { useLongPress } from "use-long-press"; import { ElementDropdown, UserAvatar, @@ -224,12 +225,18 @@ export default function ChatMessage({ } }; - const handleMessageClick: MouseEventHandler = () => { - if (isTabletView) { - setIsMenuOpen(true); - } + const handleLongPress = () => { + setIsMenuOpen(true); }; + const getLongPressProps = useLongPress( + isTabletView ? handleLongPress : null, + { + threshold: 400, + cancelOnMovement: true, + }, + ); + const handleContextMenu: MouseEventHandler = (event) => { if (!isTabletView) { event.preventDefault(); @@ -387,7 +394,7 @@ export default function ChatMessage({ [styles.highlightedOwn]: highlighted && !isNotCurrentUserMessage, })} - onClick={handleMessageClick} + {...getLongPressProps()} > {isNotCurrentUserMessage && !isSystemMessage && (
diff --git a/src/shared/components/Dropdown/Dropdown.tsx b/src/shared/components/Dropdown/Dropdown.tsx index 019f1f1303..68e0b0d4a0 100644 --- a/src/shared/components/Dropdown/Dropdown.tsx +++ b/src/shared/components/Dropdown/Dropdown.tsx @@ -27,6 +27,7 @@ import { GlobalOverlay } from "../GlobalOverlay"; import "./index.scss"; export interface Styles { + labelWrapper?: string; menuButton?: string; value?: string; placeholder?: string; @@ -205,7 +206,12 @@ const Dropdown: ForwardRefRenderFunction = ( onMenuToggle={handleMenuToggle} > {label && ( -
+
{label}
)} 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/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/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/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/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/commonLayout/saga/getCommons.ts b/src/store/states/commonLayout/saga/getCommons.ts index 08d44bd956..46e4efd778 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,8 +80,11 @@ 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, @@ -89,8 +92,7 @@ export function* getCommons( hasMembership, hasPermissionToAddProject, notificationsAmount: 0, - }), - ); + })); yield put( actions.getCommons.success({ diff --git a/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts b/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts index b505658868..ea01e5e933 100644 --- a/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts +++ b/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts @@ -4,6 +4,7 @@ import { CommonService, GovernanceService, ProjectService } from "@/services"; import { InboxItemType } from "@/shared/constants"; import { Awaited } from "@/shared/interfaces"; import { Common, User } from "@/shared/models"; +import { compareCommonsByLastActivity } from "@/shared/utils"; import { getPermissionsDataByAllUserCommonMemberInfo } from "../../commonLayout/saga/utils"; import * as actions from "../actions"; import { selectMultipleSpacesLayoutBreadcrumbs } from "../selectors"; @@ -102,16 +103,18 @@ export function* fetchBreadcrumbsItemsByCommonId( commonId, user?.uid, )) as Awaited>; - const projectsData: ProjectsStateItem[] = projectsInfo.map( - ({ common, hasMembership, hasPermissionToAddProject }) => ({ + const projectsData: ProjectsStateItem[] = [...projectsInfo] + .sort((prevItem, nextItem) => + compareCommonsByLastActivity(prevItem.common, nextItem.common), + ) + .map(({ common, hasMembership, hasPermissionToAddProject }) => ({ commonId: common.id, image: common.image, name: common.name, directParent: common.directParent, hasMembership, hasPermissionToAddProject, - }), - ); + })); const currentBreadcrumbs = (yield select( selectMultipleSpacesLayoutBreadcrumbs, 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"