mirror of
https://github.com/status-im/status-web.git
synced 2026-08-31 06:11:10 +00:00
port wallet detail routes (#670)
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
|
||||
---
|
||||
|
||||
port wallet detail routes
|
||||
Vendored
-3
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"npm.packageManager": "pnpm",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"eslint.useESLintClass": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
|
||||
@@ -5,7 +5,9 @@ import { BuyIcon, ReceiveBlurIcon } from '@status-im/icons/20'
|
||||
import {
|
||||
Balance,
|
||||
CurrencyAmount,
|
||||
NetworkBreakdown,
|
||||
StickyHeaderContainer,
|
||||
TokenLogo,
|
||||
} from '@status-im/wallet/components'
|
||||
import { cx } from 'class-variance-authority'
|
||||
import { notFound } from 'next/navigation'
|
||||
@@ -19,8 +21,6 @@ import { ReceiveCryptoDrawer } from '../../../../_components/receive-crypto-draw
|
||||
import { TokenAmount } from '../../../../_components/token-amount'
|
||||
import { Chart } from '../_components/chart'
|
||||
import { Loading } from '../_components/chart/loading'
|
||||
import { NetworkBreakdown } from './_components/network-breakdown'
|
||||
import { TokenLogo } from './_components/token-logo'
|
||||
|
||||
import type { ApiOutput, NetworkType } from '@status-im/wallet/data'
|
||||
|
||||
|
||||
@@ -54,9 +54,14 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"rehype-react": "^8.0.0",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.0.0",
|
||||
"superjson": "^2.2.1",
|
||||
"trpc-chrome": "^1.0.0",
|
||||
"ts-pattern": "^5.7.1",
|
||||
"unified": "^11.0.5",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
type ComponentProps,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
|
||||
import { Step } from '@status-im/components'
|
||||
import { BulletIcon, CheckIcon } from '@status-im/icons/20'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { cx } from 'class-variance-authority'
|
||||
import { match } from 'ts-pattern'
|
||||
|
||||
export function renderText(params: {
|
||||
children: React.ReactNode | React.ReactNode[]
|
||||
weight?: string
|
||||
color?: string
|
||||
parent?: string
|
||||
}) {
|
||||
const {
|
||||
children,
|
||||
weight = 'font-regular',
|
||||
color = 'text-neutral-100',
|
||||
parent,
|
||||
} = params
|
||||
|
||||
return Children.map(children, child => {
|
||||
if (typeof child === 'string') {
|
||||
return (
|
||||
<span
|
||||
className={cx(
|
||||
'text-15',
|
||||
weight,
|
||||
color,
|
||||
// 'break-word hyphens-manual break-all'
|
||||
)}
|
||||
>
|
||||
{child}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
return cloneElement(child as ReactElement<{ parent?: string }>, {
|
||||
parent,
|
||||
})
|
||||
}
|
||||
|
||||
return child
|
||||
})
|
||||
}
|
||||
|
||||
const paragraphMarginTop = '[&+p]:!mt-[1.359375rem]'
|
||||
|
||||
const paragraphMarginVertical = 'mt-5 [&+:not(:is(p))]:pt-5'
|
||||
const paragraphTextSize = 'text-15'
|
||||
|
||||
const blockquoteParagraphTextSize = '[&>p>*]:text-15'
|
||||
const markdownComponents = {
|
||||
strong: (props: ComponentProps<'strong'>) => {
|
||||
return (
|
||||
<strong {...props} className={cx('font-semibold', paragraphTextSize)}>
|
||||
{props.children}
|
||||
</strong>
|
||||
)
|
||||
},
|
||||
del: (props: ComponentProps<'del'>) => {
|
||||
return (
|
||||
<del {...props} className={cx('line-through', paragraphTextSize)}>
|
||||
{props.children}
|
||||
</del>
|
||||
)
|
||||
},
|
||||
em: (props: ComponentProps<'em'>) => {
|
||||
return (
|
||||
<em
|
||||
{...props}
|
||||
className={cx(
|
||||
'font-[Inter,-apple-system,system-ui]',
|
||||
'italic',
|
||||
paragraphTextSize,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</em>
|
||||
)
|
||||
},
|
||||
h1: (props: ComponentProps<'h1'>) => {
|
||||
return (
|
||||
<h1 {...props} className="group relative text-40 font-bold">
|
||||
{props.children}
|
||||
</h1>
|
||||
)
|
||||
},
|
||||
h2: (props: ComponentProps<'h2'> & { mb?: string; mt?: string }) => {
|
||||
const { children, id, mb = 'mb-3', mt = 'mt-5', ...rest } = props
|
||||
return (
|
||||
<h2
|
||||
id={id}
|
||||
{...rest}
|
||||
className={cx(
|
||||
mb,
|
||||
mt,
|
||||
'group relative scroll-m-[100px] text-15 font-semibold',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
},
|
||||
h3: (props: ComponentProps<'h3'>) => {
|
||||
return (
|
||||
<h3
|
||||
{...props}
|
||||
className="group relative mb-3 mt-5 scroll-m-[100px] text-15 font-semibold"
|
||||
>
|
||||
{props.children}
|
||||
</h3>
|
||||
)
|
||||
},
|
||||
h4: (props: ComponentProps<'h4'>) => {
|
||||
return (
|
||||
<h4
|
||||
{...props}
|
||||
className="group relative mb-3 mt-5 scroll-m-[100px] text-15 font-semibold"
|
||||
>
|
||||
{props.children}
|
||||
</h4>
|
||||
)
|
||||
},
|
||||
h5: (props: ComponentProps<'h5'>) => {
|
||||
return (
|
||||
<h5
|
||||
{...props}
|
||||
className="group relative mb-3 mt-5 scroll-m-[100px] text-15 font-semibold"
|
||||
>
|
||||
{props.children}
|
||||
</h5>
|
||||
)
|
||||
},
|
||||
h6: (props: ComponentProps<'h6'>) => {
|
||||
return (
|
||||
<h6
|
||||
{...props}
|
||||
className="group relative mb-3 mt-5 scroll-m-[100px] text-15 font-semibold"
|
||||
>
|
||||
{props.children}
|
||||
</h6>
|
||||
)
|
||||
},
|
||||
blockquote: (props: ComponentProps<'blockquote'>) => {
|
||||
const { children, ...rest } = props
|
||||
|
||||
const blockquoteChildren = Children.toArray(children).filter(
|
||||
child => child !== '\n',
|
||||
) as (ReactElement | string)[]
|
||||
|
||||
return (
|
||||
<blockquote
|
||||
{...rest}
|
||||
className={cx(
|
||||
blockquoteParagraphTextSize,
|
||||
'mt-5 border-l border-dashed border-neutral-30 !pt-0 pl-6',
|
||||
)}
|
||||
>
|
||||
{Children.map(blockquoteChildren, (item: ReactElement | string) => {
|
||||
if (typeof item === 'string') {
|
||||
return renderText({ children: item })
|
||||
}
|
||||
|
||||
if (item.type === 'p') {
|
||||
return cloneElement(item)
|
||||
}
|
||||
|
||||
return item
|
||||
})}
|
||||
</blockquote>
|
||||
)
|
||||
},
|
||||
a: (props: ComponentProps<'a'>) => {
|
||||
if (!props.href) {
|
||||
const { children, ...rest } = props
|
||||
return (
|
||||
<a
|
||||
{...rest}
|
||||
className={cx('text-customisation-blue-50', paragraphTextSize)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (props.href.startsWith('/')) {
|
||||
return (
|
||||
<Link
|
||||
to={props.href}
|
||||
className={cx('text-customisation-blue-50', paragraphTextSize)}
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={props.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cx('text-customisation-blue-50', paragraphTextSize)}
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
p: (props: ComponentProps<'p'> & { parent?: string }) => {
|
||||
const { children } = props
|
||||
|
||||
if (
|
||||
(children as { type?: { name?: string } })?.type?.name === 'img' ||
|
||||
props.parent === 'li'
|
||||
) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
className={cx(
|
||||
paragraphMarginVertical,
|
||||
paragraphMarginTop,
|
||||
'[:is(h1,h2,h3,h4,h5,h6)+&]:!mt-0', // immediately follows a heading as a sibling element
|
||||
// '[&:not(:has(+*))]:!mb-0', // not followed by any sibling element
|
||||
'[:is(div,td,blockquote)>&:first-child]:!mt-0', // is a first child of selected parent element
|
||||
)}
|
||||
>
|
||||
{renderText({ children })}
|
||||
</p>
|
||||
)
|
||||
},
|
||||
ul: (props: ComponentProps<'ul'>) => {
|
||||
return (
|
||||
<ul className="flex flex-col gap-3 [:is(ul)+&]:mt-5 [ul_&]:mt-3">
|
||||
{props.children}
|
||||
</ul>
|
||||
)
|
||||
},
|
||||
ol: (props: ComponentProps<'ol'> & { parent?: string }) => {
|
||||
const listItems = Children.toArray(props.children).filter(
|
||||
child => typeof child === 'object',
|
||||
)
|
||||
|
||||
return (
|
||||
<ol
|
||||
className="group flex flex-col gap-3 [:is(ol)+&]:mt-5 [ol_&]:mt-3"
|
||||
{...props}
|
||||
>
|
||||
{Children.map(listItems, (item: ReactNode, index) =>
|
||||
cloneElement(
|
||||
item as ReactElement<{ order?: number; parent?: string }>,
|
||||
{
|
||||
order: index + 1,
|
||||
parent: props.parent ?? 'ol',
|
||||
},
|
||||
),
|
||||
)}
|
||||
</ol>
|
||||
)
|
||||
},
|
||||
li: (
|
||||
props: ComponentProps<'li'> & {
|
||||
parent?: 'ol' | 'AwaitedList'
|
||||
order?: number
|
||||
variant?: React.ComponentProps<typeof Step>['variant']
|
||||
},
|
||||
) => {
|
||||
const icon = match(props.parent)
|
||||
.with('ol', () => (
|
||||
<Step variant={props.variant ?? 'primary'} value={props.order!} />
|
||||
))
|
||||
.with('AwaitedList', () => <CheckIcon className="text-success-50" />)
|
||||
.otherwise(() => <BulletIcon className="text-neutral-50" />)
|
||||
|
||||
return (
|
||||
<li className="flex items-start gap-2">
|
||||
<div className={cx('flex shrink-0 items-center', 'h-[24px]')}>
|
||||
{icon}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{renderText({
|
||||
children: props.children,
|
||||
parent: 'li',
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
},
|
||||
// handled conditionally per use case with divider component
|
||||
hr: () => {
|
||||
return <></>
|
||||
},
|
||||
img: (props: ComponentProps<'img'>) => {
|
||||
return <img {...props} alt={props.alt || ''} className="my-5 rounded-20" />
|
||||
},
|
||||
pre: (props: ComponentProps<'pre'>) => (
|
||||
<pre {...props} className="overflow-scroll scrollbar-none" />
|
||||
),
|
||||
div: (props: ComponentProps<'div'>) => {
|
||||
return <div {...props} />
|
||||
},
|
||||
code: (props: ComponentProps<'code'>) => {
|
||||
const multiline = Children.toArray(props.children).length > 1
|
||||
|
||||
if (
|
||||
!multiline &&
|
||||
(typeof props.children === 'string' ||
|
||||
(Array.isArray(props.children) &&
|
||||
typeof props.children[0] === 'string'))
|
||||
) {
|
||||
return (
|
||||
// note: https://www.figma.com/file/qSIh8wh9EVdY8S2sZce15n/Composer-for-Desktop?type=design&node-id=7850-672452&mode=design&t=V9tDjCw6RLuPF4F6-4
|
||||
<code
|
||||
{...props}
|
||||
className="inline-block rounded-10 border border-neutral-10 bg-neutral-5 px-2 font-regular"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// todo?: https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?type=design&node-id=5626-159428&mode=design&t=stTlBeUAUUi4JR0v-4
|
||||
// note: http://localhost:3000/help/getting-started/download-status-for-linux example for scrolling
|
||||
return <code className="w-fit" {...props} />
|
||||
},
|
||||
figure: (props: ComponentProps<'figure'>) => (
|
||||
<figure {...props} className="my-5" />
|
||||
),
|
||||
iframe: (props: React.ComponentProps<'iframe'>) => {
|
||||
// todo?: match youtube props to use aspect-video
|
||||
return (
|
||||
<iframe
|
||||
{...props}
|
||||
title={props.title}
|
||||
className="aspect-video size-full rounded-20"
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export { markdownComponents }
|
||||
@@ -1,11 +1,9 @@
|
||||
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
|
||||
detail?: React.ReactNode
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
@@ -45,12 +43,10 @@ const actionsButtonsData = {
|
||||
},
|
||||
}
|
||||
|
||||
// Mock data. todo? Replace with actual data
|
||||
const account: Account = {
|
||||
name: 'Peachy Wallet',
|
||||
const account = {
|
||||
name: 'Account 1',
|
||||
emoji: '🍑',
|
||||
color: 'magenta',
|
||||
address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
|
||||
}
|
||||
|
||||
const SplittedLayout = (props: Props) => {
|
||||
@@ -152,9 +148,16 @@ const SplittedLayout = (props: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden basis-1/2 flex-col bg-neutral-10 2xl:flex">
|
||||
{/* {detail} */}
|
||||
{detail}
|
||||
<div className="relative hidden basis-1/2 flex-col bg-white-100 2xl:flex">
|
||||
<div className="relative z-20">{detail}</div>
|
||||
|
||||
<div
|
||||
className="absolute z-10 size-full"
|
||||
style={{
|
||||
backgroundColor: 'rgba(245, 246, 248, 0.24)',
|
||||
}}
|
||||
/>
|
||||
<div className="absolute z-0 size-full bg-gradient-to-r from-[#F5F6F83D] to-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
type Props = {
|
||||
isWalletLoading: boolean
|
||||
address?: string
|
||||
}
|
||||
|
||||
// todo: export trpc client with api router and used instead
|
||||
// todo: cache
|
||||
const useAssets = (props: Props) => {
|
||||
const { address, isWalletLoading } = props
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['assets', address],
|
||||
queryFn: async () => {
|
||||
if (!address) {
|
||||
throw new Error('No wallet address available')
|
||||
}
|
||||
|
||||
const url = new URL('http://localhost:3030/api/trpc/assets.all')
|
||||
url.searchParams.set(
|
||||
'input',
|
||||
JSON.stringify({
|
||||
json: {
|
||||
address,
|
||||
networks: [
|
||||
'ethereum',
|
||||
'optimism',
|
||||
'arbitrum',
|
||||
'base',
|
||||
'polygon',
|
||||
'bsc',
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch.')
|
||||
}
|
||||
|
||||
const body = await response.json()
|
||||
|
||||
return body.result.data.json.assets
|
||||
},
|
||||
enabled: !!address && !isWalletLoading,
|
||||
staleTime: 60 * 60 * 1000,
|
||||
gcTime: 60 * 60 * 1000,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
})
|
||||
}
|
||||
|
||||
export { useAssets }
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
|
||||
import type { NetworkType } from '@status-im/wallet/data'
|
||||
|
||||
const PAGE_LIMIT = 20
|
||||
|
||||
const DEFAULT_SORT = {
|
||||
assets: { column: 'name', direction: 'asc' as const },
|
||||
collectibles: { column: 'name', direction: 'asc' as const },
|
||||
} as const
|
||||
|
||||
export const SORT_OPTIONS = {
|
||||
assets: {
|
||||
name: 'Name',
|
||||
balance: 'Balance',
|
||||
'24h': '24H%',
|
||||
value: 'Value',
|
||||
price: 'Price',
|
||||
},
|
||||
collectibles: {
|
||||
name: 'Name',
|
||||
collection: 'Collection',
|
||||
},
|
||||
} as const
|
||||
|
||||
type Props = {
|
||||
isWalletLoading: boolean
|
||||
address?: string
|
||||
}
|
||||
|
||||
const getCollectibles = async (
|
||||
address: string,
|
||||
networks: NetworkType[],
|
||||
search?: string,
|
||||
sort?: {
|
||||
column: 'name' | 'collection'
|
||||
direction: 'asc' | 'desc'
|
||||
},
|
||||
offset = 0,
|
||||
) => {
|
||||
const url = new URL('http://localhost:3030/api/trpc/collectibles.page')
|
||||
|
||||
url.searchParams.set(
|
||||
'input',
|
||||
JSON.stringify({
|
||||
json: {
|
||||
address,
|
||||
networks,
|
||||
limit: PAGE_LIMIT,
|
||||
offset,
|
||||
search,
|
||||
sort,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch.')
|
||||
}
|
||||
|
||||
const body = await response.json()
|
||||
return body.result.data.json.collectibles
|
||||
}
|
||||
|
||||
const useCollectibles = (props: Props) => {
|
||||
const { address, isWalletLoading } = props
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const search = searchParams.get('search') ?? undefined
|
||||
const sortParam = searchParams.get('sort')
|
||||
|
||||
const sort = {
|
||||
column:
|
||||
(sortParam?.split(',')[0] as 'name' | 'collection') ||
|
||||
DEFAULT_SORT.collectibles.column,
|
||||
direction:
|
||||
(sortParam?.split(',')[1] as 'asc' | 'desc') ||
|
||||
DEFAULT_SORT.collectibles.direction,
|
||||
}
|
||||
|
||||
const networks = searchParams.get('networks')?.split(',') ?? [
|
||||
'ethereum',
|
||||
'optimism',
|
||||
'arbitrum',
|
||||
'base',
|
||||
'polygon',
|
||||
'bsc',
|
||||
]
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: ['collectibles', address, networks, search, sort],
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
if (!address) {
|
||||
throw new Error('No wallet address available')
|
||||
}
|
||||
|
||||
const offset = pageParam * PAGE_LIMIT
|
||||
const collectibles = await getCollectibles(
|
||||
address,
|
||||
networks as NetworkType[],
|
||||
search,
|
||||
sort,
|
||||
offset,
|
||||
)
|
||||
return {
|
||||
collectibles,
|
||||
nextPage: pageParam + 1,
|
||||
}
|
||||
},
|
||||
getNextPageParam: lastPage => lastPage.nextPage,
|
||||
initialPageParam: 0,
|
||||
enabled: !!address && !isWalletLoading,
|
||||
staleTime: 60 * 60 * 1000,
|
||||
gcTime: 60 * 60 * 1000,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
})
|
||||
}
|
||||
|
||||
export { useCollectibles }
|
||||
@@ -0,0 +1,24 @@
|
||||
import production from 'react/jsx-runtime'
|
||||
import rehypeReact from 'rehype-react'
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import { unified } from 'unified'
|
||||
|
||||
import { markdownComponents as components } from '@/components/content/markdown'
|
||||
|
||||
import type { ReactElement } from 'react'
|
||||
|
||||
export const renderMarkdown = async (
|
||||
markdown: string,
|
||||
): Promise<ReactElement> => {
|
||||
const result = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkRehype)
|
||||
// @ts-expect-error - rehype-react types are not compatible with unified types
|
||||
.use(rehypeReact, {
|
||||
...production,
|
||||
components,
|
||||
})
|
||||
.process(markdown)
|
||||
return result.result as ReactElement
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import { Route as OnboardingNewImport } from './routes/onboarding/new'
|
||||
import { Route as OnboardingImportImport } from './routes/onboarding/import'
|
||||
import { Route as PortfolioCollectiblesIndexImport } from './routes/portfolio/collectibles/index'
|
||||
import { Route as PortfolioAssetsIndexImport } from './routes/portfolio/assets/index'
|
||||
import { Route as PortfolioAssetsTickerImport } from './routes/portfolio/assets/$ticker'
|
||||
import { Route as PortfolioCollectiblesNetworkContractIdImport } from './routes/portfolio/collectibles/$network/$contract/$id'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
@@ -65,6 +67,19 @@ const PortfolioAssetsIndexRoute = PortfolioAssetsIndexImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const PortfolioAssetsTickerRoute = PortfolioAssetsTickerImport.update({
|
||||
id: '/portfolio/assets/$ticker',
|
||||
path: '/portfolio/assets/$ticker',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const PortfolioCollectiblesNetworkContractIdRoute =
|
||||
PortfolioCollectiblesNetworkContractIdImport.update({
|
||||
id: '/portfolio/collectibles/$network/$contract/$id',
|
||||
path: '/portfolio/collectibles/$network/$contract/$id',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -104,6 +119,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof OnboardingIndexImport
|
||||
parentRoute: typeof OnboardingLayoutImport
|
||||
}
|
||||
'/portfolio/assets/$ticker': {
|
||||
id: '/portfolio/assets/$ticker'
|
||||
path: '/portfolio/assets/$ticker'
|
||||
fullPath: '/portfolio/assets/$ticker'
|
||||
preLoaderRoute: typeof PortfolioAssetsTickerImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/portfolio/assets/': {
|
||||
id: '/portfolio/assets/'
|
||||
path: '/portfolio/assets'
|
||||
@@ -118,6 +140,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof PortfolioCollectiblesIndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/portfolio/collectibles/$network/$contract/$id': {
|
||||
id: '/portfolio/collectibles/$network/$contract/$id'
|
||||
path: '/portfolio/collectibles/$network/$contract/$id'
|
||||
fullPath: '/portfolio/collectibles/$network/$contract/$id'
|
||||
preLoaderRoute: typeof PortfolioCollectiblesNetworkContractIdImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,8 +173,10 @@ export interface FileRoutesByFullPath {
|
||||
'/onboarding/import': typeof OnboardingImportRoute
|
||||
'/onboarding/new': typeof OnboardingNewRoute
|
||||
'/onboarding/': typeof OnboardingIndexRoute
|
||||
'/portfolio/assets/$ticker': typeof PortfolioAssetsTickerRoute
|
||||
'/portfolio/assets': typeof PortfolioAssetsIndexRoute
|
||||
'/portfolio/collectibles': typeof PortfolioCollectiblesIndexRoute
|
||||
'/portfolio/collectibles/$network/$contract/$id': typeof PortfolioCollectiblesNetworkContractIdRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
@@ -153,8 +184,10 @@ export interface FileRoutesByTo {
|
||||
'/onboarding/import': typeof OnboardingImportRoute
|
||||
'/onboarding/new': typeof OnboardingNewRoute
|
||||
'/onboarding': typeof OnboardingIndexRoute
|
||||
'/portfolio/assets/$ticker': typeof PortfolioAssetsTickerRoute
|
||||
'/portfolio/assets': typeof PortfolioAssetsIndexRoute
|
||||
'/portfolio/collectibles': typeof PortfolioCollectiblesIndexRoute
|
||||
'/portfolio/collectibles/$network/$contract/$id': typeof PortfolioCollectiblesNetworkContractIdRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
@@ -164,8 +197,10 @@ export interface FileRoutesById {
|
||||
'/onboarding/import': typeof OnboardingImportRoute
|
||||
'/onboarding/new': typeof OnboardingNewRoute
|
||||
'/onboarding/': typeof OnboardingIndexRoute
|
||||
'/portfolio/assets/$ticker': typeof PortfolioAssetsTickerRoute
|
||||
'/portfolio/assets/': typeof PortfolioAssetsIndexRoute
|
||||
'/portfolio/collectibles/': typeof PortfolioCollectiblesIndexRoute
|
||||
'/portfolio/collectibles/$network/$contract/$id': typeof PortfolioCollectiblesNetworkContractIdRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
@@ -176,16 +211,20 @@ export interface FileRouteTypes {
|
||||
| '/onboarding/import'
|
||||
| '/onboarding/new'
|
||||
| '/onboarding/'
|
||||
| '/portfolio/assets/$ticker'
|
||||
| '/portfolio/assets'
|
||||
| '/portfolio/collectibles'
|
||||
| '/portfolio/collectibles/$network/$contract/$id'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/onboarding/import'
|
||||
| '/onboarding/new'
|
||||
| '/onboarding'
|
||||
| '/portfolio/assets/$ticker'
|
||||
| '/portfolio/assets'
|
||||
| '/portfolio/collectibles'
|
||||
| '/portfolio/collectibles/$network/$contract/$id'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@@ -193,23 +232,30 @@ export interface FileRouteTypes {
|
||||
| '/onboarding/import'
|
||||
| '/onboarding/new'
|
||||
| '/onboarding/'
|
||||
| '/portfolio/assets/$ticker'
|
||||
| '/portfolio/assets/'
|
||||
| '/portfolio/collectibles/'
|
||||
| '/portfolio/collectibles/$network/$contract/$id'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
OnboardingLayoutRoute: typeof OnboardingLayoutRouteWithChildren
|
||||
PortfolioAssetsTickerRoute: typeof PortfolioAssetsTickerRoute
|
||||
PortfolioAssetsIndexRoute: typeof PortfolioAssetsIndexRoute
|
||||
PortfolioCollectiblesIndexRoute: typeof PortfolioCollectiblesIndexRoute
|
||||
PortfolioCollectiblesNetworkContractIdRoute: typeof PortfolioCollectiblesNetworkContractIdRoute
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
OnboardingLayoutRoute: OnboardingLayoutRouteWithChildren,
|
||||
PortfolioAssetsTickerRoute: PortfolioAssetsTickerRoute,
|
||||
PortfolioAssetsIndexRoute: PortfolioAssetsIndexRoute,
|
||||
PortfolioCollectiblesIndexRoute: PortfolioCollectiblesIndexRoute,
|
||||
PortfolioCollectiblesNetworkContractIdRoute:
|
||||
PortfolioCollectiblesNetworkContractIdRoute,
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute
|
||||
@@ -224,8 +270,10 @@ export const routeTree = rootRoute
|
||||
"children": [
|
||||
"/",
|
||||
"/onboarding",
|
||||
"/portfolio/assets/$ticker",
|
||||
"/portfolio/assets/",
|
||||
"/portfolio/collectibles/"
|
||||
"/portfolio/collectibles/",
|
||||
"/portfolio/collectibles/$network/$contract/$id"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
@@ -251,11 +299,17 @@ export const routeTree = rootRoute
|
||||
"filePath": "onboarding/index.tsx",
|
||||
"parent": "/onboarding"
|
||||
},
|
||||
"/portfolio/assets/$ticker": {
|
||||
"filePath": "portfolio/assets/$ticker.tsx"
|
||||
},
|
||||
"/portfolio/assets/": {
|
||||
"filePath": "portfolio/assets/index.tsx"
|
||||
},
|
||||
"/portfolio/collectibles/": {
|
||||
"filePath": "portfolio/collectibles/index.tsx"
|
||||
},
|
||||
"/portfolio/collectibles/$network/$contract/$id": {
|
||||
"filePath": "portfolio/collectibles/$network/$contract/$id.tsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Suspense } from 'react'
|
||||
|
||||
import { AssetsList } from '@status-im/wallet/components'
|
||||
import {
|
||||
createFileRoute,
|
||||
useRouter,
|
||||
useRouterState,
|
||||
} from '@tanstack/react-router'
|
||||
|
||||
import SplittedLayout from '@/components/splitted-layout'
|
||||
import { useAssets } from '@/hooks/use-assets'
|
||||
|
||||
import { useWallet } from '../../../providers/wallet-context'
|
||||
import { Token } from './-components/token'
|
||||
|
||||
export const Route = createFileRoute('/portfolio/assets/$ticker')({
|
||||
component: Component,
|
||||
})
|
||||
|
||||
function Component() {
|
||||
const { currentWallet, isLoading: isWalletLoading } = useWallet()
|
||||
|
||||
const params = Route.useParams()
|
||||
const ticker = params.ticker
|
||||
const router = useRouter()
|
||||
const routerState = useRouterState()
|
||||
const pathname = routerState.location.pathname
|
||||
const address = currentWallet?.activeAccounts[0].address
|
||||
|
||||
const { data: assets, isLoading } = useAssets({
|
||||
address,
|
||||
isWalletLoading,
|
||||
})
|
||||
|
||||
if (!currentWallet || !address) {
|
||||
return <div>No wallet selected</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="hidden 2xl:block">
|
||||
<SplittedLayout
|
||||
list={
|
||||
assets ? (
|
||||
<AssetsList
|
||||
assets={assets}
|
||||
onSelect={url => {
|
||||
const ticker = url.split('/').pop()
|
||||
if (!ticker) return
|
||||
router.navigate({
|
||||
to: '/portfolio/assets/$ticker',
|
||||
params: { ticker },
|
||||
})
|
||||
}}
|
||||
clearSearch={() => {
|
||||
console.log('Search cleared')
|
||||
}}
|
||||
searchParams={new URLSearchParams()}
|
||||
pathname={pathname}
|
||||
/>
|
||||
) : (
|
||||
<div className="mt-4 flex flex-col gap-3">Empty state</div>
|
||||
)
|
||||
}
|
||||
detail={
|
||||
<Suspense fallback={<p>Loading token...</p>}>
|
||||
<Token ticker={ticker} />
|
||||
</Suspense>
|
||||
}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="block 2xl:hidden">
|
||||
<Token ticker={ticker} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Button, Tooltip } from '@status-im/components'
|
||||
import { BuyIcon, ReceiveBlurIcon } from '@status-im/icons/20'
|
||||
import {
|
||||
Balance,
|
||||
CurrencyAmount,
|
||||
NetworkBreakdown,
|
||||
StickyHeaderContainer,
|
||||
TokenAmount,
|
||||
TokenLogo,
|
||||
} from '@status-im/wallet/components'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { cx } from 'class-variance-authority'
|
||||
|
||||
import { renderMarkdown } from '@/lib/markdown'
|
||||
|
||||
import type { ApiOutput, NetworkType } from '@status-im/wallet/data'
|
||||
|
||||
type Props = {
|
||||
ticker: string
|
||||
}
|
||||
|
||||
const Token = (props: Props) => {
|
||||
const { ticker } = props
|
||||
const [markdownContent, setMarkdownContent] = useState<React.ReactNode>(null)
|
||||
|
||||
const token = useQuery<
|
||||
ApiOutput['assets']['token'] | ApiOutput['assets']['nativeToken']
|
||||
>({
|
||||
queryKey: ['token', ticker],
|
||||
queryFn: async () => {
|
||||
const endpoint = ticker.startsWith('0x')
|
||||
? 'assets.token'
|
||||
: 'assets.nativeToken'
|
||||
const url = new URL(`http://localhost:3030/api/trpc/${endpoint}`)
|
||||
url.searchParams.set(
|
||||
'input',
|
||||
JSON.stringify({
|
||||
json: {
|
||||
address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
|
||||
networks: [
|
||||
'ethereum',
|
||||
'optimism',
|
||||
'arbitrum',
|
||||
'base',
|
||||
'polygon',
|
||||
'bsc',
|
||||
] as NetworkType[],
|
||||
...(ticker.startsWith('0x')
|
||||
? { contract: ticker }
|
||||
: { symbol: ticker }),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch.')
|
||||
}
|
||||
|
||||
const body = await response.json()
|
||||
return body.result.data.json
|
||||
},
|
||||
staleTime: 60 * 60 * 1000, // 1 hour
|
||||
gcTime: 60 * 60 * 1000, // 1 hour
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
})
|
||||
|
||||
const { data: typedToken, isLoading } = token
|
||||
|
||||
useEffect(() => {
|
||||
const processMarkdown = async () => {
|
||||
if (typedToken) {
|
||||
const metadata = Object.values(typedToken.assets)[0].metadata
|
||||
const content = await renderMarkdown(
|
||||
metadata.about || 'No description available.',
|
||||
)
|
||||
setMarkdownContent(content)
|
||||
}
|
||||
}
|
||||
processMarkdown()
|
||||
}, [typedToken])
|
||||
|
||||
if (isLoading || !typedToken) {
|
||||
return <p>Loading</p>
|
||||
}
|
||||
|
||||
const metadata = Object.values(typedToken.assets)[0].metadata
|
||||
const uppercasedTicker = typedToken.summary.symbol
|
||||
const icon = typedToken.summary.icon
|
||||
|
||||
return (
|
||||
<StickyHeaderContainer
|
||||
className="-translate-x-0 !py-3 !pl-3 pr-[50px] 2xl:w-auto 2xl:!px-12 2xl:!py-4"
|
||||
leftSlot={
|
||||
<TokenLogo
|
||||
variant="small"
|
||||
name={typedToken.summary.name}
|
||||
ticker={uppercasedTicker}
|
||||
icon={icon}
|
||||
/>
|
||||
}
|
||||
rightSlot={
|
||||
<div className="flex items-center gap-1 pt-px">
|
||||
<Button size="32" iconBefore={<BuyIcon />}>
|
||||
<span className="block max-w-20 truncate">
|
||||
Buy {typedToken.summary.name}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<Button size="32" iconBefore={<ReceiveBlurIcon />}>
|
||||
Receive
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="-mt-8 grid gap-10 p-4 pt-0 2xl:mt-0 2xl:p-12 2xl:pt-0">
|
||||
<div>
|
||||
<TokenLogo
|
||||
name={typedToken.summary.name}
|
||||
ticker={uppercasedTicker}
|
||||
icon={icon}
|
||||
/>
|
||||
<div className="my-6 2xl:mt-0">
|
||||
<Balance variant="token" summary={typedToken.summary} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="32" iconBefore={<BuyIcon />} variant="primary">
|
||||
Buy {typedToken.summary.name}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="32"
|
||||
variant="outline"
|
||||
iconBefore={<ReceiveBlurIcon />}
|
||||
>
|
||||
Receive
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{typedToken.summary.total_balance > 0 && (
|
||||
<NetworkBreakdown token={typedToken} />
|
||||
)}
|
||||
|
||||
{/* <ErrorBoundary fallback={<div>Error loading chart</div>}>
|
||||
<Suspense
|
||||
key={keyHash}
|
||||
fallback={
|
||||
<div className="mt-8">
|
||||
<Loading />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AssetChart
|
||||
address={address}
|
||||
slug={slug}
|
||||
symbol={token.summary.symbol}
|
||||
/>
|
||||
</Suspense>
|
||||
</ErrorBoundary> */}
|
||||
|
||||
<div>
|
||||
<div className="grid grid-cols-2 2xl:grid-cols-4">
|
||||
{[
|
||||
{
|
||||
label: 'Market Cap',
|
||||
value: (
|
||||
<CurrencyAmount
|
||||
value={metadata.market_cap}
|
||||
format="compact"
|
||||
/>
|
||||
),
|
||||
tooltip: (
|
||||
<CurrencyAmount
|
||||
value={metadata.market_cap}
|
||||
format="standard"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Fully diluted',
|
||||
value: (
|
||||
<CurrencyAmount
|
||||
value={metadata.fully_dilluted}
|
||||
format="compact"
|
||||
/>
|
||||
),
|
||||
tooltip: (
|
||||
<CurrencyAmount
|
||||
value={metadata.fully_dilluted}
|
||||
format="standard"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Circulation',
|
||||
value: (
|
||||
<TokenAmount value={metadata.circulation} format="compact" />
|
||||
),
|
||||
tooltip: (
|
||||
<TokenAmount value={metadata.circulation} format="standard" />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Total supply',
|
||||
value: (
|
||||
<TokenAmount value={metadata.total_supply} format="compact" />
|
||||
),
|
||||
tooltip: (
|
||||
<TokenAmount
|
||||
value={metadata.total_supply}
|
||||
format="standard"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'All time high',
|
||||
value: (
|
||||
<CurrencyAmount
|
||||
value={metadata.all_time_high}
|
||||
format="standard"
|
||||
/>
|
||||
),
|
||||
tooltip: (
|
||||
<CurrencyAmount
|
||||
value={metadata.all_time_high}
|
||||
format="standard"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'All time low',
|
||||
value: (
|
||||
<CurrencyAmount
|
||||
value={metadata.all_time_low}
|
||||
format="standard"
|
||||
/>
|
||||
),
|
||||
tooltip: (
|
||||
<CurrencyAmount
|
||||
value={metadata.all_time_low}
|
||||
format="standard"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '24h Volume',
|
||||
value: (
|
||||
<TokenAmount value={metadata.volume_24} format="compact" />
|
||||
),
|
||||
tooltip: (
|
||||
<TokenAmount value={metadata.volume_24} format="standard" />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Rank by Mcap',
|
||||
value: metadata?.rank_by_market_cap,
|
||||
},
|
||||
].map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cx(
|
||||
'border-dashed border-neutral-10 py-4',
|
||||
index % 2 !== 0 && 'pl-4',
|
||||
index % 2 !== 1 && 'border-r pr-4',
|
||||
index % 4 !== 3 && '2xl:border-r 2xl:pr-4',
|
||||
index < 6 && 'border-b',
|
||||
index < 4 && '2xl:border-b',
|
||||
)}
|
||||
>
|
||||
<Tooltip content={item.tooltip} side="top">
|
||||
<div>
|
||||
<div className="text-13 font-regular text-neutral-50">
|
||||
{item.label}
|
||||
</div>
|
||||
<div className="text-13 font-medium text-neutral-100">
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-5 flex-col gap-2">
|
||||
<div className="text-neutral-100">{markdownContent}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StickyHeaderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export { Token }
|
||||
@@ -1,89 +1,29 @@
|
||||
// import { Suspense } from 'react'
|
||||
|
||||
import { AssetsList, PinExtension } from '@status-im/wallet/components'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router'
|
||||
|
||||
import SplittedLayout from '@/components/splitted-layout'
|
||||
import { useAssets } from '@/hooks/use-assets'
|
||||
import { usePinExtension } from '@/hooks/use-pin-extension'
|
||||
|
||||
import { useWallet } from '../../../providers/wallet-context'
|
||||
|
||||
export const Route = createFileRoute('/portfolio/assets/')({
|
||||
component: RouteComponent,
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
title: 'Extension | Wallet | Portfolio',
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: Component,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
function Component() {
|
||||
const { currentWallet, isLoading: isWalletLoading } = useWallet()
|
||||
const { isPinExtension, handleClose } = usePinExtension()
|
||||
|
||||
const handleSelect = (url: string, options?: { scroll?: boolean }) => {
|
||||
// Handle the selection of an asset
|
||||
console.log('Selected asset URL:', url)
|
||||
console.log('Scroll option:', options?.scroll)
|
||||
}
|
||||
const address = currentWallet?.activeAccounts[0].address
|
||||
|
||||
// todo: export trpc client with api router and used instead
|
||||
// todo: cache
|
||||
const { data: assets, isLoading } = useQuery({
|
||||
queryKey: ['assets', currentWallet?.activeAccounts[0].address],
|
||||
queryFn: async () => {
|
||||
if (!currentWallet?.activeAccounts[0].address) {
|
||||
throw new Error('No wallet address available')
|
||||
}
|
||||
|
||||
const url = new URL(
|
||||
`${import.meta.env.WXT_STATUS_API_URL}/api/trpc/assets.all`,
|
||||
)
|
||||
url.searchParams.set(
|
||||
'input',
|
||||
// encodeURIComponent(
|
||||
JSON.stringify({
|
||||
json: {
|
||||
address: currentWallet.activeAccounts[0].address,
|
||||
networks: [
|
||||
'ethereum',
|
||||
'optimism',
|
||||
'arbitrum',
|
||||
'base',
|
||||
'polygon',
|
||||
'bsc',
|
||||
],
|
||||
},
|
||||
}),
|
||||
// ),
|
||||
)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch assets.')
|
||||
}
|
||||
|
||||
const body = await response.json()
|
||||
return body.result.data.json.assets
|
||||
},
|
||||
enabled: !!currentWallet?.activeAccounts[0].address && !isWalletLoading,
|
||||
staleTime: 60 * 60 * 1000, // 1 hour
|
||||
gcTime: 60 * 60 * 1000, // 1 hour
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
const router = useRouter()
|
||||
const { data: assets, isLoading } = useAssets({
|
||||
address,
|
||||
isWalletLoading,
|
||||
})
|
||||
|
||||
if (!currentWallet) {
|
||||
if (!currentWallet || !address) {
|
||||
return <div>No wallet selected</div>
|
||||
}
|
||||
|
||||
@@ -94,18 +34,24 @@ function RouteComponent() {
|
||||
assets ? (
|
||||
<AssetsList
|
||||
assets={assets}
|
||||
onSelect={handleSelect}
|
||||
onSelect={url => {
|
||||
const ticker = url.split('/').pop()
|
||||
if (!ticker) return
|
||||
router.navigate({
|
||||
to: '/portfolio/assets/$ticker',
|
||||
params: { ticker },
|
||||
})
|
||||
}}
|
||||
clearSearch={() => {
|
||||
console.log('Search cleared')
|
||||
}}
|
||||
searchParams={new URLSearchParams()}
|
||||
pathname="/portfolio/"
|
||||
pathname="/portfolio/assets"
|
||||
/>
|
||||
) : (
|
||||
<div className="mt-4 flex flex-col gap-3">Empty state</div>
|
||||
)
|
||||
}
|
||||
detail={<>Detail for asset</>}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{isPinExtension && (
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Suspense } from 'react'
|
||||
|
||||
import { CollectiblesGrid as CollectiblesList } from '@status-im/wallet/components'
|
||||
import {
|
||||
createFileRoute,
|
||||
useRouter,
|
||||
useRouterState,
|
||||
} from '@tanstack/react-router'
|
||||
|
||||
import SplittedLayout from '@/components/splitted-layout'
|
||||
|
||||
import { useWallet } from '../../../../../providers/wallet-context'
|
||||
import { Collectible } from '../../-components/collectible'
|
||||
import { LinkCollectible } from '../../-components/link-collectibe'
|
||||
|
||||
import type { NetworkType } from '@status-im/wallet/data'
|
||||
|
||||
export const Route = createFileRoute(
|
||||
'/portfolio/collectibles/$network/$contract/$id',
|
||||
)({
|
||||
component: Component,
|
||||
})
|
||||
|
||||
function Component() {
|
||||
const { currentWallet, isLoading: isWalletLoading } = useWallet()
|
||||
|
||||
const router = useRouter()
|
||||
const routerState = useRouterState()
|
||||
const params = Route.useParams()
|
||||
const { network, contract, id } = params
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const search = searchParams.get('search') ?? undefined
|
||||
|
||||
const pathname = routerState.location.pathname
|
||||
const address = currentWallet?.activeAccounts[0].address
|
||||
|
||||
const { data, fetchNextPage, isFetchingNextPage, hasNextPage, isLoading } =
|
||||
useCollectibles({
|
||||
address,
|
||||
isWalletLoading,
|
||||
})
|
||||
|
||||
const collectibles = useMemo(() => {
|
||||
return data?.pages.flatMap(page => page.collectibles ?? []) ?? []
|
||||
}, [data?.pages])
|
||||
|
||||
if (!currentWallet || !address) {
|
||||
return <div>No wallet selected</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="hidden 2xl:block">
|
||||
<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}
|
||||
onSelect={url => {
|
||||
const [network, contract, id] = url.split('/').slice(-3)
|
||||
router.navigate({
|
||||
to: '/portfolio/collectibles/$network/$contract/$id',
|
||||
params: { network, contract, id },
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isLoading={isLoading}
|
||||
detail={
|
||||
<Suspense fallback={<p>Loading collectible...</p>}>
|
||||
<Collectible
|
||||
network={network as NetworkType}
|
||||
contract={contract}
|
||||
id={id}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="block 2xl:hidden">
|
||||
<Collectible
|
||||
network={network as NetworkType}
|
||||
contract={contract}
|
||||
id={id}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
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 { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import type { NetworkType } from '@status-im/wallet/data'
|
||||
|
||||
type Props = {
|
||||
network: NetworkType
|
||||
contract: string
|
||||
id: string
|
||||
}
|
||||
|
||||
const Collectible = (props: Props) => {
|
||||
const { network, contract, id } = props
|
||||
|
||||
const { data: collectible, isLoading } = useQuery({
|
||||
queryKey: ['collectible', network, contract, id],
|
||||
queryFn: async () => {
|
||||
const url = new URL(
|
||||
'http://localhost:3030/api/trpc/collectibles.collectible',
|
||||
)
|
||||
url.searchParams.set(
|
||||
'input',
|
||||
JSON.stringify({
|
||||
json: {
|
||||
contract,
|
||||
tokenId: id,
|
||||
network,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch.')
|
||||
}
|
||||
|
||||
const body = await response.json()
|
||||
return body.result.data.json
|
||||
},
|
||||
})
|
||||
if (isLoading || !collectible) {
|
||||
return <p>Loading</p>
|
||||
}
|
||||
|
||||
const imageUrl = collectible.image || collectible.thumbnail
|
||||
const imageAlt = collectible.name || 'Collectible image'
|
||||
|
||||
return (
|
||||
<div className="overflow-auto p-4 pr-3 2xl:p-12">
|
||||
<div className="mb-10 flex gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="2xl:mb-6">
|
||||
<div className="mb-2 flex items-center gap-1.5">
|
||||
<div className="text-15 font-semibold text-neutral-100">
|
||||
{collectible.collection.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-1 mt-6 2xl:mt-0">
|
||||
<div className="text-27 font-semibold text-neutral-100">
|
||||
{collectible.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{collectible.floor_price && collectible.price_eur && (
|
||||
<div className="mb-6 flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-13 font-medium text-neutral-50">
|
||||
{collectible.floor_price} {collectible.currency}
|
||||
</div>
|
||||
<div
|
||||
className="size-0.5 rounded-full bg-neutral-40"
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="text-13 font-medium text-neutral-50">
|
||||
<CurrencyAmount
|
||||
value={collectible.price_eur}
|
||||
format="standard"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="32"
|
||||
variant="outline"
|
||||
iconBefore={<OpenseaIcon className="text-social-opensea" />}
|
||||
iconAfter={<ExternalIcon />}
|
||||
href={collectible.links.opensea}
|
||||
>
|
||||
View on OpenSea
|
||||
</Button>
|
||||
<Button
|
||||
size="32"
|
||||
variant="outline"
|
||||
icon={<OptionsIcon />}
|
||||
aria-label="More options"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{imageUrl ? (
|
||||
<div className="aspect-square size-[140px] rounded-16">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={imageAlt}
|
||||
className="size-full rounded-16 object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex aspect-square size-[140px] flex-col items-center justify-center gap-1 rounded-16 border border-dashed border-neutral-20 bg-neutral-2.5 p-1 text-13 font-semibold text-neutral-40">
|
||||
<SadIcon />
|
||||
No image available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8">
|
||||
<div>
|
||||
<div className="mb-1 text-15 font-semibold text-neutral-100">
|
||||
About
|
||||
</div>
|
||||
<div className="mb-5">{collectible.about}</div>
|
||||
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(155px,1fr))] gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<NetworkLogo name={collectible.network} size={16} />
|
||||
<span className="capitalize">{collectible.network}</span>
|
||||
</div>
|
||||
{collectible.standard !== 'NOT_A_CONTRACT' && (
|
||||
<>
|
||||
<div className="font-mono">{collectible.contract}</div>
|
||||
<div>{collectible.standard}</div>
|
||||
</>
|
||||
)}
|
||||
{collectible.collection.size && (
|
||||
<div>{collectible.collection.size}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="h-px w-full border-t border-dashed border-neutral-20"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{collectible.traits && (
|
||||
<div>
|
||||
<div className="mb-3 text-15 font-semibold text-neutral-100">
|
||||
Traits
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(155px,1fr))] gap-3">
|
||||
{Object.entries(collectible.traits as Record<string, string>).map(
|
||||
([trait, value], index) => (
|
||||
<div key={index}>
|
||||
<div className="text-13 font-medium text-neutral-50">
|
||||
{trait}
|
||||
</div>
|
||||
<div className="text-13 font-medium text-neutral-100">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Collectible }
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Link as LinkBase, useRouter } from '@tanstack/react-router'
|
||||
|
||||
type LinkProps = {
|
||||
href: string
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const LinkCollectible = (props: LinkProps) => {
|
||||
const { href, className, children } = props
|
||||
const router = useRouter()
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const [network, contract, id] = href.split('/').slice(-3)
|
||||
|
||||
router.navigate({
|
||||
to: '/portfolio/collectibles/$network/$contract/$id',
|
||||
params: { network, contract, id },
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkBase to={href} className={className} onClick={handleClick}>
|
||||
{children}
|
||||
</LinkBase>
|
||||
)
|
||||
}
|
||||
|
||||
export { LinkCollectible }
|
||||
@@ -1,49 +1,18 @@
|
||||
import { CollectiblesGrid as CollectiblesList } from '@status-im/wallet/components'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { createFileRoute, Link as LinkBase } from '@tanstack/react-router'
|
||||
import {
|
||||
CollectiblesGrid as CollectiblesList,
|
||||
PinExtension,
|
||||
} from '@status-im/wallet/components'
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router'
|
||||
|
||||
import SplittedLayout from '@/components/splitted-layout'
|
||||
import { useCollectibles } from '@/hooks/use-collectibles'
|
||||
import { usePinExtension } from '@/hooks/use-pin-extension'
|
||||
import { LinkCollectible } from '@/routes/portfolio/collectibles/-components/link-collectibe'
|
||||
|
||||
import { useWallet } from '../../../providers/wallet-context'
|
||||
|
||||
import type { NetworkType } from '@status-im/wallet/data'
|
||||
|
||||
const DEFAULT_SORT = {
|
||||
assets: { column: 'name', direction: 'asc' as const },
|
||||
collectibles: { column: 'name', direction: 'asc' as const },
|
||||
} as const
|
||||
|
||||
export const SORT_OPTIONS = {
|
||||
assets: {
|
||||
name: 'Name',
|
||||
balance: 'Balance',
|
||||
'24h': '24H%',
|
||||
value: 'Value',
|
||||
price: 'Price',
|
||||
},
|
||||
collectibles: {
|
||||
name: 'Name',
|
||||
collection: 'Collection',
|
||||
},
|
||||
} 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,
|
||||
component: Component,
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
@@ -53,140 +22,66 @@ export const Route = createFileRoute('/portfolio/collectibles/')({
|
||||
}),
|
||||
})
|
||||
|
||||
const getCollectibles = async (
|
||||
address: string,
|
||||
networks: NetworkType[],
|
||||
search?: string,
|
||||
sort?: {
|
||||
column: 'name' | 'collection'
|
||||
direction: 'asc' | 'desc'
|
||||
},
|
||||
) => {
|
||||
const url = new URL('http://localhost:3030/api/trpc/collectibles.page')
|
||||
|
||||
url.searchParams.set(
|
||||
'input',
|
||||
JSON.stringify({
|
||||
json: {
|
||||
address,
|
||||
networks,
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
search,
|
||||
sort,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch.')
|
||||
}
|
||||
|
||||
const body = await response.json()
|
||||
return body.result.data.json.collectibles
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
function Component() {
|
||||
const { currentWallet, isLoading: isWalletLoading } = useWallet()
|
||||
const { isPinExtension, handleClose } = usePinExtension()
|
||||
|
||||
const handleSelect = (url: string, options?: { scroll?: boolean }) => {
|
||||
// Handle the selection of an asset
|
||||
console.log('Selected asset URL:', url)
|
||||
console.log('Scroll option:', options?.scroll)
|
||||
}
|
||||
const router = useRouter()
|
||||
|
||||
// todo: export trpc client with api router and used instead
|
||||
// todo: cache
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const search = searchParams.get('search') ?? undefined
|
||||
const sortParam = searchParams.get('sort')
|
||||
|
||||
const pathname = window.location.pathname
|
||||
const address = currentWallet?.activeAccounts[0].address
|
||||
const sort = {
|
||||
column:
|
||||
(sortParam?.split(',')[0] as 'name' | 'collection') ||
|
||||
DEFAULT_SORT.collectibles.column,
|
||||
direction:
|
||||
(sortParam?.split(',')[1] as 'asc' | 'desc') ||
|
||||
DEFAULT_SORT.collectibles.direction,
|
||||
}
|
||||
|
||||
const networks = searchParams.get('networks')?.split(',') ?? [
|
||||
'ethereum',
|
||||
'optimism',
|
||||
'arbitrum',
|
||||
'base',
|
||||
'polygon',
|
||||
'bsc',
|
||||
]
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
|
||||
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[],
|
||||
search,
|
||||
sort,
|
||||
)
|
||||
return {
|
||||
collectibles,
|
||||
nextPage: pageParam + 1,
|
||||
}
|
||||
},
|
||||
getNextPageParam: lastPage => lastPage.nextPage,
|
||||
initialPageParam: 0,
|
||||
enabled: !!address && !isWalletLoading,
|
||||
staleTime: 60 * 60 * 1000,
|
||||
gcTime: 60 * 60 * 1000,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
const { data, fetchNextPage, isFetchingNextPage, isLoading, hasNextPage } =
|
||||
useCollectibles({
|
||||
address,
|
||||
isWalletLoading,
|
||||
})
|
||||
|
||||
const collectibles = useMemo(() => {
|
||||
return data?.pages.flatMap(page => page.collectibles ?? []) ?? []
|
||||
}, [data?.pages])
|
||||
|
||||
if (!currentWallet) {
|
||||
if (!currentWallet || !address) {
|
||||
return <div>No wallet selected</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}
|
||||
/>
|
||||
<>
|
||||
<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}
|
||||
onSelect={url => {
|
||||
const [network, contract, id] = url.split('/').slice(-3)
|
||||
router.navigate({
|
||||
to: '/portfolio/collectibles/$network/$contract/$id',
|
||||
params: { network, contract, id },
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{isPinExtension && (
|
||||
<div className="absolute right-5 top-20">
|
||||
<PinExtension onClose={handleClose} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"source": "./src/index.tsx",
|
||||
"exports": {
|
||||
"./tailwind.config": {
|
||||
"types": "./dist/tailwind.config.d.ts",
|
||||
"import": "./dist/tailwind.config.es.js",
|
||||
"require": "./dist/tailwind.config.cjs.js"
|
||||
},
|
||||
|
||||
@@ -150,7 +150,7 @@ const CollectiblesGrid = (props: Props) => {
|
||||
target="_blank"
|
||||
size="32"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => clearSearch()}
|
||||
onClick={clearSearch}
|
||||
>
|
||||
Clear search
|
||||
</Button>
|
||||
|
||||
@@ -20,6 +20,7 @@ export {
|
||||
} from './import-recovery-phrase-form'
|
||||
export { Logo, type LogoProps } from './logo'
|
||||
export { Navbar } from './nav-bar'
|
||||
export { NetworkBreakdown } from './network-breakdown'
|
||||
export { NetworkExplorerLogo } from './network-explorer-logo'
|
||||
export { NetworkLogo } from './network-logo'
|
||||
export { PercentageChange } from './percentage-change'
|
||||
@@ -34,4 +35,6 @@ export {
|
||||
export { Slider, type SliderProps } from './slider'
|
||||
export { StickyHeaderContainer } from './sticky-header-container'
|
||||
export { getTabLinkClassName, TabLink } from './tab-link'
|
||||
export { TokenAmount } from './token-amount'
|
||||
export { TokenLogo } from './token-logo'
|
||||
export { Tooltip } from './tooltip'
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { CurrencyAmount, NetworkLogo } from '@status-im/wallet/components'
|
||||
import { CurrencyAmount } from '../currency-amount'
|
||||
import { NetworkLogo } from '../network-logo'
|
||||
|
||||
import type { ApiOutput, NetworkType } from '@status-im/wallet/data'
|
||||
|
||||
@@ -110,7 +110,7 @@ const StickyHeaderContainer = (props: Props) => {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative h-[calc(100vh-56px)] overflow-auto scrollbar-stable"
|
||||
className="relative h-[calc(100vh-56px)] w-full overflow-auto scrollbar-stable"
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
|
||||
-3
@@ -1,6 +1,3 @@
|
||||
// We need to add this eslint-disable rule since we can't use next/image in this use case because we don't know the source of the image. Therefore we use the native <img> tag.
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
|
||||
import { cx } from 'class-variance-authority'
|
||||
|
||||
type Props = {
|
||||
Generated
+61
-4
@@ -924,6 +924,18 @@ importers:
|
||||
react-hook-form:
|
||||
specifier: ^7.54.2
|
||||
version: 7.56.3(react@19.1.0)
|
||||
rehype-react:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
rehype-stringify:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.1
|
||||
remark-parse:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
remark-rehype:
|
||||
specifier: ^11.0.0
|
||||
version: 11.1.2
|
||||
superjson:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.2
|
||||
@@ -933,6 +945,9 @@ importers:
|
||||
ts-pattern:
|
||||
specifier: ^5.7.1
|
||||
version: 5.7.1
|
||||
unified:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5
|
||||
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))
|
||||
@@ -11823,6 +11838,9 @@ packages:
|
||||
hast-util-to-html@8.0.4:
|
||||
resolution: {integrity: sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==}
|
||||
|
||||
hast-util-to-html@9.0.5:
|
||||
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
|
||||
|
||||
@@ -11893,6 +11911,9 @@ packages:
|
||||
html-void-elements@2.0.1:
|
||||
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
html-whitespace-sensitive-tag-names@2.0.0:
|
||||
resolution: {integrity: sha512-SQdIvTTtnHAx72xGUIUudvVOCjeWvV1U7rvSFnNGxTGRw3ZC7RES4Gw6dm1nMYD60TXvm6zjk/bWqgNc5pjQaw==}
|
||||
|
||||
@@ -15479,6 +15500,9 @@ packages:
|
||||
peerDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
rehype-react@8.0.0:
|
||||
resolution: {integrity: sha512-vzo0YxYbB2HE+36+9HWXVdxNoNDubx63r5LBzpxBGVWM8s9mdnMdbmuJBAX6TTyuGdZjZix6qU3GcSuKCIWivw==}
|
||||
|
||||
rehype-recma@1.0.0:
|
||||
resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
|
||||
|
||||
@@ -15488,6 +15512,9 @@ packages:
|
||||
rehype-slug@6.0.0:
|
||||
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
|
||||
|
||||
rehype-stringify@10.0.1:
|
||||
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
|
||||
|
||||
rehype-stringify@9.0.4:
|
||||
resolution: {integrity: sha512-Uk5xu1YKdqobe5XpSskwPvo1XeHUUucWEQSl8hTrXt5selvca1e8K1EZ37E6YoZ4BT8BCqCdVfQW7OfHfthtVQ==}
|
||||
|
||||
@@ -21117,7 +21144,7 @@ snapshots:
|
||||
bufferutil: 4.0.9
|
||||
cross-fetch: 4.1.0
|
||||
date-fns: 2.30.0
|
||||
debug: 4.3.7
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
eciesjs: 0.4.14
|
||||
eventemitter2: 6.4.9
|
||||
readable-stream: 3.6.2
|
||||
@@ -32515,6 +32542,20 @@ snapshots:
|
||||
stringify-entities: 4.0.4
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-to-html@9.0.5:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
ccount: 2.0.1
|
||||
comma-separated-tokens: 2.0.3
|
||||
hast-util-whitespace: 3.0.0
|
||||
html-void-elements: 3.0.0
|
||||
mdast-util-to-hast: 13.2.0
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
stringify-entities: 4.0.4
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
@@ -32609,6 +32650,8 @@ snapshots:
|
||||
|
||||
html-void-elements@2.0.1: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
html-whitespace-sensitive-tag-names@2.0.0: {}
|
||||
|
||||
htmlnano@2.1.1(postcss@8.4.47)(svgo@2.8.0)(typescript@5.2.2):
|
||||
@@ -34965,7 +35008,7 @@ snapshots:
|
||||
|
||||
micromark@2.11.4:
|
||||
dependencies:
|
||||
debug: 4.3.7
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
parse-entities: 2.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -34995,7 +35038,7 @@ snapshots:
|
||||
micromark@4.0.0:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.3.7
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
decode-named-character-reference: 1.0.2
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.1
|
||||
@@ -37135,6 +37178,14 @@ snapshots:
|
||||
hast-util-whitespace: 2.0.1
|
||||
unified: 10.1.2
|
||||
|
||||
rehype-react@8.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-jsx-runtime: 2.3.6
|
||||
unified: 11.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
rehype-recma@1.0.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
@@ -37161,6 +37212,12 @@ snapshots:
|
||||
hast-util-to-string: 3.0.0
|
||||
unist-util-visit: 5.0.0
|
||||
|
||||
rehype-stringify@10.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-html: 9.0.5
|
||||
unified: 11.0.5
|
||||
|
||||
rehype-stringify@9.0.4:
|
||||
dependencies:
|
||||
'@types/hast': 2.3.10
|
||||
@@ -39361,7 +39418,7 @@ snapshots:
|
||||
bundle-require: 4.2.1(esbuild@0.18.20)
|
||||
cac: 6.7.14
|
||||
chokidar: 3.6.0
|
||||
debug: 4.3.7
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
esbuild: 0.18.20
|
||||
execa: 5.1.1
|
||||
globby: 11.1.0
|
||||
|
||||
Reference in New Issue
Block a user