Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

feat: Dashboard Safe Apps #3738

Merged
merged 34 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3f4b716
feat: Add Safe route for dashboard (#3759)
usame-algan Apr 5, 2022
5ee1050
feat: add harcoded WC app
Mar 29, 2022
85c3f82
Add a redirect for Safe Apps + Bookmark handler
katspaugh Mar 29, 2022
6d023da
feat: display "official" apps after pinned apps
Mar 29, 2022
f1f09c6
Add "Explore" Card
Mar 30, 2022
043affe
refactor: extract official app idss to enum
Mar 30, 2022
2ad4770
fix: render after safe apps info response
Mar 30, 2022
e0715a2
fix: import explore icon as module
Mar 30, 2022
79d5e6f
fix: remove duplicated safe apps
Mar 30, 2022
2b1f275
fix: memoize safeApps data
Mar 30, 2022
ff36879
fix: change useMemo dependency
Mar 30, 2022
9d15334
fix: move related data inside the same function. Use hook isLoading.
Mar 30, 2022
7c6cd51
fix: GENERIC_APPS_ROUTE route
Apr 7, 2022
78c05d4
feat: track timestamp when opening safeApp
Apr 7, 2022
9a1729e
feat: track openingCount when opening safeApp
Apr 7, 2022
8ce38f3
feat: track txCount when creating a transaction from ReviewConfirm
Apr 7, 2022
171d354
fix: keep previous data when tracking on opening
Apr 8, 2022
36273d3
fix: Adds ranking function for tracked safe apps
usame-algan Apr 8, 2022
96b13b1
fix: unify rankTrackedSafeApps input types
Apr 8, 2022
8dfd294
fix: change localstorage prefered module
Apr 8, 2022
962a5be
feat: display top ranked apps
Apr 8, 2022
da3ec11
fix: track opening SafeApp in a separate hook
Apr 11, 2022
d0d5860
fix: move app count tracking to a separate module
Apr 11, 2022
47d2cef
chore: Add comments to the sorting formula
Apr 11, 2022
93d4999
chore: move app usage related functions to the same file
Apr 11, 2022
aba59ac
fix: improve the setItem in the LS logic
Apr 11, 2022
3491252
feat: add Skeleton Cards
Apr 11, 2022
12b81db
fix: remove breadcrumbs in Dashboard
Apr 11, 2022
7fac871
feat: always display the "Explore" card
Apr 11, 2022
75feb58
rename safe app tracking methods. extract card dimensions to constants
Apr 12, 2022
c76c2a8
include random apps to fill the gaps in the widget
Apr 12, 2022
b94a01a
prop drill the safeApp number id
Apr 12, 2022
b71f360
code comments clean up
Apr 12, 2022
b80f4a7
fix: make logo height fix to align SafeApp name
Apr 13, 2022
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
5 changes: 5 additions & 0 deletions src/assets/icons/explore.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
98 changes: 98 additions & 0 deletions src/components/Dashboard/SafeApps/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { ReactElement, useCallback, useEffect, useState } from 'react'
import styled from 'styled-components'
import { Text, Title } from '@gnosis.pm/safe-react-components'
import { Bookmark, BookmarkBorder } from '@material-ui/icons'
import { IconButton } from '@material-ui/core'
import { Link, generatePath } from 'react-router-dom'
import { GENERIC_APPS_ROUTE } from 'src/routes/routes'

const StyledLink = styled(Link)`
text-decoration: none;
color: black;
`

export const CARD_WIDTH = 260
export const CARD_HEIGHT = 200
export const CARD_PADDING = 24

const StyledCard = styled.div`
position: relative;
width: ${CARD_WIDTH}px;
height: ${CARD_HEIGHT}px;
background-color: white;
border-radius: 8px;
padding: ${CARD_PADDING}px;
`

const StyledLogo = styled.img`
display: block;
width: 60px;
height: auto;
`

const IconBtn = styled(IconButton)`
&.MuiButtonBase-root {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
padding: 5px;
}

svg {
width: 16px;
height: 16px;
}
`

type CardProps = {
name: string
description: string
logoUri: string
appUri: string
isPinned: boolean
onPin: () => void
}

const Card = (props: CardProps): ReactElement => {
const appRoute = generatePath(GENERIC_APPS_ROUTE) + `?appUrl=${props.appUri}`
const { isPinned, onPin } = props
const [localPinned, setLocalPinned] = useState<boolean>(isPinned)

const handlePinClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
setLocalPinned((prev) => !prev)
},
[setLocalPinned],
)

useEffect(() => {
if (localPinned === isPinned) return

// Add a small delay when pinning/unpinning for visual feedback
const delay = setTimeout(onPin, 500)
return () => clearTimeout(delay)
}, [localPinned, isPinned, onPin])
katspaugh marked this conversation as resolved.
Show resolved Hide resolved

