mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-04-22 03:07:55 +00:00
feat: add scores tab and DOT token to send list
- Add Puanlar (Scores) tab to Xelat section showing trust, staking, referral, tiki scores - Add scores.ts lib with frontend fallback for staking and trust score calculation - Add DOT token (asset ID 1001) to sendable tokens list
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pezkuwi-telegram-miniapp",
|
||||
"version": "1.0.122",
|
||||
"version": "1.0.123",
|
||||
"type": "module",
|
||||
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
|
||||
"author": "Pezkuwichain Team",
|
||||
|
||||
@@ -703,7 +703,7 @@ export function WalletDashboard({ onDisconnect }: Props) {
|
||||
}
|
||||
|
||||
// Token types for send
|
||||
type SendToken = 'HEZ' | 'PEZ' | 'USDT';
|
||||
type SendToken = 'HEZ' | 'PEZ' | 'USDT' | 'DOT';
|
||||
|
||||
interface TokenOption {
|
||||
symbol: SendToken;
|
||||
@@ -738,6 +738,14 @@ const SEND_TOKENS: TokenOption[] = [
|
||||
assetId: 1000,
|
||||
decimals: 6,
|
||||
},
|
||||
{
|
||||
symbol: 'DOT',
|
||||
name: 'Polkadot',
|
||||
chain: 'Asset Hub',
|
||||
icon: '/tokens/DOT.png',
|
||||
assetId: 1001,
|
||||
decimals: 10,
|
||||
},
|
||||
];
|
||||
|
||||
// Send Tab
|
||||
@@ -754,6 +762,7 @@ function SendTab({ onBack }: { onBack: () => void }) {
|
||||
const [txHash, setTxHash] = useState('');
|
||||
const [pezBalance, setPezBalance] = useState<string>('0.0000');
|
||||
const [usdtBalance, setUsdtBalance] = useState<string>('0.00');
|
||||
const [dotBalance, setDotBalance] = useState<string>('0.0000');
|
||||
|
||||
// Fetch PEZ and USDT balances when Asset Hub is available
|
||||
useEffect(() => {
|
||||
@@ -778,6 +787,15 @@ function SendTab({ onBack }: { onBack: () => void }) {
|
||||
const usdtAmount = assetData.balance.toString();
|
||||
setUsdtBalance((parseInt(usdtAmount) / 1e6).toFixed(2));
|
||||
}
|
||||
|
||||
// Fetch DOT balance (Asset ID: 1001, 10 decimals)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const dotResult = await (assetHubApi.query.assets as any).account(1001, keypair.address);
|
||||
if (dotResult.isSome) {
|
||||
const assetData = dotResult.unwrap();
|
||||
const dotAmount = assetData.balance.toString();
|
||||
setDotBalance((parseInt(dotAmount) / 1e10).toFixed(4));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch asset balances:', err);
|
||||
}
|
||||
@@ -790,6 +808,7 @@ function SendTab({ onBack }: { onBack: () => void }) {
|
||||
if (selectedToken === 'HEZ') return balance ?? '0.0000';
|
||||
if (selectedToken === 'PEZ') return pezBalance;
|
||||
if (selectedToken === 'USDT') return usdtBalance;
|
||||
if (selectedToken === 'DOT') return dotBalance;
|
||||
return '0.0000';
|
||||
};
|
||||
|
||||
@@ -857,7 +876,10 @@ function SendTab({ onBack }: { onBack: () => void }) {
|
||||
setError('Mainnet API amade nîne');
|
||||
return;
|
||||
}
|
||||
if ((selectedToken === 'PEZ' || selectedToken === 'USDT') && !assetHubApi) {
|
||||
if (
|
||||
(selectedToken === 'PEZ' || selectedToken === 'USDT' || selectedToken === 'DOT') &&
|
||||
!assetHubApi
|
||||
) {
|
||||
setError('Asset Hub API amade nîne');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
/**
|
||||
* Score Systems Integration for Telegram Mini App
|
||||
* Based on pwap/shared/lib/scores.ts
|
||||
*
|
||||
* All scores come from People Chain (people-rpc.pezkuwichain.io)
|
||||
* - Trust Score: pezpallet-trust
|
||||
* - Referral Score: pezpallet-referral
|
||||
* - Staking Score: pezpallet-staking-score (with frontend fallback)
|
||||
* - Tiki Score: pezpallet-tiki
|
||||
*/
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import { calculateReferralScore, getReferralCount } from './referral';
|
||||
|
||||
// ========================================
|
||||
// TYPE DEFINITIONS
|
||||
// ========================================
|
||||
|
||||
export interface UserScores {
|
||||
trustScore: number;
|
||||
referralScore: number;
|
||||
stakingScore: number;
|
||||
tikiScore: number;
|
||||
totalScore: number;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STAKING SCORE FRONTEND FALLBACK
|
||||
// ========================================
|
||||
// Until runtime upgrade deploys, calculate staking score
|
||||
// directly from Relay Chain data without People Chain pallet
|
||||
|
||||
const STAKING_SCORE_STORAGE_KEY = 'pez_staking_score_tracking';
|
||||
const UNITS = 1_000_000_000_000; // 10^12
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
const MONTH_IN_MS = 30 * DAY_IN_MS;
|
||||
|
||||
interface StakingTrackingData {
|
||||
[address: string]: {
|
||||
startTime: number;
|
||||
lastChecked: number;
|
||||
lastStakeAmount: string;
|
||||
};
|
||||
}
|
||||
|
||||
function getStakingTrackingData(): StakingTrackingData {
|
||||
if (typeof window === 'undefined') return {};
|
||||
try {
|
||||
const stored = localStorage.getItem(STAKING_SCORE_STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveStakingTrackingData(data: StakingTrackingData): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
localStorage.setItem(STAKING_SCORE_STORAGE_KEY, JSON.stringify(data));
|
||||
} catch (err) {
|
||||
console.error('Failed to save staking tracking data:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch staking details directly from Relay Chain
|
||||
*/
|
||||
export async function fetchRelayStakingDetails(
|
||||
relayApi: ApiPromise,
|
||||
address: string
|
||||
): Promise<{ stakedAmount: bigint; nominationsCount: number } | null> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (!(relayApi?.query as any)?.staking) return null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let ledger = await (relayApi.query.staking as any).ledger?.(address);
|
||||
let stashAddress = address;
|
||||
|
||||
// If no ledger, check if this is a stash account
|
||||
if (!ledger || ledger.isEmpty || ledger.isNone) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const bonded = await (relayApi.query.staking as any).bonded?.(address);
|
||||
if (bonded && !bonded.isEmpty && !bonded.isNone) {
|
||||
const controller = bonded.toString();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ledger = await (relayApi.query.staking as any).ledger?.(controller);
|
||||
stashAddress = address;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ledger || ledger.isEmpty || ledger.isNone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ledgerJson = ledger.toJSON() as { active?: string | number };
|
||||
const active = BigInt(ledgerJson?.active || 0);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const nominations = await (relayApi.query.staking as any).nominators?.(stashAddress);
|
||||
const nominationsJson = nominations?.toJSON() as { targets?: unknown[] } | null;
|
||||
const nominationsCount = nominationsJson?.targets?.length || 0;
|
||||
|
||||
return {
|
||||
stakedAmount: active,
|
||||
nominationsCount,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch relay staking details:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate base score from staked HEZ amount
|
||||
*/
|
||||
function calculateBaseStakingScore(stakedHez: number): number {
|
||||
if (stakedHez <= 0) return 0;
|
||||
if (stakedHez <= 100) return 20;
|
||||
if (stakedHez <= 250) return 30;
|
||||
if (stakedHez <= 750) return 40;
|
||||
return 50; // 751+ HEZ
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time multiplier based on months staked
|
||||
*/
|
||||
function getStakingTimeMultiplier(monthsStaked: number): number {
|
||||
if (monthsStaked >= 12) return 2.0;
|
||||
if (monthsStaked >= 6) return 1.7;
|
||||
if (monthsStaked >= 3) return 1.4;
|
||||
if (monthsStaked >= 1) return 1.2;
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
export interface FrontendStakingScoreResult {
|
||||
score: number;
|
||||
stakedAmount: bigint;
|
||||
stakedHez: number;
|
||||
trackingStarted: Date | null;
|
||||
monthsStaked: number;
|
||||
timeMultiplier: number;
|
||||
nominationsCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get staking score using frontend fallback
|
||||
*/
|
||||
export async function getFrontendStakingScore(
|
||||
relayApi: ApiPromise,
|
||||
address: string
|
||||
): Promise<FrontendStakingScoreResult> {
|
||||
const emptyResult: FrontendStakingScoreResult = {
|
||||
score: 0,
|
||||
stakedAmount: 0n,
|
||||
stakedHez: 0,
|
||||
trackingStarted: null,
|
||||
monthsStaked: 0,
|
||||
timeMultiplier: 1.0,
|
||||
nominationsCount: 0,
|
||||
};
|
||||
|
||||
if (!relayApi || !address) return emptyResult;
|
||||
|
||||
const details = await fetchRelayStakingDetails(relayApi, address);
|
||||
|
||||
if (!details || details.stakedAmount === 0n) {
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
const trackingData = getStakingTrackingData();
|
||||
const now = Date.now();
|
||||
|
||||
if (!trackingData[address]) {
|
||||
trackingData[address] = {
|
||||
startTime: now,
|
||||
lastChecked: now,
|
||||
lastStakeAmount: details.stakedAmount.toString(),
|
||||
};
|
||||
saveStakingTrackingData(trackingData);
|
||||
} else {
|
||||
trackingData[address].lastChecked = now;
|
||||
trackingData[address].lastStakeAmount = details.stakedAmount.toString();
|
||||
saveStakingTrackingData(trackingData);
|
||||
}
|
||||
|
||||
const userTracking = trackingData[address];
|
||||
const trackingStarted = new Date(userTracking.startTime);
|
||||
const msStaked = now - userTracking.startTime;
|
||||
const monthsStaked = Math.floor(msStaked / MONTH_IN_MS);
|
||||
|
||||
const stakedHez = Number(details.stakedAmount) / UNITS;
|
||||
const baseScore = calculateBaseStakingScore(stakedHez);
|
||||
const timeMultiplier = getStakingTimeMultiplier(monthsStaked);
|
||||
const finalScore = Math.min(Math.floor(baseScore * timeMultiplier), 100);
|
||||
|
||||
return {
|
||||
score: finalScore,
|
||||
stakedAmount: details.stakedAmount,
|
||||
stakedHez,
|
||||
trackingStarted,
|
||||
monthsStaked,
|
||||
timeMultiplier,
|
||||
nominationsCount: details.nominationsCount,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TIKI SCORE
|
||||
// ========================================
|
||||
|
||||
export interface TikiInfo {
|
||||
roleId: number;
|
||||
level: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const TIKI_ROLE_SCORES: Record<number, number> = {
|
||||
1: 10, // Basic
|
||||
2: 20, // Bronze
|
||||
3: 30, // Silver
|
||||
4: 40, // Gold
|
||||
5: 50, // Platinum
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch user's tiki roles from People Chain
|
||||
*/
|
||||
export async function fetchUserTikis(peopleApi: ApiPromise, address: string): Promise<TikiInfo[]> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (!(peopleApi?.query as any)?.tiki) return [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await (peopleApi.query.tiki as any).userRoles?.(address);
|
||||
|
||||
if (!result || result.isEmpty) return [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const roles = result.toJSON() as any[];
|
||||
return roles.map((role) => ({
|
||||
roleId: role.roleId || role.role_id || 0,
|
||||
level: role.level || 0,
|
||||
name: role.name || 'Unknown',
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch tiki roles:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate tiki score from user's roles
|
||||
*/
|
||||
export function calculateTikiScore(tikis: TikiInfo[]): number {
|
||||
if (!tikis.length) return 0;
|
||||
|
||||
// Get highest role score
|
||||
let maxScore = 0;
|
||||
for (const tiki of tikis) {
|
||||
const roleScore = TIKI_ROLE_SCORES[tiki.roleId] || 0;
|
||||
maxScore = Math.max(maxScore, roleScore);
|
||||
}
|
||||
|
||||
return Math.min(maxScore, 50); // Capped at 50
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tiki score for a user
|
||||
*/
|
||||
export async function getTikiScore(peopleApi: ApiPromise, address: string): Promise<number> {
|
||||
try {
|
||||
const tikis = await fetchUserTikis(peopleApi, address);
|
||||
return calculateTikiScore(tikis);
|
||||
} catch (err) {
|
||||
console.error('Error fetching tiki score:', err);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TRUST SCORE CALCULATION
|
||||
// ========================================
|
||||
// Formula from pezpallet-trust:
|
||||
// weighted_sum = staking*100 + referral*300 + perwerde*300 + tiki*300
|
||||
// trust_score = (staking * weighted_sum) / 100
|
||||
|
||||
const SCORE_MULTIPLIER_BASE = 100;
|
||||
|
||||
export interface FrontendTrustScoreResult {
|
||||
trustScore: number;
|
||||
stakingScore: number;
|
||||
referralScore: number;
|
||||
perwerdeScore: number;
|
||||
tikiScore: number;
|
||||
weightedSum: number;
|
||||
isCitizen: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is a citizen (KYC approved)
|
||||
*/
|
||||
export async function checkCitizenshipStatus(
|
||||
peopleApi: ApiPromise | null,
|
||||
address: string
|
||||
): Promise<boolean> {
|
||||
if (!peopleApi || !address) return false;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (!(peopleApi.query as any)?.identityKyc?.kycStatuses) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const status = await (peopleApi.query.identityKyc as any).kycStatuses(address);
|
||||
const statusStr = status.toString();
|
||||
|
||||
return statusStr === 'Approved' || statusStr === '3';
|
||||
} catch (err) {
|
||||
console.error('Error checking citizenship status:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Perwerde (education) score
|
||||
*/
|
||||
export async function getPerwerdeScore(
|
||||
peopleApi: ApiPromise | null,
|
||||
address: string
|
||||
): Promise<number> {
|
||||
if (!peopleApi || !address) return 0;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (!(peopleApi.query as any)?.perwerde) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((peopleApi.query.perwerde as any).userScores) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const score = await (peopleApi.query.perwerde as any).userScores(address);
|
||||
if (!score.isEmpty) {
|
||||
return Number(score.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (err) {
|
||||
console.error('Error fetching perwerde score:', err);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate all scores with frontend fallback
|
||||
*/
|
||||
export async function getAllScoresWithFallback(
|
||||
peopleApi: ApiPromise | null,
|
||||
relayApi: ApiPromise | null,
|
||||
address: string
|
||||
): Promise<FrontendTrustScoreResult & { isFromFrontend: boolean }> {
|
||||
const emptyResult: FrontendTrustScoreResult & { isFromFrontend: boolean } = {
|
||||
trustScore: 0,
|
||||
stakingScore: 0,
|
||||
referralScore: 0,
|
||||
perwerdeScore: 0,
|
||||
tikiScore: 0,
|
||||
weightedSum: 0,
|
||||
isCitizen: false,
|
||||
isFromFrontend: true,
|
||||
};
|
||||
|
||||
if (!address) return emptyResult;
|
||||
|
||||
// Check citizenship status
|
||||
const isCitizen = await checkCitizenshipStatus(peopleApi, address);
|
||||
|
||||
// Get component scores in parallel
|
||||
const [stakingResult, referralCount, perwerdeScore, tikiScore] = await Promise.all([
|
||||
relayApi ? getFrontendStakingScore(relayApi, address) : Promise.resolve({ score: 0 }),
|
||||
peopleApi ? getReferralCount(peopleApi, address) : Promise.resolve(0),
|
||||
getPerwerdeScore(peopleApi, address),
|
||||
peopleApi ? getTikiScore(peopleApi, address) : Promise.resolve(0),
|
||||
]);
|
||||
|
||||
const stakingScore = stakingResult.score;
|
||||
const referralScore = calculateReferralScore(referralCount);
|
||||
|
||||
// If staking score is 0, trust score is 0 (matches pallet logic)
|
||||
if (stakingScore === 0) {
|
||||
return {
|
||||
...emptyResult,
|
||||
referralScore,
|
||||
perwerdeScore,
|
||||
tikiScore,
|
||||
isCitizen,
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate weighted sum (matching pallet formula)
|
||||
const weightedSum =
|
||||
stakingScore * 100 + referralScore * 300 + perwerdeScore * 300 + tikiScore * 300;
|
||||
|
||||
// Calculate final trust score
|
||||
// trust_score = (staking * weighted_sum) / 100
|
||||
const trustScore = Math.floor((stakingScore * weightedSum) / SCORE_MULTIPLIER_BASE);
|
||||
|
||||
return {
|
||||
trustScore,
|
||||
stakingScore,
|
||||
referralScore,
|
||||
perwerdeScore,
|
||||
tikiScore,
|
||||
weightedSum,
|
||||
isCitizen,
|
||||
isFromFrontend: true,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SCORE DISPLAY HELPERS
|
||||
// ========================================
|
||||
|
||||
export function getScoreColor(score: number): string {
|
||||
if (score >= 200) return 'text-purple-400';
|
||||
if (score >= 150) return 'text-pink-400';
|
||||
if (score >= 100) return 'text-blue-400';
|
||||
if (score >= 70) return 'text-cyan-400';
|
||||
if (score >= 40) return 'text-teal-400';
|
||||
if (score >= 20) return 'text-green-400';
|
||||
return 'text-gray-400';
|
||||
}
|
||||
|
||||
export function getScoreRating(score: number): string {
|
||||
if (score >= 250) return 'Efsane';
|
||||
if (score >= 200) return 'Pir Baş';
|
||||
if (score >= 150) return 'Baş';
|
||||
if (score >= 100) return 'Navîn';
|
||||
if (score >= 70) return 'Têr';
|
||||
if (score >= 40) return 'Destpêk';
|
||||
if (score >= 20) return 'Nû';
|
||||
return 'Sifir';
|
||||
}
|
||||
|
||||
export function formatStakedAmount(amount: bigint): string {
|
||||
const hez = Number(amount) / UNITS;
|
||||
if (hez >= 1000000) return `${(hez / 1000000).toFixed(2)}M`;
|
||||
if (hez >= 1000) return `${(hez / 1000).toFixed(2)}K`;
|
||||
return hez.toFixed(2);
|
||||
}
|
||||
+224
-2
@@ -17,6 +17,10 @@ import {
|
||||
Award,
|
||||
Zap,
|
||||
Coins,
|
||||
Shield,
|
||||
Target,
|
||||
Sparkles,
|
||||
GraduationCap,
|
||||
} from 'lucide-react';
|
||||
import { cn, formatAddress } from '@/lib/utils';
|
||||
import { useTelegram } from '@/hooks/useTelegram';
|
||||
@@ -24,6 +28,15 @@ import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useReferral } from '@/contexts/ReferralContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { SocialLinks } from '@/components/SocialLinks';
|
||||
import {
|
||||
getAllScoresWithFallback,
|
||||
getFrontendStakingScore,
|
||||
formatStakedAmount,
|
||||
getScoreColor,
|
||||
getScoreRating,
|
||||
type FrontendTrustScoreResult,
|
||||
type FrontendStakingScoreResult,
|
||||
} from '@/lib/scores';
|
||||
|
||||
// Activity tracking constants
|
||||
const ACTIVITY_STORAGE_KEY = 'pezkuwi_last_active';
|
||||
@@ -33,12 +46,17 @@ export function RewardsSection() {
|
||||
const { hapticImpact, hapticNotification, shareUrl, showAlert } = useTelegram();
|
||||
const { user: authUser } = useAuth();
|
||||
const { stats, myReferrals, loading, refreshStats } = useReferral();
|
||||
const { isConnected } = useWallet();
|
||||
const { isConnected, address, api, peopleApi } = useWallet();
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'referrals'>('overview');
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'referrals' | 'scores'>('overview');
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [timeRemaining, setTimeRemaining] = useState<string | null>(null);
|
||||
const [userScores, setUserScores] = useState<
|
||||
(FrontendTrustScoreResult & { isFromFrontend: boolean }) | null
|
||||
>(null);
|
||||
const [stakingDetails, setStakingDetails] = useState<FrontendStakingScoreResult | null>(null);
|
||||
const [scoresLoading, setScoresLoading] = useState(false);
|
||||
|
||||
// Check activity status
|
||||
const checkActivityStatus = useCallback(() => {
|
||||
@@ -75,6 +93,36 @@ export function RewardsSection() {
|
||||
};
|
||||
}, [checkActivityStatus]);
|
||||
|
||||
// Fetch user scores when on scores tab
|
||||
const fetchUserScores = useCallback(async () => {
|
||||
if (!address) {
|
||||
setUserScores(null);
|
||||
setStakingDetails(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setScoresLoading(true);
|
||||
try {
|
||||
const [scores, staking] = await Promise.all([
|
||||
getAllScoresWithFallback(peopleApi, api, address),
|
||||
api ? getFrontendStakingScore(api, address) : Promise.resolve(null),
|
||||
]);
|
||||
setUserScores(scores);
|
||||
setStakingDetails(staking);
|
||||
} catch (err) {
|
||||
console.error('Error fetching scores:', err);
|
||||
} finally {
|
||||
setScoresLoading(false);
|
||||
}
|
||||
}, [api, peopleApi, address]);
|
||||
|
||||
// Fetch scores when tab changes to scores or on initial load
|
||||
useEffect(() => {
|
||||
if (activeTab === 'scores' && address) {
|
||||
fetchUserScores();
|
||||
}
|
||||
}, [activeTab, address, fetchUserScores]);
|
||||
|
||||
const handleActivate = () => {
|
||||
hapticNotification('success');
|
||||
localStorage.setItem(ACTIVITY_STORAGE_KEY, Date.now().toString());
|
||||
@@ -159,6 +207,7 @@ export function RewardsSection() {
|
||||
{[
|
||||
{ id: 'overview' as const, label: 'Geşbîn' },
|
||||
{ id: 'referrals' as const, label: 'Referral' },
|
||||
{ id: 'scores' as const, label: 'Puanlar' },
|
||||
].map(({ id, label }) => (
|
||||
<button
|
||||
key={id}
|
||||
@@ -493,6 +542,179 @@ export function RewardsSection() {
|
||||
)}
|
||||
</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" />
|
||||
Pûana Pêbaweriyê (Trust)
|
||||
</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">
|
||||
Rêze: {getScoreRating(userScores?.trustScore || 0)}
|
||||
</span>
|
||||
{userScores?.isCitizen && (
|
||||
<span className="bg-green-500/30 text-green-200 px-2 py-1 rounded-full text-xs">
|
||||
Welatî
|
||||
</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">Staking</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-blue-400">
|
||||
{userScores?.stakingScore ?? 0}
|
||||
</p>
|
||||
{stakingDetails && stakingDetails.stakedAmount > 0n && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{formatStakedAmount(stakingDetails.stakedAmount)} HEZ
|
||||
</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">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">
|
||||
{stats?.referralCount ?? 0} kes
|
||||
</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">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">Rola NFT</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">Perwerde</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-pink-400">
|
||||
{userScores?.perwerdeScore ?? 0}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Xwendin</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Staking Details Card */}
|
||||
{stakingDetails && stakingDetails.stakedAmount > 0n && (
|
||||
<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">
|
||||
<TrendingUp className="w-4 h-4 text-blue-400" />
|
||||
Staking Hûrgelan
|
||||
</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">HEZ Staked</span>
|
||||
<span className="text-foreground font-medium">
|
||||
{formatStakedAmount(stakingDetails.stakedAmount)} HEZ
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-border/30">
|
||||
<span className="text-muted-foreground">Demjimêr</span>
|
||||
<span className="text-foreground font-medium">
|
||||
{stakingDetails.monthsStaked} meh
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-border/30">
|
||||
<span className="text-muted-foreground">Pir Zêdeker</span>
|
||||
<span className="text-foreground font-medium">
|
||||
{stakingDetails.timeMultiplier}x
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2">
|
||||
<span className="text-muted-foreground">Nominasyon</span>
|
||||
<span className="text-foreground font-medium">
|
||||
{stakingDetails.nominationsCount}
|
||||
</span>
|
||||
</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" />
|
||||
Formûla Pûanê
|
||||
</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">
|
||||
Staking 0 ise Trust pûan 0 dibe. Berî her tiştî stake bike!
|
||||
</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')} />
|
||||
Pûanan Nûve Bike
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.122",
|
||||
"buildTime": "2026-02-06T17:04:46.943Z",
|
||||
"buildNumber": 1770397486943
|
||||
"version": "1.0.123",
|
||||
"buildTime": "2026-02-06T22:10:09.600Z",
|
||||
"buildNumber": 1770415809601
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user