Skip to content

Commit

Permalink
feat: show projects in custom orbit pages (#1961)
Browse files Browse the repository at this point in the history
  • Loading branch information
dewanshparashar authored Oct 14, 2024
1 parent ac26607 commit ea0ab15
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 4 deletions.
8 changes: 8 additions & 0 deletions packages/arb-token-bridge-ui/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ module.exports = {
distDir: 'build',
productionBrowserSourceMaps: true,
reactStrictMode: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'portal.arbitrum.io'
}
]
},
async headers() {
return [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import dayjs from 'dayjs'
import { useState, useMemo, useCallback } from 'react'
import { useState, useMemo, useCallback, useEffect } from 'react'
import Tippy from '@tippyjs/react'
import { constants, utils } from 'ethers'
import { utils } from 'ethers'
import { useLatest } from 'react-use'
import { useAccount, useNetwork, useSigner } from 'wagmi'
import { TransactionResponse } from '@ethersproject/providers'
import { twMerge } from 'tailwind-merge'

import { useAppState } from '../../state'
import { getNetworkName } from '../../util/networks'
import { getNetworkName, isNetwork } from '../../util/networks'
import {
TokenDepositCheckDialog,
TokenDepositCheckDialogType
Expand Down Expand Up @@ -75,6 +75,7 @@ import { ExternalLink } from '../common/ExternalLink'
import { isExperimentalFeatureEnabled } from '../../util'
import { useIsTransferAllowed } from './hooks/useIsTransferAllowed'
import { MoveFundsButton } from './MoveFundsButton'
import { ProjectsListing } from '../common/ProjectsListing'
import { useAmountBigNumber } from './hooks/useAmountBigNumber'

const signerUndefinedError = 'Signer is undefined'
Expand Down Expand Up @@ -183,8 +184,15 @@ export function TransferPanel() {

const { destinationAddressError } = useDestinationAddressError()

const [showProjectsListing, setShowProjectsListing] = useState(false)

const isBatchTransfer = isBatchTransferSupported && Number(amount2) > 0

useEffect(() => {
// hide Project listing when networks are changed
setShowProjectsListing(false)
}, [childChain.id, parentChain.id])

function closeWithResetTokenImportDialog() {
setTokenQueryParam(undefined)
setImportTokenModalStatus(ImportTokenModalStatus.CLOSED)
Expand Down Expand Up @@ -828,6 +836,11 @@ export function TransferPanel() {
setTransferring(false)
clearAmountInput()

// for custom orbit pages, show Projects' listing after transfer
if (isDepositMode && isNetwork(childChain.id).isOrbitChain) {
setShowProjectsListing(true)
}

await (sourceChainTransaction as TransactionResponse).wait()

// tx confirmed, update balances
Expand Down Expand Up @@ -1007,6 +1020,8 @@ export function TransferPanel() {
</Tippy>
)}
</div>

{showProjectsListing && <ProjectsListing />}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Chain, useAccount } from 'wagmi'
import { useMedia } from 'react-use'

import { useAppState } from '../../state'
import { getExplorerUrl, isNetwork } from '../../util/networks'
import { getExplorerUrl } from '../../util/networks'
import { useDestinationAddressStore } from './AdvancedSettings'
import { ExternalLink } from '../common/ExternalLink'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Image from 'next/image'
import { ExternalLink } from './ExternalLink'
import { PORTAL_API_ENDPOINT } from '../../constants'

export type PortalProject = {
chains: string[]
description: string
id: string
images: { logoUrl: string; bannerUrl: string }
subcategories: { id: string; title: string }[]
title: string
url: string
}

export const Project = ({
project,
onClick,
isTestnetMode
}: {
project: PortalProject
onClick?: () => void
isTestnetMode: boolean
}) => {
return (
<ExternalLink
className="relative flex h-full min-h-[150px] w-full flex-col gap-2 overflow-hidden rounded-md border border-white/30 bg-dark p-4 hover:bg-dark-hover hover:opacity-100"
aria-label={`${project.title}`}
href={
isTestnetMode
? PORTAL_API_ENDPOINT
: `${PORTAL_API_ENDPOINT}?project=${project.id}`
}
onClick={onClick}
>
{/* Normal project contents */}
<div className="flex w-full flex-row gap-1">
{/* Logos */}
<div className="flex shrink-0 grow-0 flex-col gap-2 overflow-hidden bg-cover bg-center">
{/* Project logo */}
<div className="relative flex h-[50px] w-[50px] items-center justify-center overflow-hidden rounded-md bg-white p-[1px]">
<div className="[&:hover_span]:opacity-100">
<Image
alt={`${project.title} logo`}
src={project.images.logoUrl}
width={50}
height={50}
className="rounded-md"
/>
</div>
</div>
</div>

{/* Content */}
<div className="relative grow text-left">
<div className="flex flex-col gap-1 px-2">
<h5 className="relative flex items-center gap-2 text-left text-lg font-semibold leading-7">
{project.title}
</h5>
<p className="mb-2 line-clamp-3 text-sm opacity-70">
{project.description}
</p>

<p className="flex flex-wrap justify-start gap-2 text-center leading-6 text-gray-700">
{project.subcategories.slice(0, 2).map(subcategory => (
<span
key={subcategory.id}
className="inline-flex items-start justify-start gap-2 break-words rounded bg-black px-1.5 py-0.5 text-xs font-normal text-white/60"
>
{subcategory.title.replaceAll('/', ' / ')}
</span>
))}
</p>
</div>
</div>
</div>
</ExternalLink>
)
}
170 changes: 170 additions & 0 deletions packages/arb-token-bridge-ui/src/components/common/ProjectsListing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { useMemo } from 'react'
import axios from 'axios'
import useSWRImmutable from 'swr/immutable'
import { ChevronRightIcon } from '@heroicons/react/24/outline'
import { useNetworks } from '../../hooks/useNetworks'
import { getNetworkName, isNetwork } from '../../util/networks'
import { PortalProject, Project } from './PortalProject'
import { PORTAL_API_ENDPOINT } from '../../constants'
import { ExternalLink } from './ExternalLink'
import { getBridgeUiConfigForChain } from '../../util/bridgeUiConfig'
import { getChainQueryParamForChain } from '../../types/ChainQueryParam'
import { trackEvent } from '../../util/AnalyticsUtils'
import { useIsTestnetMode } from '../../hooks/useIsTestnetMode'
import { isTestingEnvironment } from '../../util/CommonUtils'

const shuffleArray = (array: PortalProject[]) => {
return array.sort(() => Math.random() - 0.5)
}

const generateTestnetProjects = (
chainId: number,
count: number
): PortalProject[] => {
const {
network: { name: chainName, logo: chainImage }
} = getBridgeUiConfigForChain(chainId)

return [...Array(count)].map((_, key) => ({
chains: [chainName],
description: `This is a featured project deployed on ${chainName}.`,
id: `project_${key}`,
images: {
logoUrl: chainImage,
bannerUrl: chainImage
},
subcategories: [
{ id: 'defi', title: 'Defi' },
{ id: 'nfts', title: 'NFTs' }
],
title: `Featured Project ${key + 1}`,
url: PORTAL_API_ENDPOINT
}))
}

const fetchProjects = async (
chainId: number,
isTestnetMode: boolean
): Promise<PortalProject[]> => {
const isChainOrbit = isNetwork(chainId).isOrbitChain
const chainSlug = getChainQueryParamForChain(chainId)

if (!isChainOrbit || !chainSlug) {
return []
}

if (isTestnetMode) {
return isTestingEnvironment ? generateTestnetProjects(chainId, 6) : [] // don't show any test projects in production
}

try {
const response = await axios.get(
`${PORTAL_API_ENDPOINT}/api/projects?chains=${chainSlug}`
)
return response.data as PortalProject[]
} catch (error) {
console.warn('Error fetching projects:', error)
return []
}
}

export const ProjectsListing = () => {
const [{ destinationChain }] = useNetworks()
const [isTestnetMode] = useIsTestnetMode()

const isDestinationChainOrbit = isNetwork(destinationChain.id).isOrbitChain
const { color: destinationChainUIcolor } = getBridgeUiConfigForChain(
destinationChain.id
)
const destinationChainSlug = getChainQueryParamForChain(destinationChain.id)

const {
data: projects,
error,
isLoading
} = useSWRImmutable(
isDestinationChainOrbit
? [destinationChain.id, isTestnetMode, 'fetchProjects']
: null,
([destinationChainId, isTestnetMode]) =>
fetchProjects(destinationChainId, isTestnetMode)
)

// Shuffle projects and limit to 4
const randomizedProjects = useMemo(
() => (projects ? shuffleArray(projects).slice(0, 4) : []),
[projects]
)

if (
isLoading ||
!projects ||
projects.length === 0 ||
typeof error !== 'undefined'
) {
return null
}

return (
<div
className="flex flex-col gap-3 border-y bg-dark p-4 text-white sm:rounded-md sm:border"
style={{
borderColor: destinationChainUIcolor
}}
>
<h2 className="text-lg">
Explore Apps on {getNetworkName(destinationChain.id)}
</h2>

{isTestnetMode && (
<div className="text-xs text-white/70">
<b>Development-mode only</b>. These are placeholder projects for
showing how this feature works in non-production mode. Real projects
are fetched from the Portal for mainnet Orbit chains.
</div>
)}

<div className="grid gap-3 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2">
{randomizedProjects.map(project => (
<Project
key={project.id}
project={project}
isTestnetMode={isTestnetMode}
onClick={() => {
if (isTestnetMode) return

trackEvent('Project Click', {
network: getNetworkName(destinationChain.id),
projectName: project.title
})
}}
/>
))}
</div>
{projects.length > 4 && (
<ExternalLink
href={
isTestnetMode
? PORTAL_API_ENDPOINT
: `${PORTAL_API_ENDPOINT}/projects?chains=${destinationChainSlug}`
}
className="flex w-min flex-nowrap items-center gap-2 self-end whitespace-nowrap rounded-md border p-2 text-sm"
style={{
borderColor: destinationChainUIcolor,
backgroundColor: `${destinationChainUIcolor}66`
}}
onClick={() => {
if (isTestnetMode) return

trackEvent('Show All Projects Click', {
network: getNetworkName(destinationChain.id)
})
}}
>
See all
<ChevronRightIcon className="h-3 w-3" />
</ExternalLink>
)}
</div>
)
}
2 changes: 2 additions & 0 deletions packages/arb-token-bridge-ui/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ export const MULTICALL_TESTNET_ADDRESS =
export const ETHER_TOKEN_LOGO = '/images/EthereumLogoRound.svg'

export const ether = { name: 'Ether', symbol: 'ETH', decimals: 18 } as const

export const PORTAL_API_ENDPOINT = 'https://portal.arbitrum.io'
7 changes: 7 additions & 0 deletions packages/arb-token-bridge-ui/src/util/AnalyticsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ type AnalyticsEventMap = {
complete: boolean
version: number
}
'Project Click': {
network: string
projectName: string
}
'Show All Projects Click': {
network: string
}
}

type AnalyticsEvent = keyof AnalyticsEventMap
Expand Down
1 change: 1 addition & 0 deletions packages/arb-token-bridge-ui/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ module.exports = {
'gray-dark': '#6D6D6D',
'line-gray': '#F4F4F4',
dark: '#1A1C1D', // (or default-black)
'dark-hover': '#2b2e30', // (or default-black-hover)

'bg-gray-1': '#191919',

Expand Down

0 comments on commit ea0ab15

Please sign in to comment.