361 lines
8.6 KiB
TypeScript
361 lines
8.6 KiB
TypeScript
|
|
// Points API Client
|
||
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api';
|
||
|
|
|
||
|
|
// =====================
|
||
|
|
// Type Definitions
|
||
|
|
// =====================
|
||
|
|
|
||
|
|
export interface WalletRegisterData {
|
||
|
|
walletAddress: string;
|
||
|
|
inviteCode: string;
|
||
|
|
usedCount: number;
|
||
|
|
memberTier: string;
|
||
|
|
vipLevel: number;
|
||
|
|
totalPoints: number;
|
||
|
|
globalRank: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface DashboardData {
|
||
|
|
totalPoints: number;
|
||
|
|
globalRank: number;
|
||
|
|
topPercentage: string;
|
||
|
|
memberTier: string;
|
||
|
|
vipLevel: number;
|
||
|
|
pointsToNextTier: number;
|
||
|
|
nextTier: string;
|
||
|
|
season: {
|
||
|
|
seasonNumber: number;
|
||
|
|
seasonName: string;
|
||
|
|
isLive: boolean;
|
||
|
|
endTime: string;
|
||
|
|
daysRemaining: number;
|
||
|
|
hoursRemaining: number;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface LeaderboardUser {
|
||
|
|
rank: number;
|
||
|
|
address: string;
|
||
|
|
points: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface LeaderboardData {
|
||
|
|
topUsers: LeaderboardUser[];
|
||
|
|
myRank: number;
|
||
|
|
myPoints: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface InviteCodeData {
|
||
|
|
code: string;
|
||
|
|
usedCount: number;
|
||
|
|
maxUses: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface RoleCount {
|
||
|
|
icon: string;
|
||
|
|
label: string;
|
||
|
|
current: number;
|
||
|
|
target: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface TeamTVLData {
|
||
|
|
currentTVL: string;
|
||
|
|
targetTVL: string;
|
||
|
|
progressPercent: number;
|
||
|
|
totalMembers: number;
|
||
|
|
roles: RoleCount[];
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ActivityRecord {
|
||
|
|
type: string;
|
||
|
|
userAddress: string;
|
||
|
|
friendAddress?: string;
|
||
|
|
inviteCode?: string;
|
||
|
|
points: number;
|
||
|
|
createdAt: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ActivitiesData {
|
||
|
|
activities: ActivityRecord[];
|
||
|
|
pagination: {
|
||
|
|
page: number;
|
||
|
|
pageSize: number;
|
||
|
|
total: number;
|
||
|
|
totalPage: number;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// =====================
|
||
|
|
// API Functions
|
||
|
|
// =====================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Register or retrieve a user by wallet address
|
||
|
|
* @param walletAddress - The connected wallet address
|
||
|
|
*/
|
||
|
|
export async function registerWallet(walletAddress: string): Promise<WalletRegisterData | null> {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`${API_BASE_URL}/points/wallet-register`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ wallet_address: walletAddress }),
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
console.error('Wallet register API error:', response.status);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
if (!data.success) {
|
||
|
|
console.error('Wallet register error:', data.error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return data.data;
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error registering wallet:', error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch user points dashboard
|
||
|
|
*/
|
||
|
|
export async function fetchDashboard(walletAddress?: string): Promise<DashboardData | null> {
|
||
|
|
try {
|
||
|
|
const params = walletAddress ? `?wallet_address=${encodeURIComponent(walletAddress)}` : '';
|
||
|
|
const response = await fetch(`${API_BASE_URL}/points/dashboard${params}`, {
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
console.error('Dashboard API error:', response.status);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!data.success) {
|
||
|
|
console.error('Dashboard API error:', data.error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return data.data;
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching dashboard:', error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch leaderboard
|
||
|
|
* @param limit - Number of top users to fetch (default: 5)
|
||
|
|
* @param walletAddress - Optional wallet address to identify current user's rank
|
||
|
|
*/
|
||
|
|
export async function fetchLeaderboard(limit: number = 5, walletAddress?: string): Promise<LeaderboardData | null> {
|
||
|
|
try {
|
||
|
|
const params = new URLSearchParams({ limit: limit.toString() });
|
||
|
|
if (walletAddress) params.set('wallet_address', walletAddress);
|
||
|
|
const response = await fetch(`${API_BASE_URL}/points/leaderboard?${params}`, {
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
console.error('Leaderboard API error:', response.status);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!data.success) {
|
||
|
|
console.error('Leaderboard API error:', data.error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return data.data;
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching leaderboard:', error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch user's invite code information
|
||
|
|
*/
|
||
|
|
export async function fetchInviteCode(): Promise<InviteCodeData | null> {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`${API_BASE_URL}/points/invite-code`, {
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Failed to fetch invite code');
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!data.success) {
|
||
|
|
throw new Error(data.error || 'Failed to fetch invite code');
|
||
|
|
}
|
||
|
|
|
||
|
|
return data.data;
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching invite code:', error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Bind an invite code
|
||
|
|
* @param code - Invite code to bind
|
||
|
|
* @param signature - User signature for verification
|
||
|
|
* @param walletAddress - Optional wallet address of the current user
|
||
|
|
*/
|
||
|
|
export async function bindInviteCode(code: string, signature: string, walletAddress?: string): Promise<{ success: boolean; message?: string; error?: string }> {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`${API_BASE_URL}/points/bind-invite`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
},
|
||
|
|
body: JSON.stringify({ code, signature, wallet_address: walletAddress }),
|
||
|
|
});
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!response.ok || !data.success) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: data.error || 'Failed to bind invite code',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
success: true,
|
||
|
|
message: data.message,
|
||
|
|
};
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error binding invite code:', error);
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch team TVL statistics
|
||
|
|
* @param walletAddress - Optional wallet address to identify current user's team
|
||
|
|
*/
|
||
|
|
export async function fetchTeamTVL(walletAddress?: string): Promise<TeamTVLData | null> {
|
||
|
|
try {
|
||
|
|
const params = walletAddress ? `?wallet_address=${encodeURIComponent(walletAddress)}` : '';
|
||
|
|
const response = await fetch(`${API_BASE_URL}/points/team${params}`, {
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
console.error('Team TVL API error:', response.status, response.statusText);
|
||
|
|
|
||
|
|
// 返回默认值而不是 null
|
||
|
|
return {
|
||
|
|
currentTVL: "$0",
|
||
|
|
targetTVL: "$10M",
|
||
|
|
progressPercent: 0,
|
||
|
|
totalMembers: 0,
|
||
|
|
roles: []
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!data.success) {
|
||
|
|
console.error('Team TVL API error:', data.error);
|
||
|
|
// 返回默认值
|
||
|
|
return {
|
||
|
|
currentTVL: "$0",
|
||
|
|
targetTVL: "$10M",
|
||
|
|
progressPercent: 0,
|
||
|
|
totalMembers: 0,
|
||
|
|
roles: []
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return data.data;
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching team TVL:', error);
|
||
|
|
// 返回默认值而不是 null
|
||
|
|
return {
|
||
|
|
currentTVL: "$0",
|
||
|
|
targetTVL: "$10M",
|
||
|
|
progressPercent: 0,
|
||
|
|
totalMembers: 0,
|
||
|
|
roles: []
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetch user activities
|
||
|
|
* @param type - Activity type: 'all', 'referrals', 'deposits'
|
||
|
|
* @param page - Page number (default: 1)
|
||
|
|
* @param pageSize - Items per page (default: 10)
|
||
|
|
* @param walletAddress - Optional wallet address
|
||
|
|
*/
|
||
|
|
export async function fetchActivities(
|
||
|
|
type: 'all' | 'referrals' | 'deposits' = 'all',
|
||
|
|
page: number = 1,
|
||
|
|
pageSize: number = 10,
|
||
|
|
walletAddress?: string
|
||
|
|
): Promise<ActivitiesData | null> {
|
||
|
|
try {
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
type,
|
||
|
|
page: page.toString(),
|
||
|
|
pageSize: pageSize.toString(),
|
||
|
|
});
|
||
|
|
if (walletAddress) params.set('wallet_address', walletAddress);
|
||
|
|
|
||
|
|
const response = await fetch(`${API_BASE_URL}/points/activities?${params}`, {
|
||
|
|
cache: 'no-store',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Failed to fetch activities');
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (!data.success) {
|
||
|
|
throw new Error(data.error || 'Failed to fetch activities');
|
||
|
|
}
|
||
|
|
|
||
|
|
return data.data;
|
||
|
|
} catch (error: unknown) {
|
||
|
|
console.error('Error fetching activities:', error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Format points to display format (e.g., 52000 -> "52k")
|
||
|
|
*/
|
||
|
|
export function formatPoints(points: number): string {
|
||
|
|
if (points >= 1000000) {
|
||
|
|
return `${(points / 1000000).toFixed(1)}M`;
|
||
|
|
} else if (points >= 1000) {
|
||
|
|
return `${(points / 1000).toFixed(0)}k`;
|
||
|
|
}
|
||
|
|
return points.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculate progress percentage
|
||
|
|
*/
|
||
|
|
export function calculateProgress(current: number, target: number): number {
|
||
|
|
if (target === 0) return 0;
|
||
|
|
return Math.round((current / target) * 100 * 100) / 100;
|
||
|
|
}
|