Skip to content

Commit

Permalink
feat: Add new collection selector modal
Browse files Browse the repository at this point in the history
  • Loading branch information
LautaroPetaccio committed Jul 5, 2024
1 parent 3d0f94d commit 066c597
Show file tree
Hide file tree
Showing 23 changed files with 429 additions and 35 deletions.
20 changes: 10 additions & 10 deletions src/components/CollectionsPage/CollectionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,17 @@ export default function CollectionsPage(props: Props) {
}
}, [address, sort])

const handleNewCollection = useCallback(() => {
onOpenModal('CreateCollectionModal')
}, [onOpenModal])

const handleNewThirdPartyCollection = useCallback(() => {
onOpenModal('CreateLinkedWearablesCollectionModal')
}, [onOpenModal, isLinkedWearablesV2Enabled])

const handleNewCollection = useCallback(() => {
if (isLinkedWearablesV2Enabled) {
onOpenModal('CreateLinkedWearablesCollectionModal')
onOpenModal('CreateCollectionSelectorModal')
} else {
onOpenModal('CreateThirdPartyCollectionModal')
onOpenModal('CreateCollectionModal')
}
}, [onOpenModal])
}, [onOpenModal, isLinkedWearablesV2Enabled])

const handleSearchChange = (_evt: React.ChangeEvent<HTMLInputElement>, data: InputOnChangeData) => {
setSearch(data.value)
Expand Down Expand Up @@ -221,7 +221,7 @@ export default function CollectionsPage(props: Props) {
isClearable
/>
<Row className="actions" grow={false}>
{isThirdPartyManager && (
{isThirdPartyManager && !isLinkedWearablesV2Enabled && (
<Button className="action-button" size="small" basic onClick={handleNewThirdPartyCollection}>
{t('collections_page.new_third_party_collection')}
</Button>
Expand All @@ -231,12 +231,12 @@ export default function CollectionsPage(props: Props) {
{t('item_editor.open')}
</Button>
<Button className="action-button" size="small" primary onClick={handleNewCollection}>
{t('collections_page.new_collection')}
<UIIcon name="plus square" /> {t('collections_page.new_collection')}
</Button>
</Row>
</div>
)
}, [search, isThirdPartyManager, handleSearchChange, handleNewThirdPartyCollection, handleOpenEditor, handleNewCollection])
}, [search, isThirdPartyManager, handleSearchChange, handleOpenEditor, handleNewCollection])

const renderViewActions = useCallback(() => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { connect } from 'react-redux'
import { getAddress } from 'decentraland-dapps/dist/modules/wallet/selectors'
import { isLoadingType } from 'decentraland-dapps/dist/modules/loading/selectors'
import { openModal } from 'decentraland-dapps/dist/modules/modal'
import { RootState } from 'modules/common/types'
import { getLoading, getError } from 'modules/collection/selectors'
import { SAVE_COLLECTION_REQUEST, saveCollectionRequest } from 'modules/collection/actions'
import { MapStateProps, MapDispatchProps, MapDispatch } from './CreateCollectionModal.types'
import { MapStateProps, MapDispatchProps, MapDispatch, OwnProps } from './CreateCollectionModal.types'
import CreateCollectionModal from './CreateCollectionModal'
import { getIsLinkedWearablesV2Enabled } from 'modules/features/selectors'

const mapState = (state: RootState): MapStateProps => ({
address: getAddress(state),
isLoading: isLoadingType(getLoading(state), SAVE_COLLECTION_REQUEST),
isLinkedWearablesV2Enabled: getIsLinkedWearablesV2Enabled(state),
error: getError(state)
})

const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({
const mapDispatch = (dispatch: MapDispatch, ownProps: OwnProps): MapDispatchProps => ({
onBack: () => {
ownProps.onClose()
dispatch(openModal('CreateCollectionSelectorModal'))
},
onSubmit: collection => dispatch(saveCollectionRequest(collection))
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default class CreateCollectionModal extends React.PureComponent<Props, St
}

render() {
const { name, onClose, isLoading, error } = this.props
const { name, onClose, onBack, isLoading, error, isLinkedWearablesV2Enabled } = this.props
const { collectionName } = this.state
const isDisabled = !collectionName || isLoading

Expand All @@ -48,8 +48,13 @@ export default class CreateCollectionModal extends React.PureComponent<Props, St
}

return (
<Modal name={name} onClose={onClose} size="tiny">
<ModalNavigation title={t('create_collection_modal.title')} subtitle={t('create_collection_modal.subtitle')} onClose={onClose} />
<Modal name={name} onClose={onClose} size="small">
<ModalNavigation
title={t('create_collection_modal.title')}
subtitle={t('create_collection_modal.subtitle')}
onClose={onClose}
onBack={isLinkedWearablesV2Enabled ? onBack : undefined}
/>
<Form onSubmit={this.handleSubmit} disabled={isDisabled}>
<ModalContent>
<Field
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { Dispatch } from 'redux'
import { ModalProps } from 'decentraland-dapps/dist/providers/ModalProvider/ModalProvider.types'
import { SaveCollectionRequestAction, saveCollectionRequest } from 'modules/collection/actions'
import { OpenModalAction } from 'decentraland-dapps/dist/modules/modal'

export type Props = ModalProps & {
address?: string
isLoading: boolean
onSubmit: typeof saveCollectionRequest
onBack: () => void
isLinkedWearablesV2Enabled: boolean
error: string | null
}

export type State = {
collectionName: string
}

export type MapStateProps = Pick<Props, 'address' | 'isLoading' | 'error'>
export type MapDispatchProps = Pick<Props, 'onSubmit'>
export type MapDispatch = Dispatch<SaveCollectionRequestAction>
export type MapStateProps = Pick<Props, 'address' | 'isLoading' | 'error' | 'isLinkedWearablesV2Enabled'>
export type MapDispatchProps = Pick<Props, 'onSubmit' | 'onBack'>
export type OwnProps = Pick<Props, 'metadata' | 'onClose' | 'name'>
export type MapDispatch = Dispatch<SaveCollectionRequestAction | OpenModalAction>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { connect } from 'react-redux'
import { openModal } from 'decentraland-dapps/dist/modules/modal'
import { isLoadingThirdParties, isThirdPartyManager } from 'modules/thirdParty/selectors'
import { MapDispatchProps, MapDispatch, OwnProps, MapStateProps } from './CreateCollectionSelectorModal.types'
import { CreateCollectionSelectorModal } from './CreateCollectionSelectorModal'
import { RootState } from 'modules/common/types'

const mapState = (state: RootState): MapStateProps => ({
isThirdPartyManager: isThirdPartyManager(state),
isLoadingThirdParties: isLoadingThirdParties(state)
})

const mapDispatch = (dispatch: MapDispatch, ownProps: OwnProps): MapDispatchProps => ({
onCreateCollection: () => {
ownProps.onClose()
dispatch(openModal('CreateCollectionModal'))
},
onCreateLinkedWearablesCollection: () => {
ownProps.onClose()
dispatch(openModal('CreateLinkedWearablesCollectionModal'))
}
})

export default connect(mapState, mapDispatch)(CreateCollectionSelectorModal)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.modalContent {
display: flex;
flex-direction: row;
gap: 16px;
}

.collectionSelection {
position: relative;
flex: 1 1 0px;
display: flex;
flex-direction: column;
align-items: center;
padding: 33px;
background-color: #2e2c33;
border-radius: 8px;
}

.collectionSelection .disabled {
width: 100%;
height: 100%;
border-radius: 8px;
position: absolute;
top: 0;
left: 0;
opacity: 0.3;
background-color: black;
}

.collectionSelection img {
width: 133px;
height: 133px;
}

.collectionSelection .content {
display: flex;
flex-direction: column;
flex-grow: 1;
}

.collectionSelection .content .text {
text-align: center;
margin-top: 32px;
}

.collectionSelection .actions {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 15px;
margin-top: 24px;
}

.collectionSelection .actions a {
font-size: 14px;
text-transform: uppercase;
z-index: 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { renderWithProviders } from 'specs/utils'
import { CreateCollectionSelectorModal } from './CreateCollectionSelectorModal'
import { Props } from './CreateCollectionSelectorModal.types'
import { CREATE_BUTTON_TEST_ID, DISABLED_DATA_TEST_ID } from './constants'
import userEvent from '@testing-library/user-event'

export function renderWorldContributorTab(props: Partial<Props>) {
return renderWithProviders(
<CreateCollectionSelectorModal
onCreateCollection={jest.fn()}
onCreateLinkedWearablesCollection={jest.fn()}
metadata={{}}
name="aName"
onClose={jest.fn()}
isLoadingThirdParties={false}
isThirdPartyManager={false}
{...props}
/>
)
}

describe('when clicking on the create collection button', () => {
let renderedComponent: ReturnType<typeof renderWorldContributorTab>
let onCreateCollection: jest.Mock
let onCreateLinkedWearablesCollection: jest.Mock

beforeEach(() => {
onCreateCollection = jest.fn()
onCreateLinkedWearablesCollection = jest.fn()
renderedComponent = renderWorldContributorTab({
onCreateCollection,
onCreateLinkedWearablesCollection,
isThirdPartyManager: true
})
})

describe('and the button belongs to the classic collections', () => {
let createButton: HTMLElement
beforeEach(() => {
createButton = renderedComponent.getAllByTestId(CREATE_BUTTON_TEST_ID)[0]
userEvent.click(createButton)
})

it('should call the the onCreateCollection prop method', () => {
expect(onCreateCollection).toHaveBeenCalled()
})
})

describe('and the button belongs to the linked collections', () => {
beforeEach(() => {
const createButton = renderedComponent.getAllByTestId(CREATE_BUTTON_TEST_ID)[1]
userEvent.click(createButton)
})

it('should call the onCreateLinkedWearablesCollection method prop', () => {
expect(onCreateLinkedWearablesCollection).toHaveBeenCalled()
})
})
})

describe('and the linked collections are being loaded', () => {
let renderedComponent: ReturnType<typeof renderWorldContributorTab>
beforeEach(() => {
renderedComponent = renderWorldContributorTab({ isLoadingThirdParties: true })
})

it('should show the disabled overlay for the linked collections', () => {
const disabledOverlay = renderedComponent.getByTestId(DISABLED_DATA_TEST_ID)
expect(disabledOverlay).toBeInTheDocument()
})

it('should disable the create button for the linked collections', () => {
const createButton = renderedComponent.getAllByTestId(CREATE_BUTTON_TEST_ID)[1]
expect(createButton).toBeDisabled()
})

it('should set the button as loading', () => {
const createButton = renderedComponent.getAllByTestId(CREATE_BUTTON_TEST_ID)[1]
expect(createButton).toHaveClass('loading')
})
})

describe('and the user is not a third party manager', () => {
let renderedComponent: ReturnType<typeof renderWorldContributorTab>
beforeEach(() => {
renderedComponent = renderWorldContributorTab({ isThirdPartyManager: false })
})

it('should show the disabled overlay for the linked collections', () => {
const disabledOverlay = renderedComponent.getByTestId(DISABLED_DATA_TEST_ID)
expect(disabledOverlay).toBeInTheDocument()
})

it('should disable the create button for the linked collections', () => {
const createButton = renderedComponent.getAllByTestId(CREATE_BUTTON_TEST_ID)[1]
expect(createButton).toBeDisabled()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Button, ModalContent, ModalNavigation } from 'decentraland-ui'
import classNames from 'classnames'
import Modal from 'decentraland-dapps/dist/containers/Modal'
import { t } from 'decentraland-dapps/dist/modules/translation'
import { config } from 'config'
import ethereumSvg from '../../../icons/ethereum.svg'
import polygonSvg from '../../../icons/polygon.svg'
import { Props } from './CreateCollectionSelectorModal.types'
import styles from './CreateCollectionSelectorModal.module.css'
import { CREATE_BUTTON_TEST_ID, DISABLED_DATA_TEST_ID } from './constants'

const CollectionSelection = ({
image,
title,
subtitle,
disabled,
isLoading,
onCreate,
learnMoreUrl
}: {
image: string
title: string
subtitle: string
disabled?: boolean
isLoading?: boolean
learnMoreUrl: string
onCreate: () => void
}) => {
return (
<div className={classNames(styles.collectionSelection)}>
{disabled && <div data-testid={DISABLED_DATA_TEST_ID} className={styles.disabled}></div>}
<img src={image} alt={title} />
<div className={styles.content}>
<div className={styles.text}>
<h2>{title}</h2>
<p>{subtitle}</p>
</div>
</div>
<div className={styles.actions}>
<Button data-testid={CREATE_BUTTON_TEST_ID} primary disabled={disabled} loading={isLoading} onClick={onCreate}>
{t('create_collection_selector_modal.actions.create')}
</Button>
<a href={learnMoreUrl}>{t('create_collection_selector_modal.actions.learn_more')}</a>
</div>
</div>
)
}
const COLLECTIONS_LEARN_MORE_URL = `${config.get('DOCS_URL')}/creator/wearables-and-emotes/manage-collections/creating-collection/`
const LINKED_COLLECTIONS_LEARN_MORE_URL = `${config.get('DOCS_URL')}/creator/wearables/linked-wearables/`

export const CreateCollectionSelectorModal = (props: Props) => {
const { onClose, onCreateCollection, onCreateLinkedWearablesCollection, name, isThirdPartyManager, isLoadingThirdParties } = props

return (
<Modal name={name} onClose={onClose} size="small">
<ModalNavigation
title={t('create_collection_selector_modal.title')}
subtitle={t('create_collection_selector_modal.subtitle')}
onClose={onClose}
/>
<ModalContent>
<div className={styles.modalContent}>
<CollectionSelection
// Temporary image for the collections
image={ethereumSvg}
title={t('create_collection_selector_modal.collection.title')}
subtitle={t('create_collection_selector_modal.collection.subtitle')}
onCreate={onCreateCollection}
learnMoreUrl={COLLECTIONS_LEARN_MORE_URL}
/>
<CollectionSelection
// Temporary image for the linked wearables collections
image={polygonSvg}
title={t('create_collection_selector_modal.linked_collection.title')}
subtitle={t('create_collection_selector_modal.linked_collection.subtitle')}
onCreate={onCreateLinkedWearablesCollection}
isLoading={isLoadingThirdParties}
disabled={!isThirdPartyManager || isLoadingThirdParties}
learnMoreUrl={LINKED_COLLECTIONS_LEARN_MORE_URL}
/>
</div>
</ModalContent>
</Modal>
)
}
Loading

0 comments on commit 066c597

Please sign in to comment.