mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-04-21 23:37:55 +00:00
feat: add PEZ epoch rewards display and claim functionality
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pezkuwi-telegram-miniapp",
|
||||
"version": "1.0.216",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
|
||||
"author": "Pezkuwichain Team",
|
||||
|
||||
@@ -147,6 +147,20 @@ const ar: Translations = {
|
||||
citizenCountTitle: 'الأكراد في العالم',
|
||||
citizenCountDesc: 'المواطنون المسجلون على PezkuwiChain',
|
||||
beCitizen: 'كن مواطناً',
|
||||
pezRewardsTitle: 'مكافآت PEZ الدورية',
|
||||
epoch: 'الدورة',
|
||||
claimPeriod: 'فترة المطالبة',
|
||||
epochClosed: 'مغلقة',
|
||||
epochOpen: 'مفتوحة',
|
||||
scoreRecorded: 'مسجّل',
|
||||
claimablePez: 'PEZ قابل للمطالبة',
|
||||
claim: 'طالب',
|
||||
claimAll: 'طالب بالكل',
|
||||
claimPez: 'طالب بـ PEZ',
|
||||
claimingReward: 'جاري المطالبة بالمكافأة...',
|
||||
claimSuccess: 'تم المطالبة بالمكافأة بنجاح!',
|
||||
claimFailed: 'فشلت المطالبة بالمكافأة',
|
||||
noPezRewards: 'لا توجد مكافآت PEZ قابلة للمطالبة',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
|
||||
@@ -148,6 +148,20 @@ const ckb: Translations = {
|
||||
citizenCountTitle: 'ژمارەی کورد لە جیهان',
|
||||
citizenCountDesc: 'هاوڵاتییانی تۆمارکراو لە PezkuwiChain',
|
||||
beCitizen: 'ببە هاوڵاتی',
|
||||
pezRewardsTitle: 'خەڵاتەکانی PEZ ی سەردەم',
|
||||
epoch: 'سەردەم',
|
||||
claimPeriod: 'ماوەی داواکردن',
|
||||
epochClosed: 'داخراوە',
|
||||
epochOpen: 'کراوەیە',
|
||||
scoreRecorded: 'تۆمارکرا',
|
||||
claimablePez: 'PEZ ی داواکراو',
|
||||
claim: 'داوا بکە',
|
||||
claimAll: 'هەموو داوا بکە',
|
||||
claimPez: 'PEZ داوا بکە',
|
||||
claimingReward: 'خەڵات داوا دەکرێت...',
|
||||
claimSuccess: 'خەڵات بە سەرکەوتوویی داوا کرا!',
|
||||
claimFailed: 'داواکردنی خەڵات سەرنەکەوت',
|
||||
noPezRewards: 'خەڵاتی PEZ ی داواکراو نییە',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
|
||||
@@ -147,6 +147,20 @@ const en: Translations = {
|
||||
citizenCountTitle: 'Kurds in the World',
|
||||
citizenCountDesc: 'Citizens registered on PezkuwiChain',
|
||||
beCitizen: 'Be Citizen',
|
||||
pezRewardsTitle: 'PEZ Epoch Rewards',
|
||||
epoch: 'Epoch',
|
||||
claimPeriod: 'Claim Period',
|
||||
epochClosed: 'Closed',
|
||||
epochOpen: 'Open',
|
||||
scoreRecorded: 'Recorded',
|
||||
claimablePez: 'Claimable PEZ',
|
||||
claim: 'Claim',
|
||||
claimAll: 'Claim All',
|
||||
claimPez: 'Claim PEZ',
|
||||
claimingReward: 'Claiming reward...',
|
||||
claimSuccess: 'Reward claimed successfully!',
|
||||
claimFailed: 'Failed to claim reward',
|
||||
noPezRewards: 'No claimable PEZ rewards',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
|
||||
@@ -147,6 +147,20 @@ const fa: Translations = {
|
||||
citizenCountTitle: 'کوردها در جهان',
|
||||
citizenCountDesc: 'شهروندان ثبتشده در PezkuwiChain',
|
||||
beCitizen: 'شهروند شوید',
|
||||
pezRewardsTitle: 'پاداشهای PEZ دورهای',
|
||||
epoch: 'دوره',
|
||||
claimPeriod: 'دوره مطالبه',
|
||||
epochClosed: 'بسته',
|
||||
epochOpen: 'باز',
|
||||
scoreRecorded: 'ثبت شد',
|
||||
claimablePez: 'PEZ قابل مطالبه',
|
||||
claim: 'مطالبه',
|
||||
claimAll: 'مطالبه همه',
|
||||
claimPez: 'مطالبه PEZ',
|
||||
claimingReward: 'در حال مطالبه پاداش...',
|
||||
claimSuccess: 'پاداش با موفقیت مطالبه شد!',
|
||||
claimFailed: 'مطالبه پاداش ناموفق بود',
|
||||
noPezRewards: 'پاداش PEZ قابل مطالبهای وجود ندارد',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
|
||||
@@ -152,6 +152,20 @@ const krd: Translations = {
|
||||
citizenCountTitle: 'Hejmara Kurd Le Cîhanê',
|
||||
citizenCountDesc: 'Welatiyên ku li ser PezkuwiChain qeyd bûne',
|
||||
beCitizen: 'Bibe Welatî',
|
||||
pezRewardsTitle: 'Xelatên PEZ yên Serdemê',
|
||||
epoch: 'Serdem',
|
||||
claimPeriod: 'Dema Daxwazkirinê',
|
||||
epochClosed: 'Girtî',
|
||||
epochOpen: 'Vekirî',
|
||||
scoreRecorded: 'Hat tomarkirin',
|
||||
claimablePez: 'PEZ yên Daxwazkir',
|
||||
claim: 'Daxwaz Bike',
|
||||
claimAll: 'Hemûyan Daxwaz Bike',
|
||||
claimPez: 'PEZ Daxwaz Bike',
|
||||
claimingReward: 'Xelat tê daxwazkirin...',
|
||||
claimSuccess: 'Xelat bi serkeftin hat daxwazkirin!',
|
||||
claimFailed: 'Daxwazkirina xelatê biserneket',
|
||||
noPezRewards: 'Xelatên PEZ yên daxwazkir tune ne',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
|
||||
@@ -147,6 +147,20 @@ const tr: Translations = {
|
||||
citizenCountTitle: 'Dünyadaki Kürt Sayısı',
|
||||
citizenCountDesc: "PezkuwiChain'de kayıtlı vatandaşlar",
|
||||
beCitizen: 'Vatandaş Ol',
|
||||
pezRewardsTitle: 'PEZ Dönem Ödülleri',
|
||||
epoch: 'Dönem',
|
||||
claimPeriod: 'Talep Dönemi',
|
||||
epochClosed: 'Kapalı',
|
||||
epochOpen: 'Açık',
|
||||
scoreRecorded: 'Kaydedildi',
|
||||
claimablePez: 'Talep Edilebilir PEZ',
|
||||
claim: 'Talep Et',
|
||||
claimAll: 'Tümünü Talep Et',
|
||||
claimPez: 'PEZ Talep Et',
|
||||
claimingReward: 'Ödül talep ediliyor...',
|
||||
claimSuccess: 'Ödül başarıyla talep edildi!',
|
||||
claimFailed: 'Ödül talep edilemedi',
|
||||
noPezRewards: 'Talep edilebilir PEZ ödülü yok',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
|
||||
@@ -149,6 +149,20 @@ export interface Translations {
|
||||
citizenCountTitle: string;
|
||||
citizenCountDesc: string;
|
||||
beCitizen: string;
|
||||
pezRewardsTitle: string;
|
||||
epoch: string;
|
||||
claimPeriod: string;
|
||||
epochClosed: string;
|
||||
epochOpen: string;
|
||||
scoreRecorded: string;
|
||||
claimablePez: string;
|
||||
claim: string;
|
||||
claimAll: string;
|
||||
claimPez: string;
|
||||
claimingReward: string;
|
||||
claimSuccess: string;
|
||||
claimFailed: string;
|
||||
noPezRewards: string;
|
||||
};
|
||||
|
||||
// Wallet section
|
||||
|
||||
@@ -36,6 +36,18 @@ export interface StakingScoreStatus {
|
||||
durationBlocks: number;
|
||||
}
|
||||
|
||||
export type EpochStatus = 'Open' | 'ClaimPeriod' | 'Closed';
|
||||
|
||||
export interface PezRewardInfo {
|
||||
currentEpoch: number;
|
||||
epochStatus: EpochStatus;
|
||||
hasRecordedThisEpoch: boolean;
|
||||
userScoreCurrentEpoch: number;
|
||||
claimableRewards: { epoch: number; amount: string }[];
|
||||
totalClaimable: string;
|
||||
hasPendingClaim: boolean;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TRUST SCORE (pezpallet-trust on People Chain)
|
||||
// ========================================
|
||||
@@ -550,6 +562,194 @@ export async function recordTrustScore(
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PEZ REWARDS (pezRewards pallet on People Chain)
|
||||
// ========================================
|
||||
|
||||
function formatBalancePlanck(planck: string): string {
|
||||
const num = BigInt(planck);
|
||||
const whole = num / BigInt(10 ** 12);
|
||||
const frac = num % BigInt(10 ** 12);
|
||||
const fracStr = frac.toString().padStart(12, '0').slice(0, 4);
|
||||
return `${whole}.${fracStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PEZ rewards information for an account
|
||||
* Queries pezRewards pallet on People Chain
|
||||
*/
|
||||
export async function getPezRewards(
|
||||
peopleApi: ApiPromise,
|
||||
address: string
|
||||
): Promise<PezRewardInfo | null> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (!(peopleApi?.query as any)?.pezRewards?.epochInfo) {
|
||||
console.warn('PezRewards pallet not available on People Chain');
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const epochInfoResult = await (peopleApi.query as any).pezRewards.epochInfo();
|
||||
if (!epochInfoResult) return null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const epochInfo = epochInfoResult.toJSON() as any;
|
||||
const currentEpoch: number = epochInfo.currentEpoch ?? epochInfo.current_epoch ?? 0;
|
||||
|
||||
// Get current epoch status
|
||||
let epochStatus: EpochStatus = 'Open';
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const statusResult = await (peopleApi.query as any).pezRewards.epochStatus(currentEpoch);
|
||||
const statusStr = statusResult.toString();
|
||||
if (statusStr === 'ClaimPeriod') epochStatus = 'ClaimPeriod';
|
||||
else if (statusStr === 'Closed') epochStatus = 'Closed';
|
||||
else epochStatus = 'Open';
|
||||
} catch {
|
||||
// Default to Open
|
||||
}
|
||||
|
||||
// Check if user has recorded their score this epoch
|
||||
let hasRecordedThisEpoch = false;
|
||||
let userScoreCurrentEpoch = 0;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const userScoreResult = await (peopleApi.query as any).pezRewards.userEpochScores(
|
||||
currentEpoch,
|
||||
address
|
||||
);
|
||||
if (userScoreResult.isSome) {
|
||||
hasRecordedThisEpoch = true;
|
||||
const scoreCodec = userScoreResult.unwrap() as { toString: () => string };
|
||||
userScoreCurrentEpoch = Number(scoreCodec.toString());
|
||||
}
|
||||
} catch {
|
||||
// User hasn't recorded
|
||||
}
|
||||
|
||||
// Check for claimable rewards from completed epochs
|
||||
const claimableRewards: { epoch: number; amount: string }[] = [];
|
||||
let totalClaimable = BigInt(0);
|
||||
|
||||
for (let i = Math.max(0, currentEpoch - 3); i < currentEpoch; i++) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pastStatusResult = await (peopleApi.query as any).pezRewards.epochStatus(i);
|
||||
const pastStatus = pastStatusResult.toString();
|
||||
if (pastStatus !== 'ClaimPeriod') continue;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const claimedResult = await (peopleApi.query as any).pezRewards.claimedRewards(i, address);
|
||||
if (claimedResult.isSome) continue;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const userScoreResult = await (peopleApi.query as any).pezRewards.userEpochScores(
|
||||
i,
|
||||
address
|
||||
);
|
||||
if (!userScoreResult.isSome) continue;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const epochPoolResult = await (peopleApi.query as any).pezRewards.epochRewardPools(i);
|
||||
if (!epochPoolResult.isSome) continue;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const epochPool = (epochPoolResult.unwrap() as any).toJSON();
|
||||
const userScore = BigInt(
|
||||
(userScoreResult.unwrap() as { toString: () => string }).toString()
|
||||
);
|
||||
const rewardPerPoint = BigInt(
|
||||
epochPool.rewardPerTrustPoint || epochPool.reward_per_trust_point || '0'
|
||||
);
|
||||
|
||||
const rewardAmount = userScore * rewardPerPoint;
|
||||
const rewardFormatted = formatBalancePlanck(rewardAmount.toString());
|
||||
|
||||
if (parseFloat(rewardFormatted) > 0) {
|
||||
claimableRewards.push({ epoch: i, amount: rewardFormatted });
|
||||
totalClaimable += rewardAmount;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Error checking epoch ${i} rewards:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentEpoch,
|
||||
epochStatus,
|
||||
hasRecordedThisEpoch,
|
||||
userScoreCurrentEpoch,
|
||||
claimableRewards,
|
||||
totalClaimable: formatBalancePlanck(totalClaimable.toString()),
|
||||
hasPendingClaim: claimableRewards.length > 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('PEZ rewards not available:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim PEZ reward for a specific epoch
|
||||
* Calls pezRewards.claimReward(epochIndex)
|
||||
*/
|
||||
export async function claimPezReward(
|
||||
peopleApi: ApiPromise,
|
||||
keypair: KeyringPair,
|
||||
epochIndex: number
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tx = peopleApi.tx as any;
|
||||
if (!tx?.pezRewards?.claimReward) {
|
||||
return { success: false, error: 'pezRewards pallet not available' };
|
||||
}
|
||||
|
||||
const result = await new Promise<{ success: boolean; error?: string }>((resolve) => {
|
||||
tx.pezRewards
|
||||
.claimReward(epochIndex)
|
||||
.signAndSend(
|
||||
keypair,
|
||||
{ nonce: -1 },
|
||||
({
|
||||
status,
|
||||
dispatchError,
|
||||
}: {
|
||||
status: {
|
||||
isInBlock: boolean;
|
||||
isFinalized: boolean;
|
||||
};
|
||||
dispatchError?: { isModule: boolean; asModule: unknown; toString: () => string };
|
||||
}) => {
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'claimReward failed';
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = peopleApi.registry.findMetaError(
|
||||
dispatchError.asModule as Parameters<typeof peopleApi.registry.findMetaError>[0]
|
||||
);
|
||||
errorMessage = `${decoded.section}.${decoded.name}`;
|
||||
}
|
||||
resolve({ success: false, error: errorMessage });
|
||||
return;
|
||||
}
|
||||
resolve({ success: true });
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch((error: Error) => resolve({ success: false, error: error.message }));
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// COMPREHENSIVE SCORE FETCHING
|
||||
// ========================================
|
||||
|
||||
+170
-1
@@ -37,11 +37,14 @@ import {
|
||||
getStakingScoreStatus,
|
||||
startScoreTracking,
|
||||
recordTrustScore,
|
||||
getPezRewards,
|
||||
claimPezReward,
|
||||
formatDuration,
|
||||
getScoreColor,
|
||||
getScoreRating,
|
||||
type UserScores,
|
||||
type StakingScoreStatus,
|
||||
type PezRewardInfo,
|
||||
} from '@/lib/scores';
|
||||
import {
|
||||
getStakingRewards,
|
||||
@@ -78,6 +81,8 @@ export function RewardsSection() {
|
||||
const [scoresLoading, setScoresLoading] = useState(false);
|
||||
const [citizenshipStatus, setCitizenshipStatus] = useState<CitizenshipStatus>('NotStarted');
|
||||
const [citizenCount, setCitizenCount] = useState<number | null>(null);
|
||||
const [pezRewards, setPezRewards] = useState<PezRewardInfo | null>(null);
|
||||
const [claimingEpoch, setClaimingEpoch] = useState<number | null>(null);
|
||||
const [showConfirmAnimation, setShowConfirmAnimation] = useState(false);
|
||||
const [showTrackingAnimation, setShowTrackingAnimation] = useState(false);
|
||||
const [trackingAnimationText, setTrackingAnimationText] = useState('');
|
||||
@@ -122,19 +127,22 @@ export function RewardsSection() {
|
||||
setUserScores(null);
|
||||
setStakingStatus(null);
|
||||
setStakingRewards(null);
|
||||
setPezRewards(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setScoresLoading(true);
|
||||
try {
|
||||
const [scores, staking, rewards] = await Promise.all([
|
||||
const [scores, staking, rewards, pezRewardsData] = await Promise.all([
|
||||
getAllScores(peopleApi, address),
|
||||
peopleApi ? getStakingScoreStatus(peopleApi, address) : Promise.resolve(null),
|
||||
getStakingRewards(address),
|
||||
peopleApi ? getPezRewards(peopleApi, address) : Promise.resolve(null),
|
||||
]);
|
||||
setUserScores(scores);
|
||||
setStakingStatus(staking);
|
||||
setStakingRewards(rewards);
|
||||
setPezRewards(pezRewardsData);
|
||||
} catch (err) {
|
||||
console.error('Error fetching scores:', err);
|
||||
} finally {
|
||||
@@ -226,6 +234,60 @@ export function RewardsSection() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClaimReward = async (epochIndex: number) => {
|
||||
if (!peopleApi || !keypair) return;
|
||||
setClaimingEpoch(epochIndex);
|
||||
setTrackingAnimationText(t('rewards.claimingReward'));
|
||||
setShowTrackingAnimation(true);
|
||||
hapticImpact('medium');
|
||||
try {
|
||||
const result = await claimPezReward(peopleApi, keypair, epochIndex);
|
||||
if (result.success) {
|
||||
hapticNotification('success');
|
||||
showAlert(t('rewards.claimSuccess'));
|
||||
fetchUserScores();
|
||||
} else {
|
||||
hapticNotification('error');
|
||||
showAlert(result.error || t('rewards.claimFailed'));
|
||||
}
|
||||
} catch (err) {
|
||||
hapticNotification('error');
|
||||
showAlert(err instanceof Error ? err.message : t('rewards.claimFailed'));
|
||||
} finally {
|
||||
setShowTrackingAnimation(false);
|
||||
setClaimingEpoch(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClaimAll = async () => {
|
||||
if (!peopleApi || !keypair || !pezRewards?.claimableRewards.length) return;
|
||||
setTrackingAnimationText(t('rewards.claimingReward'));
|
||||
setShowTrackingAnimation(true);
|
||||
hapticImpact('medium');
|
||||
try {
|
||||
for (const reward of pezRewards.claimableRewards) {
|
||||
setClaimingEpoch(reward.epoch);
|
||||
const result = await claimPezReward(peopleApi, keypair, reward.epoch);
|
||||
if (!result.success) {
|
||||
hapticNotification('error');
|
||||
showAlert(result.error || t('rewards.claimFailed'));
|
||||
setShowTrackingAnimation(false);
|
||||
setClaimingEpoch(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
hapticNotification('success');
|
||||
showAlert(t('rewards.claimSuccess'));
|
||||
fetchUserScores();
|
||||
} catch (err) {
|
||||
hapticNotification('error');
|
||||
showAlert(err instanceof Error ? err.message : t('rewards.claimFailed'));
|
||||
} finally {
|
||||
setShowTrackingAnimation(false);
|
||||
setClaimingEpoch(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivate = () => {
|
||||
hapticNotification('success');
|
||||
localStorage.setItem(ACTIVITY_STORAGE_KEY, Date.now().toString());
|
||||
@@ -877,6 +939,113 @@ export function RewardsSection() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PEZ Epoch Rewards */}
|
||||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||||
<h3 className="font-medium text-foreground mb-3 flex items-center gap-2">
|
||||
<Gift className="w-4 h-4 text-purple-400" />
|
||||
{t('rewards.pezRewardsTitle')}
|
||||
</h3>
|
||||
|
||||
{/* Epoch Info */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('rewards.epoch')}: {pezRewards?.currentEpoch ?? '...'}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'px-2 py-0.5 rounded-full text-xs font-medium',
|
||||
pezRewards?.epochStatus === 'ClaimPeriod'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: pezRewards?.epochStatus === 'Closed'
|
||||
? 'bg-red-500/20 text-red-400'
|
||||
: 'bg-blue-500/20 text-blue-400'
|
||||
)}
|
||||
>
|
||||
{pezRewards?.epochStatus === 'ClaimPeriod'
|
||||
? t('rewards.claimPeriod')
|
||||
: pezRewards?.epochStatus === 'Closed'
|
||||
? t('rewards.epochClosed')
|
||||
: t('rewards.epochOpen')}
|
||||
</span>
|
||||
{pezRewards?.hasRecordedThisEpoch && (
|
||||
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/20 text-green-400">
|
||||
<Check className="w-3 h-3 inline mr-1" />
|
||||
{t('rewards.scoreRecorded')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Claimable PEZ Rewards */}
|
||||
<div className="bg-purple-500/10 rounded-lg p-3 mb-3 border border-purple-500/20">
|
||||
<p className="text-xs text-purple-300 mb-1">{t('rewards.claimablePez')}</p>
|
||||
<p className="text-2xl font-bold text-purple-400">
|
||||
{pezRewards && parseFloat(pezRewards.totalClaimable) > 0
|
||||
? `${pezRewards.totalClaimable} PEZ`
|
||||
: '0 PEZ'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Claimable Epochs List */}
|
||||
{pezRewards && pezRewards.claimableRewards.length > 0 ? (
|
||||
<div className="space-y-2 mb-3">
|
||||
{pezRewards.claimableRewards.map((reward) => (
|
||||
<div
|
||||
key={reward.epoch}
|
||||
className="flex items-center justify-between py-2 px-3 bg-purple-500/5 rounded-lg border border-purple-500/10"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm text-foreground">
|
||||
{t('rewards.epoch')} #{reward.epoch}
|
||||
</p>
|
||||
<p className="text-xs text-purple-400 font-medium">
|
||||
{reward.amount} PEZ
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleClaimReward(reward.epoch)}
|
||||
disabled={!keypair || claimingEpoch !== null}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-purple-500/20 text-purple-300 hover:bg-purple-500/30 transition-all disabled:opacity-50"
|
||||
>
|
||||
{claimingEpoch === reward.epoch ? (
|
||||
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
t('rewards.claim')
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Claim All Button */}
|
||||
{pezRewards.claimableRewards.length > 1 && (
|
||||
<button
|
||||
onClick={handleClaimAll}
|
||||
disabled={!keypair || claimingEpoch !== null}
|
||||
className="w-full py-3 rounded-lg font-medium flex items-center justify-center gap-2 bg-gradient-to-r from-purple-500 to-pink-600 text-white hover:opacity-90 transition-all disabled:opacity-50"
|
||||
>
|
||||
<Gift className="w-5 h-5" />
|
||||
{t('rewards.claimAll')} ({pezRewards.totalClaimable} PEZ)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground text-center py-2">
|
||||
{t('rewards.noPezRewards')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Single Claim Button when only 1 reward */}
|
||||
{pezRewards && pezRewards.claimableRewards.length === 1 && (
|
||||
<button
|
||||
onClick={() => handleClaimReward(pezRewards.claimableRewards[0].epoch)}
|
||||
disabled={!keypair || claimingEpoch !== null}
|
||||
className="w-full py-3 rounded-lg font-medium flex items-center justify-center gap-2 bg-gradient-to-r from-purple-500 to-pink-600 text-white hover:opacity-90 transition-all disabled:opacity-50"
|
||||
>
|
||||
<Gift className="w-5 h-5" />
|
||||
{t('rewards.claimPez')} ({pezRewards.totalClaimable} PEZ)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Staking Rewards from SubQuery */}
|
||||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||||
<h3 className="font-medium text-foreground mb-3 flex items-center gap-2">
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.216",
|
||||
"buildTime": "2026-02-21T12:12:08.352Z",
|
||||
"buildNumber": 1771675928353
|
||||
"version": "1.0.217",
|
||||
"buildTime": "2026-02-21T14:53:30.534Z",
|
||||
"buildNumber": 1771685610534
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user