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:
2026-02-07 01:10:09 +03:00
parent 57f9d9e7ff
commit 1a7609c14c
5 changed files with 705 additions and 8 deletions
+1 -1
View File
@@ -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",
+24 -2
View File
@@ -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;
}
+453
View File
@@ -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
View File
@@ -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
View File
@@ -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
}