mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-11-12 05:37:07 +00:00
The People chain has no pezRewards.recordTrustScore and no userEpochScores since the payroll started holding the roll still through each claim window: a claim pays the trust score at claim time times the epoch's rate, plus the seat share for a Parlementer seat taken by the time of the roll. The app still asked for the recorded score, so the claimable list was always empty, and its Record Trust Score button signed a call that does not exist. The button, its handler and its texts are removed, and the claimable amount is worked out as the chain's entitlement does.
1350 lines
59 KiB
TypeScript
1350 lines
59 KiB
TypeScript
/**
|
||
* Rewards Section - Referral System
|
||
* Uses real blockchain data from ReferralContext
|
||
*/
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import {
|
||
Gift,
|
||
Users,
|
||
Trophy,
|
||
Copy,
|
||
Check,
|
||
Share2,
|
||
RefreshCw,
|
||
Star,
|
||
TrendingUp,
|
||
Award,
|
||
Zap,
|
||
Coins,
|
||
Shield,
|
||
Target,
|
||
Sparkles,
|
||
GraduationCap,
|
||
Play,
|
||
Globe2,
|
||
Clock,
|
||
Loader2,
|
||
} from 'lucide-react';
|
||
import { cn, formatAddress } from '@/lib/utils';
|
||
import { useTelegram } from '@/hooks/useTelegram';
|
||
import { useAuth } from '@/contexts/AuthContext';
|
||
import { useReferral } from '@/contexts/ReferralContext';
|
||
import { useWallet } from '@/contexts/WalletContext';
|
||
import { SocialLinks } from '@/components/SocialLinks';
|
||
import { useTranslation } from '@/i18n';
|
||
import {
|
||
getAllScores,
|
||
getStakingScoreStatus,
|
||
startScoreTracking,
|
||
getPezRewards,
|
||
claimPezReward,
|
||
formatDuration,
|
||
getScoreColor,
|
||
getScoreRating,
|
||
type UserScores,
|
||
type StakingScoreStatus,
|
||
type PezRewardInfo,
|
||
} from '@/lib/scores';
|
||
import {
|
||
getStakingRewards,
|
||
formatRewardAmount,
|
||
formatRewardDate,
|
||
type StakingRewardsResult,
|
||
} from '@/lib/subquery';
|
||
import {
|
||
getUnclaimedRewards,
|
||
payoutStakingReward,
|
||
payoutAllRewards,
|
||
type UnclaimedRewardsResult,
|
||
} from '@/lib/staking-rewards';
|
||
import {
|
||
getCitizenshipStatus,
|
||
getCitizenCount,
|
||
confirmCitizenship,
|
||
approveReferral,
|
||
type CitizenshipStatus,
|
||
} from '@/lib/citizenship';
|
||
import { KurdistanSun } from '@/components/KurdistanSun';
|
||
import { CitizenshipModal } from '@/components/CitizenshipModal';
|
||
|
||
// Activity tracking constants
|
||
const ACTIVITY_STORAGE_KEY = 'pezkuwi_last_active';
|
||
const ACTIVITY_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||
|
||
export function RewardsSection() {
|
||
const { hapticImpact, hapticNotification, shareUrl, showAlert } = useTelegram();
|
||
const { user: authUser } = useAuth();
|
||
const { stats, myReferrals, pendingApprovals, loading, refreshStats } = useReferral();
|
||
const { isConnected, address, peopleApi, assetHubApi, keypair } = useWallet();
|
||
const { t } = useTranslation();
|
||
|
||
const [copied, setCopied] = useState(false);
|
||
const [activeTab, setActiveTab] = useState<'overview' | 'referrals' | 'scores'>('overview');
|
||
const [isActive, setIsActive] = useState(false);
|
||
const [timeRemaining, setTimeRemaining] = useState<string | null>(null);
|
||
const [userScores, setUserScores] = useState<UserScores | null>(null);
|
||
const [stakingStatus, setStakingStatus] = useState<StakingScoreStatus | null>(null);
|
||
const [stakingRewards, setStakingRewards] = useState<StakingRewardsResult | null>(null);
|
||
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('');
|
||
const [unclaimedRewards, setUnclaimedRewards] = useState<UnclaimedRewardsResult | null>(null);
|
||
const [claimingStaking, setClaimingStaking] = useState(false);
|
||
const [claimingStakingEra, setClaimingStakingEra] = useState<number | null>(null);
|
||
const [approvingAddress, setApprovingAddress] = useState<string | null>(null);
|
||
const [showCitizenshipModal, setShowCitizenshipModal] = useState(false);
|
||
|
||
// Check activity status
|
||
const checkActivityStatus = useCallback(() => {
|
||
const lastActive = localStorage.getItem(ACTIVITY_STORAGE_KEY);
|
||
if (lastActive) {
|
||
const lastActiveTime = parseInt(lastActive, 10);
|
||
const now = Date.now();
|
||
const elapsed = now - lastActiveTime;
|
||
|
||
if (elapsed < ACTIVITY_DURATION_MS) {
|
||
setIsActive(true);
|
||
const remaining = ACTIVITY_DURATION_MS - elapsed;
|
||
const hours = Math.floor(remaining / (60 * 60 * 1000));
|
||
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
|
||
setTimeRemaining(`${hours}s ${minutes}d`);
|
||
} else {
|
||
setIsActive(false);
|
||
setTimeRemaining(null);
|
||
}
|
||
} else {
|
||
setIsActive(false);
|
||
setTimeRemaining(null);
|
||
}
|
||
}, []);
|
||
|
||
// Check activity status on mount and every minute
|
||
useEffect(() => {
|
||
const timeoutId = setTimeout(checkActivityStatus, 0);
|
||
const interval = setInterval(checkActivityStatus, 60000);
|
||
return () => {
|
||
clearTimeout(timeoutId);
|
||
clearInterval(interval);
|
||
};
|
||
}, [checkActivityStatus]);
|
||
|
||
// Fetch user scores when on scores tab
|
||
const fetchUserScores = useCallback(async () => {
|
||
if (!address) {
|
||
setUserScores(null);
|
||
setStakingStatus(null);
|
||
setStakingRewards(null);
|
||
setPezRewards(null);
|
||
setUnclaimedRewards(null);
|
||
return;
|
||
}
|
||
|
||
setScoresLoading(true);
|
||
try {
|
||
const [scores, staking, rewards, pezRewardsData, unclaimed] = await Promise.all([
|
||
getAllScores(peopleApi, address),
|
||
peopleApi ? getStakingScoreStatus(peopleApi, address) : Promise.resolve(null),
|
||
getStakingRewards(address),
|
||
peopleApi ? getPezRewards(peopleApi, address) : Promise.resolve(null),
|
||
assetHubApi ? getUnclaimedRewards(assetHubApi, address) : Promise.resolve(null),
|
||
]);
|
||
setUserScores(scores);
|
||
setStakingStatus(staking);
|
||
setStakingRewards(rewards);
|
||
setPezRewards(pezRewardsData);
|
||
setUnclaimedRewards(unclaimed);
|
||
} catch (err) {
|
||
console.error('Error fetching scores:', err);
|
||
} finally {
|
||
setScoresLoading(false);
|
||
}
|
||
}, [peopleApi, assetHubApi, address]);
|
||
|
||
// Fetch scores when tab changes to scores or on initial load
|
||
useEffect(() => {
|
||
if (activeTab === 'scores' && address) {
|
||
fetchUserScores();
|
||
}
|
||
}, [activeTab, address, fetchUserScores]);
|
||
|
||
// Fetch citizenship status and citizen count
|
||
useEffect(() => {
|
||
if (!peopleApi) return;
|
||
if (address) {
|
||
getCitizenshipStatus(peopleApi, address).then(setCitizenshipStatus);
|
||
}
|
||
getCitizenCount(peopleApi).then(setCitizenCount);
|
||
}, [peopleApi, address]);
|
||
|
||
const handleConfirmCitizenship = async () => {
|
||
if (!peopleApi || !keypair) return;
|
||
setShowConfirmAnimation(true);
|
||
hapticImpact('medium');
|
||
try {
|
||
const result = await confirmCitizenship(peopleApi, keypair);
|
||
if (result.success) {
|
||
hapticNotification('success');
|
||
setCitizenshipStatus('Approved');
|
||
showAlert(t('rewards.citizenshipConfirmed'));
|
||
} else {
|
||
hapticNotification('error');
|
||
showAlert(result.error || t('rewards.citizenshipFailed'));
|
||
}
|
||
} catch (err) {
|
||
hapticNotification('error');
|
||
showAlert(err instanceof Error ? err.message : t('rewards.citizenshipFailed'));
|
||
} finally {
|
||
setShowConfirmAnimation(false);
|
||
}
|
||
};
|
||
|
||
const handleStartTracking = async () => {
|
||
if (!peopleApi || !keypair) return;
|
||
setTrackingAnimationText(t('rewards.startingTracking'));
|
||
setShowTrackingAnimation(true);
|
||
hapticImpact('medium');
|
||
try {
|
||
const result = await startScoreTracking(peopleApi, keypair);
|
||
if (result.success) {
|
||
hapticNotification('success');
|
||
showAlert(t('rewards.trackingStarted'));
|
||
fetchUserScores();
|
||
} else {
|
||
hapticNotification('error');
|
||
showAlert(result.error || t('rewards.trackingFailed'));
|
||
}
|
||
} catch (err) {
|
||
hapticNotification('error');
|
||
showAlert(err instanceof Error ? err.message : t('rewards.trackingFailed'));
|
||
} finally {
|
||
setShowTrackingAnimation(false);
|
||
}
|
||
};
|
||
|
||
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 handleClaimStakingReward = async (validator: string, era: number, page?: number) => {
|
||
if (!assetHubApi || !keypair) return;
|
||
setClaimingStakingEra(era);
|
||
setTrackingAnimationText(t('rewards.claimingStakingReward'));
|
||
setShowTrackingAnimation(true);
|
||
hapticImpact('medium');
|
||
try {
|
||
const result = await payoutStakingReward(assetHubApi, keypair, validator, era, page);
|
||
if (result.success) {
|
||
hapticNotification('success');
|
||
showAlert(t('rewards.stakingClaimSuccess'));
|
||
fetchUserScores();
|
||
} else {
|
||
hapticNotification('error');
|
||
showAlert(result.error || t('rewards.stakingClaimFailed'));
|
||
}
|
||
} catch (err) {
|
||
hapticNotification('error');
|
||
showAlert(err instanceof Error ? err.message : t('rewards.stakingClaimFailed'));
|
||
} finally {
|
||
setShowTrackingAnimation(false);
|
||
setClaimingStakingEra(null);
|
||
}
|
||
};
|
||
|
||
const handleClaimAllStaking = async () => {
|
||
if (!assetHubApi || !keypair || !unclaimedRewards?.unclaimed.length) return;
|
||
setClaimingStaking(true);
|
||
setTrackingAnimationText(t('rewards.claimingStakingReward'));
|
||
setShowTrackingAnimation(true);
|
||
hapticImpact('medium');
|
||
try {
|
||
const result = await payoutAllRewards(assetHubApi, keypair, unclaimedRewards.unclaimed);
|
||
if (result.success) {
|
||
hapticNotification('success');
|
||
showAlert(t('rewards.stakingClaimSuccess'));
|
||
fetchUserScores();
|
||
} else {
|
||
hapticNotification('error');
|
||
showAlert(result.error || t('rewards.stakingClaimFailed'));
|
||
}
|
||
} catch (err) {
|
||
hapticNotification('error');
|
||
showAlert(err instanceof Error ? err.message : t('rewards.stakingClaimFailed'));
|
||
} finally {
|
||
setShowTrackingAnimation(false);
|
||
setClaimingStaking(false);
|
||
}
|
||
};
|
||
|
||
const handleActivate = () => {
|
||
hapticNotification('success');
|
||
localStorage.setItem(ACTIVITY_STORAGE_KEY, Date.now().toString());
|
||
setIsActive(true);
|
||
setTimeRemaining('24s 0d');
|
||
showAlert(t('rewards.activatedAlert'));
|
||
};
|
||
|
||
const handleApproveReferral = async (applicantAddress: string) => {
|
||
if (!peopleApi || !keypair) return;
|
||
setApprovingAddress(applicantAddress);
|
||
hapticImpact('medium');
|
||
try {
|
||
const result = await approveReferral(peopleApi, keypair, applicantAddress);
|
||
if (result.success) {
|
||
hapticNotification('success');
|
||
showAlert(t('rewards.referralApprovalSuccess'));
|
||
refreshStats();
|
||
} else {
|
||
hapticNotification('error');
|
||
showAlert(result.error || t('rewards.referralApprovalFailed'));
|
||
}
|
||
} catch (err) {
|
||
hapticNotification('error');
|
||
showAlert(err instanceof Error ? err.message : t('rewards.referralApprovalFailed'));
|
||
} finally {
|
||
setApprovingAddress(null);
|
||
}
|
||
};
|
||
|
||
// Telegram referral link (for sharing) - use authenticated user ID
|
||
const referralLink = authUser?.telegram_id
|
||
? `https://t.me/pezkuwichainBot?start=ref_${authUser.telegram_id}`
|
||
: 'https://t.me/pezkuwichainBot';
|
||
|
||
// Full share message: invitation text + link + wallet address for manual paste
|
||
const shareMessage = address
|
||
? `${t('rewards.shareText')}\n${referralLink}\n\n${t('rewards.referralInstruction')}\n${address}`
|
||
: `${t('rewards.shareText')}\n${referralLink}`;
|
||
|
||
const handleCopy = async () => {
|
||
try {
|
||
await window.navigator.clipboard.writeText(shareMessage);
|
||
setCopied(true);
|
||
hapticNotification('success');
|
||
setTimeout(() => setCopied(false), 2000);
|
||
} catch {
|
||
showAlert(t('rewards.copyAlert'));
|
||
}
|
||
};
|
||
|
||
const handleShare = () => {
|
||
hapticImpact('medium');
|
||
shareUrl(
|
||
referralLink,
|
||
address
|
||
? `${t('rewards.shareText')}\n\n${t('rewards.referralInstruction')}\n${address}`
|
||
: t('rewards.shareText')
|
||
);
|
||
};
|
||
|
||
const handleRefresh = () => {
|
||
hapticImpact('medium');
|
||
refreshStats();
|
||
};
|
||
|
||
// Calculate points per referral based on position
|
||
const getPointsForPosition = (position: number): number => {
|
||
if (position <= 10) return 10;
|
||
if (position <= 50) return 5;
|
||
if (position <= 100) return 4;
|
||
return 0;
|
||
};
|
||
|
||
if (!isConnected) {
|
||
return (
|
||
<div className="flex flex-col h-full items-center justify-center p-8 text-center">
|
||
<Gift className="w-16 h-16 text-purple-400 mb-4" />
|
||
<h2 className="text-xl font-semibold mb-2">{t('rewards.subtitle')}</h2>
|
||
<p className="text-muted-foreground mb-4">{t('rewards.connectWalletFirst')}</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col h-full">
|
||
{/* Header */}
|
||
<header className="flex-shrink-0 px-4 py-3 border-b border-border bg-background/80 backdrop-blur-sm safe-area-top">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center">
|
||
<Gift className="w-4 h-4 text-purple-400" />
|
||
</div>
|
||
<h1 className="text-lg font-semibold">{t('rewards.title')}</h1>
|
||
</div>
|
||
<button
|
||
onClick={handleRefresh}
|
||
disabled={loading}
|
||
className="p-2 rounded-lg hover:bg-secondary"
|
||
>
|
||
<RefreshCw
|
||
className={`w-5 h-5 text-muted-foreground ${loading ? 'animate-spin' : ''}`}
|
||
/>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<div className="flex gap-1 bg-secondary/50 rounded-lg p-1">
|
||
{[
|
||
{ id: 'overview' as const, label: t('rewards.overview') },
|
||
{ id: 'referrals' as const, label: t('rewards.referral') },
|
||
{ id: 'scores' as const, label: t('rewards.scores') },
|
||
].map(({ id, label }) => (
|
||
<button
|
||
key={id}
|
||
onClick={() => {
|
||
hapticImpact('light');
|
||
setActiveTab(id);
|
||
}}
|
||
className={cn(
|
||
'flex-1 py-2 rounded-md text-sm transition-colors',
|
||
activeTab === id
|
||
? 'bg-background text-foreground shadow-sm'
|
||
: 'text-muted-foreground'
|
||
)}
|
||
>
|
||
{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</header>
|
||
|
||
<div className="flex-1 overflow-y-auto hide-scrollbar">
|
||
{/* Overview Tab */}
|
||
{activeTab === 'overview' && (
|
||
<div className="p-4 space-y-4">
|
||
{loading ? (
|
||
<div className="grid grid-cols-2 gap-3">
|
||
{[1, 2, 3, 4].map((i) => (
|
||
<div key={i} className="bg-secondary/50 rounded-xl p-4 animate-pulse">
|
||
<div className="h-4 bg-secondary rounded w-1/2 mb-2" />
|
||
<div className="h-6 bg-secondary rounded w-3/4" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<>
|
||
{/* Citizen Count Card */}
|
||
<div className="bg-gradient-to-br from-emerald-600 to-teal-700 rounded-2xl p-4 text-white">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div>
|
||
<p className="text-emerald-100 text-sm font-medium">
|
||
{t('rewards.citizenCountTitle')}
|
||
</p>
|
||
<p className="text-4xl font-bold mt-1">
|
||
{citizenCount !== null ? citizenCount.toLocaleString() : '...'}
|
||
</p>
|
||
</div>
|
||
<div className="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center">
|
||
<KurdistanSun size={40} />
|
||
</div>
|
||
</div>
|
||
<p className="text-emerald-200/80 text-xs mb-3">
|
||
{t('rewards.citizenCountDesc')}
|
||
</p>
|
||
<button
|
||
onClick={() => {
|
||
hapticImpact('medium');
|
||
setShowCitizenshipModal(true);
|
||
}}
|
||
className="w-full py-2.5 bg-white/20 hover:bg-white/30 rounded-xl text-sm font-medium flex items-center justify-center gap-2 transition-colors"
|
||
>
|
||
<Globe2 className="w-4 h-4" />
|
||
{t('rewards.beCitizen')}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Score Card */}
|
||
<div className="bg-gradient-to-br from-purple-600 to-pink-600 rounded-2xl p-4 text-white">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div>
|
||
<p className="text-purple-100 text-sm">{t('rewards.referralScore')}</p>
|
||
<p className="text-4xl font-bold">{stats?.referralScore ?? 0}</p>
|
||
</div>
|
||
<div className="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center">
|
||
<Trophy className="w-8 h-8" />
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-sm text-purple-100">
|
||
<TrendingUp className="w-4 h-4" />
|
||
<span>{t('rewards.maxScore')}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Refer More Card */}
|
||
<div className="bg-gradient-to-r from-amber-500/20 to-orange-500/20 border border-amber-500/30 rounded-xl p-4">
|
||
<div className="flex items-start gap-3">
|
||
<div className="w-10 h-10 rounded-full bg-amber-500/30 flex items-center justify-center flex-shrink-0">
|
||
<Coins className="w-5 h-5 text-amber-400" />
|
||
</div>
|
||
<div>
|
||
<h4 className="font-semibold text-amber-100 mb-1">
|
||
{t('rewards.referMoreTitle')}
|
||
</h4>
|
||
<p className="text-sm text-amber-200/80">
|
||
{t('rewards.referMoreDescription')}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stats Grid */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Users className="w-4 h-4 text-green-400" />
|
||
<span className="text-xs text-muted-foreground">{t('rewards.referral')}</span>
|
||
</div>
|
||
<p className="text-2xl font-bold text-foreground">
|
||
{stats?.referralCount ?? 0}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">{t('rewards.kycApproved')}</p>
|
||
</div>
|
||
|
||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Award className="w-4 h-4 text-blue-400" />
|
||
<span className="text-xs text-muted-foreground">{t('rewards.referrer')}</span>
|
||
</div>
|
||
{stats?.whoInvitedMe ? (
|
||
<p className="text-sm font-mono text-foreground truncate">
|
||
{formatAddress(stats.whoInvitedMe, 6)}
|
||
</p>
|
||
) : (
|
||
<p className="text-sm text-muted-foreground">{t('rewards.none')}</p>
|
||
)}
|
||
<p className="text-xs text-muted-foreground">{t('rewards.invitedMe')}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Pending Referral Notification */}
|
||
{stats?.pendingReferral && (
|
||
<div className="bg-blue-900/20 border border-blue-600/30 rounded-xl p-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-full bg-blue-600/30 flex items-center justify-center">
|
||
<Award className="w-5 h-5 text-blue-400" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<div className="text-white font-semibold">
|
||
{t('rewards.pendingReferral')}
|
||
</div>
|
||
<div className="text-sm text-blue-300">
|
||
{t('rewards.completeKyc')}{' '}
|
||
<span className="font-mono">
|
||
{formatAddress(stats.pendingReferral, 6)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Citizenship Status Card */}
|
||
{(citizenshipStatus === 'PendingReferral' ||
|
||
citizenshipStatus === 'ReferrerApproved') && (
|
||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<Shield className="w-5 h-5 text-emerald-400" />
|
||
<h3 className="font-medium text-foreground">
|
||
{t('rewards.citizenshipStatus')}
|
||
</h3>
|
||
</div>
|
||
<div className="flex items-center gap-3 mb-3">
|
||
<div
|
||
className={cn(
|
||
'flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium',
|
||
citizenshipStatus === 'PendingReferral'
|
||
? 'bg-green-500/20 text-green-400'
|
||
: 'bg-secondary text-muted-foreground'
|
||
)}
|
||
>
|
||
<div
|
||
className={cn(
|
||
'w-2 h-2 rounded-full',
|
||
citizenshipStatus === 'PendingReferral'
|
||
? 'bg-green-400'
|
||
: 'bg-muted-foreground'
|
||
)}
|
||
/>
|
||
{t('rewards.pendingApproval')}
|
||
</div>
|
||
<div
|
||
className={cn(
|
||
'flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium',
|
||
citizenshipStatus === 'ReferrerApproved'
|
||
? 'bg-green-500/20 text-green-400'
|
||
: 'bg-secondary text-muted-foreground'
|
||
)}
|
||
>
|
||
{citizenshipStatus === 'ReferrerApproved' ? (
|
||
<Check className="w-3 h-3" />
|
||
) : (
|
||
<div className="w-2 h-2 rounded-full bg-muted-foreground" />
|
||
)}
|
||
{t('rewards.approved')}
|
||
</div>
|
||
</div>
|
||
{citizenshipStatus === 'PendingReferral' && (
|
||
<p className="text-sm text-muted-foreground">
|
||
{t('rewards.waitingReferrer')}
|
||
</p>
|
||
)}
|
||
{citizenshipStatus === 'ReferrerApproved' && (
|
||
<button
|
||
onClick={handleConfirmCitizenship}
|
||
disabled={!keypair}
|
||
className="w-full py-3 rounded-lg font-medium flex items-center justify-center gap-2 bg-gradient-to-r from-green-500 to-emerald-600 text-white hover:opacity-90 transition-all"
|
||
>
|
||
<Check className="w-5 h-5" />
|
||
{t('rewards.confirmCitizenship')}
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Invite Card */}
|
||
<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">
|
||
<Share2 className="w-4 h-4 text-primary" />
|
||
{t('rewards.inviteFriends')}
|
||
</h3>
|
||
|
||
<div className="bg-background rounded-lg p-3 mb-3">
|
||
<p className="text-xs text-muted-foreground mb-1">{t('rewards.yourLink')}</p>
|
||
<code className="text-sm text-foreground break-all">{referralLink}</code>
|
||
</div>
|
||
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={handleCopy}
|
||
className={cn(
|
||
'flex-1 flex items-center justify-center gap-2 py-2.5 rounded-lg text-sm font-medium transition-colors',
|
||
copied
|
||
? 'bg-green-500/20 text-green-400'
|
||
: 'bg-secondary hover:bg-secondary/80'
|
||
)}
|
||
>
|
||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||
{copied ? t('rewards.copySuccess') : t('rewards.copyLink')}
|
||
</button>
|
||
<button
|
||
onClick={handleShare}
|
||
className="flex-1 flex items-center justify-center gap-2 py-2.5 bg-primary rounded-lg text-primary-foreground text-sm font-medium"
|
||
>
|
||
<Share2 className="w-4 h-4" />
|
||
{t('rewards.shareLink')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Score System */}
|
||
<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">
|
||
<Star className="w-4 h-4 text-yellow-400" />
|
||
{t('rewards.scoreSystem')}
|
||
</h3>
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex justify-between py-2 border-b border-border/30">
|
||
<span className="text-muted-foreground">1-10 referral</span>
|
||
<span className="text-green-400 font-medium">×10 pûan</span>
|
||
</div>
|
||
<div className="flex justify-between py-2 border-b border-border/30">
|
||
<span className="text-muted-foreground">11-50 referral</span>
|
||
<span className="text-green-400 font-medium">100 + ×5</span>
|
||
</div>
|
||
<div className="flex justify-between py-2 border-b border-border/30">
|
||
<span className="text-muted-foreground">51-100 referral</span>
|
||
<span className="text-green-400 font-medium">300 + ×4</span>
|
||
</div>
|
||
<div className="flex justify-between py-2">
|
||
<span className="text-muted-foreground">101+ referral</span>
|
||
<span className="text-yellow-400 font-medium">500 (Max)</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* I am Active Button */}
|
||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center gap-2">
|
||
<Zap
|
||
className={cn('w-5 h-5', isActive ? 'text-green-400' : 'text-gray-400')}
|
||
/>
|
||
<div>
|
||
<h3 className="font-medium text-foreground">{t('rewards.activeStatus')}</h3>
|
||
{isActive && timeRemaining && (
|
||
<p className="text-xs text-green-400">
|
||
{t('rewards.timeRemaining', { time: timeRemaining })}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div
|
||
className={cn(
|
||
'px-2 py-1 rounded-full text-xs font-medium',
|
||
isActive ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
||
)}
|
||
>
|
||
{isActive ? t('rewards.active') : t('rewards.inactive')}
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-muted-foreground mb-3">
|
||
{t('rewards.activeDescription')}
|
||
</p>
|
||
<button
|
||
onClick={handleActivate}
|
||
disabled={isActive}
|
||
className={cn(
|
||
'w-full py-3 rounded-lg font-medium flex items-center justify-center gap-2 transition-all',
|
||
isActive
|
||
? 'bg-green-500/20 text-green-400 cursor-not-allowed'
|
||
: 'bg-gradient-to-r from-green-500 to-emerald-600 text-white hover:opacity-90'
|
||
)}
|
||
>
|
||
<Zap className="w-5 h-5" />
|
||
{isActive ? t('rewards.youAreActive') : t('rewards.iAmActive')}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Social Links */}
|
||
<SocialLinks />
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Referrals Tab */}
|
||
{activeTab === 'referrals' && (
|
||
<div className="p-4 space-y-4">
|
||
{/* Refer More Card */}
|
||
<div className="bg-gradient-to-r from-amber-500/20 to-orange-500/20 border border-amber-500/30 rounded-xl p-4">
|
||
<div className="flex items-start gap-3">
|
||
<div className="w-10 h-10 rounded-full bg-amber-500/30 flex items-center justify-center flex-shrink-0">
|
||
<Coins className="w-5 h-5 text-amber-400" />
|
||
</div>
|
||
<div>
|
||
<h4 className="font-semibold text-amber-100 mb-1">
|
||
{t('rewards.referMoreTitle')}
|
||
</h4>
|
||
<p className="text-sm text-amber-200/80">{t('rewards.referMoreDescription')}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* I am Active Button */}
|
||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center gap-2">
|
||
<Zap className={cn('w-5 h-5', isActive ? 'text-green-400' : 'text-gray-400')} />
|
||
<div>
|
||
<h3 className="font-medium text-foreground">{t('rewards.activeStatus')}</h3>
|
||
{isActive && timeRemaining && (
|
||
<p className="text-xs text-green-400">
|
||
{t('rewards.timeRemaining', { time: timeRemaining })}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div
|
||
className={cn(
|
||
'px-2 py-1 rounded-full text-xs font-medium',
|
||
isActive ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
||
)}
|
||
>
|
||
{isActive ? t('rewards.active') : t('rewards.inactive')}
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={handleActivate}
|
||
disabled={isActive}
|
||
className={cn(
|
||
'w-full py-3 rounded-lg font-medium flex items-center justify-center gap-2 transition-all',
|
||
isActive
|
||
? 'bg-green-500/20 text-green-400 cursor-not-allowed'
|
||
: 'bg-gradient-to-r from-green-500 to-emerald-600 text-white hover:opacity-90'
|
||
)}
|
||
>
|
||
<Zap className="w-5 h-5" />
|
||
{isActive ? t('rewards.youAreActive') : t('rewards.iAmActive')}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Pending Approvals Section */}
|
||
{pendingApprovals.length > 0 && (
|
||
<div className="bg-gradient-to-r from-orange-500/20 to-amber-500/20 border border-orange-500/30 rounded-xl p-4">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<Clock className="w-5 h-5 text-orange-400" />
|
||
<h3 className="font-semibold text-orange-100">{t('rewards.pendingApprovals')}</h3>
|
||
<span className="bg-orange-500/30 text-orange-300 text-xs font-medium px-2 py-0.5 rounded-full">
|
||
{pendingApprovals.length}
|
||
</span>
|
||
</div>
|
||
<div className="space-y-2">
|
||
{pendingApprovals.map((approval) => (
|
||
<div
|
||
key={approval.applicantAddress}
|
||
className="flex items-center gap-3 bg-black/20 rounded-lg p-3"
|
||
>
|
||
<div className="flex-1 min-w-0">
|
||
<code className="text-sm text-foreground">
|
||
{formatAddress(approval.applicantAddress, 8)}
|
||
</code>
|
||
<p className="text-xs text-orange-300">
|
||
{t('rewards.pendingReferralStatus')}
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => handleApproveReferral(approval.applicantAddress)}
|
||
disabled={approvingAddress !== null}
|
||
className="px-4 py-2 rounded-lg text-sm font-medium bg-orange-500/30 text-orange-200 hover:bg-orange-500/40 transition-all disabled:opacity-50 flex items-center gap-2"
|
||
>
|
||
{approvingAddress === approval.applicantAddress ? (
|
||
<>
|
||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||
{t('rewards.approvingReferral')}
|
||
</>
|
||
) : (
|
||
<>
|
||
<Check className="w-3.5 h-3.5" />
|
||
{t('rewards.approveReferral')}
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{loading ? (
|
||
<div className="space-y-3">
|
||
{[1, 2, 3, 4, 5].map((i) => (
|
||
<div key={i} className="bg-secondary/50 rounded-xl p-4 animate-pulse">
|
||
<div className="h-4 bg-secondary rounded w-2/3" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : myReferrals.length > 0 ? (
|
||
<div className="space-y-3">
|
||
<div className="text-sm text-muted-foreground mb-2">
|
||
{t('rewards.referralCount', { count: myReferrals.length })}
|
||
</div>
|
||
{myReferrals.map((referralAddress, index) => (
|
||
<div
|
||
key={referralAddress}
|
||
className="bg-secondary/30 rounded-xl p-4 border border-border/50 flex items-center gap-3"
|
||
>
|
||
<div className="w-10 h-10 rounded-full bg-green-500/20 flex items-center justify-center text-sm font-bold text-green-400">
|
||
{index + 1}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<code className="text-sm text-foreground">
|
||
{formatAddress(referralAddress, 8)}
|
||
</code>
|
||
<p className="text-xs text-green-400">{t('rewards.kycApproved')}</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<span className="text-green-400 text-sm font-medium">
|
||
+{getPointsForPosition(index + 1)} {t('rewards.points')}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||
<Users className="w-12 h-12 text-muted-foreground mb-4" />
|
||
<p className="text-muted-foreground">{t('rewards.noReferrals')}</p>
|
||
<p className="text-sm text-muted-foreground mt-1">{t('rewards.shareYourLink')}</p>
|
||
<button
|
||
onClick={handleShare}
|
||
className="mt-4 flex items-center gap-2 px-4 py-2 bg-primary rounded-lg text-primary-foreground text-sm font-medium"
|
||
>
|
||
<Share2 className="w-4 h-4" />
|
||
{t('rewards.shareLink')}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Scores Tab */}
|
||
{activeTab === 'scores' && (
|
||
<div className="p-4 space-y-4">
|
||
{scoresLoading ? (
|
||
<div className="space-y-4">
|
||
{[1, 2, 3, 4].map((i) => (
|
||
<div key={i} className="bg-secondary/50 rounded-xl p-4 animate-pulse">
|
||
<div className="h-6 bg-secondary rounded w-1/2 mb-2" />
|
||
<div className="h-8 bg-secondary rounded w-1/3" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<>
|
||
{/* Trust Score - Main Card */}
|
||
<div className="bg-gradient-to-br from-purple-600 to-indigo-600 rounded-2xl p-5 text-white">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div>
|
||
<p className="text-purple-100 text-sm flex items-center gap-2">
|
||
<Shield className="w-4 h-4" />
|
||
{t('rewards.trustScore')}
|
||
</p>
|
||
<p
|
||
className={cn(
|
||
'text-5xl font-bold mt-1',
|
||
getScoreColor(userScores?.trustScore || 0)
|
||
)}
|
||
>
|
||
{userScores?.trustScore ?? 0}
|
||
</p>
|
||
</div>
|
||
<div className="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center">
|
||
<Trophy className="w-8 h-8" />
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center justify-between text-sm">
|
||
<span className="text-purple-100">
|
||
{t('rewards.rank', { rating: getScoreRating(userScores?.trustScore || 0) })}
|
||
</span>
|
||
{userScores?.isCitizen && (
|
||
<span className="bg-green-500/30 text-green-200 px-2 py-1 rounded-full text-xs">
|
||
{t('rewards.citizen')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Score Components Grid */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
{/* Staking Score */}
|
||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Target className="w-4 h-4 text-blue-400" />
|
||
<span className="text-xs text-muted-foreground">{t('rewards.staking')}</span>
|
||
</div>
|
||
<p className="text-2xl font-bold text-blue-400">
|
||
{userScores?.stakingScore ?? 0}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground mt-1">
|
||
{stakingStatus?.isTracking
|
||
? stakingStatus.hasCachedData
|
||
? formatDuration(stakingStatus.durationBlocks)
|
||
: t('rewards.stakingWaitingData')
|
||
: t('rewards.stakingNotStarted')}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Referral Score */}
|
||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Users className="w-4 h-4 text-green-400" />
|
||
<span className="text-xs text-muted-foreground">{t('rewards.referral')}</span>
|
||
</div>
|
||
<p className="text-2xl font-bold text-green-400">
|
||
{userScores?.referralScore ?? 0}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground mt-1">
|
||
{t('rewards.people', { count: stats?.referralCount ?? 0 })}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Tiki Score */}
|
||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Sparkles className="w-4 h-4 text-yellow-400" />
|
||
<span className="text-xs text-muted-foreground">{t('rewards.tiki')}</span>
|
||
</div>
|
||
<p className="text-2xl font-bold text-yellow-400">
|
||
{userScores?.tikiScore ?? 0}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground mt-1">{t('rewards.nftRole')}</p>
|
||
</div>
|
||
|
||
{/* Perwerde Score */}
|
||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<GraduationCap className="w-4 h-4 text-pink-400" />
|
||
<span className="text-xs text-muted-foreground">
|
||
{t('rewards.education')}
|
||
</span>
|
||
</div>
|
||
<p className="text-2xl font-bold text-pink-400">
|
||
{userScores?.perwerdeScore ?? 0}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground mt-1">{t('rewards.reading')}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Start Tracking Button. Once tracking, there is nothing to do each month: the
|
||
chain holds every score still while an epoch is claimed. */}
|
||
{!stakingStatus?.isTracking ? (
|
||
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
|
||
<button
|
||
onClick={handleStartTracking}
|
||
disabled={!keypair || !peopleApi}
|
||
className="w-full py-3 rounded-lg font-medium flex items-center justify-center gap-2 bg-gradient-to-r from-blue-500 to-cyan-600 text-white hover:opacity-90 transition-all disabled:opacity-50"
|
||
>
|
||
<Play className="w-5 h-5" />
|
||
{t('rewards.startTracking')}
|
||
</button>
|
||
<p className="text-xs text-muted-foreground text-center mt-2">
|
||
{t('rewards.startTrackingDesc')}
|
||
</p>
|
||
</div>
|
||
) : null}
|
||
|
||
{/* 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>
|
||
</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>
|
||
|
||
{/* HEZ Staking 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">
|
||
<Coins className="w-4 h-4 text-amber-400" />
|
||
{t('rewards.stakingRewards')}
|
||
</h3>
|
||
|
||
{/* Unclaimed Rewards */}
|
||
<div className="bg-amber-500/10 rounded-lg p-3 mb-3 border border-amber-500/20">
|
||
<p className="text-xs text-amber-300 mb-1">{t('rewards.unclaimedRewards')}</p>
|
||
<p className="text-2xl font-bold text-amber-400">
|
||
{unclaimedRewards && unclaimedRewards.totalUnclaimedRaw > 0n
|
||
? `${unclaimedRewards.totalUnclaimedHez} HEZ`
|
||
: '0 HEZ'}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Unclaimed Era List */}
|
||
{unclaimedRewards && unclaimedRewards.unclaimed.length > 0 ? (
|
||
<div className="space-y-2 mb-3">
|
||
{unclaimedRewards.unclaimed.map((reward) => (
|
||
<div
|
||
key={`${reward.era}-${reward.validator}`}
|
||
className="flex items-center justify-between py-2 px-3 bg-amber-500/5 rounded-lg border border-amber-500/10"
|
||
>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm text-foreground">
|
||
{t('rewards.era')} #{reward.era}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground font-mono truncate">
|
||
{reward.validator.slice(0, 8)}...{reward.validator.slice(-6)}
|
||
</p>
|
||
<p className="text-xs text-amber-400 font-medium">
|
||
{reward.estimatedReward} HEZ
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() =>
|
||
handleClaimStakingReward(reward.validator, reward.era, reward.page)
|
||
}
|
||
disabled={!keypair || claimingStaking || claimingStakingEra !== null}
|
||
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-amber-500/20 text-amber-300 hover:bg-amber-500/30 transition-all disabled:opacity-50 ml-2"
|
||
>
|
||
{claimingStakingEra === reward.era ? (
|
||
<RefreshCw className="w-3 h-3 animate-spin" />
|
||
) : (
|
||
t('rewards.claimStakingReward')
|
||
)}
|
||
</button>
|
||
</div>
|
||
))}
|
||
|
||
{/* Claim All Button */}
|
||
{unclaimedRewards.unclaimed.length > 1 && (
|
||
<button
|
||
onClick={handleClaimAllStaking}
|
||
disabled={!keypair || claimingStaking || claimingStakingEra !== 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"
|
||
>
|
||
<Coins className="w-5 h-5" />
|
||
{t('rewards.claimAllStaking')} ({unclaimedRewards.totalUnclaimedHez} HEZ)
|
||
</button>
|
||
)}
|
||
|
||
{/* Single Claim All when only 1 reward */}
|
||
{unclaimedRewards.unclaimed.length === 1 && (
|
||
<button
|
||
onClick={() =>
|
||
handleClaimStakingReward(
|
||
unclaimedRewards.unclaimed[0].validator,
|
||
unclaimedRewards.unclaimed[0].era,
|
||
unclaimedRewards.unclaimed[0].page
|
||
)
|
||
}
|
||
disabled={!keypair || claimingStaking || claimingStakingEra !== 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"
|
||
>
|
||
<Coins className="w-5 h-5" />
|
||
{t('rewards.claimStakingReward')} ({unclaimedRewards.totalUnclaimedHez}{' '}
|
||
HEZ)
|
||
</button>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<p className="text-sm text-muted-foreground text-center py-2">
|
||
{t('rewards.noUnclaimedRewards')}
|
||
</p>
|
||
)}
|
||
|
||
{/* Reward History (SubQuery) */}
|
||
{stakingRewards && stakingRewards.rewards.length > 0 && (
|
||
<div className="border-t border-border/30 pt-3 mt-3">
|
||
<p className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
|
||
<TrendingUp className="w-3 h-3" />
|
||
{t('rewards.rewardHistory')}
|
||
</p>
|
||
<div className="bg-amber-500/5 rounded-lg p-2 mb-2 border border-amber-500/10">
|
||
<p className="text-xs text-amber-300">{t('rewards.totalRewards')}</p>
|
||
<p className="text-lg font-bold text-amber-400">
|
||
{stakingRewards.totalAccumulatedHez.toFixed(4)} HEZ
|
||
</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
{stakingRewards.rewards.map((reward) => (
|
||
<div
|
||
key={reward.id}
|
||
className="flex items-center justify-between py-1.5 border-b border-border/20 last:border-0"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<div
|
||
className={cn(
|
||
'w-2 h-2 rounded-full',
|
||
reward.type === 'REWARD' ? 'bg-green-400' : 'bg-red-400'
|
||
)}
|
||
/>
|
||
<div>
|
||
<p className="text-sm text-foreground">
|
||
{reward.type === 'REWARD' ? '+' : '-'}
|
||
{formatRewardAmount(reward.amount)} HEZ
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
Block #{reward.blockNumber}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<span className="text-xs text-muted-foreground">
|
||
{formatRewardDate(reward.timestamp)}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Score Formula Info */}
|
||
<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">
|
||
<Star className="w-4 h-4 text-yellow-400" />
|
||
{t('rewards.scoreFormula')}
|
||
</h3>
|
||
<div className="text-sm text-muted-foreground space-y-2">
|
||
<p>Trust = (Staking × Weighted Sum) / 100</p>
|
||
<p className="text-xs">
|
||
Weighted Sum = Staking×100 + Referral×300 + Perwerde×300 + Tiki×300
|
||
</p>
|
||
<div className="mt-3 p-2 bg-yellow-500/10 rounded-lg border border-yellow-500/20">
|
||
<p className="text-yellow-300 text-xs">{t('rewards.stakingZeroWarning')}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Refresh Button */}
|
||
<button
|
||
onClick={fetchUserScores}
|
||
disabled={scoresLoading}
|
||
className="w-full py-3 bg-primary rounded-lg text-primary-foreground font-medium flex items-center justify-center gap-2"
|
||
>
|
||
<RefreshCw className={cn('w-4 h-4', scoresLoading && 'animate-spin')} />
|
||
{t('rewards.refreshScores')}
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Confirm Citizenship Overlay */}
|
||
{showConfirmAnimation && (
|
||
<div className="fixed inset-0 z-50 bg-black/90 flex flex-col items-center justify-center">
|
||
<KurdistanSun size={120} />
|
||
<p className="text-white mt-6 text-lg">{t('rewards.confirmingCitizenship')}</p>
|
||
<p className="text-white/60 text-sm mt-2">{t('rewards.signingBlockchain')}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Score Tracking Overlay */}
|
||
{showTrackingAnimation && (
|
||
<div className="fixed inset-0 z-50 bg-black/90 flex flex-col items-center justify-center">
|
||
<KurdistanSun size={120} />
|
||
<p className="text-white mt-6 text-lg">{trackingAnimationText}</p>
|
||
<p className="text-white/60 text-sm mt-2">{t('rewards.signingBlockchain')}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Citizenship Application Modal */}
|
||
<CitizenshipModal
|
||
isOpen={showCitizenshipModal}
|
||
onClose={() => {
|
||
setShowCitizenshipModal(false);
|
||
if (peopleApi && address) {
|
||
getCitizenshipStatus(peopleApi, address).then(setCitizenshipStatus);
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|