@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,68 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { View, Text } from 'react-native';
|
|
3
|
+
import type { NFTListing } from '../types/algorand';
|
|
4
|
+
import { formatCurrency } from '../utils/format';
|
|
5
|
+
import { SearchSheet } from '../ui/SearchSheet';
|
|
6
|
+
|
|
7
|
+
interface NFTSearchProps {
|
|
8
|
+
data: NFTListing[];
|
|
9
|
+
onSelect?: (listing: NFTListing) => void;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function NFTSearch({
|
|
15
|
+
data,
|
|
16
|
+
onSelect,
|
|
17
|
+
placeholder = 'Search NFTs...',
|
|
18
|
+
className,
|
|
19
|
+
}: NFTSearchProps) {
|
|
20
|
+
const [selected, setSelected] = useState<NFTListing | null>(null);
|
|
21
|
+
|
|
22
|
+
const handleSelect = useCallback(
|
|
23
|
+
(listing: NFTListing) => {
|
|
24
|
+
setSelected(listing);
|
|
25
|
+
onSelect?.(listing);
|
|
26
|
+
},
|
|
27
|
+
[onSelect],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const renderItem = useCallback(
|
|
31
|
+
(listing: NFTListing) => (
|
|
32
|
+
<View className="flex-row items-center gap-3">
|
|
33
|
+
<View className="w-8 h-8 rounded-lg bg-zinc-800 items-center justify-center">
|
|
34
|
+
<Text className="text-xs text-zinc-400">NFT</Text>
|
|
35
|
+
</View>
|
|
36
|
+
<View className="flex-1">
|
|
37
|
+
<Text className="text-sm font-medium text-white" numberOfLines={1}>
|
|
38
|
+
{listing.nft.name}
|
|
39
|
+
</Text>
|
|
40
|
+
<Text className="text-xs text-zinc-400">
|
|
41
|
+
{listing.collection || `#${listing.nft.id}`}
|
|
42
|
+
</Text>
|
|
43
|
+
</View>
|
|
44
|
+
<Text className="text-xs font-medium text-white">
|
|
45
|
+
{formatCurrency(listing.price, listing.currency)}
|
|
46
|
+
</Text>
|
|
47
|
+
</View>
|
|
48
|
+
),
|
|
49
|
+
[],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<View className={className}>
|
|
54
|
+
<SearchSheet
|
|
55
|
+
data={data}
|
|
56
|
+
placeholder={placeholder}
|
|
57
|
+
onSelect={handleSelect}
|
|
58
|
+
renderItem={renderItem}
|
|
59
|
+
/>
|
|
60
|
+
{selected && (
|
|
61
|
+
<View className="mt-3 p-3 rounded-lg bg-zinc-800 border border-zinc-700">
|
|
62
|
+
<Text className="text-xs font-medium text-zinc-400 mb-1">Selected NFT</Text>
|
|
63
|
+
{renderItem(selected)}
|
|
64
|
+
</View>
|
|
65
|
+
)}
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, Pressable } from 'react-native';
|
|
3
|
+
import Animated, { FadeInDown } from 'react-native-reanimated';
|
|
4
|
+
import Svg, { Path } from 'react-native-svg';
|
|
5
|
+
import type { Poll as PollType, ComponentSize } from '../types/algorand';
|
|
6
|
+
import { formatRelativeTime } from '../utils/format';
|
|
7
|
+
import { SizeContainer } from '../ui/SizeContainer';
|
|
8
|
+
import { StatusBadge } from '../ui/StatusBadge';
|
|
9
|
+
import { ProgressBar } from '../ui/ProgressBar';
|
|
10
|
+
|
|
11
|
+
function VoteIcon({ 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="m9 12 2 2 4-4" />
|
|
15
|
+
<Path d="M5 7c0-1.1.9-2 2-2h10a2 2 0 0 1 2 2v12H5V7Z" />
|
|
16
|
+
<Path d="M22 19H2" />
|
|
17
|
+
</Svg>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function UsersIcon({ size = 12, color = '#a1a1aa' }: { 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="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
|
25
|
+
<Path d="M9 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z" />
|
|
26
|
+
<Path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
|
27
|
+
<Path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
28
|
+
</Svg>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function TrendingUpIcon({ size = 12, color = '#34d399' }: { size?: number; color?: string }) {
|
|
33
|
+
return (
|
|
34
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
35
|
+
<Path d="m22 7-8.5 8.5-5-5L2 17" />
|
|
36
|
+
<Path d="M16 7h6v6" />
|
|
37
|
+
</Svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ClockIcon({ size = 12, color = '#71717a' }: { size?: number; color?: string }) {
|
|
42
|
+
return (
|
|
43
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
44
|
+
<Path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Z" />
|
|
45
|
+
<Path d="M12 6v6l4 2" />
|
|
46
|
+
</Svg>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function CalendarIcon({ size = 12, color = '#71717a' }: { size?: number; color?: string }) {
|
|
51
|
+
return (
|
|
52
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
53
|
+
<Path d="M8 2v4" />
|
|
54
|
+
<Path d="M16 2v4" />
|
|
55
|
+
<Path d="M3 10h18" />
|
|
56
|
+
<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" />
|
|
57
|
+
</Svg>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface PollProps {
|
|
62
|
+
data: PollType;
|
|
63
|
+
showVoteButton?: boolean;
|
|
64
|
+
compact?: boolean;
|
|
65
|
+
size?: ComponentSize;
|
|
66
|
+
className?: string;
|
|
67
|
+
onVote?: (pollId: string, optionId: string) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function PollComponent({
|
|
71
|
+
data: poll,
|
|
72
|
+
showVoteButton = true,
|
|
73
|
+
compact = false,
|
|
74
|
+
size = 'full',
|
|
75
|
+
className,
|
|
76
|
+
onVote,
|
|
77
|
+
}: PollProps) {
|
|
78
|
+
const isExpired = poll.expiresAt ? new Date() > poll.expiresAt : false;
|
|
79
|
+
const canVote = poll.status === 'active' && !isExpired;
|
|
80
|
+
const hasGating = poll.gating !== undefined;
|
|
81
|
+
const totalPower = poll.options.reduce((sum, opt) => sum + opt.votingPower, 0);
|
|
82
|
+
|
|
83
|
+
const isFullscreen = size === 'fullscreen';
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<SizeContainer size={size} className={className}>
|
|
87
|
+
<Animated.View
|
|
88
|
+
entering={FadeInDown.duration(400).springify()}
|
|
89
|
+
className={`rounded-2xl ${isFullscreen ? 'border border-zinc-800' : 'bg-zinc-900/80'}`}
|
|
90
|
+
>
|
|
91
|
+
<View className={compact ? 'p-2' : 'p-5'}>
|
|
92
|
+
{/* Header */}
|
|
93
|
+
<View className={compact ? 'mb-1' : 'mb-3'}>
|
|
94
|
+
<View className="flex-row items-center gap-2 flex-wrap mb-1">
|
|
95
|
+
<VoteIcon size={16} color={poll.status === 'active' ? '#34d399' : '#71717a'} />
|
|
96
|
+
<Text className="text-sm font-medium text-zinc-400">Poll</Text>
|
|
97
|
+
<StatusBadge
|
|
98
|
+
label={poll.status.charAt(0).toUpperCase() + poll.status.slice(1)}
|
|
99
|
+
variant={poll.status === 'active' ? 'success' : 'neutral'}
|
|
100
|
+
/>
|
|
101
|
+
{hasGating && <StatusBadge label="Gated" variant="primary" />}
|
|
102
|
+
{isExpired && <StatusBadge label="Expired" variant="error" />}
|
|
103
|
+
</View>
|
|
104
|
+
|
|
105
|
+
<Text className={`font-semibold text-white ${compact ? 'text-xs mb-0.5' : 'text-base mb-2'}`}>
|
|
106
|
+
{poll.question}
|
|
107
|
+
</Text>
|
|
108
|
+
|
|
109
|
+
<View className="flex-row items-center justify-between">
|
|
110
|
+
<View className="flex-row items-center gap-3">
|
|
111
|
+
<View className="flex-row items-center gap-1">
|
|
112
|
+
<UsersIcon size={12} color="#71717a" />
|
|
113
|
+
<Text className="text-xs text-zinc-400">{poll.totalVotes} votes</Text>
|
|
114
|
+
</View>
|
|
115
|
+
{!compact && (
|
|
116
|
+
<View className="flex-row items-center gap-1">
|
|
117
|
+
<TrendingUpIcon size={12} color="#71717a" />
|
|
118
|
+
<Text className="text-xs text-zinc-400">{totalPower.toLocaleString()} power</Text>
|
|
119
|
+
</View>
|
|
120
|
+
)}
|
|
121
|
+
</View>
|
|
122
|
+
{poll.expiresAt && (
|
|
123
|
+
<View className="flex-row items-center gap-1">
|
|
124
|
+
<ClockIcon size={12} color="#71717a" />
|
|
125
|
+
<Text className="text-xs text-zinc-400">
|
|
126
|
+
{canVote ? 'Ends' : 'Ended'} {formatRelativeTime(poll.expiresAt)}
|
|
127
|
+
</Text>
|
|
128
|
+
</View>
|
|
129
|
+
)}
|
|
130
|
+
</View>
|
|
131
|
+
</View>
|
|
132
|
+
|
|
133
|
+
{/* Gating Info */}
|
|
134
|
+
{hasGating && poll.gating && !compact && (
|
|
135
|
+
<View className="mb-3 bg-purple-500/5 rounded-xl px-3 py-2.5">
|
|
136
|
+
<Text className="text-xs font-medium text-purple-300 mb-1">Voting Requirements</Text>
|
|
137
|
+
<Text className="text-xs text-purple-200">
|
|
138
|
+
{poll.gating.type === 'asset-holding' && 'Requires holding specified assets to participate'}
|
|
139
|
+
{poll.gating.type === 'nfd-verified' && 'Requires verified NFD to participate'}
|
|
140
|
+
</Text>
|
|
141
|
+
</View>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{/* Options */}
|
|
145
|
+
<View className={compact ? 'gap-1' : 'gap-2'}>
|
|
146
|
+
{poll.options.map((option, index) => {
|
|
147
|
+
const percentage = poll.totalVotes > 0 ? (option.votes / poll.totalVotes) * 100 : 0;
|
|
148
|
+
const powerPercentage =
|
|
149
|
+
totalPower > 0 ? (option.votingPower / totalPower) * 100 : 0;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Pressable
|
|
153
|
+
key={option.id}
|
|
154
|
+
onPress={() => canVote && showVoteButton && onVote?.(poll.id, option.id)}
|
|
155
|
+
disabled={!canVote || !showVoteButton}
|
|
156
|
+
className={`rounded-lg border overflow-hidden ${
|
|
157
|
+
canVote && showVoteButton
|
|
158
|
+
? 'border-zinc-800 active:border-purple-500'
|
|
159
|
+
: 'border-zinc-800'
|
|
160
|
+
}`}
|
|
161
|
+
>
|
|
162
|
+
{/* Background fill */}
|
|
163
|
+
<View className="absolute inset-0 bg-purple-500/10" style={{ width: `${percentage}%` }} />
|
|
164
|
+
|
|
165
|
+
<View className={compact ? 'p-2' : 'p-3'}>
|
|
166
|
+
<View className="flex-row items-center justify-between mb-1">
|
|
167
|
+
<View className="flex-row items-center gap-2 flex-1 mr-2">
|
|
168
|
+
<Text
|
|
169
|
+
className={`font-medium text-white ${compact ? 'text-xs' : 'text-sm'}`}
|
|
170
|
+
numberOfLines={1}
|
|
171
|
+
>
|
|
172
|
+
{option.text}
|
|
173
|
+
</Text>
|
|
174
|
+
{index === 0 && poll.status === 'active' && (
|
|
175
|
+
<View className="flex-row items-center gap-0.5 px-1.5 py-0.5 rounded-full bg-green-500/20">
|
|
176
|
+
<TrendingUpIcon size={10} color="#34d399" />
|
|
177
|
+
<Text className="text-xs font-medium text-green-400">Leading</Text>
|
|
178
|
+
</View>
|
|
179
|
+
)}
|
|
180
|
+
</View>
|
|
181
|
+
<View className="items-end">
|
|
182
|
+
<Text className={`font-bold text-white ${compact ? 'text-xs' : 'text-sm'}`}>
|
|
183
|
+
{percentage.toFixed(1)}%
|
|
184
|
+
</Text>
|
|
185
|
+
{!compact && (
|
|
186
|
+
<Text className="text-xs text-zinc-400">{option.votes} votes</Text>
|
|
187
|
+
)}
|
|
188
|
+
</View>
|
|
189
|
+
</View>
|
|
190
|
+
|
|
191
|
+
{!compact && (
|
|
192
|
+
<View>
|
|
193
|
+
<ProgressBar
|
|
194
|
+
percentage={percentage}
|
|
195
|
+
heightClass="h-1.5"
|
|
196
|
+
trackClass="bg-zinc-800"
|
|
197
|
+
fillClass="bg-purple-500"
|
|
198
|
+
/>
|
|
199
|
+
<Text className="text-xs text-zinc-600 mt-1">
|
|
200
|
+
Voting Power: {option.votingPower.toLocaleString()} ({powerPercentage.toFixed(1)}%)
|
|
201
|
+
</Text>
|
|
202
|
+
</View>
|
|
203
|
+
)}
|
|
204
|
+
</View>
|
|
205
|
+
</Pressable>
|
|
206
|
+
);
|
|
207
|
+
})}
|
|
208
|
+
</View>
|
|
209
|
+
|
|
210
|
+
{/* Vote CTA */}
|
|
211
|
+
{showVoteButton && canVote && !compact && (
|
|
212
|
+
<View className="mt-3 bg-purple-500/5 rounded-xl px-3 py-2.5 items-center">
|
|
213
|
+
<View className="flex-row items-center gap-2">
|
|
214
|
+
<VoteIcon size={16} color="#c084fc" />
|
|
215
|
+
<Text className="text-sm font-medium text-purple-300">Tap any option above to vote</Text>
|
|
216
|
+
</View>
|
|
217
|
+
</View>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Footer */}
|
|
221
|
+
{!compact && (
|
|
222
|
+
<View className="flex-row items-center justify-between mt-3 pt-3 border-t border-zinc-800">
|
|
223
|
+
<View className="flex-row items-center gap-1.5">
|
|
224
|
+
<CalendarIcon />
|
|
225
|
+
<Text className="text-xs text-zinc-600">Created {formatRelativeTime(poll.createdAt)}</Text>
|
|
226
|
+
</View>
|
|
227
|
+
<Text className="text-xs text-zinc-600">by {poll.creator.slice(0, 8)}...</Text>
|
|
228
|
+
</View>
|
|
229
|
+
)}
|
|
230
|
+
</View>
|
|
231
|
+
</Animated.View>
|
|
232
|
+
</SizeContainer>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { View, Text } from 'react-native';
|
|
3
|
+
import Svg, { Path } from 'react-native-svg';
|
|
4
|
+
import type { Poll } from '../types/algorand';
|
|
5
|
+
import { formatRelativeTime } from '../utils/format';
|
|
6
|
+
import { SearchSheet } from '../ui/SearchSheet';
|
|
7
|
+
|
|
8
|
+
function VoteIcon({ size = 16, color = '#a1a1aa' }: { size?: number; color?: string }) {
|
|
9
|
+
return (
|
|
10
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
11
|
+
<Path d="m9 12 2 2 4-4" />
|
|
12
|
+
<Path d="M5 7c0-1.1.9-2 2-2h10a2 2 0 0 1 2 2v12H5V7Z" />
|
|
13
|
+
<Path d="M22 19H2" />
|
|
14
|
+
</Svg>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PollSearchProps {
|
|
19
|
+
data: Poll[];
|
|
20
|
+
onSelect?: (poll: Poll) => void;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function PollSearch({
|
|
26
|
+
data,
|
|
27
|
+
onSelect,
|
|
28
|
+
placeholder = 'Search polls...',
|
|
29
|
+
className,
|
|
30
|
+
}: PollSearchProps) {
|
|
31
|
+
const [selected, setSelected] = useState<Poll | null>(null);
|
|
32
|
+
|
|
33
|
+
const handleSelect = useCallback(
|
|
34
|
+
(poll: Poll) => {
|
|
35
|
+
setSelected(poll);
|
|
36
|
+
onSelect?.(poll);
|
|
37
|
+
},
|
|
38
|
+
[onSelect],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const renderItem = useCallback(
|
|
42
|
+
(poll: Poll) => (
|
|
43
|
+
<View className="flex-row items-center gap-3">
|
|
44
|
+
<VoteIcon size={16} color={poll.status === 'active' ? '#34d399' : '#71717a'} />
|
|
45
|
+
<View className="flex-1">
|
|
46
|
+
<Text className="text-sm font-medium text-white" numberOfLines={1}>
|
|
47
|
+
{poll.question}
|
|
48
|
+
</Text>
|
|
49
|
+
<Text className="text-xs text-zinc-400">
|
|
50
|
+
{poll.totalVotes} votes - {poll.options.length} options - {formatRelativeTime(poll.createdAt)}
|
|
51
|
+
</Text>
|
|
52
|
+
</View>
|
|
53
|
+
<View className={`px-2 py-0.5 rounded-full ${poll.status === 'active' ? 'bg-green-500/20' : 'bg-zinc-700'}`}>
|
|
54
|
+
<Text className={`text-xs font-medium ${poll.status === 'active' ? 'text-green-400' : 'text-zinc-400'}`}>
|
|
55
|
+
{poll.status.charAt(0).toUpperCase() + poll.status.slice(1)}
|
|
56
|
+
</Text>
|
|
57
|
+
</View>
|
|
58
|
+
</View>
|
|
59
|
+
),
|
|
60
|
+
[],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<View className={className}>
|
|
65
|
+
<SearchSheet
|
|
66
|
+
data={data}
|
|
67
|
+
placeholder={placeholder}
|
|
68
|
+
onSelect={handleSelect}
|
|
69
|
+
renderItem={renderItem}
|
|
70
|
+
/>
|
|
71
|
+
{selected && (
|
|
72
|
+
<View className="mt-3 p-3 rounded-lg bg-zinc-800 border border-zinc-700">
|
|
73
|
+
<Text className="text-xs font-medium text-zinc-400 mb-1">Selected Poll</Text>
|
|
74
|
+
{renderItem(selected)}
|
|
75
|
+
</View>
|
|
76
|
+
)}
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
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 { RaffleListing as RaffleListingType, ComponentSize } from '../types/algorand';
|
|
7
|
+
import { formatCurrency, formatRelativeTime } from '../utils/format';
|
|
8
|
+
import { SizeContainer } from '../ui/SizeContainer';
|
|
9
|
+
import { StatusBadge } from '../ui/StatusBadge';
|
|
10
|
+
import { ProgressBar } from '../ui/ProgressBar';
|
|
11
|
+
|
|
12
|
+
function TicketIcon({ size = 16, 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="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z" />
|
|
16
|
+
<Path d="M13 5v2" />
|
|
17
|
+
<Path d="M13 17v2" />
|
|
18
|
+
<Path d="M13 11v2" />
|
|
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 CalendarIcon({ size = 16, color = '#71717a' }: { 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="M8 2v4" />
|
|
40
|
+
<Path d="M16 2v4" />
|
|
41
|
+
<Path d="M3 10h18" />
|
|
42
|
+
<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" />
|
|
43
|
+
</Svg>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function TimerIcon({ size = 16, color = '#a1a1aa' }: { size?: number; color?: string }) {
|
|
48
|
+
return (
|
|
49
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
50
|
+
<Path d="M10 2h4" />
|
|
51
|
+
<Path d="M12 14V6" />
|
|
52
|
+
<Path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Z" />
|
|
53
|
+
</Svg>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface RaffleListingProps {
|
|
58
|
+
data: RaffleListingType;
|
|
59
|
+
showEntryButton?: boolean;
|
|
60
|
+
size?: ComponentSize;
|
|
61
|
+
className?: string;
|
|
62
|
+
imageUrl?: string;
|
|
63
|
+
onEnter?: (raffle: RaffleListingType) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function RaffleListingComponent({
|
|
67
|
+
data: raffle,
|
|
68
|
+
showEntryButton = true,
|
|
69
|
+
size = 'full',
|
|
70
|
+
className,
|
|
71
|
+
imageUrl,
|
|
72
|
+
onEnter,
|
|
73
|
+
}: RaffleListingProps) {
|
|
74
|
+
const isActive = raffle.status === 'active';
|
|
75
|
+
const isUpcoming = raffle.status === 'upcoming';
|
|
76
|
+
const ticketsRemaining = raffle.ticketCount - raffle.entryCount;
|
|
77
|
+
const progressPercentage = (raffle.entryCount / raffle.ticketCount) * 100;
|
|
78
|
+
|
|
79
|
+
const statusVariant = isActive ? 'success' : isUpcoming ? 'info' : 'neutral';
|
|
80
|
+
|
|
81
|
+
const isFullscreen = size === 'fullscreen';
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<SizeContainer size={size} className={className}>
|
|
85
|
+
<Animated.View
|
|
86
|
+
entering={FadeInDown.duration(400).springify()}
|
|
87
|
+
className={`rounded-2xl ${isFullscreen ? 'border border-zinc-800' : 'bg-zinc-900/80'}`}
|
|
88
|
+
>
|
|
89
|
+
<View className="p-5">
|
|
90
|
+
{/* Header */}
|
|
91
|
+
<View className="flex-row items-start gap-3 mb-4">
|
|
92
|
+
{imageUrl && (
|
|
93
|
+
<View className="w-14 h-14 rounded-lg overflow-hidden bg-zinc-800/50">
|
|
94
|
+
<Image source={{ uri: imageUrl }} style={{ width: 56, height: 56 }} contentFit="cover" />
|
|
95
|
+
</View>
|
|
96
|
+
)}
|
|
97
|
+
<View className="flex-1">
|
|
98
|
+
<View className="flex-row items-center gap-2 flex-wrap mb-1">
|
|
99
|
+
<TicketIcon size={16} color={isActive ? '#34d399' : '#71717a'} />
|
|
100
|
+
<Text className="text-sm font-medium text-zinc-400">Raffle Event</Text>
|
|
101
|
+
<StatusBadge
|
|
102
|
+
label={raffle.status.charAt(0).toUpperCase() + raffle.status.slice(1)}
|
|
103
|
+
variant={statusVariant}
|
|
104
|
+
/>
|
|
105
|
+
<View className="px-2 py-0.5 rounded-full bg-zinc-700">
|
|
106
|
+
<Text className="text-xs font-medium text-white">
|
|
107
|
+
{raffle.entryCount}/{raffle.ticketCount} entries
|
|
108
|
+
</Text>
|
|
109
|
+
</View>
|
|
110
|
+
</View>
|
|
111
|
+
<Text className="text-base font-bold text-white mb-1">{raffle.title}</Text>
|
|
112
|
+
<Text className="text-sm text-zinc-300 leading-5">{raffle.description}</Text>
|
|
113
|
+
</View>
|
|
114
|
+
</View>
|
|
115
|
+
|
|
116
|
+
{/* Prize Pool */}
|
|
117
|
+
<View className="mb-4 bg-yellow-500/5 rounded-xl px-3 py-2.5">
|
|
118
|
+
<View className="flex-row items-center gap-2 mb-2">
|
|
119
|
+
<TrophyIcon size={16} color="#eab308" />
|
|
120
|
+
<Text className="text-sm font-medium text-yellow-300">Prize Pool</Text>
|
|
121
|
+
</View>
|
|
122
|
+
{raffle.prizes.map((prize, index) => (
|
|
123
|
+
<View key={index} className="flex-row items-center justify-between mb-1">
|
|
124
|
+
<Text className="text-sm font-medium text-white">{prize.name}</Text>
|
|
125
|
+
<Text className="text-xs font-mono text-yellow-300">#{prize.id}</Text>
|
|
126
|
+
</View>
|
|
127
|
+
))}
|
|
128
|
+
</View>
|
|
129
|
+
|
|
130
|
+
{/* Entry Stats */}
|
|
131
|
+
<View className="flex-row gap-6 mb-4">
|
|
132
|
+
<View>
|
|
133
|
+
<Text className="text-2xl font-bold text-white">
|
|
134
|
+
{formatCurrency(raffle.pricePerEntry, raffle.entryAsset.unitName)}
|
|
135
|
+
</Text>
|
|
136
|
+
<Text className="text-xs text-zinc-400">Per Entry</Text>
|
|
137
|
+
</View>
|
|
138
|
+
<View>
|
|
139
|
+
<Text className="text-base font-semibold text-white">
|
|
140
|
+
{raffle.entryCount}/{raffle.ticketCount}
|
|
141
|
+
</Text>
|
|
142
|
+
<Text className="text-xs text-zinc-400">Entries</Text>
|
|
143
|
+
</View>
|
|
144
|
+
<View>
|
|
145
|
+
<Text className="text-base font-semibold text-white">{ticketsRemaining}</Text>
|
|
146
|
+
<Text className="text-xs text-zinc-400">Remaining</Text>
|
|
147
|
+
</View>
|
|
148
|
+
</View>
|
|
149
|
+
|
|
150
|
+
{/* Progress */}
|
|
151
|
+
<View className="mb-4">
|
|
152
|
+
<ProgressBar
|
|
153
|
+
percentage={progressPercentage}
|
|
154
|
+
trackClass="bg-zinc-800"
|
|
155
|
+
fillClass="bg-purple-500"
|
|
156
|
+
/>
|
|
157
|
+
<View className="flex-row justify-between mt-1">
|
|
158
|
+
<Text className="text-xs text-zinc-400">{progressPercentage.toFixed(1)}% sold</Text>
|
|
159
|
+
<Text className="text-xs text-zinc-400">{ticketsRemaining} left</Text>
|
|
160
|
+
</View>
|
|
161
|
+
</View>
|
|
162
|
+
|
|
163
|
+
{/* Timing */}
|
|
164
|
+
<View className="flex-row items-center gap-2 mb-4">
|
|
165
|
+
<CalendarIcon size={16} />
|
|
166
|
+
<Text className="text-sm text-zinc-300">
|
|
167
|
+
{isUpcoming ? 'Starts' : isActive ? 'Ends' : 'Ended'}{' '}
|
|
168
|
+
{formatRelativeTime(isUpcoming ? raffle.startTime : raffle.endTime)}
|
|
169
|
+
</Text>
|
|
170
|
+
</View>
|
|
171
|
+
|
|
172
|
+
{/* Entry Button */}
|
|
173
|
+
{showEntryButton && (isActive || isUpcoming) && (
|
|
174
|
+
<Pressable
|
|
175
|
+
onPress={() => isActive && ticketsRemaining > 0 && onEnter?.(raffle)}
|
|
176
|
+
disabled={!isActive || ticketsRemaining === 0}
|
|
177
|
+
className={`flex-row items-center justify-center gap-2 py-3 px-4 rounded-xl ${
|
|
178
|
+
!isActive || ticketsRemaining === 0
|
|
179
|
+
? 'bg-zinc-800/50'
|
|
180
|
+
: 'bg-purple-600'
|
|
181
|
+
}`}
|
|
182
|
+
>
|
|
183
|
+
{!isActive ? (
|
|
184
|
+
<>
|
|
185
|
+
<TimerIcon size={16} color="#a1a1aa" />
|
|
186
|
+
<Text className="text-sm font-medium text-zinc-400">
|
|
187
|
+
{isUpcoming ? 'Not Started' : 'Raffle Ended'}
|
|
188
|
+
</Text>
|
|
189
|
+
</>
|
|
190
|
+
) : ticketsRemaining === 0 ? (
|
|
191
|
+
<Text className="text-sm font-medium text-zinc-400">Sold Out</Text>
|
|
192
|
+
) : (
|
|
193
|
+
<>
|
|
194
|
+
<TicketIcon size={16} color="#fff" />
|
|
195
|
+
<Text className="text-sm font-medium text-white">Enter Raffle</Text>
|
|
196
|
+
</>
|
|
197
|
+
)}
|
|
198
|
+
</Pressable>
|
|
199
|
+
)}
|
|
200
|
+
</View>
|
|
201
|
+
</Animated.View>
|
|
202
|
+
</SizeContainer>
|
|
203
|
+
);
|
|
204
|
+
}
|