Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions web-v2/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AppProviders } from '@/providers/AppProviders'
import ErrorBoundary from './components/ErrorBoundary'
import BorrowPage from './pages/Borrow'
import DashboardPage from './pages/Dashboard'
import DemoPage from './pages/Demo'
import DesignSystemPage from './pages/DesignSystem'
import SupplyPage from './pages/Supply'

Expand Down Expand Up @@ -35,6 +36,10 @@ const router = createBrowserRouter([
path: RoutePath.DesignSystem,
element: <DesignSystemPage />,
},
{
path: RoutePath.Demo,
element: <DemoPage />,
},
]
: []),
],
Expand Down
51 changes: 47 additions & 4 deletions web-v2/src/api/indexer/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { useQuery, type UseQueryResult } from '@tanstack/react-query'
import {
type QueryKey,
useQuery,
type UseQueryOptions,
type UseQueryResult,
} from '@tanstack/react-query'

import { GC_TIME_MS, STALE_TIME_MS } from '../staleTime'
import {
Expand All @@ -8,17 +13,47 @@ import {
fetchOfferParticipants,
fetchOfferParticipantsHistory,
fetchOffers,
fetchOffersBatch,
fetchOfferUtxos,
type ListOffersParams,
} from './methods'
import { offersQueryKeys } from './queryKeys'
import type { OfferDetails, OfferParticipant, OfferShort, OfferUtxo } from './schemas'

export function useOffers(params: ListOffersParams = {}): UseQueryResult<OfferShort[]> {
export function useOffers(
params: ListOffersParams = {},
options: {
refetchInterval?: number
placeholderData?: UseQueryOptions<
OfferShort[],
Error,
OfferShort[],
QueryKey
>['placeholderData']
} = {},
): UseQueryResult<OfferShort[]> {
return useQuery({
queryKey: offersQueryKeys.list(params),
queryFn: ({ signal }) => fetchOffers(params, { signal }),
staleTime: STALE_TIME_MS.medium,
refetchInterval: options.refetchInterval,
placeholderData: options.placeholderData,
})
}

// Fetch offers by exact id list (POST /offers/batch). Use for the user's own
// offers, where the id set is known and must be resolved fully — unlike the
// paginated `useOffers`, which only returns one page.
export function useOffersBatch(
ids: string[],
options: { refetchInterval?: number } = {},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we really need to specify custom options here, let's create a common interface for extra query options, e.g.:

interface ExtraQueryOptions {
  refetchInterval?: number
  staleTime?: number
  ...
}

If not, then we just can use default options inside queries

): UseQueryResult<OfferDetails[]> {
return useQuery({
queryKey: offersQueryKeys.batch(ids),
queryFn: ({ signal }) => fetchOffersBatch(ids, { signal }),
staleTime: STALE_TIME_MS.realtime,
refetchInterval: options.refetchInterval,
enabled: ids.length > 0,
})
}

Expand Down Expand Up @@ -59,20 +94,28 @@ export function useOfferParticipantsHistory(offerId: string): UseQueryResult<Off
})
}

export function useOfferIdsByScript(scriptPubkeyHex: string): UseQueryResult<string[]> {
export function useOfferIdsByScript(
scriptPubkeyHex: string,
options: { refetchInterval?: number } = {},
): UseQueryResult<string[]> {
return useQuery({
queryKey: offersQueryKeys.byScript(scriptPubkeyHex),
queryFn: ({ signal }) => fetchOfferIdsByScript(scriptPubkeyHex, { signal }),
staleTime: STALE_TIME_MS.realtime,
refetchInterval: options.refetchInterval,
enabled: !!scriptPubkeyHex,
})
}

export function useOfferIdsByBorrowerPubkey(borrowerPubkeyHex: string): UseQueryResult<string[]> {
export function useOfferIdsByBorrowerPubkey(
borrowerPubkeyHex: string,
options: { refetchInterval?: number } = {},
): UseQueryResult<string[]> {
return useQuery({
queryKey: offersQueryKeys.byBorrower(borrowerPubkeyHex),
queryFn: ({ signal }) => fetchOfferIdsByBorrowerPubkey(borrowerPubkeyHex, { signal }),
staleTime: STALE_TIME_MS.realtime,
refetchInterval: options.refetchInterval,
enabled: !!borrowerPubkeyHex,
})
}
8 changes: 8 additions & 0 deletions web-v2/src/api/indexer/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,17 @@ function postBatch<Schema extends z.ZodTypeAny>(
})
}

