Loading states for main view (#732)

Co-authored-by: Jakub Kotula <520927+jkbktl@users.noreply.github.com>
This commit is contained in:
marcelines
2025-07-12 01:24:14 +09:00
committed by GitHub
co-authored by Jakub Kotula
parent 777709058a
commit 8d4d607a9a
35 changed files with 968 additions and 254 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@status-im/wallet': patch
'portfolio': patch
'wallet': patch
---
loading states
+3 -3
View File
@@ -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}</>
}
+103
View File
@@ -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 }
+159 -104
View File
@@ -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>
)
}
+44
View File
@@ -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}
/>
+9 -1
View File
@@ -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: {
+5
View File
@@ -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 }
+7
View File
@@ -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)',
]
+1
View File
@@ -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,
}
}
+1
View File
@@ -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 => {