@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,220 @@
|
|
|
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 { AuctionListing as AuctionListingType, ComponentSize } from '../types/algorand';
|
|
7
|
+
import { formatCurrency, formatRelativeTime } from '../utils/format';
|
|
8
|
+
import { SizeContainer } from '../ui/SizeContainer';
|
|
9
|
+
import { StatusBadge } from '../ui/StatusBadge';
|
|
10
|
+
|
|
11
|
+
function GavelIcon({ size = 16, color = '#a1a1aa' }: { size?: number; color?: string }) {
|
|
12
|
+
return (
|
|
13
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
14
|
+
<Path d="m14.5 12.5-8 8a2.119 2.119 0 1 1-3-3l8-8" />
|
|
15
|
+
<Path d="m16 16 6-6" />
|
|
16
|
+
<Path d="m8 8 6-6" />
|
|
17
|
+
<Path d="m9 7 8 8" />
|
|
18
|
+
<Path d="m21 11-8-8" />
|
|
19
|
+
</Svg>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function TrophyIcon({ size = 16, color = '#eab308' }: { size?: number; color?: string }) {
|
|
24
|
+
return (
|
|
25
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
26
|
+
<Path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6" />
|
|
27
|
+
<Path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18" />
|
|
28
|
+
<Path d="M4 22h16" />
|
|
29
|
+
<Path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22" />
|
|
30
|
+
<Path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22" />
|
|
31
|
+
<Path d="M18 2H6v7a6 6 0 0 0 12 0V2Z" />
|
|
32
|
+
</Svg>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function TrendingUpIcon({ size = 12, color = '#60a5fa' }: { size?: number; color?: string }) {
|
|
37
|
+
return (
|
|
38
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
39
|
+
<Path d="m22 7-8.5 8.5-5-5L2 17" />
|
|
40
|
+
<Path d="M16 7h6v6" />
|
|
41
|
+
</Svg>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function CalendarIcon({ size = 16, color = '#71717a' }: { size?: number; color?: string }) {
|
|
46
|
+
return (
|
|
47
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
48
|
+
<Path d="M8 2v4" />
|
|
49
|
+
<Path d="M16 2v4" />
|
|
50
|
+
<Path d="M3 10h18" />
|
|
51
|
+
<Path d="M21 8V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z" />
|
|
52
|
+
</Svg>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function TimerIcon({ size = 12, color = '#fb923c' }: { size?: number; color?: string }) {
|
|
57
|
+
return (
|
|
58
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
59
|
+
<Path d="M10 2h4" />
|
|
60
|
+
<Path d="M12 14V6" />
|
|
61
|
+
<Path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Z" />
|
|
62
|
+
</Svg>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface AuctionListingProps {
|
|
67
|
+
data: AuctionListingType;
|
|
68
|
+
showBidButton?: boolean;
|
|
69
|
+
size?: ComponentSize;
|
|
70
|
+
className?: string;
|
|
71
|
+
imageUrl?: string;
|
|
72
|
+
onBid?: (auction: AuctionListingType) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function AuctionListingComponent({
|
|
76
|
+
data: auction,
|
|
77
|
+
showBidButton = true,
|
|
78
|
+
size = 'full',
|
|
79
|
+
className,
|
|
80
|
+
imageUrl,
|
|
81
|
+
onBid,
|
|
82
|
+
}: AuctionListingProps) {
|
|
83
|
+
const isActive = auction.status === 'active';
|
|
84
|
+
const isUpcoming = auction.status === 'upcoming';
|
|
85
|
+
const timeRemaining = new Date(auction.endTime).getTime() - new Date().getTime();
|
|
86
|
+
const isEndingSoon = timeRemaining < 24 * 60 * 60 * 1000 && timeRemaining > 0;
|
|
87
|
+
|
|
88
|
+
const statusVariant = isActive ? 'success' : isUpcoming ? 'info' : 'neutral';
|
|
89
|
+
|
|
90
|
+
const isFullscreen = size === 'fullscreen';
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<SizeContainer size={size} className={className}>
|
|
94
|
+
<Animated.View
|
|
95
|
+
entering={FadeInDown.duration(400).springify()}
|
|
96
|
+
className={`rounded-2xl ${isFullscreen ? 'border border-zinc-800' : 'bg-zinc-900/80'}`}
|
|
97
|
+
>
|
|
98
|
+
<View className="p-5">
|
|
99
|
+
{/* Header */}
|
|
100
|
+
<View className="flex-row items-start gap-3 mb-4">
|
|
101
|
+
{imageUrl && (
|
|
102
|
+
<View className="w-14 h-14 rounded-lg overflow-hidden bg-zinc-800/50">
|
|
103
|
+
<Image source={{ uri: imageUrl }} style={{ width: 56, height: 56 }} contentFit="cover" />
|
|
104
|
+
</View>
|
|
105
|
+
)}
|
|
106
|
+
<View className="flex-1">
|
|
107
|
+
<View className="flex-row items-center gap-2 flex-wrap mb-1">
|
|
108
|
+
<GavelIcon size={16} color={isActive ? '#34d399' : '#71717a'} />
|
|
109
|
+
<Text className="text-sm font-medium text-zinc-400">Auction Event</Text>
|
|
110
|
+
<StatusBadge
|
|
111
|
+
label={auction.status.charAt(0).toUpperCase() + auction.status.slice(1)}
|
|
112
|
+
variant={statusVariant}
|
|
113
|
+
/>
|
|
114
|
+
{isActive && isEndingSoon && (
|
|
115
|
+
<StatusBadge label="Ending Soon" variant="error" />
|
|
116
|
+
)}
|
|
117
|
+
<View className="px-2 py-0.5 rounded-full bg-zinc-700">
|
|
118
|
+
<Text className="text-xs font-medium text-white">{auction.bidCount} bids</Text>
|
|
119
|
+
</View>
|
|
120
|
+
</View>
|
|
121
|
+
<Text className="text-base font-bold text-white mb-1">{auction.title}</Text>
|
|
122
|
+
<Text className="text-sm text-zinc-300 leading-5">{auction.description}</Text>
|
|
123
|
+
</View>
|
|
124
|
+
</View>
|
|
125
|
+
|
|
126
|
+
{/* Auction Item */}
|
|
127
|
+
<View className="mb-4 bg-yellow-500/5 rounded-xl px-3 py-2.5">
|
|
128
|
+
<View className="flex-row items-center gap-2 mb-2">
|
|
129
|
+
<TrophyIcon size={16} color="#eab308" />
|
|
130
|
+
<Text className="text-sm font-medium text-yellow-300">Auction Item</Text>
|
|
131
|
+
</View>
|
|
132
|
+
{auction.prizes.map((prize, index) => (
|
|
133
|
+
<View key={index} className="flex-row items-center justify-between mb-1">
|
|
134
|
+
<Text className="text-sm font-medium text-white">{prize.name}</Text>
|
|
135
|
+
<Text className="text-xs font-mono text-yellow-300">#{prize.id}</Text>
|
|
136
|
+
</View>
|
|
137
|
+
))}
|
|
138
|
+
</View>
|
|
139
|
+
|
|
140
|
+
{/* Bid Stats */}
|
|
141
|
+
<View className="flex-row gap-6 mb-4">
|
|
142
|
+
<View>
|
|
143
|
+
<Text className="text-2xl font-bold text-white">
|
|
144
|
+
{formatCurrency(auction.currentHighestBid, auction.bidAsset.unitName)}
|
|
145
|
+
</Text>
|
|
146
|
+
<Text className="text-xs text-zinc-400">Current Bid</Text>
|
|
147
|
+
</View>
|
|
148
|
+
<View>
|
|
149
|
+
<Text className="text-base font-semibold text-white">
|
|
150
|
+
{formatCurrency(auction.minimumNextBid, auction.bidAsset.unitName)}
|
|
151
|
+
</Text>
|
|
152
|
+
<Text className="text-xs text-zinc-400">Min Next Bid</Text>
|
|
153
|
+
</View>
|
|
154
|
+
<View>
|
|
155
|
+
<Text className="text-base font-semibold text-white">{auction.bidCount}</Text>
|
|
156
|
+
<Text className="text-xs text-zinc-400">Total Bids</Text>
|
|
157
|
+
</View>
|
|
158
|
+
</View>
|
|
159
|
+
|
|
160
|
+
{/* Bid Fee Pool */}
|
|
161
|
+
{auction.bidFeePercentage && auction.currentBidFeePool > 0 && (
|
|
162
|
+
<View className="mb-4 bg-blue-500/5 rounded-xl px-3 py-2.5">
|
|
163
|
+
<View className="flex-row items-center gap-1.5 mb-1">
|
|
164
|
+
<TrendingUpIcon size={12} color="#60a5fa" />
|
|
165
|
+
<Text className="text-xs font-medium text-blue-300">
|
|
166
|
+
Bid Fee Pool ({auction.bidFeePercentage}%)
|
|
167
|
+
</Text>
|
|
168
|
+
</View>
|
|
169
|
+
<Text className="text-xs text-blue-200">
|
|
170
|
+
{formatCurrency(auction.currentBidFeePool, auction.bidAsset.unitName)} collected
|
|
171
|
+
</Text>
|
|
172
|
+
</View>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{/* Timing */}
|
|
176
|
+
<View className="flex-row items-center gap-6 mb-4">
|
|
177
|
+
<View className="flex-row items-center gap-2">
|
|
178
|
+
<CalendarIcon size={16} />
|
|
179
|
+
<Text className="text-sm text-zinc-300">
|
|
180
|
+
{isUpcoming ? 'Starts' : isActive ? 'Ends' : 'Ended'}{' '}
|
|
181
|
+
{formatRelativeTime(isUpcoming ? auction.startTime : auction.endTime)}
|
|
182
|
+
</Text>
|
|
183
|
+
</View>
|
|
184
|
+
{auction.timeExtended && (
|
|
185
|
+
<View className="flex-row items-center gap-1">
|
|
186
|
+
<TimerIcon size={12} color="#fb923c" />
|
|
187
|
+
<Text className="text-xs text-orange-300">Extended</Text>
|
|
188
|
+
</View>
|
|
189
|
+
)}
|
|
190
|
+
</View>
|
|
191
|
+
|
|
192
|
+
{/* Bid Button */}
|
|
193
|
+
{showBidButton && (isActive || isUpcoming) && (
|
|
194
|
+
<Pressable
|
|
195
|
+
onPress={() => isActive && onBid?.(auction)}
|
|
196
|
+
disabled={!isActive}
|
|
197
|
+
className={`flex-row items-center justify-center gap-2 py-3 px-4 rounded-xl ${
|
|
198
|
+
!isActive ? 'bg-zinc-800/50' : 'bg-purple-600'
|
|
199
|
+
}`}
|
|
200
|
+
>
|
|
201
|
+
{!isActive ? (
|
|
202
|
+
<>
|
|
203
|
+
<TimerIcon size={16} color="#a1a1aa" />
|
|
204
|
+
<Text className="text-sm font-medium text-zinc-400">
|
|
205
|
+
{isUpcoming ? 'Not Started' : 'Auction Ended'}
|
|
206
|
+
</Text>
|
|
207
|
+
</>
|
|
208
|
+
) : (
|
|
209
|
+
<>
|
|
210
|
+
<GavelIcon size={16} color="#fff" />
|
|
211
|
+
<Text className="text-sm font-medium text-white">Place Bid</Text>
|
|
212
|
+
</>
|
|
213
|
+
)}
|
|
214
|
+
</Pressable>
|
|
215
|
+
)}
|
|
216
|
+
</View>
|
|
217
|
+
</Animated.View>
|
|
218
|
+
</SizeContainer>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, Pressable } from 'react-native';
|
|
3
|
+
import { Image } from 'expo-image';
|
|
4
|
+
import * as Linking from 'expo-linking';
|
|
5
|
+
import Animated, { FadeInDown } from 'react-native-reanimated';
|
|
6
|
+
import Svg, { Path } from 'react-native-svg';
|
|
7
|
+
import type { NFDProfile as NFDProfileType, ComponentSize } from '../types/algorand';
|
|
8
|
+
import { formatAddress, formatRelativeTime } from '../utils/format';
|
|
9
|
+
import { CopyButton } from '../ui/CopyButton';
|
|
10
|
+
import { SizeContainer } from '../ui/SizeContainer';
|
|
11
|
+
|
|
12
|
+
function VerifiedIcon({ size = 16, color = '#60a5fa' }: { 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="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z" />
|
|
16
|
+
<Path d="m9 12 2 2 4-4" />
|
|
17
|
+
</Svg>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function UserIcon({ size = 24, color = '#71717a' }: { size?: number; color?: string }) {
|
|
22
|
+
return (
|
|
23
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
24
|
+
<Path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
|
25
|
+
<Path d="M12 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z" />
|
|
26
|
+
</Svg>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ExternalLinkIcon({ size = 16, color = '#a1a1aa' }: { size?: number; color?: string }) {
|
|
31
|
+
return (
|
|
32
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
33
|
+
<Path d="M15 3h6v6" />
|
|
34
|
+
<Path d="M10 14 21 3" />
|
|
35
|
+
<Path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
36
|
+
</Svg>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface NFDProfileProps {
|
|
41
|
+
data: NFDProfileType;
|
|
42
|
+
showBio?: boolean;
|
|
43
|
+
showProperties?: boolean;
|
|
44
|
+
compact?: boolean;
|
|
45
|
+
size?: ComponentSize;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function NFDProfileComponent({
|
|
50
|
+
data: profile,
|
|
51
|
+
showBio = true,
|
|
52
|
+
showProperties = true,
|
|
53
|
+
compact = false,
|
|
54
|
+
size = 'full',
|
|
55
|
+
className,
|
|
56
|
+
}: NFDProfileProps) {
|
|
57
|
+
const handleSocialPress = (key: string, value: string) => {
|
|
58
|
+
let url: string;
|
|
59
|
+
if (key === 'website') {
|
|
60
|
+
url = value.startsWith('http') ? value : `https://${value}`;
|
|
61
|
+
} else {
|
|
62
|
+
url = `https://${key}.com/${value.replace('@', '')}`;
|
|
63
|
+
}
|
|
64
|
+
Linking.openURL(url);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (compact) {
|
|
68
|
+
return (
|
|
69
|
+
<View className={`flex-row items-center gap-1.5 px-2 py-1 rounded-full bg-zinc-800/60 ${className ?? ''}`}>
|
|
70
|
+
<View className="w-5 h-5 rounded-full overflow-hidden bg-zinc-700 items-center justify-center">
|
|
71
|
+
{profile.avatar ? (
|
|
72
|
+
<Image source={{ uri: profile.avatar }} style={{ width: 20, height: 20 }} contentFit="cover" />
|
|
73
|
+
) : (
|
|
74
|
+
<UserIcon size={12} />
|
|
75
|
+
)}
|
|
76
|
+
</View>
|
|
77
|
+
{profile.verified && <VerifiedIcon size={12} />}
|
|
78
|
+
<Text className="text-xs font-medium text-white" numberOfLines={1}>{profile.name}</Text>
|
|
79
|
+
<CopyButton value={profile.name} size={12} color="#a1a1aa" />
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const isFullscreen = size === 'fullscreen';
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<SizeContainer size={size} className={className}>
|
|
88
|
+
<Animated.View
|
|
89
|
+
entering={FadeInDown.duration(400).springify()}
|
|
90
|
+
className={`rounded-2xl ${isFullscreen ? 'border border-zinc-800' : 'bg-zinc-900/80'}`}
|
|
91
|
+
>
|
|
92
|
+
<View className="p-5">
|
|
93
|
+
{/* Header */}
|
|
94
|
+
<View className="flex-row items-start gap-3 mb-3">
|
|
95
|
+
<View className="w-14 h-14 rounded-full overflow-hidden bg-zinc-800 items-center justify-center">
|
|
96
|
+
{profile.avatar ? (
|
|
97
|
+
<Image source={{ uri: profile.avatar }} style={{ width: 56, height: 56 }} contentFit="cover" />
|
|
98
|
+
) : (
|
|
99
|
+
<UserIcon size={28} />
|
|
100
|
+
)}
|
|
101
|
+
</View>
|
|
102
|
+
|
|
103
|
+
<View className="flex-1">
|
|
104
|
+
<View className="flex-row items-center gap-2 mb-1 flex-wrap">
|
|
105
|
+
<Text className="text-base font-bold text-white" numberOfLines={1}>{profile.name}</Text>
|
|
106
|
+
{profile.verified && <VerifiedIcon size={18} />}
|
|
107
|
+
</View>
|
|
108
|
+
<Text className="text-xs font-mono text-zinc-400 mb-2">
|
|
109
|
+
{formatAddress(profile.address)}
|
|
110
|
+
</Text>
|
|
111
|
+
<View className="flex-row gap-1">
|
|
112
|
+
<CopyButton value={profile.name} size={14} color="#a1a1aa" />
|
|
113
|
+
<CopyButton value={profile.address} size={14} color="#a1a1aa" />
|
|
114
|
+
</View>
|
|
115
|
+
</View>
|
|
116
|
+
</View>
|
|
117
|
+
|
|
118
|
+
{/* Bio */}
|
|
119
|
+
{showBio && profile.bio && (
|
|
120
|
+
<View className="mb-3">
|
|
121
|
+
<Text className="text-sm text-zinc-300 leading-5">{profile.bio}</Text>
|
|
122
|
+
</View>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{/* Verification Badge */}
|
|
126
|
+
<View className="mb-3">
|
|
127
|
+
<View
|
|
128
|
+
className={`self-start flex-row items-center gap-2 px-3 py-1 rounded-full ${
|
|
129
|
+
profile.verified ? 'bg-blue-500/20' : 'bg-zinc-800'
|
|
130
|
+
}`}
|
|
131
|
+
>
|
|
132
|
+
{profile.verified ? (
|
|
133
|
+
<>
|
|
134
|
+
<VerifiedIcon size={12} />
|
|
135
|
+
<Text className="text-xs font-medium text-blue-400">Verified NFD</Text>
|
|
136
|
+
</>
|
|
137
|
+
) : (
|
|
138
|
+
<>
|
|
139
|
+
<UserIcon size={12} color="#a1a1aa" />
|
|
140
|
+
<Text className="text-xs font-medium text-zinc-400">Unverified</Text>
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</View>
|
|
144
|
+
</View>
|
|
145
|
+
|
|
146
|
+
{/* Social Links */}
|
|
147
|
+
{showProperties && Object.keys(profile.properties).length > 0 && (
|
|
148
|
+
<View className="mb-3">
|
|
149
|
+
<Text className="text-sm font-medium text-white mb-2">Social Links</Text>
|
|
150
|
+
<View className="flex-row flex-wrap gap-2">
|
|
151
|
+
{Object.entries(profile.properties).map(([key, value]) => (
|
|
152
|
+
<Pressable
|
|
153
|
+
key={key}
|
|
154
|
+
onPress={() => handleSocialPress(key, value)}
|
|
155
|
+
className="flex-row items-center gap-1.5 px-3 py-1.5 rounded-lg bg-zinc-800"
|
|
156
|
+
>
|
|
157
|
+
<ExternalLinkIcon size={14} color="#a1a1aa" />
|
|
158
|
+
<Text className="text-xs font-medium text-zinc-300 capitalize">{key}</Text>
|
|
159
|
+
</Pressable>
|
|
160
|
+
))}
|
|
161
|
+
</View>
|
|
162
|
+
</View>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{/* Created Date */}
|
|
166
|
+
<Text className="text-xs text-zinc-600">
|
|
167
|
+
Created {formatRelativeTime(profile.createdAt)}
|
|
168
|
+
</Text>
|
|
169
|
+
</View>
|
|
170
|
+
</Animated.View>
|
|
171
|
+
</SizeContainer>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { View, Text } from 'react-native';
|
|
3
|
+
import { Image } from 'expo-image';
|
|
4
|
+
import Svg, { Path } from 'react-native-svg';
|
|
5
|
+
import type { NFDProfile } from '../types/algorand';
|
|
6
|
+
import { formatAddress } from '../utils/format';
|
|
7
|
+
import { SearchSheet } from '../ui/SearchSheet';
|
|
8
|
+
|
|
9
|
+
function VerifiedIcon({ size = 12, color = '#60a5fa' }: { size?: number; color?: string }) {
|
|
10
|
+
return (
|
|
11
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
12
|
+
<Path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z" />
|
|
13
|
+
<Path d="m9 12 2 2 4-4" />
|
|
14
|
+
</Svg>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface NFDSearchProps {
|
|
19
|
+
data: NFDProfile[];
|
|
20
|
+
onSelect?: (profile: NFDProfile) => void;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function NFDSearch({
|
|
26
|
+
data,
|
|
27
|
+
onSelect,
|
|
28
|
+
placeholder = 'Search NFD profiles...',
|
|
29
|
+
className,
|
|
30
|
+
}: NFDSearchProps) {
|
|
31
|
+
const [selected, setSelected] = useState<NFDProfile | null>(null);
|
|
32
|
+
|
|
33
|
+
const handleSelect = useCallback(
|
|
34
|
+
(profile: NFDProfile) => {
|
|
35
|
+
setSelected(profile);
|
|
36
|
+
onSelect?.(profile);
|
|
37
|
+
},
|
|
38
|
+
[onSelect],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const renderItem = useCallback(
|
|
42
|
+
(profile: NFDProfile) => (
|
|
43
|
+
<View className="flex-row items-center gap-3">
|
|
44
|
+
<View className="w-8 h-8 rounded-full bg-zinc-800 overflow-hidden items-center justify-center">
|
|
45
|
+
{profile.avatar ? (
|
|
46
|
+
<Image source={{ uri: profile.avatar }} style={{ width: 32, height: 32 }} contentFit="cover" />
|
|
47
|
+
) : (
|
|
48
|
+
<Text className="text-xs text-zinc-400">NFD</Text>
|
|
49
|
+
)}
|
|
50
|
+
</View>
|
|
51
|
+
<View className="flex-1">
|
|
52
|
+
<View className="flex-row items-center gap-1">
|
|
53
|
+
<Text className="text-sm font-medium text-white" numberOfLines={1}>
|
|
54
|
+
{profile.name}
|
|
55
|
+
</Text>
|
|
56
|
+
{profile.verified && <VerifiedIcon />}
|
|
57
|
+
</View>
|
|
58
|
+
<Text className="text-xs text-zinc-400">{formatAddress(profile.address)}</Text>
|
|
59
|
+
</View>
|
|
60
|
+
</View>
|
|
61
|
+
),
|
|
62
|
+
[],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<View className={className}>
|
|
67
|
+
<SearchSheet
|
|
68
|
+
data={data}
|
|
69
|
+
placeholder={placeholder}
|
|
70
|
+
onSelect={handleSelect}
|
|
71
|
+
renderItem={renderItem}
|
|
72
|
+
/>
|
|
73
|
+
{selected && (
|
|
74
|
+
<View className="mt-3 p-3 rounded-lg bg-zinc-800 border border-zinc-700">
|
|
75
|
+
<Text className="text-xs font-medium text-zinc-400 mb-1">Selected NFD</Text>
|
|
76
|
+
{renderItem(selected)}
|
|
77
|
+
</View>
|
|
78
|
+
)}
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
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 { NFTListing as NFTListingType, ComponentSize } from '../types/algorand';
|
|
7
|
+
import { formatCurrency, formatRelativeTime } from '../utils/format';
|
|
8
|
+
import { SizeContainer } from '../ui/SizeContainer';
|
|
9
|
+
import { StatusBadge } from '../ui/StatusBadge';
|
|
10
|
+
|
|
11
|
+
function ShieldIcon({ size = 12, color = '#93c5fd' }: { size?: number; color?: string }) {
|
|
12
|
+
return (
|
|
13
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
14
|
+
<Path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
|
|
15
|
+
</Svg>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function LockIcon({ size = 12, color = '#fdba74' }: { size?: number; color?: string }) {
|
|
20
|
+
return (
|
|
21
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
22
|
+
<Path d="M16 10V7a4 4 0 0 0-8 0v3" />
|
|
23
|
+
<Path d="M5 10h14a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V11a1 1 0 0 1 1-1Z" />
|
|
24
|
+
</Svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ShoppingCartIcon({ size = 16, color = '#fff' }: { size?: number; color?: string }) {
|
|
29
|
+
return (
|
|
30
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
31
|
+
<Path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" />
|
|
32
|
+
<Path d="M3 6h18" />
|
|
33
|
+
<Path d="M16 10a4 4 0 0 1-8 0" />
|
|
34
|
+
</Svg>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function CalendarIcon({ size = 12, color = '#71717a' }: { size?: number; color?: string }) {
|
|
39
|
+
return (
|
|
40
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
41
|
+
<Path d="M8 2v4" />
|
|
42
|
+
<Path d="M16 2v4" />
|
|
43
|
+
<Path d="M3 10h18" />
|
|
44
|
+
<Path d="M21 8V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z" />
|
|
45
|
+
</Svg>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface NFTListingProps {
|
|
50
|
+
data: NFTListingType;
|
|
51
|
+
showPurchaseButton?: boolean;
|
|
52
|
+
size?: ComponentSize;
|
|
53
|
+
className?: string;
|
|
54
|
+
imageUrl?: string;
|
|
55
|
+
onPurchase?: (listing: NFTListingType) => void;
|
|
56
|
+
onFavorite?: (listing: NFTListingType) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function NFTListingComponent({
|
|
60
|
+
data: listing,
|
|
61
|
+
showPurchaseButton = true,
|
|
62
|
+
size = 'full',
|
|
63
|
+
className,
|
|
64
|
+
imageUrl,
|
|
65
|
+
onPurchase,
|
|
66
|
+
onFavorite,
|
|
67
|
+
}: NFTListingProps) {
|
|
68
|
+
const isReserved = !!listing.reservedFor;
|
|
69
|
+
const hasGating = listing.gating !== undefined;
|
|
70
|
+
const isExpired = listing.expiresAt ? new Date() > listing.expiresAt : false;
|
|
71
|
+
const isFullscreen = size === 'fullscreen';
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<SizeContainer size={size} className={className}>
|
|
75
|
+
<Animated.View
|
|
76
|
+
entering={FadeInDown.duration(400).springify()}
|
|
77
|
+
className={`rounded-2xl overflow-hidden ${isFullscreen ? 'border border-zinc-800' : 'bg-zinc-900/80'}`}
|
|
78
|
+
>
|
|
79
|
+
<View className="p-5">
|
|
80
|
+
{/* NFT Image */}
|
|
81
|
+
{imageUrl && (
|
|
82
|
+
<View className="mb-4 rounded-2xl overflow-hidden aspect-square">
|
|
83
|
+
<Image source={{ uri: imageUrl }} style={{ width: '100%', height: '100%' }} contentFit="cover" />
|
|
84
|
+
|
|
85
|
+
{/* Badges overlay */}
|
|
86
|
+
<View className="absolute top-2 left-2 flex-row flex-wrap gap-1">
|
|
87
|
+
{listing.authenticityBadge && (
|
|
88
|
+
<StatusBadge label="Verified" variant="info" icon={<ShieldIcon />} />
|
|
89
|
+
)}
|
|
90
|
+
{isReserved && (
|
|
91
|
+
<StatusBadge label="Reserved" variant="warning" icon={<LockIcon />} />
|
|
92
|
+
)}
|
|
93
|
+
{hasGating && <StatusBadge label="Gated" variant="primary" />}
|
|
94
|
+
{isExpired && <StatusBadge label="Expired" variant="error" />}
|
|
95
|
+
</View>
|
|
96
|
+
</View>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{/* NFT Info */}
|
|
100
|
+
<View className="mb-4">
|
|
101
|
+
<Text className="text-lg font-bold text-white mb-1">{listing.nft.name}</Text>
|
|
102
|
+
<View className="flex-row items-center gap-2">
|
|
103
|
+
<Text className="text-xs font-mono text-zinc-400">#{listing.nft.id}</Text>
|
|
104
|
+
{listing.collection && (
|
|
105
|
+
<View className="px-2 py-0.5 rounded-full bg-zinc-800">
|
|
106
|
+
<Text className="text-xs text-zinc-300">{listing.collection}</Text>
|
|
107
|
+
</View>
|
|
108
|
+
)}
|
|
109
|
+
</View>
|
|
110
|
+
</View>
|
|
111
|
+
|
|
112
|
+
{/* Price & Stats */}
|
|
113
|
+
<View className="flex-row gap-6 mb-3">
|
|
114
|
+
<View>
|
|
115
|
+
<Text className="text-2xl font-bold text-white">
|
|
116
|
+
{formatCurrency(listing.price, listing.currency)}
|
|
117
|
+
</Text>
|
|
118
|
+
<Text className="text-xs text-zinc-400">Price</Text>
|
|
119
|
+
</View>
|
|
120
|
+
<View>
|
|
121
|
+
<Text className="text-base font-semibold text-white">
|
|
122
|
+
{listing.views.toLocaleString()}
|
|
123
|
+
</Text>
|
|
124
|
+
<Text className="text-xs text-zinc-400">Views</Text>
|
|
125
|
+
</View>
|
|
126
|
+
<View>
|
|
127
|
+
<Text className="text-base font-semibold text-white">
|
|
128
|
+
{listing.favorites.toLocaleString()}
|
|
129
|
+
</Text>
|
|
130
|
+
<Text className="text-xs text-zinc-400">Favorites</Text>
|
|
131
|
+
</View>
|
|
132
|
+
</View>
|
|
133
|
+
|
|
134
|
+
{/* Gating Info */}
|
|
135
|
+
{hasGating && listing.gating && (
|
|
136
|
+
<View className="mb-3 bg-purple-500/5 rounded-xl px-3 py-2.5">
|
|
137
|
+
<Text className="text-xs font-medium text-purple-300 mb-1">Gated Access</Text>
|
|
138
|
+
<Text className="text-xs text-purple-200">
|
|
139
|
+
{listing.gating.type === 'token' &&
|
|
140
|
+
`Requires ${listing.gating.requirement} ${listing.gating.asset} tokens`}
|
|
141
|
+
{listing.gating.type === 'nft' &&
|
|
142
|
+
`Requires NFT from ${listing.gating.collection}`}
|
|
143
|
+
</Text>
|
|
144
|
+
</View>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* Purchase Button */}
|
|
148
|
+
{showPurchaseButton && !isExpired && (
|
|
149
|
+
<Pressable
|
|
150
|
+
onPress={() => !isReserved && onPurchase?.(listing)}
|
|
151
|
+
disabled={isReserved}
|
|
152
|
+
className={`flex-row items-center justify-center gap-2 py-3 px-4 rounded-xl ${
|
|
153
|
+
isReserved
|
|
154
|
+
? 'bg-zinc-800/50'
|
|
155
|
+
: 'bg-purple-600'
|
|
156
|
+
}`}
|
|
157
|
+
>
|
|
158
|
+
{isReserved ? (
|
|
159
|
+
<>
|
|
160
|
+
<LockIcon size={16} color="#a1a1aa" />
|
|
161
|
+
<Text className="text-sm font-medium text-zinc-400">Reserved</Text>
|
|
162
|
+
</>
|
|
163
|
+
) : (
|
|
164
|
+
<>
|
|
165
|
+
<ShoppingCartIcon />
|
|
166
|
+
<Text className="text-sm font-medium text-white">Purchase NFT</Text>
|
|
167
|
+
</>
|
|
168
|
+
)}
|
|
169
|
+
</Pressable>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{/* Listed Date */}
|
|
173
|
+
<View className="flex-row items-center gap-1.5 mt-3">
|
|
174
|
+
<CalendarIcon />
|
|
175
|
+
<Text className="text-xs text-zinc-600">Listed {formatRelativeTime(listing.listedAt)}</Text>
|
|
176
|
+
</View>
|
|
177
|
+
</View>
|
|
178
|
+
</Animated.View>
|
|
179
|
+
</SizeContainer>
|
|
180
|
+
);
|
|
181
|
+
}
|