mirror of
https://github.com/status-im/status-web.git
synced 2026-08-31 06:11:10 +00:00
update sticky bar for assets and collectibles (#665)
Co-authored-by: Felicio <felicio@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@status-im/wallet': patch
|
||||
'portfolio': patch
|
||||
'wallet': patch
|
||||
---
|
||||
|
||||
update sticky bar for assets and collectibles
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { CurrencyAmount } from '../../../../../_components/currency-amount'
|
||||
import { NetworkLogo } from '../../../../../_components/network-logo'
|
||||
import { CurrencyAmount, NetworkLogo } from '@status-im/wallet/components'
|
||||
|
||||
import type { ApiOutput, NetworkType } from '@status-im/wallet/data'
|
||||
|
||||
|
||||
@@ -2,17 +2,19 @@ import { Suspense } from 'react'
|
||||
|
||||
import { Button, Tooltip } from '@status-im/components'
|
||||
import { BuyIcon, ReceiveBlurIcon } from '@status-im/icons/20'
|
||||
import { StickyHeaderContainer } from '@status-im/wallet/components'
|
||||
import {
|
||||
Balance,
|
||||
CurrencyAmount,
|
||||
StickyHeaderContainer,
|
||||
} from '@status-im/wallet/components'
|
||||
import { cx } from 'class-variance-authority'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
|
||||
import { getAPIClient } from '../../../../..//data/api'
|
||||
import { Balance } from '../../../../_components/balance'
|
||||
import { BuyCryptoDrawer } from '../../../../_components/buy-crypto-drawer'
|
||||
import { portfolioComponents } from '../../../../_components/content'
|
||||
import { CurrencyAmount } from '../../../../_components/currency-amount'
|
||||
import { ReceiveCryptoDrawer } from '../../../../_components/receive-crypto-drawer'
|
||||
import { TokenAmount } from '../../../../_components/token-amount'
|
||||
import { Chart } from '../_components/chart'
|
||||
|
||||
+1
-2
@@ -3,10 +3,9 @@
|
||||
import { Button } from '@status-im/components'
|
||||
import { ExternalIcon, OptionsIcon, SadIcon } from '@status-im/icons/20'
|
||||
import { OpenseaIcon } from '@status-im/icons/social'
|
||||
import { CurrencyAmount, NetworkLogo } from '@status-im/wallet/components'
|
||||
|
||||
import { getAPIClient } from '../../../../../../../data/api'
|
||||
import { CurrencyAmount } from '../../../../../../_components/currency-amount'
|
||||
import { NetworkLogo } from '../../../../../../_components/network-logo'
|
||||
import { ImageLightbox } from './_components/image-lightbox'
|
||||
import { InfoCard } from './_components/info-card'
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Balance } from '@status-im/wallet/components'
|
||||
|
||||
import { getAPIClient } from '../../../../data/api'
|
||||
import { Balance } from '../../../_components/balance'
|
||||
|
||||
import type { NetworkType } from '@status-im/wallet/data'
|
||||
|
||||
|
||||
+1
-1
@@ -5,8 +5,8 @@ import { useMemo } from 'react'
|
||||
import { CollectiblesGrid as CollectiblesList } from '@status-im/wallet/components'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { usePathname, useSearchParams } from 'next/navigation'
|
||||
import { Link } from 'src/app/_components/link'
|
||||
|
||||
import { Link } from '../../../../_components/link'
|
||||
import { DEFAULT_SORT } from '../../../../_constants'
|
||||
import { useSearchAndSort } from '../../../../_hooks/use-search-and-sort'
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
import { Input } from '@status-im/components'
|
||||
import { SearchIcon } from '@status-im/icons/20'
|
||||
import { TabLink } from '@status-im/wallet/components'
|
||||
import { DropdownSort, TabLink } from '@status-im/wallet/components'
|
||||
import NextLink from 'next/link'
|
||||
import { useParams, usePathname } from 'next/navigation'
|
||||
import { match, P } from 'ts-pattern'
|
||||
|
||||
import { useSearchAndSort } from '../_hooks/use-search-and-sort'
|
||||
import { AdminDropdownSort } from './dropdown-sort'
|
||||
|
||||
const checkPathnameAndReturnTabValue = (
|
||||
pathname: string
|
||||
@@ -67,7 +66,7 @@ const ActionButtons = () => {
|
||||
clearable={!!inputValue}
|
||||
aria-label="Search"
|
||||
/>
|
||||
<AdminDropdownSort
|
||||
<DropdownSort
|
||||
data={sortOptions}
|
||||
onOrderByChange={onOrderByChange}
|
||||
orderByColumn={orderByColumn}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { match } from 'ts-pattern'
|
||||
|
||||
type Props = {
|
||||
value: number
|
||||
/**
|
||||
* Format options for currency display:
|
||||
* - 'compact': Shortened format with K/M/B suffixes, ideal for large amounts (e.g., $1.5K, €2M)
|
||||
* - 'standard': Regular currency format with 2 decimal places (e.g., $1,234.56, €789.00)
|
||||
* - 'precise': Higher precision format with 4 significant digits, ideal for very small amounts (e.g., $0.0001234, €0.00006789)
|
||||
*/
|
||||
format: 'compact' | 'standard' | 'precise'
|
||||
className?: string
|
||||
}
|
||||
|
||||
// TODO: get this from the user's settings
|
||||
const SYMBOL: 'EUR' | 'USD' = 'EUR'
|
||||
|
||||
export const CurrencyAmount = (props: Props) => {
|
||||
const { value, format = 'standard', className } = props
|
||||
|
||||
const formatter = useMemo(
|
||||
() =>
|
||||
match(format)
|
||||
.with(
|
||||
'compact',
|
||||
() =>
|
||||
new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: SYMBOL,
|
||||
notation: 'compact',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
)
|
||||
.with(
|
||||
'standard',
|
||||
() =>
|
||||
new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: SYMBOL,
|
||||
notation: 'standard',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
)
|
||||
.with(
|
||||
'precise',
|
||||
() =>
|
||||
new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: SYMBOL,
|
||||
notation: 'standard',
|
||||
minimumSignificantDigits: 4,
|
||||
maximumSignificantDigits: 4,
|
||||
roundingPriority: 'morePrecision',
|
||||
})
|
||||
)
|
||||
.exhaustive(),
|
||||
[format]
|
||||
)
|
||||
|
||||
return <div className={className}>{formatter.format(value)}</div>
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Avatar } from '@status-im/components'
|
||||
import { InsightsIcon } from '@status-im/icons/20'
|
||||
import { CurrencyAmount } from '@status-im/wallet/components'
|
||||
import { cva, cx } from 'class-variance-authority'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
@@ -9,7 +10,6 @@ import { usePathname } from 'next/navigation'
|
||||
import { Tooltip } from '../_components/tooltip'
|
||||
import { useMediaQuery } from '../_hooks/use-media-query'
|
||||
import { AddAddress } from './add-address'
|
||||
import { CurrencyAmount } from './currency-amount'
|
||||
import { FeatureEnabled } from './feature-enabled'
|
||||
|
||||
import type { CustomisationColorType } from '@status-im/components'
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"react-hook-form": "^7.54.2",
|
||||
"superjson": "^2.2.1",
|
||||
"trpc-chrome": "^1.0.0",
|
||||
"ts-pattern": "^5.7.1",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
|
||||
// import { Input } from '@status-im/components'
|
||||
// import { SearchIcon } from '@status-im/icons/20'
|
||||
import { DropdownSort } from '@status-im/wallet/components'
|
||||
|
||||
// import { match, P } from 'ts-pattern'
|
||||
import { TabLink } from './tab-link'
|
||||
|
||||
// const checkPathnameAndReturnTabValue = (
|
||||
// pathname: string,
|
||||
// ): 'assets' | 'collectibles' => {
|
||||
// return match(pathname)
|
||||
// .with(P.string.includes('/assets'), () => 'assets')
|
||||
// .with(P.string.includes('/collectibles'), () => 'collectibles')
|
||||
// .otherwise(() => 'assets') as 'assets' | 'collectibles'
|
||||
// }
|
||||
|
||||
// const placeholderText = {
|
||||
// assets: 'Search asset name or symbol',
|
||||
// collectibles: 'Search collection or collectible name',
|
||||
// } as const
|
||||
|
||||
type Props = {
|
||||
address: string
|
||||
pathname: string
|
||||
searchAndSortValues: {
|
||||
inputValue: string
|
||||
updateSearchParam: (value: string) => void
|
||||
orderByColumn: string
|
||||
ascending: boolean
|
||||
onOrderByChange: (value: string | number, ascending: boolean) => void
|
||||
sortOptions: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
const ActionButtons = (props: Props) => {
|
||||
// const { address, pathname, searchAndSortValues } = props
|
||||
const { address, searchAndSortValues } = props
|
||||
|
||||
// const placeholder = placeholderText[checkPathnameAndReturnTabValue(pathname)]
|
||||
|
||||
return (
|
||||
<div className="flex place-content-between">
|
||||
<div className="flex gap-1.5">
|
||||
<TabLink href={`/${address}/assets`}>Assets</TabLink>
|
||||
<TabLink href={`/${address}/collectibles`}>Collectibles</TabLink>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* <Input
|
||||
placeholder={placeholder}
|
||||
icon={<SearchIcon />}
|
||||
size="32"
|
||||
value={searchAndSortValues.inputValue}
|
||||
onChange={searchAndSortValues.updateSearchParam}
|
||||
clearable={!!searchAndSortValues.inputValue}
|
||||
aria-label="Search"
|
||||
/> */}
|
||||
<DropdownSort
|
||||
data={searchAndSortValues.sortOptions}
|
||||
onOrderByChange={searchAndSortValues.onOrderByChange}
|
||||
orderByColumn={searchAndSortValues.orderByColumn}
|
||||
ascending={searchAndSortValues.ascending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ActionButtons }
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Avatar } from '@status-im/components'
|
||||
import { Balance, StickyHeaderContainer } from '@status-im/wallet/components'
|
||||
|
||||
import type { Account } from '@status-im/wallet/components'
|
||||
|
||||
type Props = {
|
||||
list: React.ReactNode
|
||||
detail: React.ReactNode
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
// Mock data. todo? Replace with actual data
|
||||
const SUMMARY = {
|
||||
hidden: {
|
||||
total_balance: 0,
|
||||
total_eur: 0,
|
||||
total_eur_24h_change: 0.0,
|
||||
total_percentage_24h_change: 0.0,
|
||||
},
|
||||
visible: {
|
||||
total_balance: 203.0,
|
||||
total_eur: 203.0,
|
||||
total_eur_24h_change: 10.0,
|
||||
total_percentage_24h_change: 2.4,
|
||||
},
|
||||
}
|
||||
|
||||
// Includes mock data for actions buttons and options. todo? Replace with actual data
|
||||
const actionsButtonsData = {
|
||||
address: 'portfolio', // This should be the address of the account coming from the URL. Wrote to have active state
|
||||
pathname: '/portfolio/assets',
|
||||
searchAndSortValues: {
|
||||
inputValue: '',
|
||||
updateSearchParam: () => {},
|
||||
orderByColumn: 'name',
|
||||
ascending: true,
|
||||
onOrderByChange: () => {},
|
||||
sortOptions: {
|
||||
name: 'Name',
|
||||
balance: 'Balance',
|
||||
'24h': '24H%',
|
||||
value: 'Value',
|
||||
price: 'Price',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mock data. todo? Replace with actual data
|
||||
const account: Account = {
|
||||
name: 'Peachy Wallet',
|
||||
emoji: '🍑',
|
||||
color: 'magenta',
|
||||
address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
|
||||
}
|
||||
|
||||
const SplittedLayout = (props: Props) => {
|
||||
const { list, detail, isLoading } = props
|
||||
|
||||
const [showHiddenSummary, setShowHiddenSummary] = useState(false)
|
||||
|
||||
const handleShowHiddenSummary = () => {
|
||||
setShowHiddenSummary(!showHiddenSummary)
|
||||
}
|
||||
|
||||
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>
|
||||
) : (
|
||||
<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={
|
||||
showHiddenSummary ? SUMMARY.visible : SUMMARY.hidden
|
||||
}
|
||||
onShowHiddenSummary={handleShowHiddenSummary}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
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>
|
||||
</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={
|
||||
showHiddenSummary ? SUMMARY.visible : SUMMARY.hidden
|
||||
}
|
||||
onShowHiddenSummary={handleShowHiddenSummary}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ActionButtons {...actionsButtonsData} />
|
||||
</div>
|
||||
|
||||
{list}
|
||||
</div>
|
||||
</StickyHeaderContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden basis-1/2 flex-col bg-neutral-10 2xl:flex">
|
||||
{/* {detail} */}
|
||||
{detail}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SplittedLayout
|
||||
@@ -0,0 +1,21 @@
|
||||
import { getTabLinkClassName } from '@status-im/wallet/components'
|
||||
import { Link, useRouterState } from '@tanstack/react-router'
|
||||
|
||||
type Props = {
|
||||
href: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TabLink = ({ href, children, className }: Props) => {
|
||||
const { location } = useRouterState()
|
||||
const isActive = location.pathname.startsWith(href)
|
||||
|
||||
return (
|
||||
<Link to={href} className={getTabLinkClassName(isActive, className)}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export { TabLink }
|
||||
@@ -125,7 +125,7 @@ function RootComponent() {
|
||||
<div className="fixed inset-x-0 bottom-0 flex justify-center gap-4 bg-blur-neutral-100/70 p-4 text-white-100">
|
||||
<Link to="/">/index</Link>
|
||||
<Link to="/onboarding">/onboarding</Link>
|
||||
<Link to="/portfolio/assets">/portfolio/assets</Link>
|
||||
<Link to="/portfolio/assets">/portfolio</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
// import { Suspense } from 'react'
|
||||
|
||||
import { AssetsList, PinExtension, TabLink } from '@status-im/wallet/components'
|
||||
import { AssetsList, PinExtension } from '@status-im/wallet/components'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { createFileRoute, useRouterState } from '@tanstack/react-router'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import { Link } from '@/components/link'
|
||||
import SplittedLayout from '@/components/splitted-layout'
|
||||
import { usePinExtension } from '@/hooks/use-pin-extension'
|
||||
|
||||
import { useWallet } from '../../../providers/wallet-context'
|
||||
|
||||
// import { DetailDrawer } from '../../../../portfolio/src/app/[address]/@detail/_drawer'
|
||||
// import { Loading as LoadingNav } from '../../../../portfolio/src/app/[address]/@nav/loading'
|
||||
|
||||
export const Route = createFileRoute('/portfolio/assets/')({
|
||||
component: RouteComponent,
|
||||
head: () => ({
|
||||
@@ -24,8 +21,6 @@ export const Route = createFileRoute('/portfolio/assets/')({
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const { location } = useRouterState()
|
||||
const pathname = location.pathname
|
||||
const { currentWallet, isLoading: isWalletLoading } = useWallet()
|
||||
const { isPinExtension, handleClose } = usePinExtension()
|
||||
|
||||
@@ -93,52 +88,31 @@ function RouteComponent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid flex-1 divide-x divide-neutral-10 overflow-hidden">
|
||||
<div className="flex divide-x divide-default-neutral-20">
|
||||
<div className="flex grow flex-col">
|
||||
<div className="sticky top-0 z-20 flex gap-3 px-3 py-2">
|
||||
<TabLink
|
||||
href="/portfolio/assets"
|
||||
LinkComponent={Link}
|
||||
isActive={pathname === '/portfolio/assets'}
|
||||
>
|
||||
Assets
|
||||
</TabLink>
|
||||
<TabLink href="/portfolio/collectibles" LinkComponent={Link}>
|
||||
Collectibles
|
||||
</TabLink>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100vh-100px)] overflow-auto px-3">
|
||||
{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>
|
||||
) : (
|
||||
<AssetsList
|
||||
assets={assets}
|
||||
onSelect={handleSelect}
|
||||
clearSearch={() => {
|
||||
// Clear the search input
|
||||
console.log('Search cleared')
|
||||
}}
|
||||
searchParams={new URLSearchParams()}
|
||||
pathname="/portfolio/"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden basis-1/2 flex-col bg-neutral-10 2xl:flex">
|
||||
{/* {detail} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<SplittedLayout
|
||||
list={
|
||||
assets ? (
|
||||
<AssetsList
|
||||
assets={assets}
|
||||
onSelect={handleSelect}
|
||||
clearSearch={() => {
|
||||
console.log('Search cleared')
|
||||
}}
|
||||
searchParams={new URLSearchParams()}
|
||||
pathname="/portfolio/"
|
||||
/>
|
||||
) : (
|
||||
<div className="mt-4 flex flex-col gap-3">Empty state</div>
|
||||
)
|
||||
}
|
||||
detail={<>Detail for asset</>}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{isPinExtension && (
|
||||
<div className="absolute right-5 top-20">
|
||||
<PinExtension onClose={handleClose} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {
|
||||
CollectiblesGrid as CollectiblesList,
|
||||
TabLink,
|
||||
} from '@status-im/wallet/components'
|
||||
import { CollectiblesGrid as CollectiblesList } from '@status-im/wallet/components'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { createFileRoute, useRouterState } from '@tanstack/react-router'
|
||||
import { createFileRoute, Link as LinkBase } from '@tanstack/react-router'
|
||||
|
||||
import { Link } from '@/components/link'
|
||||
import SplittedLayout from '@/components/splitted-layout'
|
||||
|
||||
import { useWallet } from '../../../providers/wallet-context'
|
||||
|
||||
import type { NetworkType } from '@status-im/wallet/data'
|
||||
|
||||
@@ -28,6 +27,21 @@ export const SORT_OPTIONS = {
|
||||
},
|
||||
} as const
|
||||
|
||||
type LinkProps = {
|
||||
href: string
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Link = (props: LinkProps) => {
|
||||
const { href, className, children } = props
|
||||
return (
|
||||
<LinkBase to={href} className={className}>
|
||||
{children}
|
||||
</LinkBase>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/portfolio/collectibles/')({
|
||||
component: RouteComponent,
|
||||
head: () => ({
|
||||
@@ -80,8 +94,7 @@ const getCollectibles = async (
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
const { location } = useRouterState()
|
||||
const pathname = location.pathname
|
||||
const { currentWallet, isLoading: isWalletLoading } = useWallet()
|
||||
|
||||
const handleSelect = (url: string, options?: { scroll?: boolean }) => {
|
||||
// Handle the selection of an asset
|
||||
@@ -95,7 +108,8 @@ function RouteComponent() {
|
||||
const search = searchParams.get('search') ?? undefined
|
||||
const sortParam = searchParams.get('sort')
|
||||
|
||||
const address = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045'
|
||||
const pathname = window.location.pathname
|
||||
const address = currentWallet?.activeAccounts[0].address
|
||||
const sort = {
|
||||
column:
|
||||
(sortParam?.split(',')[0] as 'name' | 'collection') ||
|
||||
@@ -118,6 +132,10 @@ function RouteComponent() {
|
||||
useInfiniteQuery({
|
||||
queryKey: ['collectibles', address, networks, search, sort],
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
if (!address) {
|
||||
throw new Error('No wallet address available')
|
||||
}
|
||||
|
||||
const collectibles = await getCollectibles(
|
||||
address,
|
||||
networks as NetworkType[],
|
||||
@@ -131,6 +149,7 @@ function RouteComponent() {
|
||||
},
|
||||
getNextPageParam: lastPage => lastPage.nextPage,
|
||||
initialPageParam: 0,
|
||||
enabled: !!address && !isWalletLoading,
|
||||
staleTime: 60 * 60 * 1000,
|
||||
gcTime: 60 * 60 * 1000,
|
||||
refetchOnMount: false,
|
||||
@@ -142,53 +161,32 @@ function RouteComponent() {
|
||||
return data?.pages.flatMap(page => page.collectibles ?? []) ?? []
|
||||
}, [data?.pages])
|
||||
|
||||
return (
|
||||
<div className="grid flex-1 divide-x divide-neutral-10 overflow-hidden">
|
||||
<div className="flex divide-x divide-default-neutral-20">
|
||||
{/* {list} */}
|
||||
<div className="flex grow flex-col 2xl:basis-1/2">
|
||||
<div className="sticky top-0 z-20 flex gap-3 px-3 py-2">
|
||||
<TabLink href="/portfolio/assets" LinkComponent={Link}>
|
||||
Assets
|
||||
</TabLink>
|
||||
<TabLink
|
||||
href="/portfolio/collectibles"
|
||||
LinkComponent={Link}
|
||||
isActive={pathname === '/portfolio/collectibles'}
|
||||
>
|
||||
Collectibles
|
||||
</TabLink>
|
||||
</div>
|
||||
<div className="h-[calc(100vh-100px)] overflow-auto px-3">
|
||||
{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>
|
||||
) : (
|
||||
<CollectiblesList
|
||||
LinkComponent={Link}
|
||||
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}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
if (!currentWallet) {
|
||||
return <div>No wallet selected</div>
|
||||
}
|
||||
|
||||
<div className="hidden basis-1/2 flex-col bg-neutral-10 2xl:flex">
|
||||
{/* {detail} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<SplittedLayout
|
||||
list={
|
||||
<CollectiblesList
|
||||
LinkComponent={Link}
|
||||
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}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
}
|
||||
detail={<>Detail</>}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+10
-3
@@ -1,11 +1,13 @@
|
||||
import { PercentageChange } from '@status-im/wallet/components'
|
||||
import { RevealIcon } from '@status-im/icons/20'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
import { CurrencyAmount } from './currency-amount'
|
||||
import { CurrencyAmount } from '../currency-amount'
|
||||
import { PercentageChange } from '../percentage-change'
|
||||
|
||||
import type { ApiOutput } from '@status-im/wallet/data'
|
||||
|
||||
type Props = {
|
||||
onShowHiddenSummary?: () => void
|
||||
variant?: 'token'
|
||||
summary:
|
||||
| ApiOutput['assets']['all']['summary']
|
||||
@@ -23,7 +25,7 @@ const textColor = cva('text-13 font-medium', {
|
||||
})
|
||||
|
||||
export const Balance = (props: Props) => {
|
||||
const { variant, summary } = props
|
||||
const { variant, summary, onShowHiddenSummary } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -39,6 +41,11 @@ export const Balance = (props: Props) => {
|
||||
{summary.total_balance}
|
||||
</div>
|
||||
)}
|
||||
{!!onShowHiddenSummary && (
|
||||
<button onClick={onShowHiddenSummary}>
|
||||
<RevealIcon className="text-neutral-50 transition-colors hover:text-neutral-60" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
+4
-4
@@ -41,7 +41,7 @@ const SortItem = <T,>(props: SortItemProps<T>) => {
|
||||
'hidden aria-[current=true]:flex group-hover:flex',
|
||||
selectedFilterAsc && 'opacity-[100%]',
|
||||
selectedFilterDesc && 'opacity-[50%] hover:opacity-[100%]',
|
||||
notSelectedFilter && 'opacity-[50%] hover:opacity-[100%]'
|
||||
notSelectedFilter && 'opacity-[50%] hover:opacity-[100%]',
|
||||
)}
|
||||
aria-current={selectedFilterAsc}
|
||||
onClick={() => onSelect(id, true)}
|
||||
@@ -59,7 +59,7 @@ const SortItem = <T,>(props: SortItemProps<T>) => {
|
||||
'hidden aria-[current=true]:flex group-hover:flex',
|
||||
selectedFilterDesc && 'opacity-[100%]',
|
||||
selectedFilterAsc && 'opacity-[50%] hover:opacity-[100%]',
|
||||
notSelectedFilter && 'opacity-[50%] hover:opacity-[100%]'
|
||||
notSelectedFilter && 'opacity-[50%] hover:opacity-[100%]',
|
||||
)}
|
||||
aria-current={selectedFilterDesc}
|
||||
onClick={() => onSelect(id, false)}
|
||||
@@ -78,7 +78,7 @@ const SortItem = <T,>(props: SortItemProps<T>) => {
|
||||
)
|
||||
}
|
||||
|
||||
const AdminDropdownSort = <T,>(props: Props<T>) => {
|
||||
const DropdownSort = <T,>(props: Props<T>) => {
|
||||
const { data, orderByColumn, ascending, onOrderByChange } = props
|
||||
|
||||
const onChange = (value: keyof T, ascending: boolean) => {
|
||||
@@ -106,4 +106,4 @@ const AdminDropdownSort = <T,>(props: Props<T>) => {
|
||||
)
|
||||
}
|
||||
|
||||
export { AdminDropdownSort }
|
||||
export { DropdownSort }
|
||||
@@ -3,6 +3,7 @@ export * from '../utils/variants'
|
||||
export { AccountMenu } from './account-menu'
|
||||
export { type Account, Address, type AddressProps } from './address'
|
||||
export { AssetsList } from './assets-list'
|
||||
export { Balance } from './balance'
|
||||
export { BlurredCircle } from './blurred-circle'
|
||||
export { CollectiblesGrid } from './collectibles-grid'
|
||||
export {
|
||||
@@ -11,6 +12,7 @@ export {
|
||||
} from './create-password-form'
|
||||
export { CurrencyAmount } from './currency-amount'
|
||||
export { DeleteAddressAlert } from './delete-address-alert'
|
||||
export { DropdownSort } from './dropdown-sort'
|
||||
export { Image, type ImageProps } from './image'
|
||||
export {
|
||||
ImportRecoveryPhraseForm,
|
||||
@@ -31,5 +33,5 @@ export {
|
||||
} from './shorten-address'
|
||||
export { Slider, type SliderProps } from './slider'
|
||||
export { StickyHeaderContainer } from './sticky-header-container'
|
||||
export { TabLink } from './tab-link'
|
||||
export { getTabLinkClassName, TabLink } from './tab-link'
|
||||
export { Tooltip } from './tooltip'
|
||||
|
||||
@@ -36,7 +36,7 @@ const leftSlotStyles = cva('transition-opacity duration-100 ease-in-out', {
|
||||
},
|
||||
})
|
||||
|
||||
const secondaryleftSlotStyles = cva('transition-all duration-100 ease-in-out', {
|
||||
const secondaryLeftSlotStyles = cva('transition-all duration-100 ease-in-out', {
|
||||
variants: {
|
||||
showSecondaryLeftSlot: {
|
||||
false: 'pointer-events-none max-h-0 translate-y-full opacity-[0]',
|
||||
@@ -57,6 +57,7 @@ const rightSlotStyles = cva('transition-all', {
|
||||
type Props = {
|
||||
leftSlot: React.ReactNode
|
||||
secondaryLeftSlot?: React.ReactNode
|
||||
isLarge?: boolean
|
||||
rightSlot: React.ReactNode
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
@@ -66,64 +67,69 @@ const SMALL_THRESHOLD = 42
|
||||
const LARGE_THRESHOLD = 132
|
||||
|
||||
const StickyHeaderContainer = (props: Props) => {
|
||||
const { leftSlot, rightSlot, secondaryLeftSlot, className, children } = props
|
||||
const {
|
||||
leftSlot,
|
||||
rightSlot,
|
||||
secondaryLeftSlot,
|
||||
className,
|
||||
children,
|
||||
isLarge,
|
||||
} = props
|
||||
|
||||
const hasSecondaryLeftSlot = Boolean(secondaryLeftSlot)
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [showRightSlot, setShowRightSlot] = useState(false)
|
||||
const [showSecondaryLeftSlot, setShowSecondaryLeftSlot] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const scrollContainer = scrollContainerRef.current
|
||||
const threshold = SMALL_THRESHOLD
|
||||
const rightSlotThreshold = !showSecondaryLeftSlot
|
||||
? threshold
|
||||
: LARGE_THRESHOLD
|
||||
|
||||
if (!scrollContainer) return
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
const handleScroll = () => {
|
||||
const scrollPosition = Math.round(scrollContainer.scrollTop)
|
||||
const scrollTop = container.scrollTop
|
||||
|
||||
setCollapsed(scrollPosition > threshold)
|
||||
setShowRightSlot(scrollPosition > rightSlotThreshold)
|
||||
const threshold = SMALL_THRESHOLD
|
||||
const rightThreshold = hasSecondaryLeftSlot ? LARGE_THRESHOLD : threshold
|
||||
|
||||
setCollapsed(scrollTop > threshold)
|
||||
setShowRightSlot(scrollTop > rightThreshold)
|
||||
setShowSecondaryLeftSlot(
|
||||
hasSecondaryLeftSlot && scrollPosition >= LARGE_THRESHOLD,
|
||||
hasSecondaryLeftSlot && scrollTop >= LARGE_THRESHOLD,
|
||||
)
|
||||
}
|
||||
|
||||
scrollContainer.addEventListener('scroll', handleScroll, {
|
||||
passive: true,
|
||||
})
|
||||
container.addEventListener('scroll', handleScroll, { passive: true })
|
||||
handleScroll()
|
||||
|
||||
return () => {
|
||||
scrollContainer.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [hasSecondaryLeftSlot, scrollContainerRef, showSecondaryLeftSlot])
|
||||
return () => container.removeEventListener('scroll', handleScroll)
|
||||
}, [hasSecondaryLeftSlot])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex flex-1 flex-col overflow-auto scrollbar-stable"
|
||||
ref={containerRef}
|
||||
className="relative h-[calc(100vh-56px)] overflow-auto scrollbar-stable"
|
||||
>
|
||||
<div
|
||||
className={cx([
|
||||
className={cx(
|
||||
backgroundStyles({
|
||||
collapsed,
|
||||
size: showSecondaryLeftSlot ? 'large' : 'default',
|
||||
size: isLarge
|
||||
? 'large'
|
||||
: showSecondaryLeftSlot
|
||||
? 'large'
|
||||
: 'default',
|
||||
}),
|
||||
className,
|
||||
])}
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={leftSlotStyles({ collapsed })}>{leftSlot}</div>
|
||||
<div className={rightSlotStyles({ showRightSlot })}>{rightSlot}</div>
|
||||
</div>
|
||||
<div className={secondaryleftSlotStyles({ showSecondaryLeftSlot })}>
|
||||
<div className={secondaryLeftSlotStyles({ showSecondaryLeftSlot })}>
|
||||
{secondaryLeftSlot}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,14 @@ import { cx } from 'class-variance-authority'
|
||||
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
|
||||
export function getTabLinkClassName(isActive?: boolean, className?: string) {
|
||||
return cx(
|
||||
'flex items-center rounded-10 bg-neutral-10 px-3 py-1 text-15 font-medium text-neutral-100',
|
||||
isActive && 'bg-neutral-50 text-white-100',
|
||||
className,
|
||||
)
|
||||
}
|
||||
|
||||
type LinkComponentProps = {
|
||||
href: string
|
||||
className?: string
|
||||
|
||||
Generated
+8
@@ -930,6 +930,9 @@ importers:
|
||||
trpc-chrome:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)
|
||||
ts-pattern:
|
||||
specifier: ^5.7.1
|
||||
version: 5.7.1
|
||||
vite-plugin-node-polyfills:
|
||||
specifier: ^0.23.0
|
||||
version: 0.23.0(rollup@4.40.2)(vite@6.3.5(@types/node@22.7.5)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.27.0)(sass@1.80.4)(tsx@4.19.4)(yaml@2.5.1))
|
||||
@@ -16784,6 +16787,9 @@ packages:
|
||||
ts-pattern@5.3.1:
|
||||
resolution: {integrity: sha512-1RUMKa8jYQdNfmnK4jyzBK3/PS/tnjcZ1CW0v1vWDeYe5RBklc/nquw03MEoB66hVBm4BnlCfmOqDVxHyT1DpA==}
|
||||
|
||||
ts-pattern@5.7.1:
|
||||
resolution: {integrity: sha512-EGs8PguQqAAUIcQfK4E9xdXxB6s2GK4sJfT/vcc9V1ELIvC4LH/zXu2t/5fajtv6oiRCxdv7BgtVK3vWgROxag==}
|
||||
|
||||
tsconfck@3.1.5:
|
||||
resolution: {integrity: sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==}
|
||||
engines: {node: ^18 || >=20}
|
||||
@@ -39321,6 +39327,8 @@ snapshots:
|
||||
|
||||
ts-pattern@5.3.1: {}
|
||||
|
||||
ts-pattern@5.7.1: {}
|
||||
|
||||
tsconfck@3.1.5(typescript@5.8.3):
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
Reference in New Issue
Block a user