@chainloyalty/react 1.0.0
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/dist/Leaderboard.d.ts +42 -0
- package/dist/Leaderboard.d.ts.map +1 -0
- package/dist/Leaderboard.js +186 -0
- package/dist/Leaderboard.js.map +1 -0
- package/dist/RewardsDashboard.d.ts +67 -0
- package/dist/RewardsDashboard.d.ts.map +1 -0
- package/dist/RewardsDashboard.js +256 -0
- package/dist/RewardsDashboard.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/useTrackEvent.d.ts +75 -0
- package/dist/useTrackEvent.d.ts.map +1 -0
- package/dist/useTrackEvent.js +218 -0
- package/dist/useTrackEvent.js.map +1 -0
- package/package.json +33 -0
- package/src/Leaderboard.tsx +528 -0
- package/src/RewardsDashboard.tsx +602 -0
- package/src/index.ts +18 -0
- package/src/useTrackEvent.ts +273 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @chainloyalty/react - RewardsDashboard Component
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* A comprehensive, production-ready component that displays user rewards information.
|
|
6
|
+
* This embeddable component shows wallet connection status, points balance, earned badges,
|
|
7
|
+
* reward history, and tier level.
|
|
8
|
+
*
|
|
9
|
+
* Key Features:
|
|
10
|
+
* - Wallet connection display with copy functionality
|
|
11
|
+
* - Large, prominent points display
|
|
12
|
+
* - Badge gallery showing earned badges
|
|
13
|
+
* - Reward history with timestamps
|
|
14
|
+
* - Tier level indicator
|
|
15
|
+
* - Fully responsive design (mobile + desktop)
|
|
16
|
+
* - Dark theme with professional UI
|
|
17
|
+
* - Loading and empty states
|
|
18
|
+
* - Customizable via props
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import React, { useState, useEffect } from 'react';
|
|
22
|
+
import {
|
|
23
|
+
Wallet,
|
|
24
|
+
Copy,
|
|
25
|
+
CheckCircle,
|
|
26
|
+
Award,
|
|
27
|
+
TrendingUp,
|
|
28
|
+
Zap,
|
|
29
|
+
ExternalLink,
|
|
30
|
+
RefreshCw,
|
|
31
|
+
Lock,
|
|
32
|
+
} from 'lucide-react';
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Types & Interfaces
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
export interface Badge {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
icon: string; // emoji or URL
|
|
42
|
+
description: string;
|
|
43
|
+
earnedAt: string;
|
|
44
|
+
rarity: 'common' | 'rare' | 'epic' | 'legendary';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RewardHistoryItem {
|
|
48
|
+
id: string;
|
|
49
|
+
type: 'purchase' | 'referral' | 'milestone' | 'subscription' | 'feature_usage';
|
|
50
|
+
title: string;
|
|
51
|
+
points: number;
|
|
52
|
+
timestamp: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TierInfo {
|
|
57
|
+
name: string;
|
|
58
|
+
level: number;
|
|
59
|
+
icon: string;
|
|
60
|
+
nextMilestone: number;
|
|
61
|
+
currentProgress: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RewardsDashboardProps {
|
|
65
|
+
walletAddress?: string | null;
|
|
66
|
+
onConnectWallet?: () => void;
|
|
67
|
+
onDisconnectWallet?: () => void;
|
|
68
|
+
customColors?: {
|
|
69
|
+
background?: string;
|
|
70
|
+
accent1?: string;
|
|
71
|
+
accent2?: string;
|
|
72
|
+
};
|
|
73
|
+
hideHistory?: boolean;
|
|
74
|
+
hideBadges?: boolean;
|
|
75
|
+
hideTier?: boolean;
|
|
76
|
+
compactMode?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface DashboardData {
|
|
80
|
+
totalPoints: number;
|
|
81
|
+
badges: Badge[];
|
|
82
|
+
rewardHistory: RewardHistoryItem[];
|
|
83
|
+
tier: TierInfo;
|
|
84
|
+
lastUpdated: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Mock Data & API Functions
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
const generateMockBadges = (): Badge[] => {
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
id: 'badge_1',
|
|
95
|
+
name: 'Early Bird',
|
|
96
|
+
icon: '🌅',
|
|
97
|
+
description: 'Joined in the first 100 users',
|
|
98
|
+
earnedAt: '2024-01-15',
|
|
99
|
+
rarity: 'legendary',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'badge_2',
|
|
103
|
+
name: 'Referral Master',
|
|
104
|
+
icon: '🎯',
|
|
105
|
+
description: 'Referred 5+ users',
|
|
106
|
+
earnedAt: '2024-02-20',
|
|
107
|
+
rarity: 'epic',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'badge_3',
|
|
111
|
+
name: 'Subscriber',
|
|
112
|
+
icon: '🔔',
|
|
113
|
+
description: 'Active subscription holder',
|
|
114
|
+
earnedAt: '2024-02-01',
|
|
115
|
+
rarity: 'rare',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'badge_4',
|
|
119
|
+
name: 'Power User',
|
|
120
|
+
icon: 'âš¡',
|
|
121
|
+
description: 'Made 10+ transactions',
|
|
122
|
+
earnedAt: '2024-03-10',
|
|
123
|
+
rarity: 'rare',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'badge_5',
|
|
127
|
+
name: 'Champion',
|
|
128
|
+
icon: '👑',
|
|
129
|
+
description: 'Top 10 leaderboard',
|
|
130
|
+
earnedAt: '2024-03-25',
|
|
131
|
+
rarity: 'legendary',
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const generateMockRewardHistory = (): RewardHistoryItem[] => {
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
id: 'evt_1',
|
|
140
|
+
type: 'purchase',
|
|
141
|
+
title: 'Premium Purchase',
|
|
142
|
+
points: 500,
|
|
143
|
+
timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
|
144
|
+
description: 'Purchased premium features',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'evt_2',
|
|
148
|
+
type: 'referral',
|
|
149
|
+
title: 'Referral Bonus',
|
|
150
|
+
points: 250,
|
|
151
|
+
timestamp: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
|
152
|
+
description: 'Friend Bob joined via your referral',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'evt_3',
|
|
156
|
+
type: 'milestone',
|
|
157
|
+
title: 'Milestone Reached',
|
|
158
|
+
points: 1000,
|
|
159
|
+
timestamp: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
160
|
+
description: '5000 total points milestone',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'evt_4',
|
|
164
|
+
type: 'subscription',
|
|
165
|
+
title: 'Daily Subscription',
|
|
166
|
+
points: 100,
|
|
167
|
+
timestamp: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
168
|
+
description: 'Active subscription reward',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'evt_5',
|
|
172
|
+
type: 'feature_usage',
|
|
173
|
+
title: 'Feature Usage',
|
|
174
|
+
points: 75,
|
|
175
|
+
timestamp: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
|
176
|
+
description: 'Used advanced analytics feature',
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const fetchDashboardData = async (walletAddress: string): Promise<DashboardData> => {
|
|
182
|
+
// Simulate API delay
|
|
183
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
184
|
+
|
|
185
|
+
// Mock data based on wallet address
|
|
186
|
+
const totalPoints = walletAddress.length * 123 + 4567; // Pseudo-random but deterministic
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
totalPoints,
|
|
190
|
+
badges: generateMockBadges(),
|
|
191
|
+
rewardHistory: generateMockRewardHistory(),
|
|
192
|
+
tier: {
|
|
193
|
+
name: 'Platinum',
|
|
194
|
+
level: 3,
|
|
195
|
+
icon: '💎',
|
|
196
|
+
nextMilestone: 8000,
|
|
197
|
+
currentProgress: totalPoints,
|
|
198
|
+
},
|
|
199
|
+
lastUpdated: new Date().toISOString(),
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// Utility Functions
|
|
205
|
+
// ============================================================================
|
|
206
|
+
|
|
207
|
+
const shortenAddress = (address: string): string => {
|
|
208
|
+
if (!address) return '';
|
|
209
|
+
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const copyToClipboard = (text: string) => {
|
|
213
|
+
navigator.clipboard.writeText(text);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const formatDate = (isoDate: string): string => {
|
|
217
|
+
const date = new Date(isoDate);
|
|
218
|
+
const now = new Date();
|
|
219
|
+
const diffMs = now.getTime() - date.getTime();
|
|
220
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
221
|
+
|
|
222
|
+
if (diffDays === 0) return 'Today';
|
|
223
|
+
if (diffDays === 1) return 'Yesterday';
|
|
224
|
+
if (diffDays < 7) return `${diffDays} days ago`;
|
|
225
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
|
|
226
|
+
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
|
|
227
|
+
return `${Math.floor(diffDays / 365)} years ago`;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const getRarityColor = (rarity: string): string => {
|
|
231
|
+
const rarityColors: Record<string, string> = {
|
|
232
|
+
common: '#A0AEC0',
|
|
233
|
+
rare: '#3FB950',
|
|
234
|
+
epic: '#2F81F7',
|
|
235
|
+
legendary: '#F59E0B',
|
|
236
|
+
};
|
|
237
|
+
return rarityColors[rarity] || '#A0AEC0';
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Reward History Component
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
interface RewardHistoryProps {
|
|
245
|
+
items: RewardHistoryItem[];
|
|
246
|
+
accentColor: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const RewardHistory: React.FC<RewardHistoryProps> = ({ items, accentColor }) => {
|
|
250
|
+
return (
|
|
251
|
+
<div className="space-y-3">
|
|
252
|
+
{items.map((item) => (
|
|
253
|
+
<div
|
|
254
|
+
key={item.id}
|
|
255
|
+
className="flex items-start justify-between p-3 rounded-lg border border-gray-700 hover:border-gray-600 transition-colors"
|
|
256
|
+
style={{ borderLeftColor: accentColor, borderLeftWidth: '3px' }}
|
|
257
|
+
>
|
|
258
|
+
<div className="flex-1">
|
|
259
|
+
<div className="flex items-center gap-2">
|
|
260
|
+
<span className="text-sm font-semibold text-white">{item.title}</span>
|
|
261
|
+
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-700 text-gray-300">
|
|
262
|
+
{item.type}
|
|
263
|
+
</span>
|
|
264
|
+
</div>
|
|
265
|
+
{item.description && (
|
|
266
|
+
<p className="text-xs text-gray-400 mt-1">{item.description}</p>
|
|
267
|
+
)}
|
|
268
|
+
<p className="text-xs text-gray-500 mt-1">{formatDate(item.timestamp)}</p>
|
|
269
|
+
</div>
|
|
270
|
+
<div className="ml-4 text-right">
|
|
271
|
+
<p className="text-lg font-bold" style={{ color: accentColor }}>
|
|
272
|
+
+{item.points}
|
|
273
|
+
</p>
|
|
274
|
+
<p className="text-xs text-gray-400">points</p>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// Badge Grid Component
|
|
284
|
+
// ============================================================================
|
|
285
|
+
|
|
286
|
+
interface BadgeGridProps {
|
|
287
|
+
badges: Badge[];
|
|
288
|
+
compactMode?: boolean;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const BadgeGrid: React.FC<BadgeGridProps> = ({ badges, compactMode = false }) => {
|
|
292
|
+
const displayBadges = compactMode ? badges.slice(0, 4) : badges;
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div
|
|
296
|
+
className={`grid gap-3 ${compactMode ? 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4' : 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5'}`}
|
|
297
|
+
>
|
|
298
|
+
{displayBadges.map((badge) => (
|
|
299
|
+
<div
|
|
300
|
+
key={badge.id}
|
|
301
|
+
className="group relative p-3 rounded-lg border transition-all hover:scale-105 cursor-pointer"
|
|
302
|
+
style={{
|
|
303
|
+
borderColor: getRarityColor(badge.rarity),
|
|
304
|
+
backgroundColor: 'rgba(31, 41, 55, 0.5)',
|
|
305
|
+
}}
|
|
306
|
+
title={`${badge.name} - ${badge.description}`}
|
|
307
|
+
>
|
|
308
|
+
<div className="text-3xl mb-1 text-center">{badge.icon}</div>
|
|
309
|
+
<p className="text-xs font-semibold text-white text-center truncate">{badge.name}</p>
|
|
310
|
+
<p className="text-xs text-gray-400 text-center mt-0.5">
|
|
311
|
+
{new Date(badge.earnedAt).toLocaleDateString()}
|
|
312
|
+
</p>
|
|
313
|
+
|
|
314
|
+
{/* Tooltip on hover */}
|
|
315
|
+
<div className="hidden group-hover:block absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 p-2 bg-gray-900 border border-gray-700 rounded-lg text-xs text-gray-300 w-32 text-center whitespace-normal z-10">
|
|
316
|
+
{badge.description}
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
))}
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// ============================================================================
|
|
325
|
+
// Tier Card Component
|
|
326
|
+
// ============================================================================
|
|
327
|
+
|
|
328
|
+
interface TierCardProps {
|
|
329
|
+
tier: TierInfo;
|
|
330
|
+
accentColor: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const TierCard: React.FC<TierCardProps> = ({ tier, accentColor }) => {
|
|
334
|
+
const progressPercent = (tier.currentProgress / tier.nextMilestone) * 100;
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div
|
|
338
|
+
className="p-4 rounded-lg border"
|
|
339
|
+
style={{
|
|
340
|
+
borderColor: accentColor,
|
|
341
|
+
backgroundColor: 'rgba(31, 41, 55, 0.3)',
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
<div className="flex items-start justify-between mb-3">
|
|
345
|
+
<div className="flex items-center gap-2">
|
|
346
|
+
<span className="text-3xl">{tier.icon}</span>
|
|
347
|
+
<div>
|
|
348
|
+
<p className="text-sm font-semibold text-white">{tier.name}</p>
|
|
349
|
+
<p className="text-xs text-gray-400">Level {tier.level}</p>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
<div className="space-y-2">
|
|
354
|
+
<div className="flex justify-between text-xs text-gray-400">
|
|
355
|
+
<span>{tier.currentProgress.toLocaleString()} points</span>
|
|
356
|
+
<span>{tier.nextMilestone.toLocaleString()} goal</span>
|
|
357
|
+
</div>
|
|
358
|
+
<div className="w-full bg-gray-700 rounded-full h-2 overflow-hidden">
|
|
359
|
+
<div
|
|
360
|
+
className="h-full rounded-full transition-all duration-500"
|
|
361
|
+
style={{
|
|
362
|
+
width: `${Math.min(progressPercent, 100)}%`,
|
|
363
|
+
backgroundColor: accentColor,
|
|
364
|
+
}}
|
|
365
|
+
/>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// ============================================================================
|
|
373
|
+
// Main RewardsDashboard Component
|
|
374
|
+
// ============================================================================
|
|
375
|
+
|
|
376
|
+
export const RewardsDashboard: React.FC<RewardsDashboardProps> = ({
|
|
377
|
+
walletAddress = null,
|
|
378
|
+
onConnectWallet,
|
|
379
|
+
customColors = {},
|
|
380
|
+
hideHistory = false,
|
|
381
|
+
hideBadges = false,
|
|
382
|
+
hideTier = false,
|
|
383
|
+
compactMode = false,
|
|
384
|
+
}) => {
|
|
385
|
+
const [data, setData] = useState<DashboardData | null>(null);
|
|
386
|
+
const [loading, setLoading] = useState(false);
|
|
387
|
+
const [error, setError] = useState<string | null>(null);
|
|
388
|
+
const [copied, setCopied] = useState(false);
|
|
389
|
+
|
|
390
|
+
const backgroundColor = customColors.background || '#0D1117';
|
|
391
|
+
const accentColor = customColors.accent1 || '#2F81F7';
|
|
392
|
+
const accentColor2 = customColors.accent2 || '#3FB950';
|
|
393
|
+
|
|
394
|
+
// Fetch data when wallet changes
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
if (!walletAddress) {
|
|
397
|
+
setData(null);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
setLoading(true);
|
|
402
|
+
setError(null);
|
|
403
|
+
|
|
404
|
+
fetchDashboardData(walletAddress)
|
|
405
|
+
.then((result) => {
|
|
406
|
+
setData(result);
|
|
407
|
+
setLoading(false);
|
|
408
|
+
})
|
|
409
|
+
.catch((err) => {
|
|
410
|
+
setError(err.message);
|
|
411
|
+
setLoading(false);
|
|
412
|
+
});
|
|
413
|
+
}, [walletAddress]);
|
|
414
|
+
|
|
415
|
+
const handleCopyAddress = () => {
|
|
416
|
+
if (walletAddress) {
|
|
417
|
+
copyToClipboard(walletAddress);
|
|
418
|
+
setCopied(true);
|
|
419
|
+
setTimeout(() => setCopied(false), 2000);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const handleRefresh = async () => {
|
|
424
|
+
if (walletAddress) {
|
|
425
|
+
setLoading(true);
|
|
426
|
+
const result = await fetchDashboardData(walletAddress);
|
|
427
|
+
setData(result);
|
|
428
|
+
setLoading(false);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Disconnected state
|
|
433
|
+
if (!walletAddress) {
|
|
434
|
+
return (
|
|
435
|
+
<div
|
|
436
|
+
className="p-6 rounded-lg border border-gray-700 max-w-2xl"
|
|
437
|
+
style={{ backgroundColor }}
|
|
438
|
+
>
|
|
439
|
+
<div className="text-center">
|
|
440
|
+
<Lock className="w-12 h-12 mx-auto mb-4 text-gray-500" />
|
|
441
|
+
<h2 className="text-xl font-bold text-white mb-2">Connect Your Wallet</h2>
|
|
442
|
+
<p className="text-gray-400 mb-6">
|
|
443
|
+
Connect your wallet to view your rewards, badges, and track your achievements.
|
|
444
|
+
</p>
|
|
445
|
+
{onConnectWallet && (
|
|
446
|
+
<button
|
|
447
|
+
onClick={onConnectWallet}
|
|
448
|
+
className="inline-flex items-center gap-2 px-6 py-2 rounded-lg font-semibold text-white transition-all hover:opacity-90"
|
|
449
|
+
style={{
|
|
450
|
+
backgroundColor: accentColor,
|
|
451
|
+
}}
|
|
452
|
+
>
|
|
453
|
+
<Wallet className="w-5 h-5" />
|
|
454
|
+
Connect Wallet
|
|
455
|
+
</button>
|
|
456
|
+
)}
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Loading state
|
|
463
|
+
if (loading && !data) {
|
|
464
|
+
return (
|
|
465
|
+
<div
|
|
466
|
+
className="p-6 rounded-lg border border-gray-700 max-w-2xl"
|
|
467
|
+
style={{ backgroundColor }}
|
|
468
|
+
>
|
|
469
|
+
<div className="space-y-4">
|
|
470
|
+
<div className="h-20 bg-gray-700 rounded-lg animate-pulse" />
|
|
471
|
+
<div className="h-40 bg-gray-700 rounded-lg animate-pulse" />
|
|
472
|
+
<div className="h-32 bg-gray-700 rounded-lg animate-pulse" />
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Error state
|
|
479
|
+
if (error && !data) {
|
|
480
|
+
return (
|
|
481
|
+
<div
|
|
482
|
+
className="p-6 rounded-lg border border-red-700 max-w-2xl"
|
|
483
|
+
style={{ backgroundColor }}
|
|
484
|
+
>
|
|
485
|
+
<p className="text-red-400">Failed to load rewards: {error}</p>
|
|
486
|
+
<button
|
|
487
|
+
onClick={handleRefresh}
|
|
488
|
+
className="mt-4 px-4 py-2 rounded-lg text-white text-sm transition-all"
|
|
489
|
+
style={{ backgroundColor: accentColor }}
|
|
490
|
+
>
|
|
491
|
+
Try Again
|
|
492
|
+
</button>
|
|
493
|
+
</div>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!data) return null;
|
|
498
|
+
|
|
499
|
+
return (
|
|
500
|
+
<div
|
|
501
|
+
className="rounded-lg border border-gray-700 overflow-hidden"
|
|
502
|
+
style={{ backgroundColor }}
|
|
503
|
+
>
|
|
504
|
+
{/* Header */}
|
|
505
|
+
<div className="p-6 border-b border-gray-700">
|
|
506
|
+
<div className="flex items-center justify-between mb-4">
|
|
507
|
+
<h2 className="text-2xl font-bold text-white">Rewards Dashboard</h2>
|
|
508
|
+
<button
|
|
509
|
+
onClick={handleRefresh}
|
|
510
|
+
disabled={loading}
|
|
511
|
+
className="p-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700 transition-all disabled:opacity-50"
|
|
512
|
+
>
|
|
513
|
+
<RefreshCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
|
|
514
|
+
</button>
|
|
515
|
+
</div>
|
|
516
|
+
|
|
517
|
+
{/* Wallet Display */}
|
|
518
|
+
<div className="flex items-center justify-between p-3 rounded-lg bg-gray-800/50 border border-gray-700">
|
|
519
|
+
<div className="flex items-center gap-2">
|
|
520
|
+
<Wallet className="w-5 h-5" style={{ color: accentColor }} />
|
|
521
|
+
<div>
|
|
522
|
+
<p className="text-xs text-gray-400">Connected Wallet</p>
|
|
523
|
+
<p className="text-sm font-mono font-semibold text-white">{shortenAddress(walletAddress)}</p>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
<button
|
|
527
|
+
onClick={handleCopyAddress}
|
|
528
|
+
className="p-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700 transition-all"
|
|
529
|
+
title={walletAddress}
|
|
530
|
+
>
|
|
531
|
+
{copied ? <CheckCircle className="w-5 h-5" style={{ color: accentColor2 }} /> : <Copy className="w-5 h-5" />}
|
|
532
|
+
</button>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
{/* Main Content */}
|
|
537
|
+
<div className={`p-6 space-y-6 ${compactMode ? 'max-w-3xl' : ''}`} style={{backgroundColor: 'rgba(31, 41, 55, 0.2)'}}>
|
|
538
|
+
{/* Points Display */}
|
|
539
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
540
|
+
<div
|
|
541
|
+
className="p-6 rounded-lg border text-center"
|
|
542
|
+
style={{
|
|
543
|
+
borderColor: accentColor,
|
|
544
|
+
backgroundColor: 'rgba(47, 129, 247, 0.1)',
|
|
545
|
+
}}
|
|
546
|
+
>
|
|
547
|
+
<p className="text-sm text-gray-400 mb-2">Total Points</p>
|
|
548
|
+
<p className="text-4xl font-bold" style={{ color: accentColor }}>
|
|
549
|
+
{data.totalPoints.toLocaleString()}
|
|
550
|
+
</p>
|
|
551
|
+
<div className="flex items-center justify-center gap-1 mt-2 text-xs text-gray-400">
|
|
552
|
+
<TrendingUp className="w-4 h-4" />
|
|
553
|
+
<span>Keep earning rewards</span>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
{!hideTier && (
|
|
558
|
+
<TierCard tier={data.tier} accentColor={accentColor2} />
|
|
559
|
+
)}
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
{/* Badges Section */}
|
|
563
|
+
{!hideBadges && data.badges.length > 0 && (
|
|
564
|
+
<div>
|
|
565
|
+
<div className="flex items-center gap-2 mb-4">
|
|
566
|
+
<Award className="w-5 h-5" style={{ color: accentColor }} />
|
|
567
|
+
<h3 className="text-lg font-semibold text-white">
|
|
568
|
+
Badges ({data.badges.length})
|
|
569
|
+
</h3>
|
|
570
|
+
</div>
|
|
571
|
+
<BadgeGrid badges={data.badges} compactMode={compactMode} />
|
|
572
|
+
</div>
|
|
573
|
+
)}
|
|
574
|
+
|
|
575
|
+
{/* Reward History Section */}
|
|
576
|
+
{!hideHistory && data.rewardHistory.length > 0 && (
|
|
577
|
+
<div>
|
|
578
|
+
<div className="flex items-center gap-2 mb-4">
|
|
579
|
+
<Zap className="w-5 h-5" style={{ color: accentColor2 }} />
|
|
580
|
+
<h3 className="text-lg font-semibold text-white">Recent Rewards</h3>
|
|
581
|
+
</div>
|
|
582
|
+
<RewardHistory
|
|
583
|
+
items={compactMode ? data.rewardHistory.slice(0, 3) : data.rewardHistory}
|
|
584
|
+
accentColor={accentColor}
|
|
585
|
+
/>
|
|
586
|
+
</div>
|
|
587
|
+
)}
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
{/* Footer */}
|
|
591
|
+
<div className="px-6 py-3 border-t border-gray-700 flex items-center justify-between text-xs text-gray-500">
|
|
592
|
+
<span>Last updated: {formatDate(data.lastUpdated)}</span>
|
|
593
|
+
<span className="flex items-center gap-1">
|
|
594
|
+
<ExternalLink className="w-3 h-3" />
|
|
595
|
+
@chainloyalty/react
|
|
596
|
+
</span>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
export default RewardsDashboard;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @chainloyalty/react
|
|
3
|
+
* Main entry point for the ChainLoyalty React package
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Export hooks
|
|
7
|
+
export { useTrackEvent, trackEvent } from './useTrackEvent.js';
|
|
8
|
+
export type { EventData, EventType, TrackEventResponse, UseTrackEventState } from './useTrackEvent.js';
|
|
9
|
+
|
|
10
|
+
// Export components
|
|
11
|
+
export { RewardsDashboard } from './RewardsDashboard.js';
|
|
12
|
+
export type { RewardsDashboardProps, Badge, RewardHistoryItem, TierInfo, DashboardData } from './RewardsDashboard.js';
|
|
13
|
+
|
|
14
|
+
export { Leaderboard } from './Leaderboard.js';
|
|
15
|
+
export type { LeaderboardProps, LeaderboardEntry, LeaderboardTimeframe } from './Leaderboard.js';
|
|
16
|
+
|
|
17
|
+
// Version info
|
|
18
|
+
export const PACKAGE_VERSION = '1.0.0';
|