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: show projects in custom orbit pages #1961

Merged
merged 20 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
41bbee5
Dev: show portal projects in bridge
dewanshparashar Oct 3, 2024
ecc59c3
Merge branch 'master' of github.com:OffchainLabs/arbitrum-token-bridg…
dewanshparashar Oct 7, 2024
9af546b
Dev: added analytics
dewanshparashar Oct 7, 2024
fabba35
Dev: randomize list and hide projects on network switch
dewanshparashar Oct 7, 2024
b89c14f
Dev: updated endpoint
dewanshparashar Oct 7, 2024
5c5c221
Merge branch 'master' of github.com:OffchainLabs/arbitrum-token-bridg…
dewanshparashar Oct 8, 2024
3b25131
Dev: review comments
dewanshparashar Oct 8, 2024
7da4b4c
Merge branch 'master' into fs-609/portal-in-bridge
dewanshparashar Oct 8, 2024
41b76ca
Dev: handle test mode
dewanshparashar Oct 8, 2024
9e82f7d
Merge branch 'fs-609/portal-in-bridge' of github.com:OffchainLabs/arb…
dewanshparashar Oct 8, 2024
bf03da2
dev: added doscaimer
dewanshparashar Oct 8, 2024
4a925b2
Merge branch 'master' into fs-609/portal-in-bridge
dewanshparashar Oct 8, 2024
2729e8e
Merge branch 'master' into fs-609/portal-in-bridge
dewanshparashar Oct 9, 2024
38d855b
Merge branch 'master' into fs-609/portal-in-bridge
dewanshparashar Oct 9, 2024
6b6146c
Dev: add cors headers
dewanshparashar Oct 9, 2024
4d9bc84
Merge branch 'master' into fs-609/portal-in-bridge
fionnachan Oct 9, 2024
b722128
conflicts
dewanshparashar Oct 10, 2024
4dbefbe
Merge branch 'fs-609/portal-in-bridge' of github.com:OffchainLabs/arb…
dewanshparashar Oct 10, 2024
44e1af4
Dev: remove access control header
dewanshparashar Oct 10, 2024
4300b09
Merge branch 'master' into fs-609/portal-in-bridge
dewanshparashar Oct 11, 2024
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
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,5 +1,5 @@
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 { useLatest } from 'react-use'
Expand All @@ -8,7 +8,7 @@ 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'

const signerUndefinedError = 'Signer is undefined'
const transferNotAllowedError = 'Transfer not allowed'
Expand Down Expand Up @@ -180,8 +181,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 @@ -865,6 +873,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 @@ -1044,6 +1057,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'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Simple implementation of Portal Project Cards

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 @@ -36,3 +36,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