diff --git a/.vscode/settings.json b/.vscode/settings.json index 196450acf..1e261a9f3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,11 @@ "typescript.tsdk": "node_modules/typescript/lib", "npm.packageManager": "pnpm", "eslint.useESLintClass": true, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + }, "eslint.workingDirectories": [ "./packages/colors", "./packages/icons", diff --git a/apps/portfolio/src/app/[address]/@detail/assets/[ticker]/page.tsx b/apps/portfolio/src/app/[address]/@detail/assets/[ticker]/page.tsx index 1cace224b..eab0a522c 100644 --- a/apps/portfolio/src/app/[address]/@detail/assets/[ticker]/page.tsx +++ b/apps/portfolio/src/app/[address]/@detail/assets/[ticker]/page.tsx @@ -2,23 +2,25 @@ 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, + NetworkBreakdown, + StickyHeaderContainer, + TokenLogo, +} 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' 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' diff --git a/apps/portfolio/src/app/[address]/@detail/collectibles/[network]/[contract]/[id]/page.tsx b/apps/portfolio/src/app/[address]/@detail/collectibles/[network]/[contract]/[id]/page.tsx index e50e5c9e5..d12f17f46 100644 --- a/apps/portfolio/src/app/[address]/@detail/collectibles/[network]/[contract]/[id]/page.tsx +++ b/apps/portfolio/src/app/[address]/@detail/collectibles/[network]/[contract]/[id]/page.tsx @@ -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' diff --git a/apps/portfolio/src/app/[address]/@list/@balance/default.tsx b/apps/portfolio/src/app/[address]/@list/@balance/default.tsx index e0e81f2b4..9a062630e 100644 --- a/apps/portfolio/src/app/[address]/@list/@balance/default.tsx +++ b/apps/portfolio/src/app/[address]/@list/@balance/default.tsx @@ -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' diff --git a/apps/portfolio/src/app/[address]/@list/collectibles/_components/collectibles-table.tsx b/apps/portfolio/src/app/[address]/@list/collectibles/_components/collectibles-table.tsx index a678c23a2..4a5e4fca2 100644 --- a/apps/portfolio/src/app/[address]/@list/collectibles/_components/collectibles-table.tsx +++ b/apps/portfolio/src/app/[address]/@list/collectibles/_components/collectibles-table.tsx @@ -5,6 +5,7 @@ import { useMemo } from 'react' import { Button, Skeleton } from '@status-im/components' import { SadIcon } from '@status-im/icons/20' +import { useInfiniteLoading } from '@status-im/wallet/hooks' import { useInfiniteQuery } from '@tanstack/react-query' import { cx } from 'class-variance-authority' import { usePathname, useSearchParams } from 'next/navigation' @@ -12,7 +13,6 @@ import { ErrorBoundary } from 'react-error-boundary' import { Link } from '../../../../_components/link' import { DEFAULT_SORT, GRADIENTS } from '../../../../_constants' -import { useInfiniteLoading } from '../../../../_hooks/use-infinite-loading' import { useSearchAndSort } from '../../../../_hooks/use-search-and-sort' import type { GetCollectiblesProps, GetCollectiblesResponse } from '../_actions' diff --git a/apps/portfolio/src/app/_components/action-buttons.tsx b/apps/portfolio/src/app/_components/action-buttons.tsx index ebc8310ef..866795c28 100644 --- a/apps/portfolio/src/app/_components/action-buttons.tsx +++ b/apps/portfolio/src/app/_components/action-buttons.tsx @@ -2,11 +2,11 @@ import { Input } from '@status-im/components' import { SearchIcon } from '@status-im/icons/20' +import { DropdownSort } from '@status-im/wallet/components' 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' import { TabLink } from './tab-link' const checkPathnameAndReturnTabValue = ( @@ -54,7 +54,7 @@ const ActionButtons = () => { clearable={!!inputValue} aria-label="Search" /> - { - 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
{formatter.format(value)}
-} diff --git a/apps/portfolio/src/app/_components/detail-drawer.tsx b/apps/portfolio/src/app/_components/detail-drawer.tsx index 9dd9ac405..c052683a2 100644 --- a/apps/portfolio/src/app/_components/detail-drawer.tsx +++ b/apps/portfolio/src/app/_components/detail-drawer.tsx @@ -31,7 +31,7 @@ const DetailDrawer = (props: Props) => { .with( P.when(p => { const segments = p.split('/').filter(Boolean) - return p.includes('/assets/') && segments.length >= 4 + return p.includes('/assets/') && segments.length === 3 }), () => true ) diff --git a/apps/portfolio/src/app/_components/sidenav.tsx b/apps/portfolio/src/app/_components/sidenav.tsx index 5f0099444..e4da7d12f 100644 --- a/apps/portfolio/src/app/_components/sidenav.tsx +++ b/apps/portfolio/src/app/_components/sidenav.tsx @@ -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' diff --git a/apps/portfolio/src/app/_config.ts b/apps/portfolio/src/app/_config.ts index 4a821412a..5fcea15e6 100644 --- a/apps/portfolio/src/app/_config.ts +++ b/apps/portfolio/src/app/_config.ts @@ -1,14 +1,16 @@ -import { createConfig, http } from 'wagmi' +import { createClient } from 'viem' +import { createConfig, http, injected } from 'wagmi' import { arbitrum, mainnet, optimism } from 'wagmi/chains' +export const supportedChains = [mainnet, optimism, arbitrum] as const + export const config = createConfig({ - chains: [mainnet, optimism, arbitrum], + chains: supportedChains, ssr: false, - transports: { - // todo: replace public clients - [mainnet.id]: http(), - [optimism.id]: http(), - [arbitrum.id]: http(), + connectors: [injected()], + // todo: replace public clients + client({ chain }) { + return createClient({ chain, transport: http() }) }, }) diff --git a/apps/wallet/package.json b/apps/wallet/package.json index 8deb47d2a..fa2451dc7 100644 --- a/apps/wallet/package.json +++ b/apps/wallet/package.json @@ -51,8 +51,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" }, diff --git a/apps/wallet/src/components/action-buttons.tsx b/apps/wallet/src/components/action-buttons.tsx new file mode 100644 index 000000000..25746d3af --- /dev/null +++ b/apps/wallet/src/components/action-buttons.tsx @@ -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 + } +} + +const ActionButtons = (props: Props) => { + // const { address, pathname, searchAndSortValues } = props + const { address, searchAndSortValues } = props + + // const placeholder = placeholderText[checkPathnameAndReturnTabValue(pathname)] + + return ( +
+
+ Assets + Collectibles +
+
+ {/* } + size="32" + value={searchAndSortValues.inputValue} + onChange={searchAndSortValues.updateSearchParam} + clearable={!!searchAndSortValues.inputValue} + aria-label="Search" + /> */} + +
+
+ ) +} + +export { ActionButtons } diff --git a/apps/wallet/src/components/content/markdown.tsx b/apps/wallet/src/components/content/markdown.tsx new file mode 100644 index 000000000..3ede572f9 --- /dev/null +++ b/apps/wallet/src/components/content/markdown.tsx @@ -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 ( + + {child} + + ) + } + + 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 ( + + {props.children} + + ) + }, + del: (props: ComponentProps<'del'>) => { + return ( + + {props.children} + + ) + }, + em: (props: ComponentProps<'em'>) => { + return ( + + {props.children} + + ) + }, + h1: (props: ComponentProps<'h1'>) => { + return ( +

+ {props.children} +

+ ) + }, + h2: (props: ComponentProps<'h2'> & { mb?: string; mt?: string }) => { + const { children, id, mb = 'mb-3', mt = 'mt-5', ...rest } = props + return ( +

+ {children} +

+ ) + }, + h3: (props: ComponentProps<'h3'>) => { + return ( +

+ {props.children} +

+ ) + }, + h4: (props: ComponentProps<'h4'>) => { + return ( +

+ {props.children} +

+ ) + }, + h5: (props: ComponentProps<'h5'>) => { + return ( +
+ {props.children} +
+ ) + }, + h6: (props: ComponentProps<'h6'>) => { + return ( +
+ {props.children} +
+ ) + }, + blockquote: (props: ComponentProps<'blockquote'>) => { + const { children, ...rest } = props + + const blockquoteChildren = Children.toArray(children).filter( + child => child !== '\n', + ) as (ReactElement | string)[] + + return ( +
+ {Children.map(blockquoteChildren, (item: ReactElement | string) => { + if (typeof item === 'string') { + return renderText({ children: item }) + } + + if (item.type === 'p') { + return cloneElement(item) + } + + return item + })} +
+ ) + }, + a: (props: ComponentProps<'a'>) => { + if (!props.href) { + const { children, ...rest } = props + return ( + + {children} + + ) + } + + if (props.href.startsWith('/')) { + return ( + + {props.children} + + ) + } + + return ( + + {props.children} + + ) + }, + p: (props: ComponentProps<'p'> & { parent?: string }) => { + const { children } = props + + if ( + (children as { type?: { name?: string } })?.type?.name === 'img' || + props.parent === 'li' + ) { + return <>{children} + } + + return ( +

&:first-child]:!mt-0', // is a first child of selected parent element + )} + > + {renderText({ children })} +

+ ) + }, + ul: (props: ComponentProps<'ul'>) => { + return ( + + ) + }, + ol: (props: ComponentProps<'ol'> & { parent?: string }) => { + const listItems = Children.toArray(props.children).filter( + child => typeof child === 'object', + ) + + return ( +
    + {Children.map(listItems, (item: ReactNode, index) => + cloneElement( + item as ReactElement<{ order?: number; parent?: string }>, + { + order: index + 1, + parent: props.parent ?? 'ol', + }, + ), + )} +
+ ) + }, + li: ( + props: ComponentProps<'li'> & { + parent?: 'ol' | 'AwaitedList' + order?: number + variant?: React.ComponentProps['variant'] + }, + ) => { + const icon = match(props.parent) + .with('ol', () => ( + + )) + .with('AwaitedList', () => ) + .otherwise(() => ) + + return ( +
  • +
    + {icon} +
    +
    + {renderText({ + children: props.children, + parent: 'li', + })} +
    +
  • + ) + }, + // handled conditionally per use case with divider component + hr: () => { + return <> + }, + img: (props: ComponentProps<'img'>) => { + return {props.alt + }, + pre: (props: ComponentProps<'pre'>) => ( +
    +  ),
    +  div: (props: ComponentProps<'div'>) => {
    +    return 
    + }, + 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 + + ) + } + + // 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 + }, + figure: (props: ComponentProps<'figure'>) => ( +
    + ), + iframe: (props: React.ComponentProps<'iframe'>) => { + // todo?: match youtube props to use aspect-video + return ( +