mirror of
https://github.com/status-im/status-web.git
synced 2026-08-31 06:11:10 +00:00
Loading states for main view (#732)
Co-authored-by: Jakub Kotula <520927+jkbktl@users.noreply.github.com>
This commit is contained in:
co-authored by
Jakub Kotula
parent
777709058a
commit
8d4d607a9a
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@status-im/wallet': patch
|
||||
'portfolio': patch
|
||||
'wallet': patch
|
||||
---
|
||||
|
||||
loading states
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
"@status-im/wallet": patch
|
||||
"portfolio": patch
|
||||
"wallet": patch
|
||||
'@status-im/wallet': patch
|
||||
'portfolio': patch
|
||||
'wallet': patch
|
||||
---
|
||||
|
||||
limit wallet networks
|
||||
|
||||
@@ -1,70 +1,5 @@
|
||||
import { Skeleton } from '@status-im/components'
|
||||
|
||||
import { Table } from '../../../_components/table'
|
||||
import { AssetsListLoading } from '@status-im/wallet/components'
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="min-h-[calc(100svh-362px)]">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.HeaderCell>Assets</Table.HeaderCell>
|
||||
<Table.HeaderCell>Price</Table.HeaderCell>
|
||||
<Table.HeaderCell>24H%</Table.HeaderCell>
|
||||
<Table.HeaderCell>Balance</Table.HeaderCell>
|
||||
<Table.HeaderCell>Total</Table.HeaderCell>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{Array.from({ length: 12 }).map((_, index) => {
|
||||
return (
|
||||
<Table.Row key={index}>
|
||||
<Table.Cell size={36}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton height={24} width={24} className="rounded-full" />
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={120}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell size={36}>
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={56}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell size={36}>
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={56}
|
||||
className="rounded-6"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell size={36}>
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={56}
|
||||
className="rounded-6"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell size={36}>
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={56}
|
||||
className="rounded-6"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
)
|
||||
return <AssetsListLoading />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
delayMs?: number
|
||||
}
|
||||
|
||||
export function DevSuspenseWrapper({ children, delayMs = 3000 }: Props) {
|
||||
const [ready, setReady] = useState(false)
|
||||
const promiseRef = useRef<Promise<void> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready) {
|
||||
if (!promiseRef.current) {
|
||||
promiseRef.current = new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
setReady(true)
|
||||
resolve()
|
||||
}, delayMs)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [ready, delayMs])
|
||||
|
||||
if (!ready) {
|
||||
if (promiseRef.current) {
|
||||
throw promiseRef.current
|
||||
}
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Skeleton } from '@status-im/components'
|
||||
|
||||
const AccountSkeleton = ({
|
||||
variant = 'primary',
|
||||
}: {
|
||||
variant?: 'primary' | 'secondary'
|
||||
}) => (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Skeleton height={27} width={27} className="rounded-10" variant={variant} />
|
||||
<Skeleton height={16} width={73} className="rounded-10" variant={variant} />
|
||||
</div>
|
||||
)
|
||||
|
||||
const ActionButtonsSkeleton = () => (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Skeleton
|
||||
height={32}
|
||||
width={70}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
height={32}
|
||||
width={105}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
height={32}
|
||||
width={75}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
const BalanceSkeleton = ({
|
||||
variant = 'primary',
|
||||
}: {
|
||||
variant?: 'primary' | 'secondary'
|
||||
}) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Skeleton
|
||||
height={27}
|
||||
width={73}
|
||||
className="rounded-10"
|
||||
variant={variant}
|
||||
/>
|
||||
<Skeleton
|
||||
height={16}
|
||||
width={16}
|
||||
className="rounded-10"
|
||||
variant={variant}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={40}
|
||||
className="rounded-10"
|
||||
variant={variant}
|
||||
/>
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={26}
|
||||
className="rounded-10"
|
||||
variant={variant}
|
||||
/>
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={34}
|
||||
className="rounded-10"
|
||||
variant={variant}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const TabsSkeleton = () => (
|
||||
<div className="flex flex-col items-end gap-2 sm:flex-row sm:items-center">
|
||||
<Skeleton
|
||||
height={32}
|
||||
width={70}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
height={32}
|
||||
width={105}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
height={32}
|
||||
width={105}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export { AccountSkeleton, ActionButtonsSkeleton, BalanceSkeleton, TabsSkeleton }
|
||||
@@ -1,26 +1,26 @@
|
||||
import { Avatar } from '@status-im/components'
|
||||
import { Avatar, Skeleton } from '@status-im/components'
|
||||
import { Balance, StickyHeaderContainer } from '@status-im/wallet/components'
|
||||
|
||||
import { usePortfolio } from '@/hooks/use-portfolio'
|
||||
import { useWallet } from '@/providers/wallet-context'
|
||||
|
||||
import { ActionButtons } from '../components/action-buttons'
|
||||
import { RecoveryPhraseBackup } from '../components/recovery-phrase-backup'
|
||||
import {
|
||||
AccountSkeleton,
|
||||
ActionButtonsSkeleton,
|
||||
BalanceSkeleton,
|
||||
TabsSkeleton,
|
||||
} from './skeletons'
|
||||
import { TabLink } from './tab-link'
|
||||
|
||||
type Props = {
|
||||
list: React.ReactNode
|
||||
detail?: React.ReactNode
|
||||
isLoading?: boolean
|
||||
loadingState?: React.ReactNode
|
||||
}
|
||||
|
||||
const DEFAULT_SUMMARY = {
|
||||
total_balance: 0,
|
||||
total_eur: 0,
|
||||
total_eur_24h_change: 0.0,
|
||||
total_percentage_24h_change: 0.0,
|
||||
}
|
||||
|
||||
// Includes mock data for actions buttons and options. todo? Replace with actual data
|
||||
const getActionsButtonsData = (address: string | undefined) => ({
|
||||
address: address ?? '',
|
||||
pathname: '/portfolio/assets',
|
||||
@@ -40,108 +40,25 @@ const getActionsButtonsData = (address: string | undefined) => ({
|
||||
},
|
||||
})
|
||||
|
||||
const account = {
|
||||
name: 'Account 1',
|
||||
emoji: '🍑',
|
||||
color: 'magenta',
|
||||
}
|
||||
|
||||
const SplittedLayout = (props: Props) => {
|
||||
const { list, detail, isLoading } = props
|
||||
|
||||
const { currentWallet, isLoading: isWalletLoading } = useWallet()
|
||||
|
||||
const address = currentWallet?.activeAccounts[0].address
|
||||
|
||||
const { data } = useAssets({
|
||||
address,
|
||||
isWalletLoading,
|
||||
})
|
||||
|
||||
const summary = data?.summary ? data.summary : DEFAULT_SUMMARY
|
||||
const { list, detail, isLoading, loadingState } = props
|
||||
|
||||
return (
|
||||
<div className="grid flex-1 divide-x divide-neutral-10 overflow-hidden">
|
||||
{/* Main content */}
|
||||
<div className="flex divide-x divide-default-neutral-20">
|
||||
<div className="flex grow flex-col 2xl:basis-1/2">
|
||||
<div className="relative h-[calc(100vh-56px)] overflow-auto">
|
||||
{isLoading ? (
|
||||
<div className="flex min-h-full items-center justify-center">
|
||||
<div className="size-5 animate-spin rounded-full border-b-2 border-neutral-50"></div>
|
||||
<div className="relative flex h-[calc(100vh-56px)] flex-col overflow-auto">
|
||||
<StickyHeaderContainer
|
||||
isLarge
|
||||
className="px-6 xl:px-12"
|
||||
leftSlot={<HeaderLeftSlot />}
|
||||
rightSlot={<HeaderRightSlot />}
|
||||
>
|
||||
<div className="relative -mt-8 flex flex-1 flex-col px-3 xl:mt-0 xl:px-12">
|
||||
<MainContentBody />
|
||||
{isLoading ? loadingState : list}
|
||||
</div>
|
||||
) : (
|
||||
<StickyHeaderContainer
|
||||
isLarge
|
||||
className="px-6 xl:px-12"
|
||||
leftSlot={
|
||||
<>
|
||||
<div
|
||||
className="hidden items-center gap-1.5 xl:flex"
|
||||
data-customisation={account.color}
|
||||
>
|
||||
<Avatar
|
||||
type="account"
|
||||
name={account.name}
|
||||
emoji={account.emoji}
|
||||
size="24"
|
||||
bgOpacity="20"
|
||||
/>
|
||||
<div className="text-15 font-semibold text-neutral-100">
|
||||
{account.name}
|
||||
</div>
|
||||
</div>
|
||||
<Balance summary={summary} />
|
||||
</>
|
||||
}
|
||||
rightSlot={
|
||||
<div className="flex flex-col items-end gap-2 sm:flex-row sm:items-center">
|
||||
<TabLink
|
||||
href="/portfolio/assets"
|
||||
className="w-full justify-center text-center sm:w-fit"
|
||||
>
|
||||
Assets
|
||||
</TabLink>
|
||||
<TabLink href="/portfolio/collectibles">
|
||||
Collectibles
|
||||
</TabLink>
|
||||
<TabLink href="/portfolio/activity">Activity</TabLink>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="relative -mt-8 flex flex-1 flex-col px-3 xl:mt-0 xl:px-12">
|
||||
<div className="mb-5 flex flex-col gap-2 px-3">
|
||||
<div
|
||||
className="hidden items-center gap-1.5 xl:flex"
|
||||
data-customisation={account.color}
|
||||
>
|
||||
<Avatar
|
||||
type="account"
|
||||
name={account.name}
|
||||
emoji={account.emoji}
|
||||
size="24"
|
||||
bgOpacity="20"
|
||||
/>
|
||||
<div className="text-15 font-semibold text-neutral-100">
|
||||
{account.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<Balance summary={summary ?? DEFAULT_SUMMARY} />
|
||||
</div>
|
||||
|
||||
<ActionButtons {...getActionsButtonsData(address)} />
|
||||
|
||||
<div className="my-4 flex">
|
||||
<RecoveryPhraseBackup />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{list}
|
||||
</div>
|
||||
</StickyHeaderContainer>
|
||||
)}
|
||||
</StickyHeaderContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -162,3 +79,141 @@ const SplittedLayout = (props: Props) => {
|
||||
}
|
||||
|
||||
export default SplittedLayout
|
||||
|
||||
// Components
|
||||
const AccountInfo = () => {
|
||||
const { account, isWalletLoading } = usePortfolio()
|
||||
|
||||
if (isWalletLoading) {
|
||||
return <AccountSkeleton variant="secondary" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hidden items-center gap-1.5 xl:flex"
|
||||
data-customisation={account.color}
|
||||
>
|
||||
<Avatar
|
||||
type="account"
|
||||
name={account.name}
|
||||
emoji={account.emoji}
|
||||
size="24"
|
||||
bgOpacity="20"
|
||||
/>
|
||||
<div className="text-15 font-semibold text-neutral-100">
|
||||
{account.name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PortfolioBalance = ({
|
||||
variant = 'primary',
|
||||
}: {
|
||||
variant?: 'primary' | 'secondary'
|
||||
}) => {
|
||||
const { summary, isLoading } = usePortfolio()
|
||||
|
||||
if (isLoading) {
|
||||
return <BalanceSkeleton variant={variant} />
|
||||
}
|
||||
|
||||
return <Balance summary={summary} />
|
||||
}
|
||||
|
||||
const HeaderLeftSlot = () => {
|
||||
const { isLoading } = usePortfolio()
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 px-3">
|
||||
<div className="flex items-center justify-between gap-1.5">
|
||||
<AccountSkeleton />
|
||||
</div>
|
||||
<BalanceSkeleton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountInfo />
|
||||
<PortfolioBalance />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const HeaderRightSlot = () => {
|
||||
const { isLoading } = usePortfolio()
|
||||
|
||||
if (isLoading) {
|
||||
return <TabsSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end gap-2 sm:flex-row sm:items-center">
|
||||
<TabLink
|
||||
href="/portfolio/assets"
|
||||
className="w-full justify-center text-center sm:w-fit"
|
||||
>
|
||||
Assets
|
||||
</TabLink>
|
||||
<TabLink href="/portfolio/collectibles">Collectibles</TabLink>
|
||||
<TabLink href="/portfolio/activity">Activity</TabLink>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MainContentBody = () => {
|
||||
const { account, isLoading } = usePortfolio()
|
||||
const { currentWallet, isLoading: isWalletLoading } = useWallet()
|
||||
|
||||
const address = currentWallet?.activeAccounts[0].address
|
||||
|
||||
if (isLoading || isWalletLoading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 px-3">
|
||||
<div className="flex items-center justify-between gap-1.5">
|
||||
<Skeleton
|
||||
height={27}
|
||||
width={186}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<BalanceSkeleton variant="secondary" />
|
||||
</div>
|
||||
<div className="my-5 flex items-center justify-between">
|
||||
<ActionButtonsSkeleton />
|
||||
<Skeleton
|
||||
height={32}
|
||||
width={32}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-5 flex flex-col gap-2 px-3">
|
||||
<div
|
||||
className="flex items-center justify-between gap-1.5"
|
||||
data-customisation={account.color}
|
||||
>
|
||||
<AccountInfo />
|
||||
<div data-customisation="blue">
|
||||
<RecoveryPhraseBackup />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<PortfolioBalance variant="secondary" />
|
||||
</div>
|
||||
|
||||
<ActionButtons {...getActionsButtonsData(address)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useAssets } from '@/hooks/use-assets'
|
||||
import { useWallet } from '@/providers/wallet-context'
|
||||
|
||||
const DEFAULT_SUMMARY = {
|
||||
total_balance: 0,
|
||||
total_eur: 0,
|
||||
total_eur_24h_change: 0.0,
|
||||
total_percentage_24h_change: 0.0,
|
||||
}
|
||||
|
||||
const MOCK_ACCOUNT = {
|
||||
name: 'Account 1',
|
||||
emoji: '🍑',
|
||||
color: 'magenta',
|
||||
}
|
||||
|
||||
const usePortfolio = () => {
|
||||
const { currentWallet, isLoading: isWalletLoading } = useWallet()
|
||||
|
||||
const { data: assetsData, isLoading: isAssetsLoading } = useAssets({
|
||||
address: currentWallet?.activeAccounts[0]?.address,
|
||||
isWalletLoading,
|
||||
})
|
||||
|
||||
const account = {
|
||||
...MOCK_ACCOUNT,
|
||||
name: currentWallet?.name || MOCK_ACCOUNT.name,
|
||||
}
|
||||
|
||||
const summary = assetsData?.summary || DEFAULT_SUMMARY
|
||||
const isLoading = isWalletLoading || isAssetsLoading
|
||||
|
||||
return {
|
||||
currentWallet,
|
||||
account,
|
||||
summary,
|
||||
assetsData,
|
||||
isWalletLoading,
|
||||
isAssetsLoading,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
export { usePortfolio }
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ActivityList, FeedbackSection } from '@status-im/wallet/components'
|
||||
import {
|
||||
ActivityList,
|
||||
ActivityListSkeleton,
|
||||
EmptyState,
|
||||
FeedbackSection,
|
||||
} from '@status-im/wallet/components'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import SplittedLayout from '@/components/splitted-layout'
|
||||
@@ -29,11 +34,16 @@ function RouteComponent() {
|
||||
isLoadingMore={isFetchingNextPage}
|
||||
/>
|
||||
) : (
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
{!address ? 'No wallet selected' : 'No activity'}
|
||||
<div className="flex flex-1 flex-col gap-3">
|
||||
{!address ? (
|
||||
<p className="mt-4">No wallet selected</p>
|
||||
) : (
|
||||
<EmptyState variant="activity" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
loadingState={<ActivityListSkeleton />}
|
||||
detail={<FeedbackSection />}
|
||||
isLoading={isLoading || isWalletLoading}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Suspense } from 'react'
|
||||
|
||||
import { AssetsList } from '@status-im/wallet/components'
|
||||
import { AssetsList, AssetsListLoading } from '@status-im/wallet/components'
|
||||
import {
|
||||
createFileRoute,
|
||||
useRouter,
|
||||
@@ -35,7 +33,7 @@ function Component() {
|
||||
})
|
||||
|
||||
if (!currentWallet || !address) {
|
||||
return <div>No wallet selected</div>
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -67,11 +65,8 @@ function Component() {
|
||||
<div className="mt-4 flex flex-col gap-3">Empty state</div>
|
||||
)
|
||||
}
|
||||
detail={
|
||||
<Suspense fallback={<p>Loading token...</p>}>
|
||||
<Token ticker={ticker} address={address} />
|
||||
</Suspense>
|
||||
}
|
||||
loadingState={<AssetsListLoading />}
|
||||
detail={<Token ticker={ticker} address={address} />}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
TIME_FRAMES,
|
||||
TokenAmount,
|
||||
TokenLogo,
|
||||
TokenSkeleton,
|
||||
} from '@status-im/wallet/components'
|
||||
import { useCopyToClipboard } from '@status-im/wallet/hooks'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
@@ -236,7 +237,7 @@ const Token = (props: Props) => {
|
||||
}, [tokenDetail])
|
||||
|
||||
if (isLoading || !asset) {
|
||||
return <p>Loading</p>
|
||||
return <TokenSkeleton />
|
||||
}
|
||||
|
||||
const tokenAssets = tokenDetail?.assets
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AssetsList,
|
||||
AssetsListLoading,
|
||||
FeedbackSection,
|
||||
PinExtension,
|
||||
} from '@status-im/wallet/components'
|
||||
@@ -29,39 +30,34 @@ function Component() {
|
||||
})
|
||||
const isDesktop = useMediaQuery('xl')
|
||||
|
||||
if (!currentWallet || !address) {
|
||||
return <div>No wallet selected</div>
|
||||
}
|
||||
if (!currentWallet || !address) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<SplittedLayout
|
||||
list={
|
||||
data?.assets ? (
|
||||
<AssetsList
|
||||
assets={data.assets}
|
||||
onSelect={url => {
|
||||
const ticker = url.split('/').pop()
|
||||
if (!ticker) return
|
||||
router.navigate({
|
||||
to: '/portfolio/assets/$ticker',
|
||||
params: { ticker },
|
||||
...(!isDesktop && {
|
||||
viewTransition: true,
|
||||
}),
|
||||
})
|
||||
}}
|
||||
clearSearch={() => {
|
||||
console.log('Search cleared')
|
||||
}}
|
||||
searchParams={new URLSearchParams()}
|
||||
pathname="/portfolio/assets"
|
||||
/>
|
||||
) : (
|
||||
<div className="mt-4 flex flex-col gap-3">Empty state</div>
|
||||
)
|
||||
<AssetsList
|
||||
assets={data?.assets ?? []}
|
||||
onSelect={url => {
|
||||
const ticker = url.split('/').pop()
|
||||
if (!ticker) return
|
||||
router.navigate({
|
||||
to: '/portfolio/assets/$ticker',
|
||||
params: { ticker },
|
||||
...(!isDesktop && {
|
||||
viewTransition: true,
|
||||
}),
|
||||
})
|
||||
}}
|
||||
clearSearch={() => {
|
||||
console.log('Search cleared')
|
||||
}}
|
||||
searchParams={new URLSearchParams()}
|
||||
pathname="/portfolio/assets"
|
||||
/>
|
||||
}
|
||||
detail={<FeedbackSection />}
|
||||
loadingState={<AssetsListLoading />}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{isPinExtension && (
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Suspense } from 'react'
|
||||
|
||||
import { CollectiblesGrid as CollectiblesList } from '@status-im/wallet/components'
|
||||
import {
|
||||
CollectiblesGrid as CollectiblesList,
|
||||
CollectiblesGridSkeleton,
|
||||
} from '@status-im/wallet/components'
|
||||
import { createFileRoute, useRouterState } from '@tanstack/react-router'
|
||||
|
||||
import SplittedLayout from '@/components/splitted-layout'
|
||||
@@ -41,7 +44,7 @@ function Component() {
|
||||
}, [data?.pages])
|
||||
|
||||
if (!currentWallet || !address) {
|
||||
return <div>No wallet selected</div>
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -65,6 +68,7 @@ function Component() {
|
||||
hasNextPage={hasNextPage}
|
||||
/>
|
||||
}
|
||||
loadingState={<CollectiblesGridSkeleton />}
|
||||
isLoading={isLoading}
|
||||
detail={
|
||||
<Suspense fallback={<p>Loading collectible...</p>}>
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
SadIcon,
|
||||
} from '@status-im/icons/20'
|
||||
import { OpenseaIcon } from '@status-im/icons/social'
|
||||
import { CurrencyAmount, NetworkLogo } from '@status-im/wallet/components'
|
||||
import {
|
||||
CollectibleSkeleton,
|
||||
CurrencyAmount,
|
||||
NetworkLogo,
|
||||
} from '@status-im/wallet/components'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
|
||||
@@ -58,8 +62,9 @@ const Collectible = (props: Props) => {
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
})
|
||||
|
||||
if (isLoading || !collectible) {
|
||||
return <p>Loading</p>
|
||||
return <CollectibleSkeleton />
|
||||
}
|
||||
|
||||
const imageUrl = collectible.image || collectible.thumbnail
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useMemo } from 'react'
|
||||
|
||||
import {
|
||||
CollectiblesGrid as CollectiblesList,
|
||||
CollectiblesGridSkeleton,
|
||||
EmptyState,
|
||||
FeedbackSection,
|
||||
PinExtension,
|
||||
} from '@status-im/wallet/components'
|
||||
@@ -39,29 +41,34 @@ function Component() {
|
||||
}, [data?.pages])
|
||||
|
||||
if (!currentWallet || !address) {
|
||||
return <div>No wallet selected</div>
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SplittedLayout
|
||||
list={
|
||||
<CollectiblesList
|
||||
LinkComponent={LinkCollectible}
|
||||
address={address}
|
||||
collectibles={collectibles}
|
||||
fetchNextPage={fetchNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
pathname={pathname}
|
||||
search={search}
|
||||
searchParams={searchParams}
|
||||
clearSearch={() => {
|
||||
// Clear the search input
|
||||
console.log('Search cleared')
|
||||
}}
|
||||
hasNextPage={hasNextPage}
|
||||
/>
|
||||
collectibles.length === 0 && !isLoading ? (
|
||||
<EmptyState variant="collectible" />
|
||||
) : (
|
||||
<CollectiblesList
|
||||
LinkComponent={LinkCollectible}
|
||||
address={address}
|
||||
collectibles={collectibles}
|
||||
fetchNextPage={fetchNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
pathname={pathname}
|
||||
search={search}
|
||||
searchParams={searchParams}
|
||||
clearSearch={() => {
|
||||
// Clear the search input
|
||||
console.log('Search cleared')
|
||||
}}
|
||||
hasNextPage={hasNextPage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
loadingState={<CollectiblesGridSkeleton />}
|
||||
detail={<FeedbackSection />}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
@@ -286,6 +286,14 @@ export default {
|
||||
from: { opacity: '0', transform: 'translateY(2px)' },
|
||||
to: { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
// skeleton with gradient colors animation in loop
|
||||
'gradient-skeleton': {
|
||||
'0%': { backgroundPosition: '0% 0%' },
|
||||
'25%': { backgroundPosition: '100% 0%' },
|
||||
'50%': { backgroundPosition: '100% 100%' },
|
||||
'75%': { backgroundPosition: '0% 100%' },
|
||||
'100%': { backgroundPosition: '0% 0%' },
|
||||
},
|
||||
},
|
||||
|
||||
animation: {
|
||||
@@ -310,8 +318,8 @@ export default {
|
||||
float: 'float 6s ease-in-out infinite',
|
||||
'float-reverse': 'float-reverse 6s ease-in-out infinite',
|
||||
pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
|
||||
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
'gradient-skeleton': 'gradient-skeleton 5s linear infinite',
|
||||
},
|
||||
|
||||
backgroundImage: {
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
"types": "./dist/src/hooks/index.d.ts",
|
||||
"import": "./dist/hooks/index.es.js",
|
||||
"require": "./dist/hooks/index.cjs.js"
|
||||
},
|
||||
"./constants": {
|
||||
"types": "./dist/src/constants/index.d.ts",
|
||||
"import": "./dist/constants/index.es.js",
|
||||
"require": "./dist/constants/index.cjs.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Skeleton } from '@status-im/components'
|
||||
|
||||
const ActivityItemSkeleton = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-[2fr_1fr_1fr] gap-4 rounded-12 p-3 transition-colors focus-within:bg-neutral-5 hover:bg-neutral-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Skeleton
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full"
|
||||
variant="secondary"
|
||||
/>
|
||||
<div className="absolute bottom-[-4px] right-[-4px]">
|
||||
<Skeleton
|
||||
width={18}
|
||||
height={18}
|
||||
className="rounded-full border border-white-100"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Skeleton
|
||||
width={120}
|
||||
height={16}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width={100}
|
||||
height={14}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end justify-center gap-2">
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={16}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width={60}
|
||||
height={14}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Skeleton
|
||||
width={60}
|
||||
height={16}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width={16}
|
||||
height={16}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ActivityItemSkeleton }
|
||||
@@ -52,7 +52,7 @@ const ActivityItem = (props: ActivityItemProps) => {
|
||||
<a
|
||||
href={`https://etherscan.io/tx/${activity.hash}`}
|
||||
target="_blank"
|
||||
className="grid grid-cols-[2fr_1fr_1fr] gap-4 p-3 transition-colors focus-within:bg-neutral-5 hover:bg-neutral-5"
|
||||
className="grid grid-cols-[2fr_1fr_1fr] gap-4 rounded-12 p-3 transition-colors focus-within:bg-neutral-5 hover:bg-neutral-5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ActivityItemSkeleton } from './activity-item-skeleton'
|
||||
|
||||
const ActivityListSkeleton = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{Array.from({ length: 6 }).map((_, idx) => (
|
||||
<ActivityItemSkeleton key={idx} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ActivityListSkeleton }
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Skeleton } from '@status-im/components'
|
||||
|
||||
import * as Table from '../table'
|
||||
|
||||
const AssetsListLoading = () => {
|
||||
return (
|
||||
<div
|
||||
aria-busy="true"
|
||||
aria-label="Loading assets"
|
||||
className="min-h-[calc(100svh-362px)]"
|
||||
>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.HeaderCell>Assets</Table.HeaderCell>
|
||||
<Table.HeaderCell>Price</Table.HeaderCell>
|
||||
<Table.HeaderCell>24H%</Table.HeaderCell>
|
||||
<Table.HeaderCell>Balance</Table.HeaderCell>
|
||||
<Table.HeaderCell>Total</Table.HeaderCell>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{Array.from({ length: 12 }).map((_, index) => {
|
||||
return (
|
||||
<Table.Row key={index} className="pointer-events-none">
|
||||
<Table.Cell size={36}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton height={24} width={24} className="rounded-full" />
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={120}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell size={36}>
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={56}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell size={36}>
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={56}
|
||||
className="rounded-6"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell size={36}>
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={56}
|
||||
className="rounded-6"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell size={36}>
|
||||
<Skeleton
|
||||
height={10}
|
||||
width={56}
|
||||
className="rounded-6"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { AssetsListLoading }
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Skeleton } from '@status-im/components'
|
||||
|
||||
const CollectibleSkeleton = () => {
|
||||
return (
|
||||
<div
|
||||
aria-busy="true"
|
||||
aria-label="Loading collectible"
|
||||
className="overflow-auto p-4 pr-3 2xl:p-12"
|
||||
>
|
||||
<div className="z-30 flex items-center gap-1 py-4">
|
||||
<Skeleton
|
||||
width={60}
|
||||
height={16}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-10 flex gap-4">
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<Skeleton
|
||||
width={120}
|
||||
height={16}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Skeleton
|
||||
width={140}
|
||||
height={32}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
width={40}
|
||||
height={32}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton
|
||||
width={140}
|
||||
height={140}
|
||||
className="aspect-square -translate-y-12 rounded-16"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="-mt-4 grid gap-8">
|
||||
<div>
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={16}
|
||||
className="mb-2 rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
<div className="mb-5 flex flex-col gap-2">
|
||||
<Skeleton
|
||||
width="100%"
|
||||
height={12}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width="90%"
|
||||
height={12}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width="80%"
|
||||
height={12}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(155px,1fr))] gap-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
width="100%"
|
||||
height={16}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px w-full border-t border-dashed border-neutral-20" />
|
||||
|
||||
<div>
|
||||
<Skeleton
|
||||
width={60}
|
||||
height={16}
|
||||
className="mb-3 rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(155px,1fr))] gap-3">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div key={index} className="flex flex-col gap-1">
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={13}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width={100}
|
||||
height={13}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { CollectibleSkeleton }
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Skeleton } from '@status-im/components'
|
||||
|
||||
import { GRADIENTS } from '../../constants'
|
||||
|
||||
const CollectiblesGridSkeleton = () => {
|
||||
return (
|
||||
<div
|
||||
aria-busy="true"
|
||||
aria-label="Loading collectibles"
|
||||
className="grid grid-cols-2 gap-3 overscroll-contain lg:grid-cols-4"
|
||||
>
|
||||
{GRADIENTS.slice(0, 4).map((gradient, index) => {
|
||||
return (
|
||||
<div key={index} className="relative">
|
||||
<div
|
||||
className="aspect-square h-[calc(100%-36px)] w-full animate-gradient-skeleton rounded-12"
|
||||
style={{
|
||||
background: gradient,
|
||||
backgroundSize: '200% 200%',
|
||||
}}
|
||||
/>
|
||||
<div className="absolute top-0 aspect-square h-[calc(100%-36px)] w-full rounded-12 bg-blur-white/70" />
|
||||
<div className="flex items-center gap-1 py-2">
|
||||
<Skeleton
|
||||
height={20}
|
||||
width={20}
|
||||
className="rounded-6"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
height={20}
|
||||
width={120}
|
||||
className="rounded-6"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { CollectiblesGridSkeleton }
|
||||
@@ -2,25 +2,12 @@ import { Button, Skeleton } from '@status-im/components'
|
||||
import { SadIcon } from '@status-im/icons/20'
|
||||
import { cx } from 'class-variance-authority'
|
||||
|
||||
import { GRADIENTS } from '../../constants/gradients'
|
||||
import { useInfiniteLoading } from '../../hooks/use-infinite-loading'
|
||||
|
||||
import type { Collectible } from '@status-im/wallet/data'
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
|
||||
export const GRADIENTS = [
|
||||
'linear-gradient(120deg, #F6B03C, #1992D7, #7140FD)',
|
||||
'linear-gradient(190deg, #FF7D46, #7140FD, #2A4AF5)',
|
||||
'linear-gradient(145deg, #2A4AF5, #EC266C, #F6B03C)',
|
||||
'linear-gradient(195deg, #216266, #FF7D46, #2A4AF5)',
|
||||
'linear-gradient(45deg, #7140FD, #216266, #F6B03C)',
|
||||
'linear-gradient(145deg, #F6B03C, #1992D7, #7140FD)',
|
||||
'linear-gradient(45deg, #F6B03C, #1992D7, #7140FD)',
|
||||
'linear-gradient(145deg, #FF7D46, #7140FD, #2A4AF5)',
|
||||
'linear-gradient(45deg, #2A4AF5, #EC266C, #F6B03C)',
|
||||
'linear-gradient(125deg, #216266, #FF7D46, #2A4AF5)',
|
||||
'linear-gradient(145deg, #F6B03C, #1992D7, #7140FD)',
|
||||
'linear-gradient(145deg, #F6B03C, #1992D7, #7140FD)',
|
||||
]
|
||||
type LinkComponentProps = {
|
||||
href: string
|
||||
className?: string
|
||||
@@ -156,7 +143,7 @@ const CollectiblesGrid = (props: Props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isLoading && hasNextPage && (
|
||||
{hasNextPage && isLoading && (
|
||||
<div className="grid grid-cols-2 gap-3 overscroll-contain lg:grid-cols-4">
|
||||
{GRADIENTS.slice(0, 4).map((gradient, index) => {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { match } from 'ts-pattern'
|
||||
|
||||
import { Image } from '../image'
|
||||
|
||||
import type { ImageId } from '../image'
|
||||
|
||||
type Props = {
|
||||
variant: 'activity' | 'collectible'
|
||||
}
|
||||
|
||||
const EmptyState = (props: Props) => {
|
||||
const { variant } = props
|
||||
|
||||
const content = match(variant)
|
||||
.with('activity', () => ({
|
||||
imageId: 'admin/empty/devices:241:240' as ImageId,
|
||||
imageAlt: 'No activity',
|
||||
title: 'No Activity',
|
||||
description: 'Transaction history will be visible on this page',
|
||||
}))
|
||||
.with('collectible', () => ({
|
||||
imageId: 'Portfolio/Empty States/No_Collectibles:161:160' as ImageId,
|
||||
imageAlt: 'No collectibles',
|
||||
title: 'No Collectibles',
|
||||
description: 'Your collectibles will be visible on this page',
|
||||
}))
|
||||
.exhaustive()
|
||||
|
||||
return (
|
||||
<div className="m-auto flex flex-col items-center justify-center text-center">
|
||||
<Image id={content.imageId} alt={content.imageAlt} className="size-20" />
|
||||
<h2 className="mb-0.5 text-15 font-600">{content.title}</h2>
|
||||
<p className="text-13">{content.description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { EmptyState }
|
||||
@@ -2,8 +2,11 @@ export type * from '../types'
|
||||
export * from '../utils/variants'
|
||||
export { AccountMenu } from './account-menu'
|
||||
export { ActivityList, type ActivityListProps } from './activity-list'
|
||||
export { ActivityItemSkeleton } from './activity-list/components/activity-item-skeleton'
|
||||
export { ActivityListSkeleton } from './activity-list/components/activity-list-skeleton'
|
||||
export { type Account, Address, type AddressProps } from './address'
|
||||
export { AssetsList } from './assets-list'
|
||||
export { AssetsListLoading } from './assets-list-loading'
|
||||
export { Balance } from './balance'
|
||||
export { BlurredCircle } from './blurred-circle'
|
||||
export {
|
||||
@@ -21,7 +24,9 @@ export {
|
||||
DEFAULT_TIME_FRAME,
|
||||
TIME_FRAMES,
|
||||
} from './chart'
|
||||
export { CollectibleSkeleton } from './collectible-skeleton'
|
||||
export { CollectiblesGrid } from './collectibles-grid'
|
||||
export { CollectiblesGridSkeleton } from './collectibles-grid/collectibles-grid-skeleton'
|
||||
export {
|
||||
CreatePasswordForm,
|
||||
type CreatePasswordFormValues,
|
||||
@@ -29,6 +34,7 @@ export {
|
||||
export { CurrencyAmount } from './currency-amount'
|
||||
export { DeleteAddressAlert } from './delete-address-alert'
|
||||
export { DropdownSort } from './dropdown-sort'
|
||||
export { EmptyState } from './empty-state'
|
||||
export { FeedbackPopover, FeedbackSection } from './feedback'
|
||||
export { Image, type ImageProps } from './image'
|
||||
export {
|
||||
@@ -64,4 +70,5 @@ export { StickyHeaderContainer } from './sticky-header-container'
|
||||
export { getTabLinkClassName, TabLink } from './tab-link'
|
||||
export { TokenAmount } from './token-amount'
|
||||
export { TokenLogo } from './token-logo'
|
||||
export { TokenSkeleton } from './token-skeleton'
|
||||
export { Tooltip } from './tooltip'
|
||||
|
||||
@@ -21,6 +21,7 @@ type PasswordModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: (password: string) => Promise<void>
|
||||
isLoading?: boolean
|
||||
buttonLabel?: string
|
||||
}
|
||||
|
||||
const PasswordModal = (props: PasswordModalProps) => {
|
||||
@@ -37,7 +38,13 @@ const PasswordModal = (props: PasswordModalProps) => {
|
||||
},
|
||||
})
|
||||
|
||||
const { open, onOpenChange, onConfirm, isLoading = false } = props
|
||||
const {
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
isLoading = false,
|
||||
buttonLabel = 'Confirm',
|
||||
} = props
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
@@ -154,7 +161,7 @@ const PasswordModal = (props: PasswordModalProps) => {
|
||||
<span>Signing...</span>
|
||||
</div>
|
||||
) : (
|
||||
'Confirm'
|
||||
buttonLabel
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -539,6 +539,7 @@ const SendAssetsModal = (props: Props) => {
|
||||
onOpenChange={handlePasswordModalClose}
|
||||
onConfirm={handlePasswordConfirm}
|
||||
isLoading={isTransactionSigning}
|
||||
buttonLabel="Sign Transaction"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -110,7 +110,7 @@ const StickyHeaderContainer = (props: Props) => {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative h-[calc(100vh-56px)] w-full overflow-auto scrollbar-stable"
|
||||
className="relative flex h-[calc(100vh-56px)] w-full flex-col overflow-auto scrollbar-stable"
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Skeleton } from '@status-im/components'
|
||||
|
||||
const TokenSkeleton = () => {
|
||||
return (
|
||||
<div
|
||||
aria-busy="true"
|
||||
aria-label="Loading token"
|
||||
className="grid gap-10 p-4 2xl:p-12"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-full"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={18}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex flex-col gap-2">
|
||||
<Skeleton
|
||||
width={120}
|
||||
height={18}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={12}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={32}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={32}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={32}
|
||||
className="rounded-10"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Skeleton
|
||||
height={48}
|
||||
width="100%"
|
||||
className="rounded-12"
|
||||
variant="secondary"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-y-4 2xl:grid-cols-4">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={index} className="flex flex-col gap-1 pr-4">
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={14}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width={100}
|
||||
height={18}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton
|
||||
width="60%"
|
||||
height={14}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width="80%"
|
||||
height={14}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width="70%"
|
||||
height={14}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
<Skeleton
|
||||
width="50%"
|
||||
height={14}
|
||||
variant="secondary"
|
||||
className="rounded-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { TokenSkeleton }
|
||||
@@ -0,0 +1,14 @@
|
||||
export const GRADIENTS = [
|
||||
'linear-gradient(120deg, #F6B03C, #1992D7, #7140FD)',
|
||||
'linear-gradient(190deg, #FF7D46, #7140FD, #2A4AF5)',
|
||||
'linear-gradient(145deg, #2A4AF5, #EC266C, #F6B03C)',
|
||||
'linear-gradient(195deg, #216266, #FF7D46, #2A4AF5)',
|
||||
'linear-gradient(45deg, #7140FD, #216266, #F6B03C)',
|
||||
'linear-gradient(145deg, #F6B03C, #1992D7, #7140FD)',
|
||||
'linear-gradient(45deg, #F6B03C, #1992D7, #7140FD)',
|
||||
'linear-gradient(145deg, #FF7D46, #7140FD, #2A4AF5)',
|
||||
'linear-gradient(45deg, #2A4AF5, #EC266C, #F6B03C)',
|
||||
'linear-gradient(125deg, #216266, #FF7D46, #2A4AF5)',
|
||||
'linear-gradient(145deg, #F6B03C, #1992D7, #7140FD)',
|
||||
'linear-gradient(145deg, #F6B03C, #1992D7, #7140FD)',
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
export { GRADIENTS } from './gradients'
|
||||
@@ -17,7 +17,7 @@ const useInfiniteLoading = (props: Props) => {
|
||||
const endOfPageRef = useRef<HTMLDivElement>(null)
|
||||
const entry = useIntersectionObserver(endOfPageRef, {
|
||||
rootMargin,
|
||||
threshold: 0.1,
|
||||
threshold: 0.5,
|
||||
})
|
||||
|
||||
const isVisible = !!entry?.isIntersecting
|
||||
@@ -34,6 +34,7 @@ const useInfiniteLoading = (props: Props) => {
|
||||
return {
|
||||
endOfPageRef,
|
||||
isLoading,
|
||||
isVisible,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export default defineConfig(({ mode }) => {
|
||||
'tailwind.config': './tailwind.config.ts',
|
||||
'data/index': './src/data/index.ts',
|
||||
'hooks/index': './src/hooks/index.ts',
|
||||
'constants/index': './src/constants/index.ts',
|
||||
},
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: format => {
|
||||
|
||||
Reference in New Issue
Block a user