From 2c20069a9017f07a027cfc98a3530aa67831e728 Mon Sep 17 00:00:00 2001 From: Alvin Dimas Satria <56182633+alvin371@users.noreply.github.com> Date: Tue, 9 May 2023 16:08:52 +0700 Subject: [PATCH] fix: Adjustment on Create Post to Timeline (#1824) * fix: set debio tips (#1753) feat: set debio tips * chore(deps): bump google-github-actions/release-please-action from 3.7.4 to 3.7.5 (#1754) chore(deps): bump google-github-actions/release-please-action Bumps [google-github-actions/release-please-action](https://github.com/google-github-actions/release-please-action) from 3.7.4 to 3.7.5. - [Release notes](https://github.com/google-github-actions/release-please-action/releases) - [Changelog](https://github.com/google-github-actions/release-please-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/release-please-action/compare/d3c71f9a0a55385580de793de58da057b3560862...e0b9d1885d92e9a93d5ce8656de60e3b806e542c) --- updated-dependencies: - dependency-name: google-github-actions/release-please-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: adjustment view on direclty post to timeline * fix: adjustment view on direclty post to timeline * fix: adjustment view on direclty post to timeline --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Kristian Ruben <56469224+rubenkristian@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../ExperienceListBarCreatePost.tsx | 2 +- .../Experience.skeleton.tsx | 36 + .../Experience.style.ts | 123 +++ .../ExperienceTimelineCard/Experience.tsx | 412 ++++++++ .../ModalAddToPost/ModalAddToPost.context.tsx | 9 + .../ModalAddToPost.interface.tsx | 5 + .../ModalAddToPost.provider.tsx | 327 ++++++ .../ModalAddToPost/ModalAddToPost.styles.tsx | 121 +++ .../ModalAddToPost/useModalAddToPost.hook.tsx | 19 + .../ExperienceTimelineCard/index.ts | 2 + .../Experience.container.tsx | 83 ++ .../Experience.styles.ts | 178 ++++ .../ExperienceEditor.stories.tsx | 106 ++ .../ExperienceTimelinePost.tsx | 932 ++++++++++++++++++ .../ExperienceTimelinePost/index.ts | 1 + src/components/PostCreate/PostCreate.tsx | 49 +- src/components/ProfileHeader/index.tsx | 2 +- src/hooks/use-experience-hook.ts | 3 + 18 files changed, 2396 insertions(+), 14 deletions(-) create mode 100644 src/components/ExperienceTimelineCard/Experience.skeleton.tsx create mode 100644 src/components/ExperienceTimelineCard/Experience.style.ts create mode 100644 src/components/ExperienceTimelineCard/Experience.tsx create mode 100644 src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.context.tsx create mode 100644 src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.interface.tsx create mode 100644 src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.provider.tsx create mode 100644 src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.styles.tsx create mode 100644 src/components/ExperienceTimelineCard/ModalAddToPost/useModalAddToPost.hook.tsx create mode 100644 src/components/ExperienceTimelineCard/index.ts create mode 100644 src/components/ExperienceTimelinePost/Experience.container.tsx create mode 100644 src/components/ExperienceTimelinePost/Experience.styles.ts create mode 100644 src/components/ExperienceTimelinePost/ExperienceEditor.stories.tsx create mode 100644 src/components/ExperienceTimelinePost/ExperienceTimelinePost.tsx create mode 100644 src/components/ExperienceTimelinePost/index.ts diff --git a/src/components/ExperienceList/ExperienceListBarCreatePost.tsx b/src/components/ExperienceList/ExperienceListBarCreatePost.tsx index ae5e03536..40ab176cb 100644 --- a/src/components/ExperienceList/ExperienceListBarCreatePost.tsx +++ b/src/components/ExperienceList/ExperienceListBarCreatePost.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { Experience as ExperienceCard } from '../ExpericenceRightBar'; +import { Experience as ExperienceCard } from '../ExperienceTimelineCard'; import { useStyles } from './ExperienceList.style'; import { WrappedExperience } from 'src/interfaces/experience'; diff --git a/src/components/ExperienceTimelineCard/Experience.skeleton.tsx b/src/components/ExperienceTimelineCard/Experience.skeleton.tsx new file mode 100644 index 000000000..39c6129b6 --- /dev/null +++ b/src/components/ExperienceTimelineCard/Experience.skeleton.tsx @@ -0,0 +1,36 @@ +import Card from '@material-ui/core/Card'; +import Grid from '@material-ui/core/Grid'; +import BaseSekeleton from '@material-ui/lab/Skeleton'; + +export const Skeleton = ({ menuDrawer = false }: { menuDrawer?: boolean }) => { + return ( + <Card + style={{ + padding: menuDrawer ? '5px 10px' : 20, + borderRadius: 10, + marginBottom: menuDrawer ? 10 : 30, + width: '100%', + }}> + <Grid + container + direction="row" + style={{ width: '100%' }} + alignItems="center"> + <BaseSekeleton + variant="rect" + width={menuDrawer ? 40 : 68} + height={menuDrawer ? 40 : 68} + /> + + <Grid + item + container + direction="column" + style={{ marginLeft: 20, width: 'calc(100% - 88px)' }}> + <BaseSekeleton variant="text" width={'100%'} height={24} /> + <BaseSekeleton variant="text" width={'50%'} height={16} /> + </Grid> + </Grid> + </Card> + ); +}; diff --git a/src/components/ExperienceTimelineCard/Experience.style.ts b/src/components/ExperienceTimelineCard/Experience.style.ts new file mode 100644 index 000000000..49d2d0fed --- /dev/null +++ b/src/components/ExperienceTimelineCard/Experience.style.ts @@ -0,0 +1,123 @@ +import { + alpha, + createStyles, + makeStyles, + Theme, +} from '@material-ui/core/styles'; + +type ExperienceStyleProps = { + selected: boolean; + selectable: boolean; + menuDrawer: boolean; +}; + +export const useStyles = makeStyles<Theme, ExperienceStyleProps>(theme => + createStyles({ + root: { + marginBottom: theme.spacing(1), + border: '1px solid', + borderColor: props => (props.selected ? '#6E3FC3' : '#FFF'), + borderRadius: 10, + padding: props => (props.menuDrawer ? '5px 10px' : 20), + width: '100%', + boxShadow: `0px 2px 10px rgba(0, 0, 0, 0.05)`, + position: 'relative', + + '&:hover': { + backgroundColor: alpha('#FFC857', 0.15), + borderColor: props => (props.selected ? '#6E3FC3' : 'transparent'), + + '& .MuiCardActionArea-focusHighlight': { + opacity: 0, + }, + + '&::before': { + backgroundColor: props => + props.selected ? '#6E3FC3' : 'transparent', + }, + }, + + '&::before': { + content: '""', + position: 'absolute', + width: 8, + top: 0, + left: 0, + height: '100%', + backgroundColor: props => (props.selected ? '#6E3FC3' : '#FFF'), + borderTopLeftRadius: 10, + borderBottomLeftRadius: 10, + }, + }, + image: { + width: props => (props.menuDrawer ? 40 : 68), + height: props => (props.menuDrawer ? 40 : 68), + opacity: 0.9, + borderRadius: 5, + }, + cardContent: { + width: 140, + padding: '0px 0px 0px 20px', + flexGrow: 1, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + + '&:last-child': { + paddingBottom: 0, + }, + }, + title: { + wordBreak: 'break-word', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + [theme.breakpoints.down('xs')]: { + fontSize: '14px', + }, + }, + subtitle: { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + [theme.breakpoints.down('xs')]: { + fontSize: '12px', + fontWeight: 500, + }, + }, + icon: { + [theme.breakpoints.down('xs')]: { + color: '#404040', + }, + }, + menu: { + borderRadius: 10, + marginTop: 8, + }, + delete: { + color: '#FE3636', + }, + error: { + background: '#FE3636', + color: '#FFF', + '&:hover': { + color: theme.palette.text.primary, + }, + }, + modal: { + paddingBottom: 10, + }, + input: { + width: 560, + marginBottom: 0, + marginTop: 10, + + '& .MuiInputLabel-root, .MuiInputBase-root': { + color: '#616161', + }, + [theme.breakpoints.down('xs')]: { + width: '100%', + }, + }, + }), +); diff --git a/src/components/ExperienceTimelineCard/Experience.tsx b/src/components/ExperienceTimelineCard/Experience.tsx new file mode 100644 index 000000000..48a1ad6b1 --- /dev/null +++ b/src/components/ExperienceTimelineCard/Experience.tsx @@ -0,0 +1,412 @@ +import { DuplicateIcon } from '@heroicons/react/outline'; + +import React, { useState } from 'react'; +import { useCookies } from 'react-cookie'; +import CopyToClipboard from 'react-copy-to-clipboard'; + +import getConfig from 'next/config'; +import NextImage from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; + +import { Avatar, Grid } from '@material-ui/core'; +import { TextField, InputAdornment } from '@material-ui/core'; +import Card from '@material-ui/core/Card'; +import CardActionArea from '@material-ui/core/CardActionArea'; +import CardContent from '@material-ui/core/CardContent'; +import IconButton from '@material-ui/core/IconButton'; +import Menu from '@material-ui/core/Menu'; +import BaseMenuItem from '@material-ui/core/MenuItem'; +import SvgIcon from '@material-ui/core/SvgIcon'; +import Typography from '@material-ui/core/Typography'; + +import useConfirm from '../common/Confirm/use-confirm.hook'; +import { useStyles } from './Experience.style'; + +import { COOKIE_INSTANCE_URL } from 'components/SelectServer'; +import { WithAuthorizeAction } from 'components/common/Authorization/WithAuthorizeAction'; +import { useEnqueueSnackbar } from 'components/common/Snackbar/useEnqueueSnackbar.hook'; +import { Modal } from 'src/components/atoms/Modal'; +import ShowIf from 'src/components/common/show-if.component'; +import { capitalize } from 'src/helpers/string'; +import { useExperienceHook } from 'src/hooks/use-experience-hook'; +import { WrappedExperience } from 'src/interfaces/experience'; +import { User } from 'src/interfaces/user'; +import i18n from 'src/locale'; + +type ExperienceProps = { + user?: User; + anonymous?: boolean; + userExperience: WrappedExperience; + selected: boolean; + selectable: boolean; + onSelect?: (experienceId: string) => void; + onClone?: (experienceId: string) => void; + onSubscribe?: (experienceId: string) => void; + onUnsubscribe?: (experienceId: string) => void; + onDelete?: (experienceId: string) => void; + menuDrawer?: boolean; +}; + +const MenuItem = WithAuthorizeAction(BaseMenuItem); + +const DEFAULT_IMAGE = + 'https://pbs.twimg.com/profile_images/1407599051579617281/-jHXi6y5_400x400.jpg'; + +const { publicRuntimeConfig } = getConfig(); + +export const Experience: React.FC<ExperienceProps> = props => { + const { + userExperience, + user, + anonymous = false, + selectable, + onSelect, + onClone, + onDelete, + onSubscribe, + onUnsubscribe, + menuDrawer = false, + } = props; + + const router = useRouter(); + const styles = useStyles({ ...props, menuDrawer }); + const confirm = useConfirm(); + const enqueueSnackbar = useEnqueueSnackbar(); + + const [menuAnchorElement, setMenuAnchorElement] = + useState<null | HTMLElement>(null); + const [shareAnchorElement, setShareAnchorElement] = + useState<null | HTMLElement>(null); + + const isOwnExperience = userExperience.experience.user.id === user?.id; + const experienceId = userExperience.experience.id; + const userExperienceId = userExperience.id; + const link = publicRuntimeConfig.appAuthURL + `?type=all&id=${experienceId}`; + const { userExperiencesMeta } = useExperienceHook(); + const totalOwnedExperience = + userExperiencesMeta.additionalData?.totalOwnedExperience ?? 0; + + const handleClickExperience = () => { + handleCloseSettings(); + + if (selectable && onSelect) { + onSelect(experienceId); + } + }; + + const handleCloneExperience = () => { + if ( + totalOwnedExperience >= 5 && + !user.fullAccess && + user.fullAccess !== undefined + ) { + confirm({ + title: i18n.t('LiteVersion.LimitTitleExperience'), + description: i18n.t('LiteVersion.LimitDescExperience'), + icon: 'warning', + confirmationText: i18n.t('LiteVersion.ConnectWallet'), + cancellationText: i18n.t('LiteVersion.MaybeLater'), + onConfirm: () => { + router.push({ pathname: '/wallet', query: { type: 'manage' } }); + }, + onCancel: () => { + undefined; + }, + }); + } else { + handleCloseSettings(); + + if (onClone) { + onClone(experienceId); + } + } + }; + + const [cookies] = useCookies([COOKIE_INSTANCE_URL]); + + const handleSubscribeExperience = () => { + handleCloseSettings(); + + if (!user) { + confirm({ + icon: 'followTimeline', + title: i18n.t('Confirm.Anonymous.FollowTimeline.Title'), + description: i18n.t('Confirm.Anonymous.FollowTimeline.Desc'), + confirmationText: i18n.t('General.SignIn'), + cancellationText: i18n.t('LiteVersion.MaybeLater'), + onConfirm: () => { + router.push(`/login?instance=${cookies[COOKIE_INSTANCE_URL]}`); + }, + }); + } else { + if (onSubscribe) { + onSubscribe(experienceId); + } + } + }; + + const handleCloseSettings = () => { + setMenuAnchorElement(null); + }; + + const confirmDeleteExperience = () => { + handleCloseSettings(); + + confirm({ + title: i18n.t('Experience.List.Prompt_Delete.Title'), + description: i18n.t('Experience.List.Prompt_Delete.Desc'), + icon: 'danger', + confirmationText: i18n.t('Experience.List.Prompt_Delete.Btn_Yes'), + cancellationText: i18n.t('General.Cancel'), + onConfirm: () => { + if (onDelete && userExperienceId) { + onDelete(userExperienceId); + } + }, + }); + }; + + const confirmUnsubscribe = () => { + handleCloseSettings(); + + confirm({ + title: i18n.t('Experience.List.Prompt_Unsub.Title'), + description: `${i18n.t('Experience.List.Prompt_Unsub.Desc_1')}\n ${i18n.t( + 'Experience.List.Prompt_Unsub.Desc_2', + { experience_name: userExperience.experience.name }, + )}`, + icon: 'warning', + confirmationText: i18n.t('Experience.List.Prompt_Unsub.Btn_Yes'), + onConfirm: () => { + if (onUnsubscribe && userExperienceId) { + onUnsubscribe(userExperienceId); + } + }, + }); + }; + + const openShareExperience = (event: React.MouseEvent<HTMLLIElement>) => { + console.log(event.currentTarget); + handleCloseSettings(); + + setShareAnchorElement(event.currentTarget); + }; + + const closeShareExperience = () => { + setShareAnchorElement(null); + }; + + const handleExperienceLinkCopied = () => { + enqueueSnackbar({ + message: i18n.t('Experience.List.Copy'), + variant: 'success', + }); + }; + + const isHidden = () => { + if (userExperience.private && !userExperience.friend) return true; + if (userExperience.private && userExperience.friend) return false; + return false; + }; + + return ( + <> + <Card className={styles.root}> + <CardActionArea + onClick={handleClickExperience} + disableRipple + component="div"> + <Grid + container + alignItems="center" + justifyContent="space-between" + wrap="nowrap"> + {userExperience.experience.experienceImageURL ? ( + <div + style={{ + maxWidth: menuDrawer ? 40 : 68, + maxHeight: menuDrawer ? 40 : 68, + }}> + <NextImage + alt={userExperience.experience.name} + loader={() => + userExperience.experience.experienceImageURL ?? + DEFAULT_IMAGE + } + src={ + userExperience.experience.experienceImageURL ?? + DEFAULT_IMAGE + } + placeholder="empty" + objectFit="cover" + objectPosition="center" + width={'100%'} + height={'100%'} + quality={90} + className={styles.image} + /> + </div> + ) : ( + <Avatar + alt={userExperience.experience.name} + variant="rounded" + className={styles.image}> + {userExperience.experience.name.charAt(0)} + </Avatar> + )} + + <CardContent classes={{ root: styles.cardContent }}> + <Typography className={styles.title} variant="body1"> + {userExperience.experience.name} + </Typography> + <Typography + variant="caption" + color="primary" + className={styles.subtitle}> + {userExperience.experience.user.name} + </Typography> + <Typography variant="caption" color="textSecondary"> + {isOwnExperience ? ` ${i18n.t('Experience.List.You')}` : ''} + </Typography> + <div className=""> + <Typography + variant="caption" + color="error" + className={styles.subtitle}> + Visibility:{' '} + </Typography> + <Typography + variant="caption" + color="error" + className={styles.subtitle}> + {userExperience.experience.visibility === 'selected_user' + ? 'Custom' + : capitalize(userExperience.experience.visibility)} + </Typography> + <Link + href={`/experience/[experienceId]`} + as={`/experience/${experienceId}`} + passHref> + <Typography variant="body2" color="primary"> + {i18n.t('Experience.List.Menu.View')} + </Typography> + </Link> + </div> + </CardContent> + </Grid> + </CardActionArea> + </Card> + + {menuAnchorElement && ( + <Menu + classes={{ + paper: styles.menu, + }} + anchorEl={menuAnchorElement} + getContentAnchorEl={null} + anchorOrigin={{ vertical: 'top', horizontal: 'center' }} + transformOrigin={{ vertical: 'bottom', horizontal: 'center' }} + open={Boolean(menuAnchorElement)} + onClose={handleCloseSettings}> + <Link + href={`/experience/[experienceId]`} + as={`/experience/${experienceId}`} + passHref> + <BaseMenuItem onClick={handleCloseSettings}> + {i18n.t('Experience.List.Menu.View')} + </BaseMenuItem> + </Link> + + <ShowIf condition={isOwnExperience}> + <Link + href={`/experience/[experienceId]/edit`} + as={`/experience/${experienceId}/edit`} + passHref> + <MenuItem onClick={handleCloseSettings}> + {i18n.t('Experience.List.Menu.Edit')} + </MenuItem> + </Link> + </ShowIf> + + <ShowIf condition={!isOwnExperience && !isHidden()}> + <MenuItem + onClick={handleCloneExperience} + fallback={handleCloseSettings} + disabled={anonymous}> + {i18n.t('Experience.List.Menu.Clone')} + </MenuItem> + </ShowIf> + + <ShowIf + condition={ + !userExperience.subscribed && !isOwnExperience && !isHidden() + }> + <MenuItem + onClick={handleSubscribeExperience} + fallback={handleCloseSettings}> + {i18n.t('Experience.List.Menu.Subscribe')} + </MenuItem> + </ShowIf> + + <ShowIf + condition={Boolean(userExperience.subscribed) && !isOwnExperience}> + <MenuItem + onClick={confirmUnsubscribe} + fallback={handleCloseSettings} + className={styles.delete}> + {i18n.t('Experience.List.Menu.Unsubscribe')} + </MenuItem> + </ShowIf> + <ShowIf condition={isOwnExperience}> + <MenuItem + onClick={confirmDeleteExperience} + fallback={handleCloseSettings} + className={styles.delete}> + {i18n.t('Experience.List.Menu.Delete')} + </MenuItem> + </ShowIf> + <BaseMenuItem onClick={openShareExperience}> + {i18n.t('Experience.List.Menu.Share')} + </BaseMenuItem> + </Menu> + )} + + <Modal + title={i18n.t('Experience.List.Modal.Title')} + subtitle={i18n.t('Experience.List.Modal.Subtitle')} + maxWidth="sm" + className={styles.modal} + open={Boolean(shareAnchorElement)} + onClose={closeShareExperience}> + <div className={styles.copy}> + <TextField + id="copy-post-url" + label="URL" + value={link} + variant="outlined" + disabled + fullWidth + margin="none" + className={styles.input} + InputProps={{ + endAdornment: ( + <InputAdornment position="end"> + <CopyToClipboard + text={link} + onCopy={handleExperienceLinkCopied}> + <IconButton + aria-label="copy-post-link" + style={{ padding: 0 }}> + <SvgIcon component={DuplicateIcon} color="primary" /> + </IconButton> + </CopyToClipboard> + </InputAdornment> + ), + }} + /> + </div> + </Modal> + </> + ); +}; diff --git a/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.context.tsx b/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.context.tsx new file mode 100644 index 000000000..9212a69c1 --- /dev/null +++ b/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.context.tsx @@ -0,0 +1,9 @@ +import { createContext } from 'react'; + +import { ModalAddPostExperienceProps } from './ModalAddToPost.interface'; + +export type HandleConfirmAddPostExperience = ( + props: ModalAddPostExperienceProps, +) => void; + +export default createContext<HandleConfirmAddPostExperience | null>(null); diff --git a/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.interface.tsx b/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.interface.tsx new file mode 100644 index 000000000..3af8b5010 --- /dev/null +++ b/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.interface.tsx @@ -0,0 +1,5 @@ +import { Post } from 'src/interfaces/post'; + +export type ModalAddPostExperienceProps = { + post: Post; +}; diff --git a/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.provider.tsx b/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.provider.tsx new file mode 100644 index 000000000..6475d3008 --- /dev/null +++ b/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.provider.tsx @@ -0,0 +1,327 @@ +import { InformationCircleIcon } from '@heroicons/react/outline'; + +import React, { useCallback, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { + Button, + CardActionArea, + CardContent, + CardMedia, + Checkbox, + Grid, + IconButton, + SvgIcon, + Tooltip, + Typography, +} from '@material-ui/core'; + +import ModalAddToPostContext, { + HandleConfirmAddPostExperience, +} from './ModalAddToPost.context'; +import { ModalAddPostExperienceProps } from './ModalAddToPost.interface'; +import { useStyles } from './ModalAddToPost.styles'; + +import { Empty } from 'components/atoms/Empty'; +import { useEnqueueSnackbar } from 'components/common/Snackbar/useEnqueueSnackbar.hook'; +import ShowIf from 'components/common/show-if.component'; +import { Skeleton } from 'src/components/Expericence'; +import { Modal } from 'src/components/atoms/Modal'; +import { useExperienceHook } from 'src/hooks/use-experience-hook'; +import { UserExperience, WrappedExperience } from 'src/interfaces/experience'; +import * as ExperienceAPI from 'src/lib/api/experience'; +import i18n from 'src/locale'; +import { RootState } from 'src/reducers'; +import { UserState } from 'src/reducers/user/reducer'; + +type ExperienceItemProps = { + item: WrappedExperience; + selectedExperience: string[]; + handleSelectExperience: (propsSelectedExperience: string) => void; +}; + +const ExperienceItem = ({ + item, + selectedExperience, + handleSelectExperience, +}: ExperienceItemProps) => { + const styles = useStyles(); + + const DEFAULT_IMAGE = + 'https://pbs.twimg.com/profile_images/1407599051579617281/-jHXi6y5_400x400.jpg'; + + return ( + <div className={styles.experienceCard}> + <Checkbox + checked={ + selectedExperience + ? selectedExperience.filter(ar => ar === item.experience.id) + .length > 0 + : false + } + onChange={() => { + item.experience.id && handleSelectExperience(item.experience.id); + }} + color="primary" + inputProps={{ 'aria-label': 'controlled' }} + classes={{ root: styles.fill }} + /> + <CardActionArea + onClick={() => { + item.experience.id && handleSelectExperience(item.experience.id); + }} + disableRipple + component="div"> + <Grid + container + alignItems="center" + justifyContent="space-between" + wrap="nowrap"> + <CardMedia + component="img" + className={styles.image} + image={item.experience.experienceImageURL ?? DEFAULT_IMAGE} + /> + <CardContent classes={{ root: styles.cardContent }}> + <Typography className={styles.title} variant="body1"> + {item.experience.name} + </Typography> + <Typography variant="caption" color="primary"> + {item.experience.user.name} + </Typography> + <Typography variant="caption" color="textSecondary"> + {item ? ` ${i18n.t('Experience.Modal_Add_Post.Card_Own')}` : ''} + </Typography> + </CardContent> + </Grid> + </CardActionArea> + </div> + ); +}; + +export const ModalAddToPostProvider: React.ComponentType<ModalAddPostExperienceProps> = + ({ children }) => { + const styles = useStyles(); + const { user } = useSelector<RootState, UserState>( + state => state.userState, + ); + const { + loadExperiencePostList, + loadExperienceAdded, + addPostsToExperience, + } = useExperienceHook(); + const enqueueSnackbar = useEnqueueSnackbar(); + + const [postId, setPostId] = useState<string | null>(null); + const [open, setOpen] = useState(false); + const [isSelectAll, setIsSelectAll] = useState(false); + const [selectedExperience, setSelectedExperience] = useState<string[]>([]); + const [userExperiences, setUserExperiences] = useState<UserExperience[]>( + [], + ); + const [page, setPage] = useState<number>(1); + const [loading, setLoading] = useState<boolean>(true); + + const toolTipText = i18n.t('Experience.Modal_Add_Post.Tooltip_Text'); + + const addPostToExperience = useCallback<HandleConfirmAddPostExperience>( + async props => { + setOpen(true); + const tmpAddedExperience: string[] = []; + await loadExperienceAdded(props.post.id, postsExperiences => { + postsExperiences.map(item => { + tmpAddedExperience.push(item.id); + }); + }); + + loadExperiencePostList(props.post.id, postsExperiences => { + setPostId(props.post.id); + const tmpSelectedExperience: string[] = []; + postsExperiences.map(item => { + if (tmpAddedExperience.find(post => post === item.id)) { + tmpSelectedExperience.push(item.id); + } + }); + setSelectedExperience(tmpSelectedExperience); + }); + + console.log({ userExperiences }); + }, + [userExperiences], + ); + + const handleClose = useCallback(() => { + setOpen(false); + setSelectedExperience([]); + }, []); + + const handleSelectAllExperience = () => { + const tmpUserExperience = [...userExperiences].filter( + ar => ar.userId === user?.id, + ); + let tmpSelectedExperience: string[] = []; + if (!isSelectAll) { + tmpUserExperience.map(item => { + tmpSelectedExperience.push(item.experience.id); + }); + } else { + tmpSelectedExperience = []; + } + setSelectedExperience(tmpSelectedExperience); + setIsSelectAll(!isSelectAll); + }; + + const handleSelectExperience = (propsSelectedExperience: string) => { + const tmpSelectedExperience = [...selectedExperience]; + if ( + tmpSelectedExperience.filter(ar => ar === propsSelectedExperience) + .length > 0 + ) { + const indexRemovedExperience = tmpSelectedExperience.indexOf( + propsSelectedExperience, + ); + tmpSelectedExperience.splice(indexRemovedExperience, 1); + } else { + tmpSelectedExperience.push(propsSelectedExperience); + } + setSelectedExperience(tmpSelectedExperience); + console.log({ tmpSelectedExperience }); + }; + + const handleConfirm = () => { + if (postId) { + addPostsToExperience(postId, selectedExperience, () => { + setOpen(false); + enqueueSnackbar({ + message: i18n.t('Experience.Modal_Add_Post.Success_Msg'), + variant: 'success', + }); + }); + } + }; + + const fetchUserExperiences = async () => { + setLoading(true); + const { meta, data: experiences } = + await ExperienceAPI.getUserExperiences(user.id, undefined, page); + + setUserExperiences([...userExperiences, ...experiences]); + setLoading(false); + if (meta.currentPage < meta.totalPageCount) setPage(page + 1); + }; + + const resetExperiences = () => { + setPage(1); + setUserExperiences([]); + }; + + useEffect(() => { + if (open) fetchUserExperiences(); + else resetExperiences(); + }, [open, page]); + + return ( + <> + <ModalAddToPostContext.Provider value={addPostToExperience}> + {children} + </ModalAddToPostContext.Provider> + <Modal + title={i18n.t('Experience.Modal_Add_Post.Title')} + subtitle={ + <Typography> + <Typography> + {i18n.t('Experience.Modal_Add_Post.Subtitle_1')} + </Typography> + <Typography> + {i18n.t('Experience.Modal_Add_Post.Subtitle_2')} + </Typography> + </Typography> + } + open={open} + onClose={handleClose}> + <div className={styles.root}> + <div className={styles.options}> + <div className={styles.flex}> + <Typography> + {i18n.t('Experience.Modal_Add_Post.Tooltip')} + </Typography> + <Tooltip title={toolTipText} arrow> + <IconButton + aria-label="info" + className={styles.info} + style={{ color: '#404040' }}> + <SvgIcon + component={InformationCircleIcon} + viewBox="0 0 24 24" + /> + </IconButton> + </Tooltip> + </div> + <div className={styles.flex}> + <Checkbox + checked={isSelectAll} + color="primary" + onChange={handleSelectAllExperience} + inputProps={{ 'aria-label': 'controlled' }} + classes={{ root: styles.fill }} + disabled={loading} + /> + <Typography + component="span" + color="textPrimary" + className={styles.selected}> + {i18n.t('Experience.Modal_Add_Post.Select_All')} + </Typography> + </div> + </div> + <div></div> + </div> + + <div + id="selectable-experience-list" + className={styles.experienceList}> + {userExperiences + .filter(ar => ar.userId === user?.id && ar.subscribed === false) + .map(item => { + return ( + <ExperienceItem + key={item.id} + item={item} + selectedExperience={selectedExperience} + handleSelectExperience={handleSelectExperience} + /> + ); + })} + {loading && <Skeleton />} + + <ShowIf + condition={ + userExperiences.filter(ar => ar.userId === user?.id).length === + 0 && !loading + }> + <div className={styles.containerEmpty}> + <Empty + title={i18n.t('Experience.Modal_Add_Post.Empty_Title')} + subtitle={i18n.t('Experience.Modal_Add_Post.Empty_Subtitle')} + height={true} + margin={false} + /> + </div> + </ShowIf> + </div> + <Button + size="small" + variant="contained" + color="primary" + fullWidth + onClick={handleConfirm} + disabled={ + loading || + userExperiences.filter(ar => ar.userId === user?.id).length === 0 + }> + {i18n.t('Experience.Modal_Add_Post.Btn_Confirm')} + </Button> + </Modal> + </> + ); + }; diff --git a/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.styles.tsx b/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.styles.tsx new file mode 100644 index 000000000..c7bd34728 --- /dev/null +++ b/src/components/ExperienceTimelineCard/ModalAddToPost/ModalAddToPost.styles.tsx @@ -0,0 +1,121 @@ +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; + +export const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: 438, + maxHeight: 580, + + [theme.breakpoints.down('md')]: { + width: '100%', + }, + }, + subtitle: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + options: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 30, + + '& . MuiMenu-paper': { + width: 170, + }, + }, + fill: { + '& .MuiSvgIcon-root': { + fill: 'currentColor', + }, + }, + selected: { + marginLeft: 4, + fontWeight: 600, + }, + list: { + height: 340, + overflow: 'auto', + '& .MuiListItem-root': { + paddingTop: 0, + paddingBottom: 0, + marginBottom: 12, + }, + }, + action: { + marginTop: 8, + marginBottom: 24, + }, + logo: { + height: 12, + width: 12, + marginLeft: 8, + marginTop: 4, + }, + info: { + padding: 0, + '& .MuiSvgIcon-root': { + fill: 'none', + }, + }, + flex: { + display: 'flex', + gap: '8px', + alignItems: 'center', + marginBottom: theme.spacing(0.5), + }, + experienceList: { + maxHeight: 'fit-content', + gap: '8px', + marginBottom: theme.spacing(1.5), + alignItems: 'center', + //overflow: 'scroll', + }, + experienceCard: { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(2.5), + }, + image: { + width: 68, + height: 68, + opacity: 0.9, + borderRadius: 5, + }, + cardContent: { + padding: '0px 0px 0px 20px', + flexGrow: 1, + + '&:last-child': { + paddingBottom: 0, + }, + }, + title: { + wordBreak: 'break-word', + [theme.breakpoints.down('xs')]: { + fontSize: '14px', + }, + }, + containerEmpty: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + textAlign: 'center', + flexDirection: 'column', + background: '#FFF', + height: 300, + width: '100%', + }, + emptyTitle: { + fontWeight: 700, + fontSize: '18px', + marginBottom: '12px', + textAlign: 'center', + }, + emptySubtitle: { + fontSize: '14px', + }, + }), +); diff --git a/src/components/ExperienceTimelineCard/ModalAddToPost/useModalAddToPost.hook.tsx b/src/components/ExperienceTimelineCard/ModalAddToPost/useModalAddToPost.hook.tsx new file mode 100644 index 000000000..686f33741 --- /dev/null +++ b/src/components/ExperienceTimelineCard/ModalAddToPost/useModalAddToPost.hook.tsx @@ -0,0 +1,19 @@ +import { useContext } from 'react'; + +import ModalAddToPostContext, { + HandleConfirmAddPostExperience, +} from './ModalAddToPost.context'; + +const useModalAddToPost = (): HandleConfirmAddPostExperience => { + const addPostToExperience = useContext(ModalAddToPostContext); + + if (!addPostToExperience) { + throw new Error( + 'addPostToExperience must be used within a ModalAddToPostProvider', + ); + } + + return addPostToExperience; +}; + +export default useModalAddToPost; diff --git a/src/components/ExperienceTimelineCard/index.ts b/src/components/ExperienceTimelineCard/index.ts new file mode 100644 index 000000000..f3939f537 --- /dev/null +++ b/src/components/ExperienceTimelineCard/index.ts @@ -0,0 +1,2 @@ +export * from './Experience'; +export * from './Experience.skeleton'; diff --git a/src/components/ExperienceTimelinePost/Experience.container.tsx b/src/components/ExperienceTimelinePost/Experience.container.tsx new file mode 100644 index 000000000..c0e9facd1 --- /dev/null +++ b/src/components/ExperienceTimelinePost/Experience.container.tsx @@ -0,0 +1,83 @@ +import React from 'react'; + +import { useRouter } from 'next/router'; + +import { useStyles } from './Experience.styles'; +import { ExperienceTimelinePost } from './ExperienceTimelinePost'; + +import debounce from 'lodash/debounce'; +import { TopNavbarComponent } from 'src/components/atoms/TopNavbar'; +import { useExperienceHook } from 'src/hooks/use-experience-hook'; +import { useSearchHook } from 'src/hooks/use-search.hooks'; +import { useUpload } from 'src/hooks/use-upload.hook'; +import { ExperienceProps } from 'src/interfaces/experience'; +import i18n from 'src/locale'; + +export const ExperienceContainer: React.FC = () => { + // TODO: separate hook for tag, people and experience + const { + selectedExperience, + tags, + people, + saveExperience, + searchTags, + searchPeople, + loadExperience, + } = useExperienceHook(); + const { searchUsers, users } = useSearchHook(); + const style = useStyles(); + + const { uploadImage } = useUpload(); + const router = useRouter(); + + const onImageUpload = async (files: File[]) => { + const url = await uploadImage(files[0]); + + return url ?? ''; + }; + + const onSave = (attributes: ExperienceProps) => { + saveExperience(attributes, (experienceId: string) => { + router.push(`/experience/${experienceId}`); + + loadExperience(); + }); + }; + + const handleSearchTags = debounce((query: string) => { + searchTags(query); + }, 300); + + const handleSearchPeople = debounce((query: string) => { + searchPeople(query); + }, 300); + + const handleSearchUser = debounce((query: string) => { + searchUsers(query); + }, 300); + + return ( + <> + <div className={style.mb}> + <TopNavbarComponent + description={i18n.t('TopNavbar.Subtitle.Experience_Create')} + sectionTitle={i18n.t('TopNavbar.Title.Experience')} + /> + </div> + <div className={style.box}> + <ExperienceTimelinePost + isEdit={false} + experience={selectedExperience} + tags={tags} + people={people} + onSearchTags={handleSearchTags} + onImageUpload={onImageUpload} + onSearchPeople={handleSearchPeople} + onSave={onSave} + onSearchUser={handleSearchUser} + users={users} + /> + </div> + </> + ); +}; diff --git a/src/components/ExperienceTimelinePost/Experience.styles.ts b/src/components/ExperienceTimelinePost/Experience.styles.ts new file mode 100644 index 000000000..47b8e8295 --- /dev/null +++ b/src/components/ExperienceTimelinePost/Experience.styles.ts @@ -0,0 +1,178 @@ +import { + createStyles, + makeStyles, + Theme, + alpha, +} from '@material-ui/core/styles'; + +export const useStyles = makeStyles((theme: Theme) => + createStyles({ + '@global': { + ' .MuiAutocomplete-option[aria-selected="true"]': { + background: 'none', + }, + ' .MuiAutocomplete-option[data-focus="true"]': { + backgroundColor: alpha('#FFC857', 0.15), + }, + ' .MuiAutocomplete-tag .MuiSvgIcon-root': { + width: 14, + height: 14, + }, + + ' .MuiFormHelperText-root': { + marginLeft: 0, + + [theme.breakpoints.down('xs')]: { + fontSize: 12, + }, + }, + }, + root: { + background: '#FFF', + borderRadius: 10, + marginBottom: 24, + + '& .MuiAutocomplete-popupIndicatorOpen': { + transform: 'none', + }, + + [theme.breakpoints.down('xs')]: { + padding: '20px', + }, + }, + header: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing(2), + }, + content: { + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(3), + }, + row1: { + minWidth: 100, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + paddingTop: 8, + gap: 8, + }, + boxImage: { + width: 100, + height: 100, + borderRadius: 10, + border: '2px dashed #C2C2C2', + backgroundColor: '#F5F5F5', + }, + row2: { + width: '100%', + overflow: 'hidden', + paddingTop: 8, + }, + title: { + marginBottom: 30, + fontSize: theme.typography.h5.fontSize, + fontWeight: 400, + }, + preview: { + marginBottom: 30, + + '& .MuiListItem-root:hover': { + backgroundColor: alpha('#FFC857', 0.15), + + '&::before,&::after': { + content: '""', + position: 'absolute', + width: 30, + height: '100%', + top: 0, + backgroundColor: alpha('#FFC857', 0.15), + }, + '&::before': { + left: -30, + }, + '&::after': { + right: -30, + }, + }, + }, + postTextContainer: { + border: '1px solid #E5E5E5', + width: '100%', + padding: '20px', + borderRadius: '5px', + justifyContent: 'center', + alignItems: 'center', + textAlign: 'center', + marginBottom: 36, + }, + textPost: { + fontWeight: 600, + fontSize: 18, + }, + textPostDetail: { + fontWeight: 400, + fontSize: 14, + marginTop: 9, + }, + label: { + background: '#FFF', + paddingLeft: 6, + paddingRight: 6, + }, + social: { + color: theme.palette.primary.main, + }, + people: {}, + removePeople: { + '& .MuiSvgIcon-root': { + fill: 'currentColor', + }, + }, + mb: { + marginBottom: '10px', + }, + loading: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + position: 'absolute', + zIndex: 999, + width: '100%', + height: '100%', + textAlign: 'center', + }, + option: { + width: '100%', + }, + counter: { + position: 'absolute', + right: 0, + bottom: 0, + color: '#898888', + }, + box: { + [theme.breakpoints.down('xs')]: { + padding: '0px 20px 20px 20px', + }, + }, + fill: { + fill: 'currentColor', + '& .MuiSvgIcon-root': { + fill: 'currentColor', + }, + }, + formControl: { + marginBottom: 0, + }, + customVisibility: { + maxHeight: '300px', + overflowY: 'scroll', + border: '1px solid #FFD24D', + borderRadius: '4px', + padding: '0 10px', + }, + }), +); diff --git a/src/components/ExperienceTimelinePost/ExperienceEditor.stories.tsx b/src/components/ExperienceTimelinePost/ExperienceEditor.stories.tsx new file mode 100644 index 000000000..8f8330272 --- /dev/null +++ b/src/components/ExperienceTimelinePost/ExperienceEditor.stories.tsx @@ -0,0 +1,106 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import React from 'react'; + +import { SocialsEnum } from '../../interfaces/social'; +import { ExperienceTimelinePost } from './ExperienceTimelinePost'; + +export default { + title: 'UI Revamp v2.0/components/Experience Editor', + component: ExperienceTimelinePost, + argTypes: {}, +} as ComponentMeta<typeof ExperienceTimelinePost>; + +const Template: ComponentStory<typeof ExperienceTimelinePost> = args => ( + <ExperienceTimelinePost {...args} /> +); + +export const CreateExperience = Template.bind({}); +CreateExperience.args = { + onSave: console.log, + onImageUpload: async (files: File[]) => { + return 'https://res.cloudinary.com/dsget80gs/lu2f67ljt0oqnaacuu7y.jpg'; + }, + tags: [ + { + id: 'crypto', + count: 0, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'nsfw', + count: 0, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + people: [ + { + id: '1', + name: 'Person 1', + originUserId: '1', + platform: SocialsEnum.FACEBOOK, + profilePictureURL: + 'https://res.cloudinary.com/dsget80gs/bd75blw2pnmpj9aqwdxm.png', + username: 'personone', + }, + { + id: '2', + name: 'Person 2', + originUserId: '2', + platform: SocialsEnum.FACEBOOK, + profilePictureURL: + 'https://res.cloudinary.com/dsget80gs/rvi6x1stnczatom2jq2y.jpg', + username: 'persontwo', + }, + ], +}; + +export const EditExperience = Template.bind({}); +EditExperience.args = { + onSave: console.log, + onImageUpload: async (files: File[]) => { + return 'https://res.cloudinary.com/dsget80gs/lu2f67ljt0oqnaacuu7y.jpg'; + }, + experience: { + name: 'Example experience', + description: 'Sample', + people: [], + allowedTags: ['crypto', 'near'], + }, + tags: [ + { + id: 'crypto', + count: 0, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'nsfw', + count: 0, + createdAt: new Date(), + updatedAt: new Date(), + }, + ], + people: [ + { + id: '1', + name: 'Person 1', + originUserId: '1', + platform: SocialsEnum.FACEBOOK, + profilePictureURL: + 'https://res.cloudinary.com/dsget80gs/bd75blw2pnmpj9aqwdxm.png', + username: 'personone', + }, + { + id: '2', + name: 'Person 2', + originUserId: '2', + platform: SocialsEnum.FACEBOOK, + profilePictureURL: + 'https://res.cloudinary.com/dsget80gs/rvi6x1stnczatom2jq2y.jpg', + username: 'persontwo', + }, + ], +}; diff --git a/src/components/ExperienceTimelinePost/ExperienceTimelinePost.tsx b/src/components/ExperienceTimelinePost/ExperienceTimelinePost.tsx new file mode 100644 index 000000000..01b9cfd14 --- /dev/null +++ b/src/components/ExperienceTimelinePost/ExperienceTimelinePost.tsx @@ -0,0 +1,932 @@ +import { + SearchIcon, + XCircleIcon, + PlusCircleIcon, + ChevronDownIcon, +} from '@heroicons/react/solid'; + +import React, { useState, useEffect, useRef } from 'react'; + +import { useRouter } from 'next/router'; + +import { + FormControl, + FormHelperText, + IconButton, + InputLabel, + OutlinedInput, + SvgIcon, + TextField, + Typography, +} from '@material-ui/core'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import { + Autocomplete, + AutocompleteChangeReason, + AutocompleteRenderOptionState, +} from '@material-ui/lab'; + +import { + ExperienceProps, + VisibilityItem, + Tag, + SelectedUserIds, +} from '../../interfaces/experience'; +import { People } from '../../interfaces/people'; +import { Dropzone } from '../atoms/Dropzone'; +import { ListItemPeopleComponent } from '../atoms/ListItem/ListItemPeople'; +import { Loading } from '../atoms/Loading'; +import ShowIf from '../common/show-if.component'; +import { useStyles } from './Experience.styles'; + +import { debounce, isEmpty } from 'lodash'; +import { useExperienceHook } from 'src/hooks/use-experience-hook'; +import { useSearchHook } from 'src/hooks/use-search.hooks'; +import { User } from 'src/interfaces/user'; +import * as UserAPI from 'src/lib/api/user'; +import i18n from 'src/locale'; + +type ExperienceEditorProps = { + type?: 'Clone' | 'Edit' | 'Create'; + isEdit?: boolean; + experience?: ExperienceProps; + tags: Tag[]; + people: People[]; + onSearchTags: (query: string) => void; + onSearchPeople: (query: string) => void; + onSave: (experience: ExperienceProps) => void; + onImageUpload: (files: File[]) => Promise<string>; + onSearchUser?: (query: string) => void; + users?: User[]; + onCancel?: () => void; +}; + +enum TagsProps { + ALLOWED = 'allowed', + PROHIBITED = 'prohibited', +} + +const DEFAULT_EXPERIENCE: ExperienceProps = { + name: '', + allowedTags: [], + people: [], + prohibitedTags: [], + visibility: '', + selectedUserIds: [], +}; + +export const ExperienceTimelinePost: React.FC<ExperienceEditorProps> = + props => { + const { + experience = DEFAULT_EXPERIENCE, + people, + tags, + onSave, + onImageUpload, + onSearchTags, + onSearchPeople, + onSearchUser, + users, + onCancel, + } = props; + const styles = useStyles(); + + const { loadPostExperience } = useExperienceHook(); + const router = useRouter(); + const { clearUsers } = useSearchHook(); + + const ref = useRef(null); + const [experienceId, setExperienceId] = useState<string | undefined>(); + console.log(experienceId); + const [newExperience, setNewExperience] = + useState<ExperienceProps>(experience); + const [image, setImage] = useState<string | undefined>( + experience.experienceImageURL, + ); + const [, setDetailChanged] = useState<boolean>(false); + const [isLoading, setIsloading] = useState<boolean>(false); + const [isSubmitted, setIsSubmitted] = useState<boolean>(false); + const [selectedVisibility, setSelectedVisibility] = + useState<VisibilityItem>(); + const [selectedUserIds, setSelectedUserIds] = useState<User[]>([]); + const [pageUserIds, setPageUserIds] = React.useState<number>(1); + const [isLoadingSelectedUser, setIsLoadingSelectedUser] = + useState<boolean>(false); + const [errors, setErrors] = useState({ + name: false, + picture: false, + tags: false, + people: false, + visibility: false, + selectedUserId: false, + }); + + useEffect(() => { + const experienceId = router.query.experienceId as string | null; + if (experienceId) { + setExperienceId(experienceId); + loadPostExperience(experienceId); + } + }, []); + + useEffect(() => { + if (isSubmitted) { + validateExperience(); + } + }, [isSubmitted, newExperience]); + + const handleSearchTags = (event: React.ChangeEvent<HTMLInputElement>) => { + const debounceSubmit = debounce(() => { + onSearchTags(event.target.value); + }, 300); + + debounceSubmit(); + }; + + const handleSearchPeople = (event: React.ChangeEvent<HTMLInputElement>) => { + const debounceSubmit = debounce(() => { + onSearchPeople(event.target.value); + }, 300); + + debounceSubmit(); + }; + + const clearSearchedPeople = () => { + const debounceSubmit = debounce(() => { + onSearchPeople(''); + }, 300); + + debounceSubmit(); + }; + + const handleImageUpload = async (files: File[]) => { + if (files.length > 0) { + setIsloading(true); + const url = await onImageUpload(files); + + setIsloading(false); + setImage(url); + setNewExperience({ ...newExperience, experienceImageURL: url }); + } else { + setNewExperience({ ...newExperience, experienceImageURL: undefined }); + } + + setDetailChanged(true); + }; + + const handleChange = + (field: keyof ExperienceProps) => + (event: React.ChangeEvent<HTMLInputElement>) => { + const value = event.target.value.trimStart(); + + setNewExperience(prevExperience => ({ + ...prevExperience, + [field]: value, + })); + + setDetailChanged(experience[field] !== value); + }; + + const handleTagsInputChange = ( + // eslint-disable-next-line @typescript-eslint/ban-types + event: React.ChangeEvent<{}>, + newValue: string, + type: TagsProps, + ) => { + const options = newValue.split(/[ ,]+/); + + let tmpTags: string[] = []; + if (type === TagsProps.ALLOWED) { + tmpTags = newExperience.allowedTags; + } else if (type === TagsProps.PROHIBITED) { + tmpTags = newExperience.prohibitedTags ?? []; + } + + const fieldValue = tmpTags + .concat(options) + .map(x => x.trim()) + .filter(x => x); + + if (options.length > 1) { + handleTagsChange(event, fieldValue, 'create-option', type); + } + }; + + const handleTagsChange = ( + // eslint-disable-next-line @typescript-eslint/ban-types + event: React.ChangeEvent<{}>, + value: string[], + reason: AutocompleteChangeReason, + type: TagsProps, + ) => { + const data = [...new Set(value.map(tag => tag.replace('#', '')))]; + + const prohibitedTagsChanged = + type === TagsProps.PROHIBITED && + (data.filter( + tag => + !experience?.prohibitedTags || + !experience.prohibitedTags.includes(tag), + ).length > 0 || + experience?.prohibitedTags?.length !== data.length); + const allowedTagsChanged = + type === TagsProps.ALLOWED && + (data.filter(tag => !experience.allowedTags.includes(tag)).length > 0 || + data.length !== experience.allowedTags.length); + + setDetailChanged(prohibitedTagsChanged || allowedTagsChanged); + + if (reason === 'remove-option') { + if (type === TagsProps.ALLOWED) { + setNewExperience(prevExperience => ({ + ...prevExperience, + allowedTags: data, + })); + } else if (type === TagsProps.PROHIBITED) { + setNewExperience(prevExperience => ({ + ...prevExperience, + prohibitedTags: data, + })); + } + } + + if (reason === 'create-option') { + if (type === TagsProps.ALLOWED) { + setNewExperience(prevExperience => ({ + ...prevExperience, + allowedTags: data, + })); + } else if (type === TagsProps.PROHIBITED) { + setNewExperience(prevExperience => ({ + ...prevExperience, + prohibitedTags: data, + })); + } + } + + if (reason === 'select-option') { + if (type === TagsProps.ALLOWED) { + setNewExperience(prevExperience => ({ + ...prevExperience, + allowedTags: data, + })); + } else if (type === TagsProps.PROHIBITED) { + setNewExperience(prevExperience => ({ + ...prevExperience, + prohibitedTags: data, + })); + } + } + }; + + const handlePeopleChange = ( + // eslint-disable-next-line @typescript-eslint/ban-types + event: React.ChangeEvent<{}>, + value: People[], + reason: AutocompleteChangeReason, + ) => { + const people = newExperience?.people ? newExperience.people : []; + if (reason === 'select-option') { + setNewExperience(prevExperience => ({ + ...prevExperience, + people: [ + ...people, + ...value.filter(option => people.indexOf(option) === -1), + ], + })); + clearSearchedPeople(); + } + + setDetailChanged(true); + }; + + const removeSelectedPeople = (selected: People) => () => { + setNewExperience(prevExperience => ({ + ...prevExperience, + people: prevExperience?.people + ? prevExperience?.people.filter(people => people.id != selected.id) + : [], + })); + + setDetailChanged(true); + }; + + const validateExperience = (): boolean => { + const validName = newExperience.name.length > 0; + // const validPicture = Boolean(newExperience.experienceImageURL); + const validTags = newExperience.allowedTags.length >= 0; + const validPeople = + newExperience.people.filter(people => !isEmpty(people.id)).length >= 0; + const validSelectedUserIds = + selectedVisibility && selectedVisibility?.id === 'selected_user' + ? selectedUserIds.length > 0 + : !isEmpty(selectedVisibility?.id); + const validVisibility = !isEmpty(selectedVisibility?.id); + + setErrors({ + name: !validName, + picture: false, + tags: !validTags, + people: !validPeople, + visibility: !validVisibility, + selectedUserId: !validSelectedUserIds, + }); + + return ( + validName && + validTags && + validPeople && + validVisibility && + validSelectedUserIds + ); + }; + + const saveExperience = () => { + setIsSubmitted(true); + + const valid = validateExperience(); + + if (valid) { + onSave(newExperience); + } else { + ref.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + }; + + const visibilityList: VisibilityItem[] = [ + { + id: 'public', + name: i18n.t('Experience.Editor.Visibility.Public'), + }, + { + id: 'private', + name: i18n.t('Experience.Editor.Visibility.OnlyMe'), + }, + { + id: 'selected_user', + name: i18n.t('Experience.Editor.Visibility.Custom'), + }, + { + id: 'friend', + name: i18n.t('Experience.Editor.Visibility.Friend_Only'), + }, + ]; + + const handleVisibilityChange = ( + // eslint-disable-next-line @typescript-eslint/ban-types + event: React.ChangeEvent<{}>, + value: VisibilityItem, + reason: AutocompleteChangeReason, + ) => { + setSelectedVisibility(value); + setNewExperience(prevExperience => ({ + ...prevExperience, + visibility: value?.id, + })); + + setDetailChanged(true); + }; + + const handleSearchUser = (event: React.ChangeEvent<HTMLInputElement>) => { + const debounceSubmit = debounce(() => { + onSearchUser(event.target.value); + }, 300); + + debounceSubmit(); + }; + + const clearSearchedUser = () => { + const debounceSubmit = debounce(() => { + onSearchUser(''); + }, 300); + + debounceSubmit(); + }; + + const handleVisibilityPeopleChange = ( + // eslint-disable-next-line @typescript-eslint/ban-types + event: React.ChangeEvent<{}>, + value: User[], + reason: AutocompleteChangeReason, + ) => { + const people = selectedUserIds ? selectedUserIds : []; + console.log({ value }); + if (reason === 'select-option') { + setSelectedUserIds([ + ...people, + ...value.filter(option => people.indexOf(option) === -1), + ]); + clearSearchedUser(); + clearUsers(); + } + + setDetailChanged(true); + }; + + const removeVisibilityPeople = (selected: User) => () => { + setSelectedUserIds( + selectedUserIds + ? selectedUserIds.filter(people => people.id != selected.id) + : [], + ); + + setDetailChanged(true); + }; + + const mappingUserIds = () => { + console.log({ selectedVisibility }); + if (selectedVisibility?.id === 'selected_user') { + const timestamp = Date.now(); + const mapIds = Object.values( + selectedUserIds.map(option => { + return { + userId: option.id, + addedAt: timestamp, + }; + }), + ); + setNewExperience(prevExperience => ({ + ...prevExperience, + selectedUserIds: mapIds, + })); + } else { + setNewExperience(prevExperience => ({ + ...prevExperience, + selectedUserIds: [], + })); + } + }; + + useEffect(() => { + mappingUserIds(); + setDetailChanged(true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedUserIds, experience]); + + const getSelectedIds = async (selected: SelectedUserIds[]) => { + setIsLoadingSelectedUser(true); + const userIds = selected.map(e => e.userId); + const response = await UserAPI.getUserByIds(userIds, pageUserIds); + setSelectedUserIds([ + ...selectedUserIds, + ...(response?.data as unknown as User[]), + ]); + setIsLoadingSelectedUser(false); + if (pageUserIds < response.meta.totalPageCount) + setPageUserIds(pageUserIds + 1); + }; + + React.useEffect(() => { + getSelectedIds(experience?.selectedUserIds); + }, [experience, pageUserIds]); + + useEffect(() => { + if (experience) { + const visibility = visibilityList.find( + option => option.id === experience?.visibility, + ); + setSelectedVisibility(visibility); + + getSelectedIds(experience?.selectedUserIds); + } + }, [experience]); + + return ( + <div className={styles.root} ref={ref}> + <div className={styles.header}> + <FormControl + classes={{ root: styles.formControl }} + style={{ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + marginLeft: 'auto', + }}> + <Typography + variant="h5" + color="primary" + onClick={saveExperience} + style={{ cursor: 'pointer' }}> + Save + </Typography> + <Typography + variant="h5" + style={{ color: 'red', marginLeft: 10, cursor: 'pointer' }} + onClick={onCancel}> + Cancel + </Typography> + </FormControl> + </div> + <div className={styles.content}> + <div className={styles.row1}> + <FormControl + fullWidth + variant="outlined" + style={{ position: 'relative', zIndex: 100 }} + error={errors.picture}> + <Dropzone + error={errors.picture} + onImageSelected={handleImageUpload} + value={image} + border="solid" + maxSize={3} + width={100} + height={100} + usage="experience" + label={i18n.t('Dropzone.Btn.Exp_Add')} + /> + <ShowIf condition={isLoading}> + <div className={styles.loading}> + <CircularProgress size={32} color="primary" /> + </div> + </ShowIf> + </FormControl> + </div> + <div className={styles.row2}> + <FormControl fullWidth variant="outlined" error={errors.name}> + <InputLabel htmlFor="experience-name"> + {i18n.t('Experience.Editor.Subtitle_1')} + </InputLabel> + <OutlinedInput + id="experience-name" + placeholder={i18n.t('Experience.Editor.Subtitle_1')} + value={newExperience?.name || ''} + onChange={handleChange('name')} + labelWidth={110} + inputProps={{ maxLength: 50 }} + /> + <FormHelperText id="experience-name-error"> + {i18n.t('Experience.Editor.Helper.Name')} + </FormHelperText> + <Typography variant="subtitle1" className={styles.counter}> + {newExperience?.name.length ?? 0}/50 + </Typography> + </FormControl> + + <Autocomplete + id="experience-visibility" + options={visibilityList} + getOptionLabel={option => option.name} + getOptionSelected={(option, value) => option?.id === value.id} + autoHighlight={false} + disableClearable + onChange={handleVisibilityChange} + value={selectedVisibility || null} + popupIcon={ + <SvgIcon + classes={{ root: styles.fill }} + component={ChevronDownIcon} + viewBox={'0 0 20 20'} + /> + } + renderInput={({ inputProps, ...rest }) => ( + <TextField + {...rest} + error={errors.visibility} + label={i18n.t('Experience.Editor.Label_4')} + placeholder={i18n.t('Experience.Editor.Placeholder_4')} + variant="outlined" + inputProps={{ ...inputProps, readOnly: true }} + /> + )} + /> + + {selectedVisibility?.id === 'selected_user' && ( + <> + <Autocomplete + id="experience-custom-visibility-people" + onBlur={clearUsers} + className={styles.people} + value={(selectedUserIds as User[]) ?? []} + multiple + options={users} + getOptionSelected={(option, value) => option.id === value.id} + filterSelectedOptions={true} + getOptionLabel={option => `${option.username} ${option.name}`} + disableClearable + autoHighlight={false} + popupIcon={ + <SvgIcon + classes={{ root: styles.fill }} + component={SearchIcon} + viewBox={'0 0 20 20'} + /> + } + onChange={handleVisibilityPeopleChange} + renderTags={() => null} + renderInput={params => ( + <TextField + {...params} + error={errors.selectedUserId} + label={i18n.t('Experience.Editor.Placeholder_5')} + placeholder={i18n.t('Experience.Editor.Placeholder_5')} + variant="outlined" + onChange={handleSearchUser} + InputProps={{ + ...params.InputProps, + endAdornment: ( + <React.Fragment> + {params.InputProps.endAdornment} + </React.Fragment> + ), + }} + helperText={i18n.t('Experience.Editor.Helper.People')} + /> + )} + renderOption={( + option, + state: AutocompleteRenderOptionState, + ) => { + if (option.id === '') return null; + return ( + <div className={styles.option}> + <ListItemPeopleComponent + id="selectable-experience-list-item" + title={option.name} + subtitle={ + <Typography variant="caption"> + @{option.username} + </Typography> + } + avatar={option.profilePictureURL} + platform={'myriad'} + action={ + <IconButton className={styles.removePeople}> + {state.selected ? ( + <SvgIcon + classes={{ root: styles.fill }} + component={XCircleIcon} + color="error" + viewBox={'0 0 20 20'} + /> + ) : ( + <SvgIcon + classes={{ root: styles.fill }} + component={PlusCircleIcon} + viewBox={'0 0 20 20'} + /> + )} + </IconButton> + } + /> + </div> + ); + }} + /> + + <div className={styles.preview}> + <div className={styles.customVisibility}> + <ShowIf condition={isLoadingSelectedUser}> + <Loading /> + </ShowIf> + {selectedUserIds + .filter(people => !isEmpty(people.id)) + .map(people => ( + <ListItemPeopleComponent + id="selected-experience-list-item" + key={people.id} + title={people.name} + subtitle={ + <Typography variant="caption"> + @{people.username} + </Typography> + } + avatar={people.profilePictureURL} + platform={'myriad'} + action={ + <IconButton + onClick={removeVisibilityPeople(people)}> + <SvgIcon + classes={{ root: styles.fill }} + component={XCircleIcon} + color="error" + viewBox={'0 0 20 20'} + /> + </IconButton> + } + /> + ))} + </div> + </div> + </> + )} + + <FormControl + fullWidth + variant="outlined" + style={{ position: 'relative' }}> + <InputLabel htmlFor="experience-description"> + {i18n.t('Experience.Editor.Subtitle_2')} + </InputLabel> + <OutlinedInput + id="experience-description" + placeholder={i18n.t('Experience.Editor.Subtitle_2')} + value={newExperience?.description || ''} + onChange={handleChange('description')} + labelWidth={70} + inputProps={{ maxLength: 280 }} + multiline + /> + <FormHelperText id="experience-description-error"> + + </FormHelperText> + <Typography variant="subtitle1" className={styles.counter}> + {newExperience?.description?.length ?? 0}/280 + </Typography> + </FormControl> + + <Autocomplete<string, true, true, true> + className={styles.fill} + id="experience-tags-include" + freeSolo + multiple + value={newExperience.allowedTags ?? []} + options={tags + .map(tag => tag.id) + .filter(tag => !newExperience.allowedTags.includes(tag))} + disableClearable + onChange={(event, value, reason) => { + handleTagsChange(event, value, reason, TagsProps.ALLOWED); + }} + onInputChange={(event, value) => { + handleTagsInputChange(event, value, TagsProps.ALLOWED); + }} + getOptionLabel={option => `#${option}`} + renderInput={params => ( + <TextField + {...params} + error={errors.tags} + label={i18n.t('Experience.Editor.Label_1')} + variant="outlined" + placeholder={ + newExperience.allowedTags.length === 0 + ? i18n.t('Experience.Editor.Placeholder_1') + : undefined + } + onChange={handleSearchTags} + helperText={''} + InputProps={{ + ...params.InputProps, + endAdornment: ( + <React.Fragment> + {params.InputProps.endAdornment} + </React.Fragment> + ), + }} + /> + )} + /> + + <Autocomplete + className={styles.fill} + id="experience-tags-exclude" + freeSolo + multiple + value={newExperience?.prohibitedTags ?? []} + options={tags + .map(tag => tag.id) + .filter(tag => !newExperience.prohibitedTags?.includes(tag))} + disableClearable + onChange={(event, value, reason) => { + handleTagsChange(event, value, reason, TagsProps.PROHIBITED); + }} + onInputChange={(event, value) => { + handleTagsInputChange(event, value, TagsProps.PROHIBITED); + }} + getOptionLabel={option => `#${option}`} + renderInput={params => ( + <TextField + {...params} + label={i18n.t('Experience.Editor.Label_2')} + variant="outlined" + placeholder={ + newExperience.prohibitedTags?.length === 0 + ? i18n.t('Experience.Editor.Placeholder_2') + : undefined + } + onChange={handleSearchTags} + InputProps={{ + ...params.InputProps, + endAdornment: ( + <React.Fragment> + {params.InputProps.endAdornment} + </React.Fragment> + ), + }} + /> + )} + /> + + <Autocomplete + id="experience-people" + className={styles.people} + value={(newExperience?.people as People[]) ?? []} + multiple + options={people} + getOptionSelected={(option, value) => option.id === value.id} + filterSelectedOptions={true} + getOptionLabel={option => `${option.username} ${option.name}`} + disableClearable + autoHighlight={false} + popupIcon={ + <SvgIcon + classes={{ root: styles.fill }} + component={SearchIcon} + viewBox={'0 0 20 20'} + /> + } + onChange={handlePeopleChange} + renderTags={() => null} + renderInput={params => ( + <TextField + {...params} + error={errors.people} + label={i18n.t('Experience.Editor.Label_3')} + placeholder={i18n.t('Experience.Editor.Placeholder_3')} + variant="outlined" + onChange={handleSearchPeople} + InputProps={{ + ...params.InputProps, + endAdornment: ( + <React.Fragment> + {params.InputProps.endAdornment} + </React.Fragment> + ), + }} + helperText={''} + /> + )} + renderOption={(option, state: AutocompleteRenderOptionState) => { + if (option.id === '') return null; + return ( + <div className={styles.option}> + <ListItemPeopleComponent + id="selectable-experience-list-item" + title={option.name} + subtitle={ + <Typography variant="caption"> + @{option.username} + </Typography> + } + avatar={option.profilePictureURL} + platform={option.platform} + action={ + <IconButton className={styles.removePeople}> + {state.selected ? ( + <SvgIcon + classes={{ root: styles.fill }} + component={XCircleIcon} + color="error" + viewBox={'0 0 20 20'} + /> + ) : ( + <SvgIcon + classes={{ root: styles.fill }} + component={PlusCircleIcon} + viewBox={'0 0 20 20'} + /> + )} + </IconButton> + } + /> + </div> + ); + }} + /> + + <div className={styles.preview}> + {newExperience.people + .filter(people => !isEmpty(people.id)) + .map(people => ( + <ListItemPeopleComponent + id="selected-experience-list-item" + key={people.id} + title={people.name} + subtitle={ + <Typography variant="caption"> + @{people.username} + </Typography> + } + avatar={people.profilePictureURL} + platform={people.platform} + action={ + <IconButton onClick={removeSelectedPeople(people)}> + <SvgIcon + classes={{ root: styles.fill }} + component={XCircleIcon} + color="error" + viewBox={'0 0 20 20'} + /> + </IconButton> + } + /> + ))} + </div> + </div> + </div> + </div> + ); + }; diff --git a/src/components/ExperienceTimelinePost/index.ts b/src/components/ExperienceTimelinePost/index.ts new file mode 100644 index 000000000..84314daf5 --- /dev/null +++ b/src/components/ExperienceTimelinePost/index.ts @@ -0,0 +1 @@ +export * from './ExperienceTimelinePost'; diff --git a/src/components/PostCreate/PostCreate.tsx b/src/components/PostCreate/PostCreate.tsx index 4e4c7a12a..5cda3af1f 100644 --- a/src/components/PostCreate/PostCreate.tsx +++ b/src/components/PostCreate/PostCreate.tsx @@ -1,6 +1,6 @@ import { ArrowLeftIcon, GiftIcon, TrashIcon } from '@heroicons/react/outline'; -import React, { useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import dynamic from 'next/dynamic'; @@ -23,8 +23,7 @@ import ExclusiveCreate from 'components/ExclusiveContentCreate/ExclusiveCreate'; import Reveal from 'components/ExclusiveContentCreate/Reveal/Reveal'; import ExperienceListBarCreatePost from 'components/ExperienceList/ExperienceListBarCreatePost'; import { useExperienceList } from 'components/ExperienceList/hooks/use-experience-list.hook'; -import { ExperiencePost } from 'components/ExperiencePost'; -import { SearchBox } from 'components/atoms/Search'; +import { ExperienceTimelinePost } from 'components/ExperienceTimelinePost'; import useConfirm from 'components/common/Confirm/use-confirm.hook'; import { getEditorSelectors } from 'components/common/Editor/store'; import { useEnqueueSnackbar } from 'components/common/Snackbar/useEnqueueSnackbar.hook'; @@ -37,10 +36,15 @@ import { useSearchHook } from 'src/hooks/use-search.hooks'; import { useUpload } from 'src/hooks/use-upload.hook'; import { InfoIconYellow } from 'src/images/Icons'; import { ExclusiveContentPost } from 'src/interfaces/exclusive'; -import { ExperienceProps, WrappedExperience } from 'src/interfaces/experience'; +import { + ExperienceProps, + UserExperience, + WrappedExperience, +} from 'src/interfaces/experience'; import { Post, PostVisibility } from 'src/interfaces/post'; import { TimelineType } from 'src/interfaces/timeline'; import { User } from 'src/interfaces/user'; +import * as ExperienceAPI from 'src/lib/api/experience'; import i18n from 'src/locale'; import { RootState } from 'src/reducers'; import { createExclusiveContent } from 'src/reducers/timeline/actions'; @@ -93,6 +97,8 @@ export const PostCreate: React.FC<PostCreateProps> = props => { [], ); const [commonUser, setCommonUser] = useState<string[]>([]); + const [userExperiences, setUserExperiences] = useState<UserExperience[]>([]); + const [page, setPage] = useState<number>(1); const Editor = isMobile ? CKEditor : PlateEditor; @@ -332,7 +338,7 @@ export const PostCreate: React.FC<PostCreateProps> = props => { state => state.userState.anonymous, shallowEqual, ); - const { list: experiences } = useExperienceList(ExperienceOwner.CURRENT_USER); + const { list: experiences } = useExperienceList(ExperienceOwner.PERSONAL); const { searchUsers, users } = useSearchHook(); const { uploadImage } = useUpload(); const onImageUpload = async (files: File[]) => { @@ -359,6 +365,28 @@ export const PostCreate: React.FC<PostCreateProps> = props => { handleCloseExperience(); }; + const fetchUserExperiences = async () => { + const { meta, data: experiences } = await ExperienceAPI.getUserExperiences( + user.id, + 'personal', + page, + ); + + setUserExperiences([...userExperiences, ...experiences]); + + if (meta.currentPage < meta.totalPageCount) setPage(page + 1); + }; + + const resetExperiences = () => { + setPage(1); + setUserExperiences([]); + }; + + useEffect(() => { + if (open) fetchUserExperiences(); + else resetExperiences(); + }, [open, page]); + const handleRemoveExperience = (experienceId: string) => { removeExperience(experienceId, () => { loadExperience(); @@ -531,16 +559,13 @@ export const PostCreate: React.FC<PostCreateProps> = props => { onChange={handleVisibilityChange} /> </div> - <div className={styles.timelineVisibility}> - <SearchBox placeholder="Search Timeline" /> - </div> <div className={styles.warningVisibility}> <InfoIconYellow /> <Typography component="span" variant="body1" style={{ - fontWeight: 700, + fontWeight: 500, marginLeft: '10px', }}> Your post visibility will be visible to{' '} @@ -574,7 +599,7 @@ export const PostCreate: React.FC<PostCreateProps> = props => { </div> <ShowIf condition={showTimelineCreate}> <div className={styles.experienceCreate}> - <ExperiencePost + <ExperienceTimelinePost isEdit={false} experience={selectedExperience} tags={tags} @@ -591,11 +616,11 @@ export const PostCreate: React.FC<PostCreateProps> = props => { </ShowIf> {/* Select Timeline */} {/* Timeline list */} - <div className={styles.timelineVisibility}> + <div> <ExperienceListBarCreatePost onDelete={handleRemoveExperience} onUnsubscribe={handleUnsubscribeExperience} - experiences={experiences} + experiences={userExperiences} selectable={true} viewPostList={handleViewPostList} user={user} diff --git a/src/components/ProfileHeader/index.tsx b/src/components/ProfileHeader/index.tsx index ea80e3416..8b5d2b759 100644 --- a/src/components/ProfileHeader/index.tsx +++ b/src/components/ProfileHeader/index.tsx @@ -239,7 +239,7 @@ export const ProfileHeaderComponent: React.FC<Props> = props => { variant="body1" component="p" className={style.username}> - @{person.username || 'username'} + @{person.username || 'username'} halo </Typography> </div> </Grid> diff --git a/src/hooks/use-experience-hook.ts b/src/hooks/use-experience-hook.ts index dd9f09914..5e62c0163 100644 --- a/src/hooks/use-experience-hook.ts +++ b/src/hooks/use-experience-hook.ts @@ -44,6 +44,7 @@ export enum ExperienceOwner { PROFILE = 'profile', TRENDING = 'trending', DISCOVER = 'DISCOVER', + PERSONAL = 'personal', } //TODO: isn't it better to rename this to something more general like, useSearchHook? @@ -74,6 +75,8 @@ export const useExperienceHook = () => { { data: WrappedExperience[]; meta: ListMeta } >(state => state.userState.experiences, shallowEqual); + console.log(experiences, 'dataaa'); + const loadExperience = () => { dispatch(loadExperiences()); };