Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add recommendations to item detail #1711

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions webapp/src/components/ActivityPage/ActivityPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import React, { useCallback, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import { Page, Header, Button, Modal } from 'decentraland-ui'
import { useLocation } from 'react-router-dom'
import {
Page,
Header,
Button,
Modal,
// Icon,
ModalNavigation
} from 'decentraland-ui'
import { t, T } from 'decentraland-dapps/dist/modules/translation/utils'

import { locations } from '../../modules/routing/locations'
Expand All @@ -13,11 +21,17 @@ import { Transaction } from './Transaction'
import { Props } from './ActivityPage.types'
import './ActivityPage.css'
import { NavigationTab } from '../Navigation/Navigation.types'
import { RECO_TYPE, Recommendation } from '../Recommendation'

const ActivityPage = (props: Props) => {
const { address, transactions, onClearHistory } = props

const location = useLocation()
const boughtItem = useMemo(
() => new URLSearchParams(location.search).get('boughtItem'),
[location.search]
)
const [showConfirmation, setShowConfirmation] = useState(false)
const [isBoughItemOpen, setIsBoughItemOpen] = useState(true)

const handleClear = useCallback(() => {
if (address) {
Expand All @@ -44,7 +58,9 @@ const ActivityPage = (props: Props) => {
id="wallet.sign_in_required"
values={{
sign_in: (
<Link to={locations.signIn(locations.activity())}>{t('wallet.sign_in')}</Link>
<Link to={locations.signIn(locations.activity())}>
{t('wallet.sign_in')}
</Link>
)
}}
/>
Expand Down Expand Up @@ -85,6 +101,21 @@ const ActivityPage = (props: Props) => {
<Navbar isFullscreen />
<Navigation activeTab={NavigationTab.ACTIVITY} />
<Page className="ActivityPage">{content}</Page>
{boughtItem ? (
<Modal open={isBoughItemOpen}>
<ModalNavigation
title={'You may also like'}
onClose={() => setIsBoughItemOpen(false)}
></ModalNavigation>
<Modal.Content>
<Recommendation
noTitle
itemId={boughtItem}
type={RECO_TYPE.SIMILARITY}
/>
</Modal.Content>
</Modal>
) : null}
<Modal size="tiny" open={showConfirmation}>
<Modal.Header>
{t('activity_page.clear_history_modal.title')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,11 @@
flex-direction: column;
gap: 8px;
}

.recommendations > div {
margin-top: 30px;
}

.recommendations > :global(.Slideshow) {
margin-bottom: 0px !important;
}
12 changes: 11 additions & 1 deletion webapp/src/components/AssetPage/ItemDetail/ItemDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AssetType } from '../../../modules/asset/types'
import GenderBadge from '../../GenderBadge'
import { AssetImage } from '../../AssetImage'
import CampaignBadge from '../../Campaign/CampaignBadge'
import { Recommendation, RECO_TYPE } from '../../Recommendation'
import CategoryBadge from '../CategoryBadge'
import SmartBadge from '../SmartBadge'
import { Description } from '../Description'
Expand All @@ -18,6 +19,7 @@ import IconBadge from '../IconBadge'
import { TransactionHistory } from '../TransactionHistory'
import { SaleActionBox } from '../SaleActionBox'
import { Props } from './ItemDetail.types'
import styles from './ItemDetail.module.css'

const ItemDetail = ({ item }: Props) => {
let description = ''
Expand Down Expand Up @@ -109,7 +111,15 @@ const ItemDetail = ({ item }: Props) => {
box={null}
showDetails
actions={<SaleActionBox asset={item} />}
below={<TransactionHistory asset={item} />}
below={
<div>
<TransactionHistory asset={item} />
<div className={styles.recommendations}>
<Recommendation itemId={item.id} type={RECO_TYPE.SIMILARITY} />
<Recommendation itemId={item.id} type={RECO_TYPE.OTHERS_BOUGHT} />
</div>
</div>
}
/>
)
}
Expand Down
10 changes: 6 additions & 4 deletions webapp/src/components/HomePage/Slideshow/ItemsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Section } from '../../../modules/vendor/decentraland/routing/types'

const ItemsSection = (props: {
view: HomepageView
viewAllButton: React.ReactNode
viewAllButton?: React.ReactNode
onChangeItemSection: (view: HomepageView, section: Section) => void
}) => {
const { view, viewAllButton, onChangeItemSection } = props
Expand Down Expand Up @@ -47,9 +47,11 @@ const ItemsSection = (props: {
{t(`menu.${Section.EMOTES}`)}
</div>
</Tabs.Tab>
<div className="view-all-button">
<Tabs.Tab>{viewAllButton}</Tabs.Tab>
</div>
{viewAllButton ? (
<div className="view-all-button">
<Tabs.Tab>{viewAllButton}</Tabs.Tab>
</div>
) : null}
</Tabs>
)

Expand Down
21 changes: 12 additions & 9 deletions webapp/src/components/HomePage/Slideshow/Slideshow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const Slideshow = (props: Props) => {
title,
subtitle,
viewAllTitle,
showViewAll,
emptyMessage,
assets,
isSubHeader,
Expand Down Expand Up @@ -63,7 +64,7 @@ const Slideshow = (props: Props) => {
(asset: Asset) => {
getAnalytics().track(events.ASSET_CLICK, {
id: asset.id,
section: title
section: title ?? 'Unknown'
})
},
[title]
Expand Down Expand Up @@ -124,25 +125,27 @@ const Slideshow = (props: Props) => {
<HeaderMenu>
<HeaderMenu.Left>
<div className="slideshow-header">
<Header sub={isSubHeader}>{title}</Header>
<Header sub>{subtitle}</Header>
{title ? <Header sub={isSubHeader}>{title}</Header> : null}
{subtitle ? <Header sub>{subtitle}</Header> : null}
{hasItemsSection ? (
<ItemsSection
view={view}
viewAllButton={viewAllButton()}
viewAllButton={showViewAll ? viewAllButton() : null}
onChangeItemSection={onChangeItemSection!}
/>
) : null}
</div>
</HeaderMenu.Left>
{!hasItemsSection ? (
{!hasItemsSection && showViewAll ? (
<HeaderMenu.Right>{viewAllButton()}</HeaderMenu.Right>
) : null}
</HeaderMenu>
<div className="assets-container">
<div className={classNames("assets", {
"full-width": assetsToRender.length === pageSize
})}>
<div
className={classNames('assets', {
'full-width': assetsToRender.length === pageSize
})}
>
{isLoading ? (
assets.length === 0 ? (
<Loader active size="massive" />
Expand All @@ -159,7 +162,7 @@ const Slideshow = (props: Props) => {
className="arrow-container arrow-container-left"
{...showArrowsHandlers}
>
{showArrows && totalPages > 1 && (
{showArrows && totalPages > 1 && (
<Button
circular
secondary
Expand Down
3 changes: 2 additions & 1 deletion webapp/src/components/HomePage/Slideshow/Slideshow.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { HomepageView } from '../../../modules/ui/asset/homepage/types'
import { Section } from '../../../modules/vendor/routing/types'

export type Props = {
title: string
title?: string
subtitle?: string
viewAllTitle?: string
emptyMessage?: string
showViewAll?: boolean
assets: Asset[]
view: HomepageView
isSubHeader?: boolean
Expand Down
75 changes: 75 additions & 0 deletions webapp/src/components/Recommendation/Recommendation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useEffect, useMemo, useState } from 'react'
import { Item } from '@dcl/schemas'
import { isErrorWithMessage } from '../../lib/error'
import { ItemAPI } from '../../modules/vendor/decentraland/item/api'
import { Slideshow } from '../HomePage/Slideshow'
import { Asset } from '../../modules/asset/types'
import { View } from '../../modules/ui/types'
import { NFT_SERVER_URL } from '../../modules/vendor/decentraland'
import { RECO_TYPE } from './Recommendation.types'

const RECOMMENDER_URL = 'http://localhost:8000'

export const Recommendation = ({
itemId,
type,
noTitle
}: {
itemId: string
type: RECO_TYPE
noTitle?: boolean
}) => {
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
const [items, setItems] = useState<Item[]>([])
const itemAPI = useMemo(
() =>
new ItemAPI(NFT_SERVER_URL, {
retries: 3,
retryDelay: 1000
}),
[]
)

useEffect(() => {
setIsLoading(true)
fetch(`${RECOMMENDER_URL}/recommendation/${itemId}?type=${type}`)
.then(response => {
if (response.ok) {
return response.json()
} else if (response.status === 404) {
throw Error('No recommendation found')
}
})
// mockedFetch()
.then(itemIDs => itemAPI.get({ ids: itemIDs }))
.then(response => {
setItems(response.data)
setIsLoading(false)
})
.catch(error => {
setIsLoading(false)
setError(isErrorWithMessage(error) ? error.message : 'Unknown error')
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

return (
<Slideshow
view={View.HOME_NEW_ITEMS}
title={
!noTitle
? type === RECO_TYPE.SIMILARITY
? 'You may also like'
: 'People also bought'
: undefined
}
onViewAll={() => undefined}
hasItemsSection={false}
showViewAll={false}
emptyMessage={error ?? undefined}
assets={items as Asset[]}
isLoading={isLoading}
/>
)
}
4 changes: 4 additions & 0 deletions webapp/src/components/Recommendation/Recommendation.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum RECO_TYPE {
SIMILARITY = 'similarity',
OTHERS_BOUGHT = 'othersBought'
}
2 changes: 2 additions & 0 deletions webapp/src/components/Recommendation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Recommendation'
export * from './Recommendation.types'
3 changes: 2 additions & 1 deletion webapp/src/modules/routing/locations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export const locations = {
contractAddress: string = ':contractAddress',
tokenId: string = ':tokenId'
) => `/contracts/${contractAddress}/tokens/${tokenId}/bid`,
activity: () => `/activity`
activity: (boughtItem?: string) =>
`/activity${boughtItem ? `?${boughtItem}` : ''}`
}

function getResource(type: AssetType) {
Expand Down
7 changes: 5 additions & 2 deletions webapp/src/modules/routing/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { AssetType } from '../asset/types'
import {
BUY_ITEM_SUCCESS,
BuyItemSuccessAction,
fetchItemRequest,
fetchItemsRequest,
fetchTrendingItemsRequest
Expand Down Expand Up @@ -601,6 +602,8 @@ function shouldResetOptions(previous: BrowseOptions, current: BrowseOptions) {
)
}

function* handleRedirectToActivity() {
yield put(push(locations.activity()))
function* handleRedirectToActivity(action: BuyItemSuccessAction) {
const boughtItem =
action.type === BUY_ITEM_SUCCESS ? action.payload.item.id : undefined
yield put(push(locations.activity(boughtItem)))
}