From 99d6b2d059291a0bbbc15f6f80a1ffe7189a1195 Mon Sep 17 00:00:00 2001 From: Kristian Ruben <56469224+rubenkristian@users.noreply.github.com> Date: Wed, 8 Mar 2023 05:53:31 +0700 Subject: [PATCH 1/7] fix: set debio tips (#1753) feat: set debio tips --- src/components/Tip/Tip.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Tip/Tip.tsx b/src/components/Tip/Tip.tsx index c966a4b42..05f732c0f 100644 --- a/src/components/Tip/Tip.tsx +++ b/src/components/Tip/Tip.tsx @@ -19,6 +19,7 @@ import { MyriadCircleIcon, PolkadotNetworkIcon, KusamaNetworkIcon, + DebioNetworkIcon, } from 'src/components/atoms/Icons'; import ShowIf from 'src/components/common/show-if.component'; import { TipsResult } from 'src/interfaces/blockchain-interface'; @@ -59,6 +60,10 @@ const networkOptions: MenuOptions[] = [ id: 'myriad', title: 'Myriad', }, + { + id: 'debio', + title: 'Debio', + }, ]; export const Tip: React.FC = props => { @@ -83,6 +88,7 @@ export const Tip: React.FC = props => { kusama: , near: , myriad: , + debio: , }), [], ); From 6a9afd4f6a6c762b72c698d27999e73b2545e714 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Mar 2023 05:54:41 +0700 Subject: [PATCH 2/7] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cicd.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 325092073..9d528019d 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -101,7 +101,7 @@ jobs: fetch-depth: 0 - name: Release id: release - uses: google-github-actions/release-please-action@d3c71f9a0a55385580de793de58da057b3560862 + uses: google-github-actions/release-please-action@e0b9d1885d92e9a93d5ce8656de60e3b806e542c with: token: ${{ secrets.PAT }} fork: true From 3c4f9696933e21df18755fb9090b032b81468db3 Mon Sep 17 00:00:00 2001 From: alvin371 Date: Thu, 13 Apr 2023 07:20:06 +0700 Subject: [PATCH 3/7] feat: create post to timeline directly --- .../ExperienceListBarCreatePost.tsx | 89 ++ .../ExperiencePost/Experience.container.tsx | 83 ++ .../ExperiencePost/Experience.styles.ts | 178 ++++ .../ExperienceEditor.stories.tsx | 106 ++ .../ExperiencePost/ExperiencePost.tsx | 929 ++++++++++++++++++ src/components/ExperiencePost/index.ts | 1 + .../PostCreate/PostCreate.styles.ts | 45 +- src/components/PostCreate/PostCreate.tsx | 217 +++- src/components/Timeline/Timeline.layout.tsx | 1 + 9 files changed, 1623 insertions(+), 26 deletions(-) create mode 100644 src/components/ExperienceList/ExperienceListBarCreatePost.tsx create mode 100644 src/components/ExperiencePost/Experience.container.tsx create mode 100644 src/components/ExperiencePost/Experience.styles.ts create mode 100644 src/components/ExperiencePost/ExperienceEditor.stories.tsx create mode 100644 src/components/ExperiencePost/ExperiencePost.tsx create mode 100644 src/components/ExperiencePost/index.ts diff --git a/src/components/ExperienceList/ExperienceListBarCreatePost.tsx b/src/components/ExperienceList/ExperienceListBarCreatePost.tsx new file mode 100644 index 000000000..ae5e03536 --- /dev/null +++ b/src/components/ExperienceList/ExperienceListBarCreatePost.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; + +import { Experience as ExperienceCard } from '../ExpericenceRightBar'; +import { useStyles } from './ExperienceList.style'; + +import { WrappedExperience } from 'src/interfaces/experience'; +import { TimelineType } from 'src/interfaces/timeline'; +import { User } from 'src/interfaces/user'; + +type ExperienceListProps = { + experiences: WrappedExperience[]; + isOnHomePage?: boolean; + user?: User; + anonymous?: boolean; + selectable: boolean; + viewPostList: (type: TimelineType, userExperience: WrappedExperience) => void; + onSubscribe?: (experienceId: string) => void; + onClone?: (experienceId: string) => void; + onPreview?: (experienceId: string) => void; + onDelete?: (experienceId: string) => void; + onUnsubscribe?: (experienceId: string) => void; +}; + +export const ExperienceListBarCreatePost: React.FC = + props => { + const { + experiences, + user, + anonymous = false, + selectable, + viewPostList, + onDelete, + onUnsubscribe, + onSubscribe, + onClone, + } = props; + + const classes = useStyles(); + const [selectedExperienceIds, setSelectedExperienceIds] = useState< + string[] + >([]); + + //TODO: still unable to only select one experience card + const handleSelectExperience = + (userExperience: WrappedExperience) => () => { + if (userExperience) { + if (selectedExperienceIds.includes(userExperience.experience.id)) { + setSelectedExperienceIds( + selectedExperienceIds.filter( + id => id !== userExperience.experience.id, + ), + ); + } else { + setSelectedExperienceIds([ + ...selectedExperienceIds, + userExperience.experience.id, + ]); + } + } + + viewPostList(TimelineType.EXPERIENCE, userExperience); + }; + + return ( +
+ {experiences.map(item => ( +
+ +
+ ))} +
+ ); + }; + +export default ExperienceListBarCreatePost; diff --git a/src/components/ExperiencePost/Experience.container.tsx b/src/components/ExperiencePost/Experience.container.tsx new file mode 100644 index 000000000..542aefe98 --- /dev/null +++ b/src/components/ExperiencePost/Experience.container.tsx @@ -0,0 +1,83 @@ +import React from 'react'; + +import { useRouter } from 'next/router'; + +import { useStyles } from './Experience.styles'; +import { ExperiencePost } from './ExperiencePost'; + +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 ( + <> +
+ +
+
+ +
+ + ); +}; diff --git a/src/components/ExperiencePost/Experience.styles.ts b/src/components/ExperiencePost/Experience.styles.ts new file mode 100644 index 000000000..47b8e8295 --- /dev/null +++ b/src/components/ExperiencePost/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/ExperiencePost/ExperienceEditor.stories.tsx b/src/components/ExperiencePost/ExperienceEditor.stories.tsx new file mode 100644 index 000000000..333831e71 --- /dev/null +++ b/src/components/ExperiencePost/ExperienceEditor.stories.tsx @@ -0,0 +1,106 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import React from 'react'; + +import { ExperiencePost } from '.'; +import { SocialsEnum } from '../../interfaces/social'; + +export default { + title: 'UI Revamp v2.0/components/Experience Editor', + component: ExperiencePost, + argTypes: {}, +} as ComponentMeta; + +const Template: ComponentStory = 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/ExperiencePost/ExperiencePost.tsx b/src/components/ExperiencePost/ExperiencePost.tsx new file mode 100644 index 000000000..9e66d9941 --- /dev/null +++ b/src/components/ExperiencePost/ExperiencePost.tsx @@ -0,0 +1,929 @@ +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; + 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 ExperiencePost: React.FC = 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(); + const [newExperience, setNewExperience] = + useState(experience); + const [image, setImage] = useState( + experience.experienceImageURL, + ); + const [, setDetailChanged] = useState(false); + const [isLoading, setIsloading] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + const [selectedVisibility, setSelectedVisibility] = + useState(); + const [selectedUserIds, setSelectedUserIds] = useState([]); + const [pageUserIds, setPageUserIds] = React.useState(1); + const [isLoadingSelectedUser, setIsLoadingSelectedUser] = + useState(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) => { + const debounceSubmit = debounce(() => { + onSearchTags(event.target.value); + }, 300); + + debounceSubmit(); + }; + + const handleSearchPeople = (event: React.ChangeEvent) => { + 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) => { + 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) => { + 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 ( +
+
+ + + Save + + + Cancel + + +
+
+
+ + + +
+ +
+
+
+
+
+ + + {i18n.t('Experience.Editor.Subtitle_1')} + + + + {i18n.t('Experience.Editor.Helper.Name')} + + + {newExperience?.name.length ?? 0}/50 + + + + option.name} + getOptionSelected={(option, value) => option?.id === value.id} + autoHighlight={false} + disableClearable + onChange={handleVisibilityChange} + value={selectedVisibility || null} + popupIcon={ + + } + renderInput={({ inputProps, ...rest }) => ( + + )} + /> + + {selectedVisibility?.id === 'selected_user' && ( + <> + option.id === value.id} + filterSelectedOptions={true} + getOptionLabel={option => `${option.username} ${option.name}`} + disableClearable + autoHighlight={false} + popupIcon={ + + } + onChange={handleVisibilityPeopleChange} + renderTags={() => null} + renderInput={params => ( + + {params.InputProps.endAdornment} + + ), + }} + helperText={i18n.t('Experience.Editor.Helper.People')} + /> + )} + renderOption={( + option, + state: AutocompleteRenderOptionState, + ) => { + if (option.id === '') return null; + return ( +
+ + @{option.username} + + } + avatar={option.profilePictureURL} + platform={'myriad'} + action={ + + {state.selected ? ( + + ) : ( + + )} + + } + /> +
+ ); + }} + /> + +
+
+ + + + {selectedUserIds + .filter(people => !isEmpty(people.id)) + .map(people => ( + + @{people.username} + + } + avatar={people.profilePictureURL} + platform={'myriad'} + action={ + + + + } + /> + ))} +
+
+ + )} + + + + {i18n.t('Experience.Editor.Subtitle_2')} + + + +   + + + {newExperience?.description?.length ?? 0}/280 + + + + + 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 => ( + + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + 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 => ( + + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + option.id === value.id} + filterSelectedOptions={true} + getOptionLabel={option => `${option.username} ${option.name}`} + disableClearable + autoHighlight={false} + popupIcon={ + + } + onChange={handlePeopleChange} + renderTags={() => null} + renderInput={params => ( + + {params.InputProps.endAdornment} + + ), + }} + helperText={''} + /> + )} + renderOption={(option, state: AutocompleteRenderOptionState) => { + if (option.id === '') return null; + return ( +
+ + @{option.username} + + } + avatar={option.profilePictureURL} + platform={option.platform} + action={ + + {state.selected ? ( + + ) : ( + + )} + + } + /> +
+ ); + }} + /> + +
+ {newExperience.people + .filter(people => !isEmpty(people.id)) + .map(people => ( + + @{people.username} + + } + avatar={people.profilePictureURL} + platform={people.platform} + action={ + + + + } + /> + ))} +
+
+
+
+ ); +}; diff --git a/src/components/ExperiencePost/index.ts b/src/components/ExperiencePost/index.ts new file mode 100644 index 000000000..54354df8a --- /dev/null +++ b/src/components/ExperiencePost/index.ts @@ -0,0 +1 @@ +export * from './ExperiencePost'; diff --git a/src/components/PostCreate/PostCreate.styles.ts b/src/components/PostCreate/PostCreate.styles.ts index c5a09489c..94f90d218 100644 --- a/src/components/PostCreate/PostCreate.styles.ts +++ b/src/components/PostCreate/PostCreate.styles.ts @@ -43,7 +43,20 @@ export const useStyles = makeStyles((theme: Theme) => }, }, action: { - padding: 30, + padding: '30px 15px', + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + position: 'relative', + [theme.breakpoints.down('xs')]: { + flexDirection: 'column', + alignItems: 'flex-start', + padding: 0, + }, + }, + timelineVisibility: { + padding: '0px 15px', + margin: '20px 0px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', @@ -54,6 +67,36 @@ export const useStyles = makeStyles((theme: Theme) => padding: 0, }, }, + timelineCreate: { + padding: '0px 15px', + margin: '20px 0px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + position: 'relative', + [theme.breakpoints.down('xs')]: { + flexDirection: 'column', + alignItems: 'flex-start', + padding: 0, + }, + }, + experienceCreate: { + margin: '20px auto', + }, + warningVisibility: { + padding: '10px 15px', + margin: '20px 0px', + display: 'flex', + alignItems: 'center', + position: 'relative', + [theme.breakpoints.down('xs')]: { + flexDirection: 'column', + alignItems: 'flex-start', + padding: 0, + }, + backgroundColor: 'rgba(255, 210, 77, 0.2)', + borderRadius: '10px', + }, option: { display: 'flex', alignItems: 'center', diff --git a/src/components/PostCreate/PostCreate.tsx b/src/components/PostCreate/PostCreate.tsx index f2b49509f..856c9ab9d 100644 --- a/src/components/PostCreate/PostCreate.tsx +++ b/src/components/PostCreate/PostCreate.tsx @@ -1,7 +1,7 @@ import { ArrowLeftIcon, GiftIcon, TrashIcon } from '@heroicons/react/outline'; import React, { useRef, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import dynamic from 'next/dynamic'; @@ -21,15 +21,28 @@ import { serialize } from './formatter'; 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 useConfirm from 'components/common/Confirm/use-confirm.hook'; import { getEditorSelectors } from 'components/common/Editor/store'; import { useEnqueueSnackbar } from 'components/common/Snackbar/useEnqueueSnackbar.hook'; import { ExclusiveContent } from 'components/common/Tipping/Tipping.interface'; +import { debounce } from 'lodash'; import ShowIf from 'src/components/common/show-if.component'; +import { ExperienceOwner } from 'src/hooks/use-experience-hook'; +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 { InfoIconYellow } from 'src/images/Icons'; import { ExclusiveContentPost } from 'src/interfaces/exclusive'; +import { ExperienceProps, 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 i18n from 'src/locale'; +import { RootState } from 'src/reducers'; import { createExclusiveContent } from 'src/reducers/timeline/actions'; const CKEditor = dynamic(() => import('../common/CKEditor/Editor'), { @@ -74,6 +87,12 @@ export const PostCreate: React.FC = props => { const [exclusiveContent, setExclusiveContent] = useState(null); const [showExclusive, setShowExclusive] = useState(false); + const [showTimelineCreate, setShowTimelineCreate] = useState(false); + const [timelineId, setTimelineId] = useState([]); + const [experiencesVisibility, setExperienceVisibility] = useState( + [], + ); + const [commonUser, setCommonUser] = useState([]); const Editor = isMobile ? CKEditor : PlateEditor; @@ -183,7 +202,7 @@ export const PostCreate: React.FC = props => { selectedUserIds: post.selectedUserIds, NSFWTag: post.NSFWTag, visibility: post.visibility ?? PostVisibility.PUBLIC, - selectedTimelineIds: post.selectedTimelineIds, + selectedTimelineIds: timelineId, }); } } @@ -252,6 +271,7 @@ export const PostCreate: React.FC = props => { setShowExclusive(false); setExclusiveContent(null); setEditorValue(''); + setTimelineId([]); onClose(); }; @@ -296,6 +316,82 @@ export const PostCreate: React.FC = props => { subtitle, }; }; + const { + selectedExperience, + tags, + people, + searchTags, + searchPeople, + saveExperience, + removeExperience, + loadExperience, + unsubscribeExperience, + } = useExperienceHook(); + const anonymous = useSelector( + state => state.userState.anonymous, + shallowEqual, + ); + const { list: experiences } = useExperienceList(ExperienceOwner.CURRENT_USER); + const { searchUsers, users } = useSearchHook(); + const { uploadImage } = useUpload(); + const onImageUpload = async (files: File[]) => { + const url = await uploadImage(files[0]); + + return url ?? ''; + }; + const handleSearchTags = debounce((query: string) => { + searchTags(query); + }, 300); + + const handleSearchPeople = debounce((query: string) => { + searchPeople(query); + }, 300); + + const handleCloseExperience = () => { + setShowTimelineCreate(false); + }; + const handleSearchUser = debounce((query: string) => { + searchUsers(query); + }, 300); + const onSave = (attributes: ExperienceProps) => { + saveExperience(attributes); + handleCloseExperience(); + }; + + const handleRemoveExperience = (experienceId: string) => { + removeExperience(experienceId, () => { + loadExperience(); + }); + }; + const handleUnsubscribeExperience = (userExperienceId: string) => { + unsubscribeExperience(userExperienceId); + }; + + const handleViewPostList = ( + type: TimelineType, + userExperience: WrappedExperience, + ) => { + if (timelineId.includes(userExperience.experience.id)) { + setTimelineId( + timelineId.filter(id => id !== userExperience.experience.id), + ); + setExperienceVisibility( + experiencesVisibility.filter( + visibility => visibility !== userExperience.experience.visibility, + ), + ); + setCommonUser( + commonUser.filter(user => user !== userExperience.experience.user.name), + ); + } else { + setTimelineId([...timelineId, userExperience.experience.id]); + setExperienceVisibility([ + ...experiencesVisibility, + userExperience.experience.visibility, + ]); + setCommonUser([...commonUser, userExperience.experience.user.name]); + } + }; return ( = props => { onError={handleErrorImport} /> - -
+
-
- - title={i18n.t('Post_Create.Visibility.Label')} - options={menuOptions} - useIconOnMobile={false} - onChange={handleVisibilityChange} - /> - - -
- {!exclusiveContent ? ( <> @@ -429,13 +509,100 @@ export const PostCreate: React.FC = props => { )} - - - - +
+
+
+ + Select Timeline to Post + + + title={'Filter Timeline'} + options={menuOptions} + useIconOnMobile={false} + onChange={handleVisibilityChange} + /> +
+
+ +
+
+ + + Your post visibility will be visible to{' '} + {experiencesVisibility.includes('private') + ? 'Only Me' + : experiencesVisibility.includes('selected_user') + ? 'Custom' + : experiencesVisibility.includes('friend') + ? 'Friends' + : 'Public'}{' '} + +
+
+ + {timelineId.length} of {experiences.length} Selected Timelines + + +
+ +
+ +
+
+ {/* Select Timeline */} + {/* Timeline list */} +
+ +
+