return (
<StyledLink to={appRoute}>
<StyledCard>
<StyledLogo src={props.logoUri} alt={`${props.name} logo`} />

<Title size="xs">{props.name}</Title>

<Text size="md" color="inputFilled">
{props.description}
</Text>

{/* Bookmark button */}
<IconBtn onClick={handlePinClick}>{localPinned ? <Bookmark /> : <BookmarkBorder />}</IconBtn>

{/* TODO: Share button */}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No share button?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per discussion:

sharing is not enabled yet by Safe Apps, so no need to tackle it here already.

I will remove the comment.

</StyledCard>
</StyledLink>
)
}

export default Card
133 changes: 133 additions & 0 deletions src/components/Dashboard/SafeApps/Grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { ReactElement, useMemo } from 'react'
import styled from 'styled-components'
import { Button } from '@gnosis.pm/safe-react-components'
import { generatePath, Link } from 'react-router-dom'
import Skeleton from '@material-ui/lab/Skeleton/Skeleton'

import { useAppList } from 'src/routes/safe/components/Apps/hooks/appList/useAppList'
import { GENERIC_APPS_ROUTE } from 'src/routes/routes'
import Card, { CARD_HEIGHT, CARD_PADDING, CARD_WIDTH } from 'src/components/Dashboard/SafeApps/Card'
import ExploreIcon from 'src/assets/icons/explore.svg'
import { SafeApp } from 'src/routes/safe/components/Apps/types'
import { getAppsUsageData, rankTrackedSafeApps } from 'src/routes/safe/components/Apps/trackAppUsageCount'

const StyledGrid = styled.div`
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
`

const SkeletonWrapper = styled.div`
border-radius: 8px;
overflow: hidden;
`

const StyledExplorerButton = styled.div`
width: 260px;
height: 200px;
background-color: white;
border-radius: 8px;
padding: 24px;

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
`

const StyledLink = styled(Link)`
text-decoration: none;

> button {
width: 200px;
}
`

// Transactions Builder && Wallet connect
const featuredAppsId = ['29', '11']

const getRandomApps = (nonRankedApps: SafeApp[], size: number) => {
const randomIndexes: string[] = []
for (let i = 1; randomIndexes.length < size; i++) {
const randomAppIndex = Math.floor(Math.random() * nonRankedApps.length).toString()
const randomAppId = nonRankedApps[randomAppIndex].id

// Do not repeat random apps or featured apps
if (!randomIndexes.includes(randomAppIndex) && !featuredAppsId.includes(randomAppId)) {
randomIndexes.push(randomAppIndex)
}
}

const randomSafeApps: SafeApp[] = []
randomIndexes.forEach((index) => {
randomSafeApps.push(nonRankedApps[index])
})

return randomSafeApps
}

const Grid = ({ size = 6 }: { size?: number }): ReactElement => {
const { allApps, pinnedSafeApps, togglePin, isLoading } = useAppList()

const displayedApps = useMemo(() => {
if (!allApps.length) return []
const trackData = getAppsUsageData()
const rankedSafeAppIds = rankTrackedSafeApps(trackData)

const topRankedSafeApps: SafeApp[] = []
rankedSafeAppIds.forEach((id) => {
const sortedApp = allApps.find((app) => app.id === id)
if (sortedApp) topRankedSafeApps.push(sortedApp)
})
Comment on lines +79 to +83
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not filter() allApps directly?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented this way to keep the ranking order from rankedSafeAppIds


const nonRankedApps = allApps.filter((app) => !rankedSafeAppIds.includes(app.id))
// Get random apps that are not ranked
const randomApps = getRandomApps(nonRankedApps, size - 1 - rankedSafeAppIds.length)

// Display size - 1 in order to always display the "Explore Safe Apps" card
return topRankedSafeApps.concat(randomApps).slice(0, size - 1)
}, [allApps, size])

const path = generatePath(GENERIC_APPS_ROUTE)

return (
<div>
<h2>Safe Apps</h2>
{isLoading ? (
<StyledGrid>
{Array.from(Array(size).keys()).map((key) => (
<SkeletonWrapper key={key}>
<Skeleton variant="rect" width={CARD_WIDTH + 2 * CARD_PADDING} height={CARD_HEIGHT + 2 * CARD_PADDING} />
</SkeletonWrapper>
))}
</StyledGrid>
) : (
<StyledGrid>
{displayedApps.map((safeApp) => (
<Card
key={safeApp.id}
name={safeApp.name}
description={safeApp.description}
logoUri={safeApp.iconUrl}
appUri={safeApp.url}
isPinned={pinnedSafeApps.some((app) => app.id === safeApp.id)}
onPin={() => togglePin(safeApp)}
/>
))}
<StyledExplorerButton>
<img alt="Explore Safe Apps" src={ExploreIcon} />
<StyledLink to={path}>
<Button size="md" color="primary" variant="contained">
Explore Safe Apps
</Button>
</StyledLink>
</StyledExplorerButton>
</StyledGrid>
)}
</div>
)
}

