@akta/algomd-rn 0.0.1-canary
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +46 -0
- package/src/components/ASA.tsx +221 -0
- package/src/components/ASASearch.tsx +79 -0
- package/src/components/Account.tsx +150 -0
- package/src/components/AccountSearch.tsx +75 -0
- package/src/components/AuctionListing.tsx +220 -0
- package/src/components/NFDProfile.tsx +173 -0
- package/src/components/NFDSearch.tsx +81 -0
- package/src/components/NFTListing.tsx +181 -0
- package/src/components/NFTSearch.tsx +68 -0
- package/src/components/Poll.tsx +234 -0
- package/src/components/PollSearch.tsx +79 -0
- package/src/components/RaffleListing.tsx +204 -0
- package/src/components/SelfFetching.tsx +358 -0
- package/src/components/TradeOffer.tsx +245 -0
- package/src/components/TradeSearch.tsx +103 -0
- package/src/components/TransactionDetails.tsx +242 -0
- package/src/components/TransactionSearch.tsx +102 -0
- package/src/components/index.ts +30 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/useAlgomdData.ts +445 -0
- package/src/hooks/useAlgorandClient.ts +39 -0
- package/src/index.ts +83 -0
- package/src/provider/AlgomdProvider.tsx +45 -0
- package/src/provider/context.ts +18 -0
- package/src/provider/imageResolver.ts +12 -0
- package/src/provider/index.ts +5 -0
- package/src/provider/networks.ts +23 -0
- package/src/provider/types.ts +17 -0
- package/src/types/algorand.ts +205 -0
- package/src/types/index.ts +1 -0
- package/src/ui/CopyButton.tsx +40 -0
- package/src/ui/DataStates.tsx +41 -0
- package/src/ui/ProgressBar.tsx +42 -0
- package/src/ui/SearchSheet.tsx +134 -0
- package/src/ui/SizeContainer.tsx +25 -0
- package/src/ui/StatusBadge.tsx +51 -0
- package/src/ui/index.ts +6 -0
- package/src/utils/format.ts +70 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/search.ts +65 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-fetching wrappers for algomd-rn display components.
|
|
3
|
+
*
|
|
4
|
+
* Each wrapper adds an optional self-fetch prop (appId, address, txId, etc.)
|
|
5
|
+
* alongside the existing `data` prop. If `data` is provided, it renders
|
|
6
|
+
* immediately. Otherwise, it fetches from algod via the hooks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import type { ReactNode } from 'react'
|
|
11
|
+
import type { ComponentSize } from '../types/algorand'
|
|
12
|
+
import { LoadingSkeleton, ErrorState } from '../ui/DataStates'
|
|
13
|
+
|
|
14
|
+
// Display components (internal names)
|
|
15
|
+
import { Account as AccountDisplay } from './Account'
|
|
16
|
+
import { ASAComponent } from './ASA'
|
|
17
|
+
import { NFTListingComponent } from './NFTListing'
|
|
18
|
+
import { NFDProfileComponent } from './NFDProfile'
|
|
19
|
+
import { TransactionDetailsComponent } from './TransactionDetails'
|
|
20
|
+
import { PollComponent } from './Poll'
|
|
21
|
+
import { RaffleListingComponent } from './RaffleListing'
|
|
22
|
+
import { AuctionListingComponent } from './AuctionListing'
|
|
23
|
+
import { TradeOfferComponent } from './TradeOffer'
|
|
24
|
+
|
|
25
|
+
// Hooks
|
|
26
|
+
import {
|
|
27
|
+
useAccountData,
|
|
28
|
+
useASAData,
|
|
29
|
+
useTransactionData,
|
|
30
|
+
useNFDProfileData,
|
|
31
|
+
usePollData,
|
|
32
|
+
useRaffleData,
|
|
33
|
+
useAuctionData,
|
|
34
|
+
useNFTListingData,
|
|
35
|
+
useTradeOfferData,
|
|
36
|
+
} from '../hooks/useAlgomdData'
|
|
37
|
+
|
|
38
|
+
// Types
|
|
39
|
+
import type {
|
|
40
|
+
AlgorandAccount,
|
|
41
|
+
ASA as ASAType,
|
|
42
|
+
NFTListing as NFTListingType,
|
|
43
|
+
NFDProfile as NFDProfileType,
|
|
44
|
+
TransactionDetails as TransactionDetailsType,
|
|
45
|
+
Poll as PollType,
|
|
46
|
+
RaffleListing as RaffleListingType,
|
|
47
|
+
AuctionListing as AuctionListingType,
|
|
48
|
+
TradeOffer as TradeOfferType,
|
|
49
|
+
} from '../types/algorand'
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Shared fetcher wrapper
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
function FetcherWrapper({
|
|
56
|
+
name,
|
|
57
|
+
isLoading,
|
|
58
|
+
error,
|
|
59
|
+
data,
|
|
60
|
+
loadingFallback,
|
|
61
|
+
errorFallback,
|
|
62
|
+
children,
|
|
63
|
+
}: {
|
|
64
|
+
name: string
|
|
65
|
+
isLoading: boolean
|
|
66
|
+
error: Error | null
|
|
67
|
+
data: unknown
|
|
68
|
+
loadingFallback?: ReactNode
|
|
69
|
+
errorFallback?: ReactNode
|
|
70
|
+
children: ReactNode
|
|
71
|
+
}) {
|
|
72
|
+
if (isLoading) return <>{loadingFallback ?? <LoadingSkeleton name={name} />}</>
|
|
73
|
+
if (error || !data) return <>{errorFallback ?? <ErrorState name={name} message={error?.message ?? 'Not found'} />}</>
|
|
74
|
+
return <>{children}</>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Poll
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
type PollSelfFetchProps = {
|
|
82
|
+
data?: PollType
|
|
83
|
+
appId?: number
|
|
84
|
+
showVoteButton?: boolean
|
|
85
|
+
compact?: boolean
|
|
86
|
+
size?: ComponentSize
|
|
87
|
+
className?: string
|
|
88
|
+
onVote?: (pollId: string, optionId: string) => void
|
|
89
|
+
loadingFallback?: ReactNode
|
|
90
|
+
errorFallback?: ReactNode
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function PollFetcher({ appId, ...rest }: Omit<PollSelfFetchProps, 'data'> & { appId: number }) {
|
|
94
|
+
const { data, isLoading, error } = usePollData(appId)
|
|
95
|
+
return (
|
|
96
|
+
<FetcherWrapper name="Poll" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
|
|
97
|
+
{data && <PollComponent data={data} showVoteButton={rest.showVoteButton} compact={rest.compact} size={rest.size} className={rest.className} onVote={rest.onVote} />}
|
|
98
|
+
</FetcherWrapper>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function Poll(props: PollSelfFetchProps) {
|
|
103
|
+
if (props.data) return <PollComponent data={props.data} showVoteButton={props.showVoteButton} compact={props.compact} size={props.size} className={props.className} onVote={props.onVote} />
|
|
104
|
+
if (props.appId != null) return <PollFetcher appId={props.appId} showVoteButton={props.showVoteButton} compact={props.compact} size={props.size} className={props.className} onVote={props.onVote} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
|
|
105
|
+
return <ErrorState name="Poll" message="Either data or appId is required" />
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// RaffleListing
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
type RaffleListingSelfFetchProps = {
|
|
113
|
+
data?: RaffleListingType
|
|
114
|
+
appId?: number
|
|
115
|
+
showEntryButton?: boolean
|
|
116
|
+
size?: ComponentSize
|
|
117
|
+
className?: string
|
|
118
|
+
imageUrl?: string
|
|
119
|
+
onEnter?: (raffle: RaffleListingType) => void
|
|
120
|
+
loadingFallback?: ReactNode
|
|
121
|
+
errorFallback?: ReactNode
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function RaffleListingFetcher({ appId, ...rest }: Omit<RaffleListingSelfFetchProps, 'data'> & { appId: number }) {
|
|
125
|
+
const { data, isLoading, error } = useRaffleData(appId)
|
|
126
|
+
return (
|
|
127
|
+
<FetcherWrapper name="Raffle" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
|
|
128
|
+
{data && <RaffleListingComponent data={data} showEntryButton={rest.showEntryButton} size={rest.size} className={rest.className} imageUrl={rest.imageUrl} onEnter={rest.onEnter} />}
|
|
129
|
+
</FetcherWrapper>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function RaffleListing(props: RaffleListingSelfFetchProps) {
|
|
134
|
+
if (props.data) return <RaffleListingComponent data={props.data} showEntryButton={props.showEntryButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onEnter={props.onEnter} />
|
|
135
|
+
if (props.appId != null) return <RaffleListingFetcher appId={props.appId} showEntryButton={props.showEntryButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onEnter={props.onEnter} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
|
|
136
|
+
return <ErrorState name="Raffle" message="Either data or appId is required" />
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// AuctionListing
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
type AuctionListingSelfFetchProps = {
|
|
144
|
+
data?: AuctionListingType
|
|
145
|
+
appId?: number
|
|
146
|
+
showBidButton?: boolean
|
|
147
|
+
size?: ComponentSize
|
|
148
|
+
className?: string
|
|
149
|
+
imageUrl?: string
|
|
150
|
+
onBid?: (auction: AuctionListingType) => void
|
|
151
|
+
loadingFallback?: ReactNode
|
|
152
|
+
errorFallback?: ReactNode
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function AuctionListingFetcher({ appId, ...rest }: Omit<AuctionListingSelfFetchProps, 'data'> & { appId: number }) {
|
|
156
|
+
const { data, isLoading, error } = useAuctionData(appId)
|
|
157
|
+
return (
|
|
158
|
+
<FetcherWrapper name="Auction" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
|
|
159
|
+
{data && <AuctionListingComponent data={data} showBidButton={rest.showBidButton} size={rest.size} className={rest.className} imageUrl={rest.imageUrl} onBid={rest.onBid} />}
|
|
160
|
+
</FetcherWrapper>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function AuctionListing(props: AuctionListingSelfFetchProps) {
|
|
165
|
+
if (props.data) return <AuctionListingComponent data={props.data} showBidButton={props.showBidButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onBid={props.onBid} />
|
|
166
|
+
if (props.appId != null) return <AuctionListingFetcher appId={props.appId} showBidButton={props.showBidButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onBid={props.onBid} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
|
|
167
|
+
return <ErrorState name="Auction" message="Either data or appId is required" />
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// NFTListing
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
type NFTListingSelfFetchProps = {
|
|
175
|
+
data?: NFTListingType
|
|
176
|
+
appId?: number
|
|
177
|
+
showPurchaseButton?: boolean
|
|
178
|
+
size?: ComponentSize
|
|
179
|
+
className?: string
|
|
180
|
+
imageUrl?: string
|
|
181
|
+
onPurchase?: (listing: NFTListingType) => void
|
|
182
|
+
onFavorite?: (listing: NFTListingType) => void
|
|
183
|
+
loadingFallback?: ReactNode
|
|
184
|
+
errorFallback?: ReactNode
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function NFTListingFetcher({ appId, ...rest }: Omit<NFTListingSelfFetchProps, 'data'> & { appId: number }) {
|
|
188
|
+
const { data, isLoading, error } = useNFTListingData(appId)
|
|
189
|
+
return (
|
|
190
|
+
<FetcherWrapper name="NFT Listing" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
|
|
191
|
+
{data && <NFTListingComponent data={data} showPurchaseButton={rest.showPurchaseButton} size={rest.size} className={rest.className} imageUrl={rest.imageUrl} onPurchase={rest.onPurchase} onFavorite={rest.onFavorite} />}
|
|
192
|
+
</FetcherWrapper>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function NFTListing(props: NFTListingSelfFetchProps) {
|
|
197
|
+
if (props.data) return <NFTListingComponent data={props.data} showPurchaseButton={props.showPurchaseButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onPurchase={props.onPurchase} onFavorite={props.onFavorite} />
|
|
198
|
+
if (props.appId != null) return <NFTListingFetcher appId={props.appId} showPurchaseButton={props.showPurchaseButton} size={props.size} className={props.className} imageUrl={props.imageUrl} onPurchase={props.onPurchase} onFavorite={props.onFavorite} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
|
|
199
|
+
return <ErrorState name="NFT Listing" message="Either data or appId is required" />
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// TradeOffer
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
type TradeOfferSelfFetchProps = {
|
|
207
|
+
data?: TradeOfferType
|
|
208
|
+
appId?: number
|
|
209
|
+
offerId?: number
|
|
210
|
+
showActions?: boolean
|
|
211
|
+
size?: ComponentSize
|
|
212
|
+
className?: string
|
|
213
|
+
currentUserAddress?: string
|
|
214
|
+
onAccept?: (offer: TradeOfferType) => void
|
|
215
|
+
onReject?: (offer: TradeOfferType) => void
|
|
216
|
+
loadingFallback?: ReactNode
|
|
217
|
+
errorFallback?: ReactNode
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function TradeOfferFetcher({ appId, offerId, ...rest }: Omit<TradeOfferSelfFetchProps, 'data'> & { appId: number; offerId: number }) {
|
|
221
|
+
const { data, isLoading, error } = useTradeOfferData(appId, offerId)
|
|
222
|
+
return (
|
|
223
|
+
<FetcherWrapper name="Trade Offer" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
|
|
224
|
+
{data && <TradeOfferComponent data={data} showActions={rest.showActions} size={rest.size} className={rest.className} currentUserAddress={rest.currentUserAddress} onAccept={rest.onAccept} onReject={rest.onReject} />}
|
|
225
|
+
</FetcherWrapper>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function TradeOffer(props: TradeOfferSelfFetchProps) {
|
|
230
|
+
if (props.data) return <TradeOfferComponent data={props.data} showActions={props.showActions} size={props.size} className={props.className} currentUserAddress={props.currentUserAddress} onAccept={props.onAccept} onReject={props.onReject} />
|
|
231
|
+
if (props.appId != null && props.offerId != null) return <TradeOfferFetcher appId={props.appId} offerId={props.offerId} showActions={props.showActions} size={props.size} className={props.className} currentUserAddress={props.currentUserAddress} onAccept={props.onAccept} onReject={props.onReject} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
|
|
232
|
+
return <ErrorState name="Trade Offer" message="Either data or (appId + offerId) is required" />
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Account
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
type AccountSelfFetchProps = {
|
|
240
|
+
data?: AlgorandAccount
|
|
241
|
+
address?: string
|
|
242
|
+
showAssets?: boolean
|
|
243
|
+
showApps?: boolean
|
|
244
|
+
compact?: boolean
|
|
245
|
+
size?: ComponentSize
|
|
246
|
+
className?: string
|
|
247
|
+
onExternalLink?: (address: string) => void
|
|
248
|
+
loadingFallback?: ReactNode
|
|
249
|
+
errorFallback?: ReactNode
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function AccountFetcher({ address, ...rest }: Omit<AccountSelfFetchProps, 'data'> & { address: string }) {
|
|
253
|
+
const { data, isLoading, error } = useAccountData(address)
|
|
254
|
+
return (
|
|
255
|
+
<FetcherWrapper name="Account" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
|
|
256
|
+
{data && <AccountDisplay data={data} showAssets={rest.showAssets} showApps={rest.showApps} compact={rest.compact} size={rest.size} className={rest.className} onExternalLink={rest.onExternalLink} />}
|
|
257
|
+
</FetcherWrapper>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function Account(props: AccountSelfFetchProps) {
|
|
262
|
+
if (props.data) return <AccountDisplay data={props.data} showAssets={props.showAssets} showApps={props.showApps} compact={props.compact} size={props.size} className={props.className} onExternalLink={props.onExternalLink} />
|
|
263
|
+
if (props.address) return <AccountFetcher address={props.address} showAssets={props.showAssets} showApps={props.showApps} compact={props.compact} size={props.size} className={props.className} onExternalLink={props.onExternalLink} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
|
|
264
|
+
return <ErrorState name="Account" message="Either data or address is required" />
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// ASA
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
type ASASelfFetchProps = {
|
|
272
|
+
data?: ASAType
|
|
273
|
+
assetId?: number
|
|
274
|
+
showDetails?: boolean
|
|
275
|
+
compact?: boolean
|
|
276
|
+
size?: ComponentSize
|
|
277
|
+
className?: string
|
|
278
|
+
imageUrl?: string
|
|
279
|
+
loadingFallback?: ReactNode
|
|
280
|
+
errorFallback?: ReactNode
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function ASAFetcher({ assetId, ...rest }: Omit<ASASelfFetchProps, 'data'> & { assetId: number }) {
|
|
284
|
+
const { data, isLoading, error } = useASAData(assetId)
|
|
285
|
+
return (
|
|
286
|
+
<FetcherWrapper name="ASA" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
|
|
287
|
+
{data && <ASAComponent data={data} showDetails={rest.showDetails} compact={rest.compact} size={rest.size} className={rest.className} imageUrl={rest.imageUrl} />}
|
|
288
|
+
</FetcherWrapper>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function ASA(props: ASASelfFetchProps) {
|
|
293
|
+
if (props.data) return <ASAComponent data={props.data} showDetails={props.showDetails} compact={props.compact} size={props.size} className={props.className} imageUrl={props.imageUrl} />
|
|
294
|
+
if (props.assetId != null) return <ASAFetcher assetId={props.assetId} showDetails={props.showDetails} compact={props.compact} size={props.size} className={props.className} imageUrl={props.imageUrl} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
|
|
295
|
+
return <ErrorState name="ASA" message="Either data or assetId is required" />
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// TransactionDetails
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
type TransactionDetailsSelfFetchProps = {
|
|
303
|
+
data?: TransactionDetailsType
|
|
304
|
+
txId?: string
|
|
305
|
+
showContext?: boolean
|
|
306
|
+
showFee?: boolean
|
|
307
|
+
compact?: boolean
|
|
308
|
+
size?: ComponentSize
|
|
309
|
+
className?: string
|
|
310
|
+
loadingFallback?: ReactNode
|
|
311
|
+
errorFallback?: ReactNode
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function TransactionDetailsFetcher({ txId, ...rest }: Omit<TransactionDetailsSelfFetchProps, 'data'> & { txId: string }) {
|
|
315
|
+
const { data, isLoading, error } = useTransactionData(txId)
|
|
316
|
+
return (
|
|
317
|
+
<FetcherWrapper name="Transaction" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
|
|
318
|
+
{data && <TransactionDetailsComponent data={data} showContext={rest.showContext} showFee={rest.showFee} compact={rest.compact} size={rest.size} className={rest.className} />}
|
|
319
|
+
</FetcherWrapper>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function TransactionDetails(props: TransactionDetailsSelfFetchProps) {
|
|
324
|
+
if (props.data) return <TransactionDetailsComponent data={props.data} showContext={props.showContext} showFee={props.showFee} compact={props.compact} size={props.size} className={props.className} />
|
|
325
|
+
if (props.txId) return <TransactionDetailsFetcher txId={props.txId} showContext={props.showContext} showFee={props.showFee} compact={props.compact} size={props.size} className={props.className} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
|
|
326
|
+
return <ErrorState name="Transaction" message="Either data or txId is required" />
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// NFDProfile
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
type NFDProfileSelfFetchProps = {
|
|
334
|
+
data?: NFDProfileType
|
|
335
|
+
name?: string
|
|
336
|
+
showBio?: boolean
|
|
337
|
+
showProperties?: boolean
|
|
338
|
+
compact?: boolean
|
|
339
|
+
size?: ComponentSize
|
|
340
|
+
className?: string
|
|
341
|
+
loadingFallback?: ReactNode
|
|
342
|
+
errorFallback?: ReactNode
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function NFDProfileFetcher({ name, ...rest }: Omit<NFDProfileSelfFetchProps, 'data'> & { name: string }) {
|
|
346
|
+
const { data, isLoading, error } = useNFDProfileData(name)
|
|
347
|
+
return (
|
|
348
|
+
<FetcherWrapper name="NFD Profile" isLoading={isLoading} error={error} data={data} loadingFallback={rest.loadingFallback} errorFallback={rest.errorFallback}>
|
|
349
|
+
{data && <NFDProfileComponent data={data} showBio={rest.showBio} showProperties={rest.showProperties} compact={rest.compact} size={rest.size} className={rest.className} />}
|
|
350
|
+
</FetcherWrapper>
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function NFDProfile(props: NFDProfileSelfFetchProps) {
|
|
355
|
+
if (props.data) return <NFDProfileComponent data={props.data} showBio={props.showBio} showProperties={props.showProperties} compact={props.compact} size={props.size} className={props.className} />
|
|
356
|
+
if (props.name) return <NFDProfileFetcher name={props.name} showBio={props.showBio} showProperties={props.showProperties} compact={props.compact} size={props.size} className={props.className} loadingFallback={props.loadingFallback} errorFallback={props.errorFallback} />
|
|
357
|
+
return <ErrorState name="NFD Profile" message="Either data or name is required" />
|
|
358
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, Pressable } from 'react-native';
|
|
3
|
+
import { Image } from 'expo-image';
|
|
4
|
+
import Animated, { FadeInDown } from 'react-native-reanimated';
|
|
5
|
+
import Svg, { Path } from 'react-native-svg';
|
|
6
|
+
import type { TradeOffer as TradeOfferType, ASA, AlgorandAccount, ComponentSize } from '../types/algorand';
|
|
7
|
+
import { formatAddress, formatRelativeTime } from '../utils/format';
|
|
8
|
+
import { CopyButton } from '../ui/CopyButton';
|
|
9
|
+
import { SizeContainer } from '../ui/SizeContainer';
|
|
10
|
+
import { StatusBadge } from '../ui/StatusBadge';
|
|
11
|
+
|
|
12
|
+
function ArrowRightLeftIcon({ size = 20, color = '#a1a1aa' }: { size?: number; color?: string }) {
|
|
13
|
+
return (
|
|
14
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
15
|
+
<Path d="m8 3 4 8 5-5 5 15H2L8 3z" />
|
|
16
|
+
</Svg>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function SwapIcon({ size = 20, color = '#a1a1aa' }: { size?: number; color?: string }) {
|
|
21
|
+
return (
|
|
22
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
23
|
+
<Path d="M16 3l4 4-4 4" />
|
|
24
|
+
<Path d="M20 7H4" />
|
|
25
|
+
<Path d="M8 21l-4-4 4-4" />
|
|
26
|
+
<Path d="M4 17h16" />
|
|
27
|
+
</Svg>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CheckIcon({ size = 16, color = '#fff' }: { size?: number; color?: string }) {
|
|
32
|
+
return (
|
|
33
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
34
|
+
<Path d="M20 6 9 17l-5-5" />
|
|
35
|
+
</Svg>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function XIcon({ size = 16, color = '#fff' }: { size?: number; color?: string }) {
|
|
40
|
+
return (
|
|
41
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
42
|
+
<Path d="M18 6 6 18" />
|
|
43
|
+
<Path d="m6 6 12 12" />
|
|
44
|
+
</Svg>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ClockIcon({ size = 16, color = '#a1a1aa' }: { size?: number; color?: string }) {
|
|
49
|
+
return (
|
|
50
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
51
|
+
<Path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Z" />
|
|
52
|
+
<Path d="M12 6v6l4 2" />
|
|
53
|
+
</Svg>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function MessageIcon({ size = 16, color = '#a1a1aa' }: { size?: number; color?: string }) {
|
|
58
|
+
return (
|
|
59
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
60
|
+
<Path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" />
|
|
61
|
+
</Svg>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Helper to determine if something is an ASA
|
|
66
|
+
function isASA(item: ASA | AlgorandAccount): item is ASA {
|
|
67
|
+
return 'unitName' in item;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function AssetItem({ asset, imageUrl }: { asset: ASA | AlgorandAccount; imageUrl?: string }) {
|
|
71
|
+
if (isASA(asset)) {
|
|
72
|
+
const isNFT = asset.total === 1 && asset.decimals === 0;
|
|
73
|
+
return (
|
|
74
|
+
<View className="flex-row items-center gap-3 p-3 rounded-lg bg-zinc-800/50 mb-2">
|
|
75
|
+
{imageUrl && (
|
|
76
|
+
<View className={`w-10 h-10 overflow-hidden bg-zinc-700 ${isNFT ? 'rounded-lg' : 'rounded-full'}`}>
|
|
77
|
+
<Image source={{ uri: imageUrl }} style={{ width: 40, height: 40 }} contentFit="cover" />
|
|
78
|
+
</View>
|
|
79
|
+
)}
|
|
80
|
+
<View className="flex-1">
|
|
81
|
+
<Text className="text-sm font-medium text-white" numberOfLines={1}>{asset.name}</Text>
|
|
82
|
+
<Text className="text-xs text-zinc-400">{asset.unitName} - #{asset.id}</Text>
|
|
83
|
+
</View>
|
|
84
|
+
{asset.price !== undefined && (
|
|
85
|
+
<Text className="text-xs font-medium text-green-400">${asset.price}</Text>
|
|
86
|
+
)}
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// AlgorandAccount
|
|
92
|
+
return (
|
|
93
|
+
<View className="flex-row items-center gap-3 p-3 rounded-lg bg-zinc-800/50 mb-2">
|
|
94
|
+
<View className="w-10 h-10 rounded-full bg-zinc-700 items-center justify-center">
|
|
95
|
+
<Text className="text-xs text-zinc-400">ACC</Text>
|
|
96
|
+
</View>
|
|
97
|
+
<View className="flex-1">
|
|
98
|
+
<Text className="text-sm font-mono text-white" numberOfLines={1}>
|
|
99
|
+
{formatAddress(asset.address)}
|
|
100
|
+
</Text>
|
|
101
|
+
<Text className="text-xs text-zinc-400">ALGO Account</Text>
|
|
102
|
+
</View>
|
|
103
|
+
</View>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function AssetGroup({ title, assets }: { title: string; assets: (ASA | AlgorandAccount)[] }) {
|
|
108
|
+
if (assets.length === 0) {
|
|
109
|
+
return (
|
|
110
|
+
<View className="p-3 rounded-lg border-2 border-dashed border-zinc-700 items-center">
|
|
111
|
+
<Text className="text-sm text-zinc-400">No assets</Text>
|
|
112
|
+
</View>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<View>
|
|
118
|
+
<Text className="text-sm font-medium text-zinc-300 mb-2">{title}</Text>
|
|
119
|
+
{assets.map((asset, index) => (
|
|
120
|
+
<AssetItem key={`${isASA(asset) ? asset.id : asset.address}-${index}`} asset={asset} />
|
|
121
|
+
))}
|
|
122
|
+
</View>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface TradeOfferProps {
|
|
127
|
+
data: TradeOfferType;
|
|
128
|
+
showActions?: boolean;
|
|
129
|
+
size?: ComponentSize;
|
|
130
|
+
className?: string;
|
|
131
|
+
currentUserAddress?: string;
|
|
132
|
+
onAccept?: (offer: TradeOfferType) => void;
|
|
133
|
+
onReject?: (offer: TradeOfferType) => void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function TradeOfferComponent({
|
|
137
|
+
data: offer,
|
|
138
|
+
showActions = true,
|
|
139
|
+
size = 'full',
|
|
140
|
+
className,
|
|
141
|
+
currentUserAddress,
|
|
142
|
+
onAccept,
|
|
143
|
+
onReject,
|
|
144
|
+
}: TradeOfferProps) {
|
|
145
|
+
const isExpired = new Date() > offer.expiresAt;
|
|
146
|
+
const isRecipient = currentUserAddress ? offer.recipients.includes(currentUserAddress) : false;
|
|
147
|
+
const canRespond = isRecipient && offer.status === 'pending' && !isExpired;
|
|
148
|
+
|
|
149
|
+
const statusVariant =
|
|
150
|
+
offer.status === 'pending'
|
|
151
|
+
? 'info'
|
|
152
|
+
: offer.status === 'accepted'
|
|
153
|
+
? 'success'
|
|
154
|
+
: offer.status === 'rejected'
|
|
155
|
+
? 'error'
|
|
156
|
+
: 'neutral';
|
|
157
|
+
|
|
158
|
+
const isFullscreen = size === 'fullscreen';
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<SizeContainer size={size} className={className}>
|
|
162
|
+
<Animated.View
|
|
163
|
+
entering={FadeInDown.duration(400).springify()}
|
|
164
|
+
className={`rounded-2xl ${isFullscreen ? 'border border-zinc-800' : 'bg-zinc-900/80'}`}
|
|
165
|
+
>
|
|
166
|
+
{/* Header */}
|
|
167
|
+
<View className="px-5 py-3 border-b border-zinc-800">
|
|
168
|
+
<View className="flex-row items-center justify-between">
|
|
169
|
+
<View className="flex-row items-center gap-2">
|
|
170
|
+
<Text className="text-base font-semibold text-white">
|
|
171
|
+
{isRecipient ? 'Incoming Trade' : 'Trade Offer'}
|
|
172
|
+
</Text>
|
|
173
|
+
<StatusBadge
|
|
174
|
+
label={offer.status.charAt(0).toUpperCase() + offer.status.slice(1)}
|
|
175
|
+
variant={statusVariant}
|
|
176
|
+
/>
|
|
177
|
+
{isExpired && <StatusBadge label="Expired" variant="error" />}
|
|
178
|
+
</View>
|
|
179
|
+
<View className="flex-row items-center gap-1">
|
|
180
|
+
<ClockIcon size={14} color="#a1a1aa" />
|
|
181
|
+
<Text className="text-xs font-mono text-zinc-300">
|
|
182
|
+
{formatRelativeTime(offer.expiresAt)}
|
|
183
|
+
</Text>
|
|
184
|
+
</View>
|
|
185
|
+
</View>
|
|
186
|
+
<View className="flex-row items-center gap-1 mt-1">
|
|
187
|
+
<Text className="text-xs font-mono text-zinc-400">
|
|
188
|
+
From {formatAddress(offer.creator)}
|
|
189
|
+
</Text>
|
|
190
|
+
<CopyButton value={offer.creator} size={12} color="#71717a" />
|
|
191
|
+
</View>
|
|
192
|
+
</View>
|
|
193
|
+
|
|
194
|
+
{/* Trade Content */}
|
|
195
|
+
<View className="p-5">
|
|
196
|
+
{/* Offering */}
|
|
197
|
+
<AssetGroup title="Offering" assets={offer.offering} />
|
|
198
|
+
|
|
199
|
+
{/* Swap Icon */}
|
|
200
|
+
<View className="items-center py-3">
|
|
201
|
+
<View className="p-2 rounded-full bg-zinc-800/60">
|
|
202
|
+
<SwapIcon size={20} color="#a1a1aa" />
|
|
203
|
+
</View>
|
|
204
|
+
</View>
|
|
205
|
+
|
|
206
|
+
{/* Requesting */}
|
|
207
|
+
<AssetGroup title="Requesting" assets={offer.requesting} />
|
|
208
|
+
</View>
|
|
209
|
+
|
|
210
|
+
{/* Message */}
|
|
211
|
+
{offer.message && (
|
|
212
|
+
<View className="px-5 pb-3 border-t border-zinc-800 pt-3">
|
|
213
|
+
<View className="bg-blue-500/5 rounded-xl px-3 py-2.5">
|
|
214
|
+
<View className="flex-row items-start gap-2">
|
|
215
|
+
<MessageIcon size={14} color="#60a5fa" />
|
|
216
|
+
<Text className="text-sm text-blue-200 flex-1 leading-5">{offer.message}</Text>
|
|
217
|
+
</View>
|
|
218
|
+
</View>
|
|
219
|
+
</View>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{/* Action Buttons */}
|
|
223
|
+
{showActions && canRespond && (
|
|
224
|
+
<View className="px-5 pb-5 flex-row gap-3">
|
|
225
|
+
<Pressable
|
|
226
|
+
onPress={() => onReject?.(offer)}
|
|
227
|
+
className="flex-1 flex-row items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-red-500/20"
|
|
228
|
+
>
|
|
229
|
+
<XIcon size={16} color="#fca5a5" />
|
|
230
|
+
<Text className="text-sm font-medium text-red-300">Reject</Text>
|
|
231
|
+
</Pressable>
|
|
232
|
+
|
|
233
|
+
<Pressable
|
|
234
|
+
onPress={() => onAccept?.(offer)}
|
|
235
|
+
className="flex-1 flex-row items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-green-600"
|
|
236
|
+
>
|
|
237
|
+
<CheckIcon size={16} color="#fff" />
|
|
238
|
+
<Text className="text-sm font-medium text-white">Accept</Text>
|
|
239
|
+
</Pressable>
|
|
240
|
+
</View>
|
|
241
|
+
)}
|
|
242
|
+
</Animated.View>
|
|
243
|
+
</SizeContainer>
|
|
244
|
+
);
|
|
245
|
+
}
|