diff --git a/web/tag-farcaster-channel/create-farcaster-channel-tag-modal.css b/web/tag-farcaster-channel/create-farcaster-channel-tag-modal.css new file mode 100644 --- /dev/null +++ b/web/tag-farcaster-channel/create-farcaster-channel-tag-modal.css @@ -0,0 +1,20 @@ +.inputLabel { + color: var(--text-background-primary-default); + margin-bottom: 8px; +} + +.dropdownInputContainer { + display: flex; + flex-direction: column; + align-items: center; +} + +.errorMessageContainer { + height: 18px; +} + +.errorMessage { + text-align: center; + color: var(--text-background-danger-default); + font-size: var(--s-font-14); +} diff --git a/web/tag-farcaster-channel/create-farcaster-channel-tag-modal.react.js b/web/tag-farcaster-channel/create-farcaster-channel-tag-modal.react.js new file mode 100644 --- /dev/null +++ b/web/tag-farcaster-channel/create-farcaster-channel-tag-modal.react.js @@ -0,0 +1,136 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; + +import { useModalContext } from 'lib/components/modal-provider.react.js'; +import { NeynarClientContext } from 'lib/components/neynar-client-provider.react.js'; +import { + tagFarcasterChannelErrorMessages, + useCreateFarcasterChannelTag, +} from 'lib/shared/community-utils.js'; +import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; + +import css from './create-farcaster-channel-tag-modal.css'; +import Button, { buttonThemes } from '../components/button.react.js'; +import Dropdown, { type DropdownOption } from '../components/dropdown.react.js'; +import Modal from '../modals/modal.react.js'; + +type Props = { + +communityID: string, +}; + +function CreateFarcasterChannelTagModal(props: Props): React.Node { + const { communityID } = props; + + const { popModal } = useModalContext(); + + const fid = useCurrentUserFID(); + invariant(fid, 'FID should be set'); + + const neynarClientContext = React.useContext(NeynarClientContext); + invariant(neynarClientContext, 'NeynarClientContext is missing'); + + const { client } = neynarClientContext; + + const [channelOptions, setChannelOptions] = React.useState< + $ReadOnlyArray, + >([]); + const [selectedOption, setSelectedOption] = React.useState(null); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + void (async () => { + const channels = await client.fetchFollowedFarcasterChannels(fid); + + const sortedChannels = channels + .sort((a, b) => a.id.localeCompare(b.id)) + .map(channel => ({ + id: channel.id, + name: `/${channel.id}`, + })); + + setChannelOptions(sortedChannels); + })(); + }, [client, fid]); + + const onChangeSelectedOption = React.useCallback((option: string) => { + setError(null); + setSelectedOption(option); + }, []); + + const { createTag, isLoading } = useCreateFarcasterChannelTag( + communityID, + setError, + popModal, + ); + + const onClickTagChannel = React.useCallback(() => { + if (!selectedOption) { + return; + } + + createTag(selectedOption); + }, [createTag, selectedOption]); + + const buttonDisabled = isLoading || !selectedOption; + + const primaryButton = React.useMemo(() => { + return ( + + ); + }, [onClickTagChannel, buttonDisabled]); + + const errorMessage = React.useMemo(() => { + if (!error) { + return null; + } + + return ( +
+ {tagFarcasterChannelErrorMessages[error] ?? 'Unknown error.'} +
+ ); + }, [error]); + + const createFarcasterChannelTagModal = React.useMemo( + () => ( + +
Farcaster channel
+
+ +
+
{errorMessage}
+
+ ), + [ + channelOptions, + errorMessage, + onChangeSelectedOption, + popModal, + primaryButton, + selectedOption, + ], + ); + + return createFarcasterChannelTagModal; +} + +export default CreateFarcasterChannelTagModal; diff --git a/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js b/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js --- a/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js +++ b/web/tag-farcaster-channel/tag-farcaster-channel-modal.react.js @@ -5,8 +5,10 @@ import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { CommunityInfo } from 'lib/types/community-types.js'; +import CreateFarcasterChannelTagModal from './create-farcaster-channel-tag-modal.react.js'; import RemoveTagButton from './remove-tag-button.react.js'; import css from './tag-farcaster-channel-modal.css'; +import Button, { buttonThemes } from '../components/button.react.js'; import Modal from '../modals/modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; @@ -17,7 +19,7 @@ function TagFarcasterChannelModal(props: Props): React.Node { const { communityID } = props; - const { popModal } = useModalContext(); + const { popModal, pushModal } = useModalContext(); const communityInfo: ?CommunityInfo = useSelector( state => state.communityStore.communityInfos[communityID], @@ -25,6 +27,12 @@ const [removeTagError, setRemoveTagError] = React.useState(); + const openCreateFarcasterChannelTagModal = React.useCallback( + () => + pushModal(), + [communityID, pushModal], + ); + const channelNameTextContent = React.useMemo(() => { if (!communityInfo?.farcasterChannelID) { return ( @@ -49,9 +57,21 @@ /> ); } - // TODO: Implement TagChannelButton - return null; - }, [communityID, communityInfo?.farcasterChannelID]); + + return ( + + ); + }, [ + communityID, + communityInfo?.farcasterChannelID, + openCreateFarcasterChannelTagModal, + ]); const tagFarcasterChannelModal = React.useMemo( () => (