export default Grid
20 changes: 3 additions & 17 deletions src/routes/Home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { ReactElement } from 'react'
import styled from 'styled-components'
import { Breadcrumb, BreadcrumbElement, Menu } from '@gnosis.pm/safe-react-components'

import Page from 'src/components/layout/Page'
import Row from 'src/components/layout/Row'
import Col from 'src/components/layout/Col'
import PendingTxsList from 'src/components/Dashboard/PendingTxs/PendingTxsList'

import AddSafeWidget from 'src/components/Dashboard/AddSafe'
import CreateSafeWidget from 'src/components/Dashboard/CreateSafe'
import SafeAppsGrid from 'src/components/Dashboard/SafeApps/Grid'
import Row from 'src/components/layout/Row'

const Card = styled.div`
background: #fff;
Expand All @@ -25,16 +23,6 @@ const Card = styled.div`
function Home(): ReactElement {
return (
<Page>
<Menu>
<Col start="sm" sm={6} xs={12}>
<Breadcrumb>
<BreadcrumbElement iconType="assets" text="Dashboard" color="primary" />
</Breadcrumb>
</Col>

<Col end="sm" sm={6} xs={12} />
</Menu>

<Row>
<Card>
<AddSafeWidget />
Expand All @@ -57,9 +45,7 @@ function Home(): ReactElement {
</Row>

<Row>
<Card>
<h2>Gas Fees</h2>
</Card>
<SafeAppsGrid size={6} />
</Row>
</Page>
)
Expand Down
29 changes: 23 additions & 6 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Redirect, Route, Switch, useLocation } from 'react-router-dom'
import { LoadingContainer } from 'src/components/LoaderContainer'
import { lastViewedSafe } from 'src/logic/currentSession/store/selectors'
import {
generateSafeRoute,
LOAD_SPECIFIC_SAFE_ROUTE,
OPEN_SAFE_ROUTE,
ADDRESSED_ROUTE,
Expand All @@ -15,12 +16,12 @@ import {
getNetworkRootRoutes,
extractSafeAddress,
SAFE_ROUTES,
generateSafeRoute,
GENERIC_APPS_ROUTE,
} from './routes'
import { getShortName } from 'src/config'
import { setChainId } from 'src/logic/config/utils'
import { setChainIdFromUrl } from 'src/utils/history'
import { usePageTracking } from 'src/utils/googleTagManager'
import { getShortName } from 'src/config'

const Welcome = React.lazy(() => import('./welcome/Welcome'))
const CreateSafePage = React.lazy(() => import('./CreateSafePage/CreateSafePage'))
Expand All @@ -30,7 +31,7 @@ const SafeContainer = React.lazy(() => import('./safe/container'))
const Routes = (): React.ReactElement => {
const location = useLocation()
const { pathname } = location
const defaultSafe = useSelector(lastViewedSafe)
const lastSafe = useSelector(lastViewedSafe)

// Google Tag Manager page tracking
usePageTracking()
Expand Down Expand Up @@ -73,20 +74,20 @@ const Routes = (): React.ReactElement => {
exact
path={ROOT_ROUTE}
render={() => {
if (defaultSafe === null) {
if (lastSafe === null) {
return (
<LoadingContainer>
<Loader size="md" />
</LoadingContainer>
)
}

if (defaultSafe) {
if (lastSafe) {
return (
<Redirect
to={generateSafeRoute(SAFE_ROUTES.DASHBOARD, {
shortName: getShortName(),
safeAddress: defaultSafe,
safeAddress: lastSafe,
})}
/>
)
Expand All @@ -96,6 +97,22 @@ const Routes = (): React.ReactElement => {
}}
/>

{/* Redirect /app/apps?appUrl=https://... to that app within the current Safe */}
<Route
exact
path={GENERIC_APPS_ROUTE}
render={() => {
if (!lastSafe) {
return <Redirect to={WELCOME_ROUTE} />
}
const redirectPath = generateSafeRoute(SAFE_ROUTES.APPS, {
shortName: getShortName(),
safeAddress: lastSafe,
})
return <Redirect to={`${redirectPath}${location.search}`} />
}}
/>

<Route component={Welcome} exact path={WELCOME_ROUTE} />

<Route component={CreateSafePage} exact path={OPEN_SAFE_ROUTE} />
Expand Down
1 change: 1 addition & 0 deletions src/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const LOAD_SPECIFIC_SAFE_ROUTE = `/load/:${SAFE_ADDRESS_SLUG}?` // ? = op
export const ROOT_ROUTE = '/'
export const WELCOME_ROUTE = '/welcome'
export const OPEN_SAFE_ROUTE = '/open'
export const GENERIC_APPS_ROUTE = '/apps'
export const LOAD_SAFE_ROUTE = generatePath(LOAD_SPECIFIC_SAFE_ROUTE) // By providing no slug, we get '/load'

// [SAFE_SECTION_SLUG], [SAFE_SUBSECTION_SLUG] populated safe routes
Expand Down
Loading