diff --git a/packages/atlas/src/api/queries/__generated__/nfts.generated.tsx b/packages/atlas/src/api/queries/__generated__/nfts.generated.tsx index 4d69c394e7..8c762dc274 100644 --- a/packages/atlas/src/api/queries/__generated__/nfts.generated.tsx +++ b/packages/atlas/src/api/queries/__generated__/nfts.generated.tsx @@ -2379,6 +2379,16 @@ export type GetFeaturedNftsVideosQuery = { }> } +export type RequestNftFeaturedMutationVariables = Types.Exact<{ + nftId: Types.Scalars['String'] + rationale: Types.Scalars['String'] +}> + +export type RequestNftFeaturedMutation = { + __typename?: 'Mutation' + requestNftFeatured: { __typename?: 'NftFeaturedRequstInfo'; rationale: string; nftId: string; createdAt: Date } +} + export const GetNftDocument = gql` query GetNft($id: String!) { ownedNftById(id: $id) { @@ -2587,3 +2597,50 @@ export type GetFeaturedNftsVideosQueryResult = Apollo.QueryResult< GetFeaturedNftsVideosQuery, GetFeaturedNftsVideosQueryVariables > +export const RequestNftFeaturedDocument = gql` + mutation RequestNftFeatured($nftId: String!, $rationale: String!) { + requestNftFeatured(nftId: $nftId, rationale: $rationale) { + rationale + nftId + createdAt + } + } +` +export type RequestNftFeaturedMutationFn = Apollo.MutationFunction< + RequestNftFeaturedMutation, + RequestNftFeaturedMutationVariables +> + +/** + * __useRequestNftFeaturedMutation__ + * + * To run a mutation, you first call `useRequestNftFeaturedMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRequestNftFeaturedMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [requestNftFeaturedMutation, { data, loading, error }] = useRequestNftFeaturedMutation({ + * variables: { + * nftId: // value for 'nftId' + * rationale: // value for 'rationale' + * }, + * }); + */ +export function useRequestNftFeaturedMutation( + baseOptions?: Apollo.MutationHookOptions +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useMutation( + RequestNftFeaturedDocument, + options + ) +} +export type RequestNftFeaturedMutationHookResult = ReturnType +export type RequestNftFeaturedMutationResult = Apollo.MutationResult +export type RequestNftFeaturedMutationOptions = Apollo.BaseMutationOptions< + RequestNftFeaturedMutation, + RequestNftFeaturedMutationVariables +> diff --git a/packages/atlas/src/api/queries/nfts.graphql b/packages/atlas/src/api/queries/nfts.graphql index e0415bea47..818c84f0c9 100644 --- a/packages/atlas/src/api/queries/nfts.graphql +++ b/packages/atlas/src/api/queries/nfts.graphql @@ -61,3 +61,11 @@ query GetFeaturedNftsVideos($limit: Int) { } } } + +mutation RequestNftFeatured($nftId: String!, $rationale: String!) { + requestNftFeatured(nftId: $nftId, rationale: $rationale) { + rationale + nftId + createdAt + } +} diff --git a/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeatureNftModal.tsx b/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeatureNftModal.tsx new file mode 100644 index 0000000000..161374b5bd --- /dev/null +++ b/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeatureNftModal.tsx @@ -0,0 +1,202 @@ +import { useApolloClient } from '@apollo/client' +import debouncePromise from 'awesome-debounce-promise' +import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useForm } from 'react-hook-form' + +import { + GetNftDocument, + GetNftQuery, + GetNftQueryVariables, + useRequestNftFeaturedMutation, +} from '@/api/queries/__generated__/nfts.generated' +import { Text } from '@/components/Text' +import { Input } from '@/components/_inputs/Input' +import { DialogModal } from '@/components/_overlays/DialogModal' +import { VideoTileViewer } from '@/components/_video/VideoTileViewer' +import { useSnackbar } from '@/providers/snackbars' +import { SentryLogger } from '@/utils/logs' + +import { PreviewWrapper, StyledFormField } from './FeaturedNftsSection.styles' + +type FeatureNftModalProps = { + isOpen: boolean + onClose: () => void +} + +export const FeatureNftModal: FC = ({ isOpen, onClose }) => { + const { + register, + handleSubmit, + trigger, + reset, + formState: { errors }, + } = useForm<{ url: string }>() + const [isInputValidating, setIsInputValidating] = useState(false) + + const handleClose = () => { + onClose() + setVideoId('') + reset({ url: '' }) + } + const [videoId, setVideoId] = useState('') + const [requestNftFeaturedMutation, { loading }] = useRequestNftFeaturedMutation({ + onError: (error) => { + displaySnackbar({ + title: 'Something went wrong', + description: 'There was a problem with sending your request. Please try again later.', + iconType: 'error', + }) + SentryLogger.error('Error during sending requestNftFeaturedMutation', 'FeatureNftModal', { videoId, error }) + }, + onCompleted: () => { + displaySnackbar({ + title: 'Video submitted succesfully', + description: 'Thanks! We will review your video shortly. Keep an eye on the featured section.', + iconType: 'success', + }) + handleClose() + }, + }) + + const { displaySnackbar } = useSnackbar() + + const inputRef = useRef(null) + + const submit = () => { + handleSubmit(async (data) => { + const videoId = /[^/]*$/.exec(data.url)?.[0] + if (!videoId) { + return + } + + await requestNftFeaturedMutation({ + variables: { + nftId: videoId, + rationale: '', + }, + }) + })() + } + + const client = useApolloClient() + + useEffect(() => { + if (isOpen) { + inputRef.current?.focus() + } + }, [isOpen]) + + const validateNft = useCallback( + async (id?: string) => { + if (!id) { + return 'Enter a valid link to your video NFT.' + } + const { + data: { ownedNftById }, + } = await client.query({ + query: GetNftDocument, + variables: { + id: id, + }, + }) + if (!ownedNftById) { + return 'This video is not an NFT.' + } + setVideoId(id) + if ( + ownedNftById.transactionalStatus?.__typename === 'TransactionalStatusIdle' || + ownedNftById.transactionalStatus?.__typename === 'TransactionalStatusInitiatedOfferToMember' + ) { + return "This video's NFT is not put on sale." + } + + if ( + ownedNftById.transactionalStatus?.__typename === 'TransactionalStatusAuction' && + ownedNftById.transactionalStatus.auction.isCompleted + ) { + return "This video's NFT is not put on sale." + } + return true + }, + [client] + ) + + const { ref, ...inputRefRest } = useMemo(() => { + return register('url', { + onChange: debouncePromise( + async () => { + await trigger('url') + setIsInputValidating(false) + }, + 500, + { + key() { + setIsInputValidating(true) + return null + }, + } + ), + required: { value: true, message: 'Enter link to your video NFT.' }, + validate: { + validUrl: (val) => { + return val.startsWith(window.location.origin + '/video/') ? true : 'Enter a valid link to your video NFT.' + }, + nftIsValid: async (val) => { + // get the last string after slash + const videoId = /[^/]*$/.exec(val)?.[0] + const validation = await validateNft(videoId) + + return validation + }, + }, + }) + }, [register, trigger, validateNft]) + + const isInputRefActiveElement = document.activeElement === inputRef.current + return ( + + + { + ref(e) + inputRef.current = e + }} + processing={isInputValidating} + autoComplete="off" + placeholder="Paste your link here" + error={!!errors.url} + /> + + + {videoId ? ( + + ) : ( + + Preview of your video will appear here + + )} + + + ) +} diff --git a/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.styles.ts b/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.styles.ts index 83b7db2875..9f748acfd8 100644 --- a/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.styles.ts +++ b/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.styles.ts @@ -1,9 +1,26 @@ import styled from '@emotion/styled' -import { sizes } from '@/styles' +import { FormField } from '@/components/_inputs/FormField' +import { cVar, sizes } from '@/styles' export const FeaturedNftsWrapper = styled.div` display: flex; flex-direction: column; gap: ${sizes(8)}; ` + +export const StyledFormField = styled(FormField)` + padding: ${sizes(6)} ${sizes(5)} ${sizes(5)}; +` + +export const PreviewWrapper = styled.div` + display: flex; + padding: ${sizes(5)}; + align-items: center; + justify-content: center; + height: 164px; + background: ${cVar('colorBackgroundMutedAlpha')}; + box-shadow: ${cVar('effectDividersBottom')}, ${cVar('effectDividersTop')}; + width: 100%; + margin-bottom: ${sizes(5)}; +` diff --git a/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.tsx b/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.tsx index 3631a1362a..8d0e7cbda1 100644 --- a/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.tsx +++ b/packages/atlas/src/views/viewer/MarketplaceView/FeaturedNftsSection/FeaturedNftsSection.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react' +import { FC, useState } from 'react' import { useNfts } from '@/api/hooks/nfts' import { OwnedNftOrderByInput } from '@/api/queries/__generated__/baseTypes.generated' @@ -13,6 +13,7 @@ import { useUser } from '@/providers/user/user.hooks' import { breakpoints } from '@/styles' import { createPlaceholderData } from '@/utils/data' +import { FeatureNftModal } from './FeatureNftModal' import { FeaturedNftsWrapper } from './FeaturedNftsSection.styles' const responsive: CarouselProps['breakpoints'] = { @@ -48,6 +49,7 @@ const responsive: CarouselProps['breakpoints'] = { export const FeaturedNftsSection: FC = () => { const { activeChannel } = useUser() + const [isFeatureNftModalOpen, setIsFeatureNfrModalOpen] = useState(false) const { nfts, loading } = useNfts({ variables: { @@ -75,6 +77,7 @@ export const FeaturedNftsSection: FC = () => { return ( + setIsFeatureNfrModalOpen(false)} /> {items.length >= 4 && (
{ actionButton={{ text: 'Submit your video NFT to be featured', onClick: () => { - // todo + setIsFeatureNfrModalOpen(true) }, }} />