Skip to content

Commit

Permalink
feat: add "try it" functionality (decentraland#562)
Browse files Browse the repository at this point in the history
* feat: add "try it" functionality

* chore: translations

* fix: disable try it button when no representation available

* feat: add emote

* feat: handle invalid color values

* feat: new UI for the preview

* feat: added tooltips

* fix: show toggle when signed out

* feat: grey out disabled toggle
  • Loading branch information
cazala authored Mar 10, 2022
1 parent 9e2ee98 commit 03b87a8
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 37 deletions.
29 changes: 23 additions & 6 deletions webapp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"dcl-crypto": "^2.2.0",
"decentraland-dapps": "^12.38.1",
"decentraland-transactions": "^1.29.1",
"decentraland-ui": "^3.21.0",
"decentraland-ui": "^3.22.0",
"dotenv": "^10.0.0",
"graphql": "^14.7.0",
"history": "^4.10.1",
Expand Down
47 changes: 47 additions & 0 deletions webapp/src/components/AssetImage/AssetImage.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
.AssetImage .image {
margin: auto;
max-height: 100%;
z-index: 0;
}

.AssetImage .rarity-background {
Expand Down Expand Up @@ -91,3 +92,49 @@
.AssetImage .ui.loader.wearable-preview-loader:after {
border-color: white transparent transparent !important;
}

.AssetImage .preview-toggle-wrapper {
z-index: 1;
position: absolute;
right: 24px;
top: 24px;
}

.AssetImage .preview-toggle-wrapper .preview-toggle {
width: 40px;
min-width: 40px !important;
height: 32px;
border-radius: 0px;
background-repeat: no-repeat;
background-position: center;
transform: none !important;
}

.AssetImage .preview-toggle-wrapper .preview-toggle:first-child {
border-radius: 8px 0px 0px 8px;
}

.AssetImage .preview-toggle-wrapper .preview-toggle:last-child {
border-radius: 0px 8px 8px 0px;
}

.AssetImage .preview-toggle-wrapper .preview-toggle.preview-toggle-wearable {
background-image: url(../../images/preview-wearable-icon.svg);
}

.AssetImage .preview-toggle-wrapper .preview-toggle.preview-toggle-avatar {
background-image: url(../../images/preview-avatar-icon.svg);
}

.AssetImage .preview-toggle-wrapper .preview-toggle.is-active {
background-color: var(--primary);
}

.AssetImage .preview-toggle-wrapper .preview-toggle.is-disabled {
cursor: not-allowed;
opacity: 0.5;
}

.AssetImage .preview-toggle-wrapper .preview-toggle.is-disabled:hover {
background-color: var(--secondary);
}
155 changes: 134 additions & 21 deletions webapp/src/components/AssetImage/AssetImage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { useCallback, useMemo, useState } from 'react'
import { NFTCategory, Rarity } from '@dcl/schemas'
import { Center, Loader, WearablePreview } from 'decentraland-ui'
import { LazyImage } from 'react-lazy-images'
import classNames from 'classnames'
import { BodyShape, NFTCategory, Rarity } from '@dcl/schemas'
import { T, t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Button, Center, Loader, Popup, WearablePreview } from 'decentraland-ui'

import { getAssetImage, getAssetName } from '../../modules/asset/utils'
import { getSelection, getCenter } from '../../modules/nft/estate/utils'
Expand All @@ -13,11 +15,33 @@ import './AssetImage.css'
const PIXEL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNiYAAAAAkAAxkR2eQAAAAASUVORK5CYII='

const DEFAULT_COLOR = 'bbbbbb'

type Color = { r: number; g: number; b: number }
type WrappedColor = { color: Color }

const valueToHex = (value: number) =>
('00' + Math.min(255, (value * 255) | 0).toString(16)).slice(-2)

const colorToHex = (color: { r: number; g: number; b: number }) =>
valueToHex(color.r) + valueToHex(color.g) + valueToHex(color.b)
const colorToHex = (color: Color): string => {
if (isColor(color)) {
return valueToHex(color.r) + valueToHex(color.g) + valueToHex(color.b)
}
const maybeWrapped = (color as unknown) as Partial<WrappedColor>
if (isWrapped(maybeWrapped)) {
return colorToHex(maybeWrapped.color!)
}

return DEFAULT_COLOR
}

// sometimes the color come from the catalyst wrapped in an extra "color": { } object
const isWrapped = (maybeWrapped: Partial<WrappedColor>) =>
maybeWrapped.color && isColor(maybeWrapped.color)
const isColor = (maybeColor: Partial<Color>) =>
typeof maybeColor.r === 'number' &&
typeof maybeColor.g === 'number' &&
typeof maybeColor.b === 'number'

const AssetImage = (props: Props) => {
const {
Expand All @@ -36,8 +60,28 @@ const AssetImage = (props: Props) => {
isDraggable
)
const [wearablePreviewError, setWearablePreviewError] = useState(false)
const handleLoad = useCallback(() => setIsLoadingWearablePreview(false), [])
const handleError = useCallback(() => setWearablePreviewError(true), [])
const [isTrying, setIsTrying] = useState(false)
const handleLoad = useCallback(() => {
setIsLoadingWearablePreview(false)
setWearablePreviewError(false)
}, [])
const handleError = useCallback(error => {
console.warn(error)
setWearablePreviewError(true)
setIsLoadingWearablePreview(false)
}, [])
const handleTryOut = useCallback(() => {
if (!isTrying) {
setIsTrying(true)
setIsLoadingWearablePreview(true)
}
}, [isTrying])
const handleShowWearable = useCallback(() => {
if (isTrying) {
setIsTrying(false)
setIsLoadingWearablePreview(true)
}
}, [isTrying])

const estateSelection = useMemo(() => (estate ? getSelection(estate) : []), [
estate
Expand Down Expand Up @@ -84,9 +128,9 @@ const AssetImage = (props: Props) => {
if (isDraggable) {
let itemId: string | undefined
let tokenId: string | undefined
let skin = 'bbbbbb'
let hair = 'bbbbbb'
let shape: 'male' | 'female' = 'male'
let skin
let hair
let bodyShape: 'male' | 'female' = 'male'
if ('itemId' in asset && asset.itemId) {
itemId = asset.itemId
} else if ('tokenId' in asset && asset.tokenId) {
Expand All @@ -95,32 +139,101 @@ const AssetImage = (props: Props) => {
if (avatar) {
skin = colorToHex(avatar.avatar.skin.color)
hair = colorToHex(avatar.avatar.hair.color)
shape = avatar.avatar.bodyShape.toLowerCase().includes('female')
bodyShape = avatar.avatar.bodyShape.toLowerCase().includes('female')
? 'female'
: 'male'
}

const hasRepresentation = avatar
? wearable &&
wearable.bodyShapes.some(shape =>
avatar.avatar.bodyShape.includes(shape)
)
: true

const missingBodyShape =
hasRepresentation || !avatar
? null
: avatar.avatar.bodyShape.includes(BodyShape.MALE)
? t('wearable_preview.missing_representation_error.male')
: t('wearable_preview.missing_representation_error.female')

wearablePreview = (
<>
{isLoadingWearablePreview && (
<Center>
<Loader
className="wearable-preview-loader"
active
size="large"
/>
</Center>
)}
<WearablePreview
contractAddress={asset.contractAddress}
itemId={itemId}
tokenId={tokenId}
profile={
isTrying ? (avatar ? avatar.ethAddress : 'default') : undefined
}
skin={skin}
hair={hair}
bodyShape={shape}
dev={isDev}
bodyShape={bodyShape}
emote="fashion-2"
onLoad={handleLoad}
onError={handleError}
dev={isDev}
/>
{isLoadingWearablePreview ? (
<Center>
<Loader
className="wearable-preview-loader"
active
size="large"
/>
</Center>
) : null}
<Popup
content={
<T
id="wearable_preview.missing_representation_error.message"
values={{ bodyShape: <b>{missingBodyShape}</b> }}
/>
}
trigger={
<div className="preview-toggle-wrapper">
<Popup
position="top center"
content={<T id="wearable_preview.toggle_wearable" />}
trigger={
<Button
size="small"
className={classNames(
'preview-toggle',
'preview-toggle-wearable',
{
'is-active': !isTrying
}
)}
onClick={handleShowWearable}
/>
}
disabled={!hasRepresentation}
/>
<Popup
position="top center"
content={<T id="wearable_preview.toggle_avatar" />}
trigger={
<Button
size="small"
className={classNames(
'preview-toggle',
'preview-toggle-avatar',
{
'is-active': isTrying,
'is-disabled': !hasRepresentation
}
)}
onClick={hasRepresentation ? handleTryOut : undefined}
/>
}
disabled={!hasRepresentation}
/>
</div>
}
position="top center"
disabled={hasRepresentation}
/>
</>
)
Expand Down
7 changes: 1 addition & 6 deletions webapp/src/components/AssetPage/BaseDetail/BaseDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import { Container } from 'decentraland-ui'
import { PageHeader } from '../../PageHeader'
import Title from '../Title'
import { Box } from '../../AssetBrowse/Box'
import ListedBadge from '../../ListedBadge'
import { Props } from './BaseDetail.types'
import './BaseDetail.css'

const BaseDetail = ({
asset,
assetImage,
isOnSale,
badges,
left,
box,
Expand All @@ -20,10 +18,7 @@ const BaseDetail = ({
}: Props) => {
return (
<div className={classNames('BaseDetail', className)}>
<PageHeader>
{assetImage}
{isOnSale && <ListedBadge className="listed-badge" />}
</PageHeader>
<PageHeader>{assetImage}</PageHeader>
<Container>
<div className="info">
<div className="left">
Expand Down
4 changes: 4 additions & 0 deletions webapp/src/images/preview-avatar-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions webapp/src/images/preview-wearable-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 11 additions & 1 deletion webapp/src/modules/translation/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"buyer": "Buyer",
"bid": "Bid",
"order": "Listing",
"mint": "Mint"
"mint": "Mint",
"back": "Back"
},
"address": {
"invalid_address": "That's not a valid address",
Expand Down Expand Up @@ -319,6 +320,15 @@
"smart": "Smart Wearable",
"smart_badge": "Smart"
},
"wearable_preview": {
"toggle_wearable": "Wearable",
"toggle_avatar": "Try On",
"missing_representation_error": {
"male": "male",
"female": "female",
"message": "This wearable doesn't have a {bodyShape} representation"
}
},
"sell_page": {
"title": "List for sale",
"update_title": "Update your listing",
Expand Down
Loading

0 comments on commit 03b87a8

Please sign in to comment.