export type SortDir = 'asc' | 'desc'

export interface ListOffersParams {
status?: OfferStatus
asset?: string
limit?: number
offset?: number
// Server-side sort. NOTE: backend does not honor these yet — sent as a
// forward-compatible convention; results are currently returned unsorted.
sortBy?: string
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be strict union type. Also if it's not ready on backend let's not front run these changes. Just hide the sort buttons for now with a todo.

sortDir?: SortDir
}

function toQueryParams(params: ListOffersParams): Record<string, string> {
Expand All @@ -54,6 +60,8 @@ function toQueryParams(params: ListOffersParams): Record<string, string> {
if (params.asset) queryParams.asset = params.asset
if (params.limit !== undefined) queryParams.limit = String(params.limit)
if (params.offset !== undefined) queryParams.offset = String(params.offset)
if (params.sortBy) queryParams.sort_by = params.sortBy
if (params.sortDir) queryParams.sort_dir = params.sortDir
return queryParams
}

Expand Down
5 changes: 3 additions & 2 deletions web-v2/src/api/indexer/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import type { ListOffersParams } from './methods'

export const offersQueryKeys = {
all: ['offers'] as const,
list: ({ status, asset, limit, offset }: ListOffersParams) =>
['offers', 'list', status, asset, limit, offset] as const,
list: ({ status, asset, limit, offset, sortBy, sortDir }: ListOffersParams) =>
['offers', 'list', status, asset, limit, offset, sortBy, sortDir] as const,
detail: (offerId: string) => ['offers', 'detail', offerId] as const,
batch: (ids: string[]) => ['offers', 'batch', [...ids].sort()] as const,
utxos: (offerId: string) => ['offers', 'utxos', offerId] as const,
participants: (offerId: string) => ['offers', 'participants', offerId] as const,
participantsHistory: (offerId: string) => ['offers', 'participants-history', offerId] as const,
Expand Down
65 changes: 51 additions & 14 deletions web-v2/src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,73 @@
import { useCallback } from 'react'
import { Link, Outlet } from 'react-router-dom'

import ArrowSquareOutIcon from '@/components/icons/ArrowSquareOutIcon'
import BellIcon from '@/components/icons/BellIcon'
import { UiButton } from '@/components/ui/UiButton'
import { WalletButton } from '@/components/WalletButton'
import { env } from '@/constants/env'
import { RoutePath } from '@/constants/routes'

const ABOUT_SIMPLICITY_URL = 'https://github.com/BlockstreamResearch/simplicity'

const NAV = [
{ to: RoutePath.Dashboard, label: 'Dashboard' },
{ to: RoutePath.Borrow, label: 'Borrow' },
{ to: RoutePath.Supply, label: 'Supply' },
...(env.DEV ? [{ to: RoutePath.DesignSystem, label: 'System' }] : []),
...(env.DEV
? [
{ to: RoutePath.DesignSystem, label: 'System' },
{ to: RoutePath.Demo, label: 'Demo' },
]
: []),
]

export default function AppLayout() {
const openAbout = useCallback(() => {
window.open(ABOUT_SIMPLICITY_URL, '_blank', 'noopener,noreferrer')
}, [])
Comment on lines +26 to +28
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use link with target blank


return (
<main className='bg-background text-foreground min-h-screen'>
<div className='mx-auto flex w-full max-w-5xl flex-col gap-8 px-4 py-8'>
<header className='flex items-center justify-between'>
<h1 className='text-h2'>Lending</h1>
<nav className='flex flex-wrap items-center gap-6 text-sm font-medium'>
<main className='bg-surface text-foreground min-h-screen'>
<div className='mx-auto flex w-full max-w-[1280px] flex-col gap-8 px-4 pt-6 pb-12 sm:px-8 lg:gap-10 lg:px-20 lg:pt-10 lg:pb-20'>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class max-w-[1280px] can be written as max-w-7xl

<header className='flex flex-wrap items-center justify-between gap-4'>
<Link to={RoutePath.Dashboard} className='flex flex-col gap-1.5'>
<span className='text-3xl leading-none font-black tracking-tight uppercase sm:text-4xl lg:text-[43px] lg:leading-[40px]'>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let it be h1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class lg:leading-[40px] can be written as lg:leading-10

Lending
</span>
<span className='text-foreground text-xs font-medium tracking-[0.16em] uppercase'>
powered by Simplicity
</span>
</Link>

<div className='flex flex-wrap items-center gap-3'>
<UiButton variant='ghost' onPress={openAbout}>
About Simplicity
<ArrowSquareOutIcon className='size-4' />
</UiButton>
{/* Notifications not wired yet — disabled placeholder. */}
<UiButton variant='primary' isIconOnly isDisabled aria-label='Notifications'>
<BellIcon className='size-5' />
</UiButton>
Comment on lines +49 to +51
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hide until available

<WalletButton />
</div>
</header>

<Outlet />

<footer className='text-muted flex flex-col gap-3 text-xs'>
<nav className='flex flex-wrap items-center gap-4 font-medium'>
{NAV.map(({ to, label }) => (
<Link key={to} className='text-accent hover:underline' to={to}>
{label}
</Link>
))}
</nav>
</header>

<Outlet />

<footer className='text-muted text-xs'>
<p>Network: {env.VITE_NETWORK}</p>
<p>API URL: {env.VITE_API_URL}</p>
<p>Esplora Base URL: {env.VITE_ESPLORA_BASE_URL}</p>
<div>
<p>Network: {env.VITE_NETWORK}</p>
<p>API URL: {env.VITE_API_URL}</p>
<p>Esplora Base URL: {env.VITE_ESPLORA_BASE_URL}</p>
</div>
</footer>
</div>
</main>
Expand Down
18 changes: 18 additions & 0 deletions web-v2/src/components/WalletButton.tsx
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have an option to disable this button? What do you think?

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { UiButton } from '@/components/ui/UiButton'
import { DEFAULT_WALLET_TYPE } from '@/lib/wallet-core/types'
import { useWallet } from '@/providers/wallet/useWallet'
import { truncateAddress } from '@/utils/format'

export function WalletButton() {
const { connectionStatus, receiveAddress, connect } = useWallet()

if (connectionStatus === 'ready' && receiveAddress) {
return <UiButton variant='secondary'>{truncateAddress(receiveAddress)}</UiButton>
}

return (
<UiButton variant='primary' onPress={() => connect(DEFAULT_WALLET_TYPE)}>
Connect Wallet
</UiButton>
)
}
23 changes: 23 additions & 0 deletions web-v2/src/components/icons/ArrowSquareOutIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { SVGProps } from 'react'

export default function ArrowSquareOutIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
fill='none'
role='presentation'
focusable='false'
aria-hidden='true'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path
d='M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}
24 changes: 24 additions & 0 deletions web-v2/src/components/icons/ArrowSquareUpIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { SVGProps } from 'react'

export default function ArrowSquareUpIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
fill='none'
role='presentation'
focusable='false'
aria-hidden='true'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<rect x='3' y='3' width='18' height='18' rx='3' stroke='currentColor' strokeWidth='1.75' />
<path
d='M12 16V8m0 0-3 3m3-3 3 3'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}
23 changes: 23 additions & 0 deletions web-v2/src/components/icons/ArrowsRotateIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { SVGProps } from 'react'

export default function ArrowsRotateIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
fill='none'
role='presentation'
focusable='false'
aria-hidden='true'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path
d='M20 4v6h-6M4 20v-6h6M4 10a8 8 0 0 1 14-3l2 3M20 14a8 8 0 0 1-14 3l-2-3'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}
23 changes: 23 additions & 0 deletions web-v2/src/components/icons/BellIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { SVGProps } from 'react'

export default function BellIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
fill='none'
role='presentation'
focusable='false'
aria-hidden='true'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path
d='M6 9a6 6 0 1 1 12 0c0 4 1.5 5.5 2 6.5H4c.5-1 2-2.5 2-6.5ZM9.5 19a2.5 2.5 0 0 0 5 0'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}
23 changes: 23 additions & 0 deletions web-v2/src/components/icons/ChevronDownIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { SVGProps } from 'react'

export default function ChevronDownIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
fill='none'
role='presentation'
focusable='false'
aria-hidden='true'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path
d='M6 9l6 6 6-6'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}
Loading