fix: remove telegram module (moved to separate repo) and fix ESLint

- Remove src/telegram/* (moved to pezkuwi-telegram-miniapp repo)
- Fix unused variable in process-withdraw Edge Function
This commit is contained in:
2026-01-29 03:45:01 +03:00
parent 7636d46c7d
commit 6da3c5b88c
12 changed files with 1 additions and 2711 deletions
-214
View File
@@ -1,214 +0,0 @@
import { useState, useEffect } from 'react';
import { useTelegram } from './hooks/useTelegram';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useWallet } from '@/contexts/WalletContext';
import { Loader2, Megaphone, MessageCircle, Gift, Smartphone, Wallet, AlertCircle, RefreshCw } from 'lucide-react';
import { cn } from '@/lib/utils';
// Sections
import { AnnouncementsSection } from './components/Announcements';
import { ForumSection } from './components/Forum';
import { RewardsSection } from './components/Rewards';
import { APKSection } from './components/APK';
import { WalletSection } from './components/Wallet';
export type Section = 'announcements' | 'forum' | 'rewards' | 'apk' | 'wallet';
interface NavItem {
id: Section;
icon: React.ReactNode;
label: string;
color: string;
}
const navItems: NavItem[] = [
{ id: 'announcements', icon: <Megaphone className="w-5 h-5" />, label: 'Duyurular', color: 'text-yellow-500' },
{ id: 'forum', icon: <MessageCircle className="w-5 h-5" />, label: 'Forum', color: 'text-blue-500' },
{ id: 'rewards', icon: <Gift className="w-5 h-5" />, label: 'Rewards', color: 'text-purple-500' },
{ id: 'apk', icon: <Smartphone className="w-5 h-5" />, label: 'APK', color: 'text-green-500' },
{ id: 'wallet', icon: <Wallet className="w-5 h-5" />, label: 'Wallet', color: 'text-cyan-500' },
];
export function TelegramApp() {
const {
isReady: isTelegramReady,
isTelegram,
startParam,
setHeaderColor,
setBackgroundColor,
hapticSelection,
} = useTelegram();
const { api, isApiReady, error: apiError } = usePezkuwi();
const { isConnected } = useWallet();
const [activeSection, setActiveSection] = useState<Section>('announcements');
const [isRetrying, setIsRetrying] = useState(false);
// Handle referral from startParam
useEffect(() => {
if (startParam) {
localStorage.setItem('referrerAddress', startParam);
console.log('[TelegramApp] Referral from startParam:', startParam);
}
}, [startParam]);
// Setup Telegram theme
useEffect(() => {
if (isTelegram) {
setHeaderColor('#030712'); // gray-950
setBackgroundColor('#030712');
}
}, [isTelegram, setHeaderColor, setBackgroundColor]);
const handleNavClick = (section: Section) => {
if (isTelegram) hapticSelection();
setActiveSection(section);
};
const handleRetry = () => {
setIsRetrying(true);
window.location.reload();
};
// Render active section
const renderSection = () => {
switch (activeSection) {
case 'announcements':
return <AnnouncementsSection />;
case 'forum':
return <ForumSection />;
case 'rewards':
return <RewardsSection />;
case 'apk':
return <APKSection />;
case 'wallet':
return <WalletSection />;
default:
return <AnnouncementsSection />;
}
};
// Loading state
if (!isTelegramReady) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-950 p-6">
<img
src="/shared/images/pezkuwi_wallet_logo.png"
alt="Pezkuwi"
className="w-20 h-20 mb-4 animate-pulse"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
<Loader2 className="w-8 h-8 text-green-500 animate-spin mb-3" />
<span className="text-gray-400 text-sm">Pezkuwi Mini App yükleniyor...</span>
</div>
);
}
// API Error state
if (apiError && !isApiReady) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-950 p-6">
<div className="w-16 h-16 rounded-full bg-red-500/20 flex items-center justify-center mb-4">
<AlertCircle className="w-8 h-8 text-red-500" />
</div>
<h2 className="text-white font-semibold text-lg mb-2">Bağlantı Hatası</h2>
<p className="text-gray-400 text-sm text-center mb-6 max-w-xs">
Pezkuwichain ağına bağlanılamadı. Lütfen internet bağlantınızı kontrol edin.
</p>
<button
onClick={handleRetry}
disabled={isRetrying}
className="flex items-center gap-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-700 text-white px-5 py-2.5 rounded-lg transition-colors"
>
{isRetrying ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<RefreshCw className="w-4 h-4" />
)}
Tekrar Dene
</button>
</div>
);
}
return (
<div className="flex flex-col h-screen bg-gray-950 text-white overflow-hidden">
{/* Header */}
<header className="flex items-center justify-between px-4 py-3 bg-gray-900 border-b border-gray-800">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center">
<span className="text-white font-bold text-sm">P</span>
</div>
<div>
<h1 className="text-white font-semibold text-sm">Pezkuwichain</h1>
<div className="flex items-center gap-1.5">
<div className={cn(
"w-1.5 h-1.5 rounded-full",
isApiReady ? "bg-green-500" : "bg-yellow-500 animate-pulse"
)} />
<span className="text-gray-500 text-xs">
{isApiReady ? 'Bağlı' : 'Bağlanıyor...'}
</span>
</div>
</div>
</div>
{isConnected && (
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span className="text-xs text-gray-400">Cüzdan Bağlı</span>
</div>
)}
</header>
{/* API connecting banner */}
{!isApiReady && (
<div className="bg-yellow-500/10 border-b border-yellow-500/20 px-4 py-2 flex items-center justify-center gap-2">
<Loader2 className="w-3 h-3 text-yellow-500 animate-spin" />
<span className="text-yellow-500 text-xs">Blockchain ağına bağlanılıyor...</span>
</div>
)}
{/* Main content */}
<main className="flex-1 overflow-hidden">
{renderSection()}
</main>
{/* Bottom Navigation */}
<nav className="bg-gray-900 border-t border-gray-800 px-2 py-2 safe-area-bottom">
<div className="flex items-center justify-around">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => handleNavClick(item.id)}
className={cn(
"flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all min-w-[60px]",
activeSection === item.id
? "bg-gray-800"
: "hover:bg-gray-800/50"
)}
>
<span className={cn(
"transition-colors",
activeSection === item.id ? item.color : "text-gray-500"
)}>
{item.icon}
</span>
<span className={cn(
"text-xs transition-colors",
activeSection === item.id ? "text-white" : "text-gray-500"
)}>
{item.label}
</span>
</button>
))}
</div>
</nav>
</div>
);
}
export default TelegramApp;
-394
View File
@@ -1,394 +0,0 @@
import { useState, useEffect } from 'react';
import { useTelegram } from '../../hooks/useTelegram';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Skeleton } from '@/components/ui/skeleton';
import {
Smartphone, Download, Clock, CheckCircle2, ExternalLink,
Shield, FileText, Wifi, ChevronDown, ChevronUp, AlertCircle,
Github, Star, Package, Loader2
} from 'lucide-react';
import { cn } from '@/lib/utils';
interface AppVersion {
version: string;
releaseDate: Date;
downloadUrl: string;
size: string;
changelog: string[];
isLatest?: boolean;
minAndroidVersion?: string;
downloads?: number;
}
// Mock versions - will be replaced with GitHub API
const appVersions: AppVersion[] = [
{
version: '1.2.0',
releaseDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
downloadUrl: 'https://github.com/pezkuwichain/pezwallet/releases/download/v1.2.0/pezwallet-v1.2.0.apk',
size: '45.2 MB',
isLatest: true,
minAndroidVersion: '7.0',
downloads: 1234,
changelog: [
'Yeni: Telegram Mini App entegrasyonu',
'Yeni: Geliştirilmiş staking arayüzü',
'Düzeltme: Bakiye yenileme sorunları',
'Düzeltme: İşlem geçmişi yüklemesi',
'İyileştirme: Genel performans',
],
},
{
version: '1.1.2',
releaseDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14),
downloadUrl: 'https://github.com/pezkuwichain/pezwallet/releases/download/v1.1.2/pezwallet-v1.1.2.apk',
size: '44.8 MB',
minAndroidVersion: '7.0',
downloads: 856,
changelog: [
'Düzeltme: Kritik güvenlik güncellemesi',
'Düzeltme: Cüzdan bağlantı kararlılığı',
'İyileştirme: İşlem imzalama',
],
},
{
version: '1.1.0',
releaseDate: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30),
downloadUrl: 'https://github.com/pezkuwichain/pezwallet/releases/download/v1.1.0/pezwallet-v1.1.0.apk',
size: '44.5 MB',
minAndroidVersion: '7.0',
downloads: 2341,
changelog: [
'Yeni: Çoklu dil desteği',
'Yeni: Geliştirilmiş karanlık tema',
'Yeni: QR kod tarama',
'Düzeltme: Çeşitli hata düzeltmeleri',
],
},
];
const features = [
{
icon: <Shield className="w-5 h-5" />,
title: 'Güvenli Cüzdan',
description: 'Anahtarlarınız, kriptonuz. Tam self-custody.',
color: 'text-green-500',
bgColor: 'bg-green-500/20',
},
{
icon: <FileText className="w-5 h-5" />,
title: 'Vatandaşlık Yönetimi',
description: 'Vatandaşlık başvurusu ve Tiki yönetimi.',
color: 'text-purple-500',
bgColor: 'bg-purple-500/20',
},
{
icon: <Wifi className="w-5 h-5" />,
title: 'Çevrimdışı Destek',
description: 'Bakiye ve geçmişi çevrimdışı görüntüleyin.',
color: 'text-blue-500',
bgColor: 'bg-blue-500/20',
},
];
function VersionCard({
version,
isExpanded,
onToggle,
onDownload,
isDownloading
}: {
version: AppVersion;
isExpanded: boolean;
onToggle: () => void;
onDownload: () => void;
isDownloading: boolean;
}) {
const formatDate = (date: Date) => {
return date.toLocaleDateString('tr-TR', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
return (
<Card className={cn(
"bg-gray-900 border-gray-800 overflow-hidden",
version.isLatest && "border-green-500/50"
)}>
<CardContent className="p-0">
<button
onClick={onToggle}
className="w-full p-4 flex items-center justify-between hover:bg-gray-800/50 transition-colors"
>
<div className="flex items-center gap-3">
<div className={cn(
"w-12 h-12 rounded-xl flex items-center justify-center",
version.isLatest ? "bg-green-500" : "bg-gray-800"
)}>
{version.isLatest ? (
<CheckCircle2 className="w-6 h-6 text-white" />
) : (
<Package className="w-6 h-6 text-gray-400" />
)}
</div>
<div className="text-left">
<div className="flex items-center gap-2">
<span className="text-white font-semibold">v{version.version}</span>
{version.isLatest && (
<Badge className="bg-green-500/20 text-green-500 border-green-500/30 text-xs">
En Son
</Badge>
)}
</div>
<div className="flex items-center gap-3 text-xs text-gray-400 mt-1">
<span>{formatDate(version.releaseDate)}</span>
<span></span>
<span>{version.size}</span>
{version.downloads && (
<>
<span></span>
<span>{version.downloads.toLocaleString()} indirme</span>
</>
)}
</div>
</div>
</div>
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-gray-400" />
) : (
<ChevronDown className="w-5 h-5 text-gray-400" />
)}
</button>
{/* Expanded content */}
{isExpanded && (
<div className="px-4 pb-4 border-t border-gray-800">
<div className="bg-gray-800 rounded-lg p-4 mt-4">
<h4 className="text-sm font-medium text-white mb-3 flex items-center gap-2">
<Star className="w-4 h-4 text-yellow-500" />
Değişiklikler
</h4>
<ul className="space-y-2">
{version.changelog.map((item, idx) => (
<li key={idx} className="text-sm text-gray-300 flex items-start gap-2">
<span className="text-green-500 mt-1.5"></span>
{item}
</li>
))}
</ul>
{version.minAndroidVersion && (
<div className="mt-4 pt-3 border-t border-gray-700 text-xs text-gray-500 flex items-center gap-2">
<Smartphone className="w-4 h-4" />
Android {version.minAndroidVersion} veya üstü gerekli
</div>
)}
<Button
onClick={onDownload}
disabled={isDownloading}
className={cn(
"w-full mt-4",
version.isLatest
? "bg-green-600 hover:bg-green-700"
: "bg-gray-700 hover:bg-gray-600"
)}
>
{isDownloading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
İndiriliyor...
</>
) : (
<>
<Download className="w-4 h-4 mr-2" />
v{version.version} İndir
</>
)}
</Button>
</div>
</div>
)}
</CardContent>
</Card>
);
}
export function APKSection() {
const { hapticImpact, openLink, showConfirm } = useTelegram();
const [expandedVersion, setExpandedVersion] = useState<string | null>(appVersions[0]?.version || null);
const [downloading, setDownloading] = useState<string | null>(null);
const handleDownload = async (version: AppVersion) => {
hapticImpact('medium');
const confirmed = await showConfirm(
`Pezwallet v${version.version} (${version.size}) indirilsin mi?`
);
if (confirmed) {
setDownloading(version.version);
openLink(version.downloadUrl);
setTimeout(() => setDownloading(null), 3000);
}
};
const handleOpenGitHub = () => {
hapticImpact('light');
openLink('https://github.com/pezkuwichain/pezwallet/releases');
};
const latestVersion = appVersions.find(v => v.isLatest);
return (
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */}
<div className="p-4 pb-0">
<div className="flex items-center justify-between mb-4">
<h2 className="text-white font-semibold text-lg flex items-center gap-2">
<Smartphone className="w-5 h-5 text-green-500" />
Pezwallet APK
</h2>
<Button
variant="ghost"
size="icon"
onClick={handleOpenGitHub}
className="h-8 w-8"
>
<Github className="w-4 h-4 text-gray-400" />
</Button>
</div>
</div>
{/* Content */}
<div className="flex-1 p-4 pt-0 space-y-4">
{/* App Banner */}
<Card className="bg-gradient-to-br from-green-600 to-emerald-700 border-0 overflow-hidden">
<CardContent className="p-4">
<div className="flex items-start gap-4">
{/* App Icon */}
<div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center flex-shrink-0">
<span className="text-3xl font-bold text-white">P</span>
</div>
<div className="flex-1">
<h3 className="text-xl font-bold text-white mb-1">Pezwallet</h3>
<p className="text-green-100 text-sm mb-3">
Pezkuwichain için resmi cüzdan uygulaması
</p>
{latestVersion && (
<Button
onClick={() => handleDownload(latestVersion)}
disabled={downloading === latestVersion.version}
className="bg-white text-green-700 hover:bg-green-50"
>
{downloading === latestVersion.version ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
İndiriliyor...
</>
) : (
<>
<Download className="w-4 h-4 mr-2" />
v{latestVersion.version} İndir
</>
)}
</Button>
)}
</div>
</div>
</CardContent>
</Card>
{/* Features */}
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-white flex items-center gap-2">
<Star className="w-4 h-4 text-yellow-500" />
Özellikler
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{features.map((feature, index) => (
<div
key={index}
className="flex items-start gap-3 p-3 bg-gray-800 rounded-lg"
>
<div className={cn(
"w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0",
feature.bgColor, feature.color
)}>
{feature.icon}
</div>
<div>
<h4 className="text-white font-medium text-sm">{feature.title}</h4>
<p className="text-gray-400 text-xs">{feature.description}</p>
</div>
</div>
))}
</CardContent>
</Card>
{/* Installation Guide */}
<Alert className="bg-yellow-500/10 border-yellow-500/30">
<AlertCircle className="w-4 h-4 text-yellow-500" />
<AlertDescription className="text-yellow-200">
<span className="font-medium text-yellow-500 block mb-2">Kurulum Rehberi</span>
<ol className="text-xs space-y-1 list-decimal list-inside text-yellow-200/80">
<li>APK dosyasını indirin</li>
<li>Cihaz Ayarlarını açın</li>
<li>"Bilinmeyen kaynaklardan yükleme"yi etkinleştirin</li>
<li>İndirilen APK dosyasını açın</li>
<li>Kurulum talimatlarını izleyin</li>
</ol>
</AlertDescription>
</Alert>
{/* Version History */}
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-white flex items-center gap-2">
<Clock className="w-4 h-4 text-gray-400" />
Sürüm Geçmişi
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{appVersions.map((version) => (
<VersionCard
key={version.version}
version={version}
isExpanded={expandedVersion === version.version}
onToggle={() => setExpandedVersion(
expandedVersion === version.version ? null : version.version
)}
onDownload={() => handleDownload(version)}
isDownloading={downloading === version.version}
/>
))}
</CardContent>
</Card>
{/* Footer note */}
<div className="text-center pb-4">
<p className="text-xs text-gray-500">
Daima resmi kaynaklardan indirdiğinizi doğrulayın
</p>
<Button
variant="link"
onClick={handleOpenGitHub}
className="text-green-500 text-xs mt-1"
>
<Github className="w-3 h-3 mr-1" />
GitHub'da tüm sürümleri görüntüle
</Button>
</div>
</div>
</div>
);
}
export default APKSection;
@@ -1,308 +0,0 @@
import { useState, useEffect } from 'react';
import { useTelegram } from '../../hooks/useTelegram';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import {
Megaphone, RefreshCw, ThumbsUp, ThumbsDown, Pin, Clock,
User, ChevronDown, ChevronUp, Bell
} from 'lucide-react';
import { cn } from '@/lib/utils';
export interface Announcement {
id: string;
title: string;
content: string;
author: string;
authorAvatar?: string;
createdAt: Date;
likes: number;
dislikes: number;
userReaction?: 'like' | 'dislike' | null;
isPinned?: boolean;
imageUrl?: string;
}
// Mock data - will be replaced with API calls
const mockAnnouncements: Announcement[] = [
{
id: '1',
title: 'Pezkuwichain Mainnet Yayında!',
content: 'Pezkuwichain mainnet artık aktif! Bu, Kürt halkı için merkezi olmayan bir dijital devlet inşa etme yolculuğumuzda önemli bir kilometre taşı.\n\nÖne çıkan özellikler:\n- Hızlı işlem kesinliği (6 saniye)\n- Düşük gas ücretleri\n- Yerleşik staking desteği\n- Demokratik yönetişim\n\nKeşfetmeye başlayın: app.pezkuwichain.io',
author: 'Pezkuwi Ekibi',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
likes: 142,
dislikes: 3,
isPinned: true,
},
{
id: '2',
title: 'Yeni Referral Programı',
content: 'Arkadaşlarınızı davet edin ve ödüller kazanın! KYC tamamlayan her arkadaşınız için trust score\'unuza katkıda bulunan bonus puanlar alacaksınız.\n\nReferans linkinizi Rewards bölümünden paylaşın.',
author: 'Pezkuwi Ekibi',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
likes: 89,
dislikes: 5,
},
{
id: '3',
title: 'Staking Ödülleri Güncellendi',
content: 'Staking ödül mekanizmasını güncelledik. HEZ stake eden vatandaşlar artık her epoch\'ta PEZ token ödülü kazanacak.\n\nMinimum stake: 10 HEZ\nEpoch süresi: 7 gün',
author: 'Pezkuwi Ekibi',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3),
likes: 67,
dislikes: 2,
},
];
function AnnouncementCard({
announcement,
onReact
}: {
announcement: Announcement;
onReact: (id: string, reaction: 'like' | 'dislike') => void;
}) {
const { hapticImpact } = useTelegram();
const [isExpanded, setIsExpanded] = useState(false);
const handleReact = (reaction: 'like' | 'dislike') => {
hapticImpact('light');
onReact(announcement.id, reaction);
};
const formatDate = (date: Date) => {
const now = new Date();
const diff = now.getTime() - date.getTime();
const hours = Math.floor(diff / (1000 * 60 * 60));
const days = Math.floor(hours / 24);
if (hours < 1) return 'Az önce';
if (hours < 24) return `${hours} saat önce`;
if (days < 7) return `${days} gün önce`;
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
};
const shouldTruncate = announcement.content.length > 200;
const displayContent = shouldTruncate && !isExpanded
? announcement.content.slice(0, 200) + '...'
: announcement.content;
return (
<Card className={cn(
"bg-gray-900 border-gray-800",
announcement.isPinned && "border-l-4 border-l-yellow-500"
)}>
<CardContent className="p-4">
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-yellow-500 to-orange-600 flex items-center justify-center">
{announcement.authorAvatar ? (
<img
src={announcement.authorAvatar}
alt={announcement.author}
className="w-full h-full rounded-full object-cover"
/>
) : (
<User className="w-5 h-5 text-white" />
)}
</div>
<div>
<p className="text-white font-medium text-sm">{announcement.author}</p>
<div className="flex items-center gap-1 text-xs text-gray-400">
<Clock className="w-3 h-3" />
<span>{formatDate(announcement.createdAt)}</span>
</div>
</div>
</div>
{announcement.isPinned && (
<Badge className="bg-yellow-500/20 text-yellow-500 border-yellow-500/30">
<Pin className="w-3 h-3 mr-1" />
Sabitlendi
</Badge>
)}
</div>
{/* Title */}
<h3 className="text-white font-semibold mb-2">{announcement.title}</h3>
{/* Content */}
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-wrap">
{displayContent}
</p>
{/* Show more/less button */}
{shouldTruncate && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-1 text-yellow-500 text-sm mt-2 hover:text-yellow-400"
>
{isExpanded ? (
<>
<ChevronUp className="w-4 h-4" />
Daha az göster
</>
) : (
<>
<ChevronDown className="w-4 h-4" />
Devamını göster
</>
)}
</button>
)}
{/* Image */}
{announcement.imageUrl && (
<div className="mt-3 rounded-lg overflow-hidden">
<img
src={announcement.imageUrl}
alt=""
className="w-full h-auto max-h-64 object-cover"
/>
</div>
)}
{/* Reactions */}
<div className="flex items-center gap-4 mt-4 pt-3 border-t border-gray-800">
<button
onClick={() => handleReact('like')}
className={cn(
"flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all",
announcement.userReaction === 'like'
? "bg-green-500/20 text-green-500"
: "bg-gray-800 text-gray-400 hover:bg-green-500/10 hover:text-green-500"
)}
>
<ThumbsUp className={cn(
"w-4 h-4",
announcement.userReaction === 'like' && "fill-current"
)} />
<span className="text-sm font-medium">{announcement.likes}</span>
</button>
<button
onClick={() => handleReact('dislike')}
className={cn(
"flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all",
announcement.userReaction === 'dislike'
? "bg-red-500/20 text-red-500"
: "bg-gray-800 text-gray-400 hover:bg-red-500/10 hover:text-red-500"
)}
>
<ThumbsDown className={cn(
"w-4 h-4",
announcement.userReaction === 'dislike' && "fill-current"
)} />
<span className="text-sm font-medium">{announcement.dislikes}</span>
</button>
</div>
</CardContent>
</Card>
);
}
export function AnnouncementsSection() {
const { hapticNotification } = useTelegram();
const [announcements, setAnnouncements] = useState<Announcement[]>(mockAnnouncements);
const [isLoading, setIsLoading] = useState(false);
const handleReact = (id: string, reaction: 'like' | 'dislike') => {
setAnnouncements(prev => prev.map(ann => {
if (ann.id !== id) return ann;
const wasLiked = ann.userReaction === 'like';
const wasDisliked = ann.userReaction === 'dislike';
const isSameReaction = ann.userReaction === reaction;
return {
...ann,
userReaction: isSameReaction ? null : reaction,
likes: reaction === 'like'
? ann.likes + (isSameReaction ? -1 : 1)
: ann.likes - (wasLiked ? 1 : 0),
dislikes: reaction === 'dislike'
? ann.dislikes + (isSameReaction ? -1 : 1)
: ann.dislikes - (wasDisliked ? 1 : 0),
};
}));
};
const handleRefresh = async () => {
setIsLoading(true);
hapticNotification('success');
await new Promise(resolve => setTimeout(resolve, 1000));
setAnnouncements(mockAnnouncements);
setIsLoading(false);
};
// Sort: pinned first, then by date
const sortedAnnouncements = [...announcements].sort((a, b) => {
if (a.isPinned && !b.isPinned) return -1;
if (!a.isPinned && b.isPinned) return 1;
return b.createdAt.getTime() - a.createdAt.getTime();
});
return (
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */}
<div className="p-4 pb-0">
<div className="flex items-center justify-between mb-4">
<h2 className="text-white font-semibold text-lg flex items-center gap-2">
<Megaphone className="w-5 h-5 text-yellow-500" />
Duyurular
</h2>
<Button
variant="ghost"
size="icon"
onClick={handleRefresh}
disabled={isLoading}
className="h-8 w-8"
>
<RefreshCw className={cn("w-4 h-4 text-gray-400", isLoading && "animate-spin")} />
</Button>
</div>
{/* Stats */}
<div className="flex items-center gap-4 mb-4">
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
<Bell className="w-4 h-4 text-yellow-500" />
<span className="text-gray-300 text-sm">{announcements.length} Duyuru</span>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
<Pin className="w-4 h-4 text-yellow-500" />
<span className="text-gray-300 text-sm">
{announcements.filter(a => a.isPinned).length} Sabitlenmiş
</span>
</div>
</div>
</div>
{/* Announcements List */}
<div className="flex-1 p-4 pt-0 space-y-4">
{isLoading ? (
<>
{[...Array(3)].map((_, i) => (
<Skeleton key={i} className="h-48 bg-gray-800" />
))}
</>
) : sortedAnnouncements.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
<Megaphone className="w-12 h-12 mb-3 opacity-50" />
<p>Henüz duyuru yok</p>
</div>
) : (
sortedAnnouncements.map(announcement => (
<AnnouncementCard
key={announcement.id}
announcement={announcement}
onReact={handleReact}
/>
))
)}
</div>
</div>
);
}
export default AnnouncementsSection;
-562
View File
@@ -1,562 +0,0 @@
import { useState } from 'react';
import { useTelegram } from '../../hooks/useTelegram';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useWallet } from '@/contexts/WalletContext';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Skeleton } from '@/components/ui/skeleton';
import {
MessageCircle, Plus, RefreshCw, Search, Pin, Clock, User,
Eye, ChevronRight, ArrowLeft, Send, ThumbsUp, MessageSquare, Hash
} from 'lucide-react';
import { cn } from '@/lib/utils';
export interface ForumThread {
id: string;
title: string;
content: string;
author: string;
authorAddress?: string;
createdAt: Date;
replyCount: number;
viewCount: number;
lastReplyAt?: Date;
lastReplyAuthor?: string;
isPinned?: boolean;
tags?: string[];
}
export interface ForumReply {
id: string;
content: string;
author: string;
authorAddress?: string;
createdAt: Date;
likes: number;
userLiked?: boolean;
}
// Mock data
const mockThreads: ForumThread[] = [
{
id: '1',
title: 'Pezkuwi Forum\'a Hoş Geldiniz!',
content: 'Bu, Pezkuwi vatandaşları için resmi topluluk forumudur. Dijital devletimiz, yönetişim, geliştirme ve daha fazlası hakkında serbestçe tartışabilirsiniz.\n\nLütfen saygılı olun ve topluluk kurallarımıza uyun.',
author: 'Admin',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
replyCount: 45,
viewCount: 1234,
isPinned: true,
tags: ['duyuru', 'kurallar'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 30),
lastReplyAuthor: 'YeniVatandaş',
},
{
id: '2',
title: 'HEZ nasıl stake edilir ve ödül kazanılır?',
content: 'Herkese merhaba! İlk HEZ tokenlarımı aldım ve stake etmeye başlamak istiyorum. Biri adım adım süreci açıklayabilir mi? Minimum miktar ne kadar?',
author: 'KriptoYeni',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 5),
replyCount: 12,
viewCount: 256,
tags: ['staking', 'yardım'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
lastReplyAuthor: 'StakingPro',
},
{
id: '3',
title: 'Öneri: Uygulamaya Kürtçe dil desteği eklenmeli',
content: 'Kürt dijital devleti olarak, tüm uygulamalarımızda tam Kürtçe dil desteği (Kurmancî ve Soranî) olması gerektiğini düşünüyorum.\n\nNe düşünüyorsunuz?',
author: 'KürtGeliştirici',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
replyCount: 28,
viewCount: 567,
tags: ['öneri', 'yerelleştirme'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 4),
lastReplyAuthor: 'DilUzmanı',
},
{
id: '4',
title: 'Hata: Cüzdan bakiyesi güncellenmiyor',
content: 'Transfer yaptıktan sonra cüzdan bakiyem hemen güncellenmiyor. Sayfayı birkaç kez yenilemem gerekiyor. Bu sorunu yaşayan başka var mı?',
author: 'TeknikKullanıcı',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 12),
replyCount: 8,
viewCount: 89,
tags: ['hata', 'cüzdan'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 6),
lastReplyAuthor: 'GeliştiriciEkibi',
},
];
const mockReplies: ForumReply[] = [
{
id: '1',
content: 'Burada olmak harika! Topluluğa katılmak için sabırsızlanıyorum.',
author: 'YeniVatandaş',
createdAt: new Date(Date.now() - 1000 * 60 * 30),
likes: 5,
},
{
id: '2',
content: 'Hoş geldiniz! Dokümantasyon bölümündeki staking rehberini kontrol etmeyi unutmayın.',
author: 'Yardımcı',
createdAt: new Date(Date.now() - 1000 * 60 * 60),
likes: 12,
},
];
function ThreadCard({ thread, onClick }: { thread: ForumThread; onClick: () => void }) {
const { hapticSelection } = useTelegram();
const handleClick = () => {
hapticSelection();
onClick();
};
const formatDate = (date: Date) => {
const now = new Date();
const diff = now.getTime() - date.getTime();
const hours = Math.floor(diff / (1000 * 60 * 60));
const days = Math.floor(hours / 24);
if (hours < 1) return 'Az önce';
if (hours < 24) return `${hours}s önce`;
if (days < 7) return `${days}g önce`;
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
};
return (
<Card
className={cn(
"bg-gray-900 border-gray-800 cursor-pointer hover:border-gray-700 transition-all",
thread.isPinned && "border-l-4 border-l-blue-500"
)}
onClick={handleClick}
>
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
{/* Title with pin badge */}
<div className="flex items-center gap-2 mb-2">
{thread.isPinned && (
<Badge className="bg-blue-500/20 text-blue-500 border-blue-500/30 text-xs">
<Pin className="w-3 h-3 mr-1" />
Sabit
</Badge>
)}
<h3 className="font-medium text-white truncate">{thread.title}</h3>
</div>
{/* Preview */}
<p className="text-gray-400 text-sm mb-3 line-clamp-2">
{thread.content.length > 100 ? thread.content.slice(0, 100) + '...' : thread.content}
</p>
{/* Tags */}
{thread.tags && thread.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-3">
{thread.tags.slice(0, 3).map(tag => (
<div
key={tag}
className="flex items-center gap-1 text-xs bg-gray-800 text-gray-300 px-2 py-1 rounded-md"
>
<Hash className="w-3 h-3" />
{tag}
</div>
))}
</div>
)}
{/* Meta info */}
<div className="flex items-center gap-4 text-xs text-gray-500">
<div className="flex items-center gap-1">
<User className="w-3 h-3" />
<span>{thread.author}</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>{formatDate(thread.createdAt)}</span>
</div>
<div className="flex items-center gap-1">
<MessageCircle className="w-3 h-3" />
<span>{thread.replyCount}</span>
</div>
<div className="flex items-center gap-1">
<Eye className="w-3 h-3" />
<span>{thread.viewCount}</span>
</div>
</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-500 flex-shrink-0" />
</div>
{/* Last reply info */}
{thread.lastReplyAt && thread.lastReplyAuthor && (
<div className="mt-3 pt-3 border-t border-gray-800 text-xs text-gray-500">
Son yanıt: <span className="text-gray-400">{thread.lastReplyAuthor}</span>{' '}
· {formatDate(thread.lastReplyAt)}
</div>
)}
</CardContent>
</Card>
);
}
function ThreadView({
thread,
replies,
onBack,
onReply,
onLikeReply,
isConnected
}: {
thread: ForumThread;
replies: ForumReply[];
onBack: () => void;
onReply: (content: string) => void;
onLikeReply: (replyId: string) => void;
isConnected: boolean;
}) {
const { hapticImpact } = useTelegram();
const [replyContent, setReplyContent] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const formatDate = (date: Date) => {
return date.toLocaleDateString('tr-TR', {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const handleSubmitReply = async () => {
if (!replyContent.trim() || isSubmitting) return;
setIsSubmitting(true);
hapticImpact('medium');
try {
await onReply(replyContent);
setReplyContent('');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="flex flex-col h-full bg-gray-950">
{/* Header */}
<div className="flex items-center gap-3 p-4 border-b border-gray-800">
<Button
variant="ghost"
size="icon"
onClick={onBack}
className="h-8 w-8"
>
<ArrowLeft className="w-4 h-4 text-gray-400" />
</Button>
<h2 className="text-white font-semibold truncate flex-1">{thread.title}</h2>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto">
{/* Original post */}
<div className="p-4 border-b border-gray-800">
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
<User className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-white font-medium text-sm">{thread.author}</p>
<div className="flex items-center gap-1 text-xs text-gray-400">
<Clock className="w-3 h-3" />
<span>{formatDate(thread.createdAt)}</span>
</div>
</div>
</div>
{/* Tags */}
{thread.tags && thread.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mb-3">
{thread.tags.map(tag => (
<Badge key={tag} variant="outline" className="text-gray-400 border-gray-700 text-xs">
<Hash className="w-3 h-3 mr-1" />
{tag}
</Badge>
))}
</div>
)}
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-wrap">
{thread.content}
</p>
</CardContent>
</Card>
</div>
{/* Replies */}
<div className="p-4">
<div className="flex items-center gap-2 mb-4">
<MessageSquare className="w-4 h-4 text-blue-500" />
<span className="text-white font-medium text-sm">
{replies.length} Yanıt
</span>
</div>
<div className="space-y-3">
{replies.map(reply => (
<Card key={reply.id} className="bg-gray-900 border-gray-800">
<CardContent className="p-3">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-gray-800 flex items-center justify-center">
<User className="w-4 h-4 text-gray-400" />
</div>
<div>
<span className="text-sm font-medium text-white">{reply.author}</span>
<span className="text-xs text-gray-500 ml-2">
{formatDate(reply.createdAt)}
</span>
</div>
</div>
<button
onClick={() => onLikeReply(reply.id)}
className={cn(
"flex items-center gap-1 px-2 py-1 rounded-md text-xs transition-all",
reply.userLiked
? "bg-green-500/20 text-green-500"
: "bg-gray-800 text-gray-500 hover:text-green-500"
)}
>
<ThumbsUp className={cn("w-3 h-3", reply.userLiked && "fill-current")} />
<span>{reply.likes}</span>
</button>
</div>
<p className="text-gray-300 text-sm whitespace-pre-wrap">{reply.content}</p>
</CardContent>
</Card>
))}
{replies.length === 0 && (
<div className="text-center text-gray-500 py-8">
<MessageCircle className="w-10 h-10 mx-auto mb-2 opacity-50" />
<p>Henüz yanıt yok. İlk yanıtı siz yazın!</p>
</div>
)}
</div>
</div>
</div>
{/* Reply input */}
{isConnected ? (
<div className="p-4 border-t border-gray-800 bg-gray-900">
<div className="flex gap-2">
<Input
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
placeholder="Yanıtınızı yazın..."
className="flex-1 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500"
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmitReply();
}
}}
/>
<Button
onClick={handleSubmitReply}
disabled={!replyContent.trim() || isSubmitting}
className="bg-blue-600 hover:bg-blue-700"
>
<Send className="w-4 h-4" />
</Button>
</div>
</div>
) : (
<div className="p-4 border-t border-gray-800 bg-gray-900 text-center text-gray-500 text-sm">
Yanıt yazmak için cüzdanınızı bağlayın
</div>
)}
</div>
);
}
export function ForumSection() {
const { hapticNotification, showAlert } = useTelegram();
const { selectedAccount } = usePezkuwi();
const { isConnected } = useWallet();
const [threads, setThreads] = useState<ForumThread[]>(mockThreads);
const [selectedThread, setSelectedThread] = useState<ForumThread | null>(null);
const [replies, setReplies] = useState<ForumReply[]>(mockReplies);
const [isLoading, setIsLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const handleRefresh = async () => {
setIsLoading(true);
hapticNotification('success');
await new Promise(resolve => setTimeout(resolve, 1000));
setThreads(mockThreads);
setIsLoading(false);
};
const handleCreateThread = () => {
if (!isConnected) {
showAlert('Konu oluşturmak için cüzdanınızı bağlayın');
return;
}
showAlert('Konu oluşturma özelliği yakında!');
};
const handleReply = async (content: string) => {
if (!isConnected || !selectedThread) return;
const newReply: ForumReply = {
id: String(Date.now()),
content,
author: selectedAccount?.meta?.name || 'Anonim',
createdAt: new Date(),
likes: 0,
};
setReplies(prev => [...prev, newReply]);
hapticNotification('success');
};
const handleLikeReply = (replyId: string) => {
setReplies(prev => prev.map(reply => {
if (reply.id !== replyId) return reply;
return {
...reply,
likes: reply.userLiked ? reply.likes - 1 : reply.likes + 1,
userLiked: !reply.userLiked,
};
}));
};
const filteredThreads = threads.filter(thread =>
thread.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
thread.content.toLowerCase().includes(searchQuery.toLowerCase())
);
// Sort: pinned first, then by date
const sortedThreads = [...filteredThreads].sort((a, b) => {
if (a.isPinned && !b.isPinned) return -1;
if (!a.isPinned && b.isPinned) return 1;
return b.createdAt.getTime() - a.createdAt.getTime();
});
if (selectedThread) {
return (
<ThreadView
thread={selectedThread}
replies={replies}
onBack={() => setSelectedThread(null)}
onReply={handleReply}
onLikeReply={handleLikeReply}
isConnected={isConnected}
/>
);
}
return (
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */}
<div className="p-4 pb-0">
<div className="flex items-center justify-between mb-4">
<h2 className="text-white font-semibold text-lg flex items-center gap-2">
<MessageCircle className="w-5 h-5 text-blue-500" />
Forum
</h2>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={handleRefresh}
disabled={isLoading}
className="h-8 w-8"
>
<RefreshCw className={cn("w-4 h-4 text-gray-400", isLoading && "animate-spin")} />
</Button>
<Button
size="icon"
onClick={handleCreateThread}
className="h-8 w-8 bg-blue-600 hover:bg-blue-700"
>
<Plus className="w-4 h-4" />
</Button>
</div>
</div>
{/* Search */}
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<Input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Konularda ara..."
className="pl-9 bg-gray-900 border-gray-800 text-white placeholder:text-gray-500"
/>
</div>
{/* Stats */}
<div className="flex items-center gap-4 mb-4">
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
<MessageSquare className="w-4 h-4 text-blue-500" />
<span className="text-gray-300 text-sm">{threads.length} Konu</span>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
<Pin className="w-4 h-4 text-blue-500" />
<span className="text-gray-300 text-sm">
{threads.filter(t => t.isPinned).length} Sabitlenmiş
</span>
</div>
</div>
</div>
{/* Thread List */}
<div className="flex-1 p-4 pt-0 space-y-3">
{isLoading ? (
<>
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-32 bg-gray-800" />
))}
</>
) : sortedThreads.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
<MessageCircle className="w-12 h-12 mb-3 opacity-50" />
<p>{searchQuery ? 'Konu bulunamadı' : 'Henüz konu yok'}</p>
{!searchQuery && (
<Button
variant="link"
onClick={handleCreateThread}
className="mt-2 text-blue-500"
>
İlk konuyu oluştur
</Button>
)}
</div>
) : (
sortedThreads.map(thread => (
<ThreadCard
key={thread.id}
thread={thread}
onClick={() => setSelectedThread(thread)}
/>
))
)}
</div>
</div>
);
}
export default ForumSection;
@@ -1,350 +0,0 @@
import { useState, useEffect } from 'react';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useReferral } from '@/contexts/ReferralContext';
import { useWallet } from '@/contexts/WalletContext';
import { useTelegram } from '../../hooks/useTelegram';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { getReferralStats, ReferralStats, getMyReferrals, calculateReferralScore } from '@shared/lib/referral';
import { getStakingInfo, StakingInfo } from '@shared/lib/staking';
import {
Gift, Users, Trophy, Copy, Check, Share2, Loader2, RefreshCw,
UserPlus, Award, Star, Calendar, Zap, ChevronRight, Clock
} from 'lucide-react';
import { cn } from '@/lib/utils';
export function RewardsSection() {
const { api, isApiReady, selectedAccount } = usePezkuwi();
const { stats, myReferrals, loading: referralLoading, refreshStats } = useReferral();
const { isConnected } = useWallet();
const { hapticNotification, hapticImpact, showAlert, openTelegramLink } = useTelegram();
const [stakingInfo, setStakingInfo] = useState<StakingInfo | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [copied, setCopied] = useState(false);
const address = selectedAccount?.address;
const referralLink = address ? `https://t.me/pezkuwichain_bot?start=${address}` : '';
// Fetch staking data
useEffect(() => {
if (!api || !isApiReady || !address) return;
const fetchData = async () => {
setIsLoading(true);
try {
const staking = await getStakingInfo(api, address);
setStakingInfo(staking);
} catch (err) {
console.error('Failed to fetch rewards data:', err);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [api, isApiReady, address]);
const handleCopyLink = async () => {
if (!referralLink) return;
try {
await navigator.clipboard.writeText(referralLink);
setCopied(true);
hapticNotification('success');
setTimeout(() => setCopied(false), 2000);
} catch {
showAlert('Link kopyalanamadı');
}
};
const handleShare = () => {
if (!referralLink) return;
hapticImpact('medium');
const text = encodeURIComponent('Pezkuwichain - Kürt Dijital Devleti! Referans linkimle katıl:');
openTelegramLink(`https://t.me/share/url?url=${encodeURIComponent(referralLink)}&text=${text}`);
};
const handleRefresh = async () => {
hapticNotification('success');
await refreshStats();
};
// Not connected state
if (!isConnected || !selectedAccount) {
return (
<div className="flex flex-col h-full overflow-y-auto">
<div className="flex-1 flex flex-col items-center justify-center p-6">
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-purple-500/20 to-pink-500/20 flex items-center justify-center mb-6">
<Gift className="w-12 h-12 text-purple-500" />
</div>
<h2 className="text-white font-semibold text-xl mb-2">Ödüller ve Referanslar</h2>
<p className="text-gray-400 text-sm text-center mb-8 max-w-xs">
Referans programına katılmak ve ödüllerinizi görmek için cüzdanınızı bağlayın.
</p>
<div className="grid grid-cols-3 gap-4 w-full max-w-sm">
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
<UserPlus className="w-6 h-6 text-green-500 mb-2" />
<span className="text-xs text-gray-400 text-center">Arkadaş Davet</span>
</div>
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
<Trophy className="w-6 h-6 text-yellow-500 mb-2" />
<span className="text-xs text-gray-400 text-center">Puan Kazan</span>
</div>
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
<Zap className="w-6 h-6 text-orange-500 mb-2" />
<span className="text-xs text-gray-400 text-center">PEZ Ödülü</span>
</div>
</div>
</div>
</div>
);
}
return (
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header Stats */}
<div className="p-4 pb-0">
<div className="flex items-center justify-between mb-4">
<h2 className="text-white font-semibold text-lg flex items-center gap-2">
<Gift className="w-5 h-5 text-purple-500" />
Ödüller
</h2>
<Button
variant="ghost"
size="icon"
onClick={handleRefresh}
disabled={referralLoading}
className="h-8 w-8"
>
<RefreshCw className={cn("w-4 h-4 text-gray-400", referralLoading && "animate-spin")} />
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-3 gap-3 mb-4">
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-3 text-center">
<div className="w-10 h-10 rounded-full bg-green-500/20 flex items-center justify-center mx-auto mb-2">
<Users className="w-5 h-5 text-green-500" />
</div>
{referralLoading ? (
<Skeleton className="h-6 w-8 mx-auto bg-gray-700" />
) : (
<p className="text-white text-xl font-bold">{stats?.referralCount || 0}</p>
)}
<p className="text-gray-500 text-xs">Referans</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-3 text-center">
<div className="w-10 h-10 rounded-full bg-yellow-500/20 flex items-center justify-center mx-auto mb-2">
<Trophy className="w-5 h-5 text-yellow-500" />
</div>
{referralLoading ? (
<Skeleton className="h-6 w-8 mx-auto bg-gray-700" />
) : (
<p className="text-white text-xl font-bold">{stats?.referralScore || 0}</p>
)}
<p className="text-gray-500 text-xs">Puan</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-3 text-center">
<div className="w-10 h-10 rounded-full bg-blue-500/20 flex items-center justify-center mx-auto mb-2">
<Award className="w-5 h-5 text-blue-500" />
</div>
{isLoading ? (
<Skeleton className="h-6 w-8 mx-auto bg-gray-700" />
) : (
<p className="text-white text-xl font-bold">{stakingInfo?.stakingScore || 0}</p>
)}
<p className="text-gray-500 text-xs">Staking</p>
</CardContent>
</Card>
</div>
</div>
{/* Referral Invite Section */}
<div className="px-4 pb-4">
<Card className="bg-gradient-to-br from-purple-600 to-pink-600 border-0">
<CardContent className="p-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center">
<UserPlus className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="text-white font-semibold">Arkadaşını Davet Et</h3>
<p className="text-purple-100 text-sm">Her referans için puan kazan!</p>
</div>
</div>
{/* Referral Link */}
<div className="bg-black/20 rounded-lg p-3 mb-3">
<p className="text-purple-200 text-xs mb-1">Referans Linkin</p>
<div className="flex items-center gap-2">
<code className="flex-1 text-white text-xs truncate">{referralLink}</code>
<button
onClick={handleCopyLink}
className="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4 text-white" />
)}
</button>
</div>
</div>
<Button
onClick={handleShare}
className="w-full bg-white text-purple-600 hover:bg-purple-50"
>
<Share2 className="w-4 h-4 mr-2" />
Telegram'da Paylaş
</Button>
</CardContent>
</Card>
</div>
{/* Score System Info */}
<div className="px-4 pb-4">
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-white flex items-center gap-2">
<Star className="w-4 h-4 text-yellow-500" />
Puan Sistemi
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center justify-between p-2 bg-gray-800 rounded-lg">
<div className="flex items-center gap-2">
<span className="text-gray-400 text-xs">1-10 referans</span>
</div>
<span className="text-green-400 text-sm font-medium">×10 puan</span>
</div>
<div className="flex items-center justify-between p-2 bg-gray-800 rounded-lg">
<div className="flex items-center gap-2">
<span className="text-gray-400 text-xs">11-50 referans</span>
</div>
<span className="text-green-400 text-sm font-medium">100 + ×5 puan</span>
</div>
<div className="flex items-center justify-between p-2 bg-gray-800 rounded-lg">
<div className="flex items-center gap-2">
<span className="text-gray-400 text-xs">51-100 referans</span>
</div>
<span className="text-green-400 text-sm font-medium">300 + ×4 puan</span>
</div>
<div className="flex items-center justify-between p-2 bg-gray-800 rounded-lg">
<div className="flex items-center gap-2">
<span className="text-gray-400 text-xs">101+ referans</span>
</div>
<span className="text-yellow-400 text-sm font-medium">500 (Max)</span>
</div>
</CardContent>
</Card>
</div>
{/* Epoch Rewards */}
{stakingInfo?.pezRewards?.hasPendingClaim && (
<div className="px-4 pb-4">
<Card className="bg-gradient-to-br from-orange-600 to-yellow-600 border-0">
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-white" />
<span className="text-white font-medium">Epoch Ödülleri</span>
</div>
<Badge className="bg-white/20 text-white border-0">
Epoch #{stakingInfo.pezRewards.currentEpoch}
</Badge>
</div>
<div className="bg-black/20 rounded-lg p-3 mb-3">
<p className="text-orange-100 text-xs mb-1">Bekleyen PEZ</p>
<p className="text-white text-2xl font-bold">
{stakingInfo.pezRewards.totalClaimable} PEZ
</p>
</div>
<div className="space-y-2 mb-3">
{stakingInfo.pezRewards.claimableRewards.map((reward) => (
<div key={reward.epoch} className="flex items-center justify-between bg-black/10 rounded-lg p-2">
<span className="text-orange-100 text-sm">Epoch #{reward.epoch}</span>
<span className="text-white font-medium">{reward.amount} PEZ</span>
</div>
))}
</div>
<Button
className="w-full bg-white text-orange-600 hover:bg-orange-50"
onClick={() => showAlert('Claim özelliği yakında!')}
>
<Zap className="w-4 h-4 mr-2" />
Tümünü Claim Et
</Button>
</CardContent>
</Card>
</div>
)}
{/* My Referrals List */}
{myReferrals && myReferrals.length > 0 && (
<div className="px-4 pb-6">
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-white flex items-center gap-2">
<Users className="w-4 h-4 text-green-500" />
Referanslarım ({myReferrals.length})
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{myReferrals.slice(0, 5).map((referral, index) => (
<div key={referral} className="flex items-center justify-between p-2 bg-gray-800 rounded-lg">
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-green-500/20 flex items-center justify-center">
<span className="text-green-500 text-xs font-bold">{index + 1}</span>
</div>
<code className="text-gray-300 text-xs">
{referral.slice(0, 6)}...{referral.slice(-4)}
</code>
</div>
<Badge variant="outline" className="text-green-400 border-green-500/30 text-xs">
KYC Onaylı
</Badge>
</div>
))}
{myReferrals.length > 5 && (
<p className="text-gray-500 text-xs text-center pt-2">
+{myReferrals.length - 5} daha fazla
</p>
)}
</CardContent>
</Card>
</div>
)}
{/* Who invited me */}
{stats?.whoInvitedMe && (
<div className="px-4 pb-6">
<Alert className="bg-blue-500/10 border-blue-500/30">
<Award className="w-4 h-4 text-blue-500" />
<AlertDescription className="text-blue-200 text-sm">
<span className="text-gray-400">Davet eden: </span>
<code className="text-blue-300">
{stats.whoInvitedMe.slice(0, 8)}...{stats.whoInvitedMe.slice(-6)}
</code>
</AlertDescription>
</Alert>
</div>
)}
</div>
);
}
export default RewardsSection;
@@ -1,394 +0,0 @@
import { useState, useEffect } from 'react';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { useWallet } from '@/contexts/WalletContext';
import { useTelegram } from '../../hooks/useTelegram';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { getAllScores, UserScores, getScoreRating } from '@shared/lib/scores';
import { getStakingInfo, StakingInfo } from '@shared/lib/staking';
import { formatBalance, formatAddress, CHAIN_CONFIG } from '@shared/lib/wallet';
import {
Wallet, Send, ArrowDownToLine, TrendingUp, Copy, Check, ExternalLink,
Loader2, RefreshCw, Trophy, Users, Star, Award, Coins, Clock, Zap
} from 'lucide-react';
import { cn } from '@/lib/utils';
export function WalletSection() {
const { api, isApiReady, selectedAccount, connectWallet, disconnectWallet, accounts } = usePezkuwi();
const { balances, refreshBalances, isConnected } = useWallet();
const { hapticNotification, hapticImpact, showAlert, openLink } = useTelegram();
const [scores, setScores] = useState<UserScores | null>(null);
const [stakingInfo, setStakingInfo] = useState<StakingInfo | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [copied, setCopied] = useState(false);
const address = selectedAccount?.address;
// Fetch data when connected
useEffect(() => {
if (!api || !isApiReady || !address) return;
const fetchData = async () => {
setIsLoading(true);
try {
const [userScores, staking] = await Promise.all([
getAllScores(api, address),
getStakingInfo(api, address),
]);
setScores(userScores);
setStakingInfo(staking);
} catch (err) {
console.error('Failed to fetch wallet data:', err);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [api, isApiReady, address]);
const handleRefresh = async () => {
if (!api || !address || isRefreshing) return;
setIsRefreshing(true);
hapticNotification('success');
try {
await refreshBalances();
const [userScores, staking] = await Promise.all([
getAllScores(api, address),
getStakingInfo(api, address),
]);
setScores(userScores);
setStakingInfo(staking);
} catch (err) {
showAlert('Yenileme başarısız');
} finally {
setIsRefreshing(false);
}
};
const handleCopyAddress = async () => {
if (!address) return;
try {
await navigator.clipboard.writeText(address);
setCopied(true);
hapticNotification('success');
setTimeout(() => setCopied(false), 2000);
} catch {
showAlert('Adres kopyalanamadı');
}
};
const handleConnect = () => {
hapticImpact('medium');
connectWallet();
};
const handleOpenExplorer = () => {
if (!address) return;
hapticImpact('light');
openLink(`https://explorer.pezkuwichain.io/account/${address}`);
};
// Not connected state
if (!isConnected || !selectedAccount) {
return (
<div className="flex flex-col h-full overflow-y-auto">
<div className="flex-1 flex flex-col items-center justify-center p-6">
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-cyan-500/20 to-blue-500/20 flex items-center justify-center mb-6">
<Wallet className="w-12 h-12 text-cyan-500" />
</div>
<h2 className="text-white font-semibold text-xl mb-2">Cüzdanınızı Bağlayın</h2>
<p className="text-gray-400 text-sm text-center mb-8 max-w-xs">
Bakiyelerinizi görüntülemek, stake etmek ve işlem yapmak için Pezkuwi cüzdanınızı bağlayın.
</p>
<Button
onClick={handleConnect}
className="bg-green-600 hover:bg-green-700 text-white px-8 py-6 text-base"
>
<Wallet className="w-5 h-5 mr-2" />
Cüzdan Bağla
</Button>
<div className="mt-8 grid grid-cols-3 gap-4 w-full max-w-sm">
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
<Coins className="w-6 h-6 text-yellow-500 mb-2" />
<span className="text-xs text-gray-400">HEZ & PEZ</span>
</div>
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
<TrendingUp className="w-6 h-6 text-purple-500 mb-2" />
<span className="text-xs text-gray-400">Staking</span>
</div>
<div className="flex flex-col items-center p-3 bg-gray-900 rounded-lg">
<Trophy className="w-6 h-6 text-cyan-500 mb-2" />
<span className="text-xs text-gray-400">Rewards</span>
</div>
</div>
</div>
</div>
);
}
return (
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Account Card */}
<div className="p-4">
<Card className="bg-gradient-to-br from-gray-900 to-gray-800 border-gray-700">
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center">
<span className="text-white font-bold">
{selectedAccount?.meta?.name?.charAt(0) || 'P'}
</span>
</div>
<div>
<p className="text-white font-medium text-sm">
{selectedAccount?.meta?.name || 'Pezkuwi Hesabı'}
</p>
<div className="flex items-center gap-1">
<code className="text-gray-400 text-xs">
{formatAddress(address || '')}
</code>
<button onClick={handleCopyAddress} className="p-1">
{copied ? (
<Check className="w-3 h-3 text-green-500" />
) : (
<Copy className="w-3 h-3 text-gray-500" />
)}
</button>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={handleRefresh}
disabled={isRefreshing}
className="h-8 w-8"
>
<RefreshCw className={cn("w-4 h-4 text-gray-400", isRefreshing && "animate-spin")} />
</Button>
<Button
variant="ghost"
size="icon"
onClick={handleOpenExplorer}
className="h-8 w-8"
>
<ExternalLink className="w-4 h-4 text-gray-400" />
</Button>
</div>
</div>
{/* Balance Display */}
<div className="bg-black/30 rounded-lg p-4 mb-3">
<p className="text-gray-400 text-xs mb-1">Toplam Bakiye</p>
{isLoading ? (
<Skeleton className="h-8 w-32 bg-gray-700" />
) : (
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold text-white">
{balances?.HEZ || '0.00'}
</span>
<span className="text-gray-400 text-sm">{CHAIN_CONFIG.symbol}</span>
</div>
)}
{balances?.PEZ && parseFloat(balances.PEZ) > 0 && (
<p className="text-green-400 text-sm mt-1">
+ {balances.PEZ} PEZ
</p>
)}
</div>
{/* Quick Actions */}
<div className="grid grid-cols-3 gap-2">
<Button
variant="outline"
className="flex flex-col items-center gap-1 h-auto py-3 bg-gray-800/50 border-gray-700 hover:bg-gray-700"
onClick={() => showAlert('Gönder özelliği yakında!')}
>
<Send className="w-5 h-5 text-blue-400" />
<span className="text-xs">Gönder</span>
</Button>
<Button
variant="outline"
className="flex flex-col items-center gap-1 h-auto py-3 bg-gray-800/50 border-gray-700 hover:bg-gray-700"
onClick={() => showAlert('Al özelliği yakında!')}
>
<ArrowDownToLine className="w-5 h-5 text-green-400" />
<span className="text-xs">Al</span>
</Button>
<Button
variant="outline"
className="flex flex-col items-center gap-1 h-auto py-3 bg-gray-800/50 border-gray-700 hover:bg-gray-700"
onClick={() => showAlert('Stake özelliği yakında!')}
>
<TrendingUp className="w-5 h-5 text-purple-400" />
<span className="text-xs">Stake</span>
</Button>
</div>
</CardContent>
</Card>
</div>
{/* Scores Section */}
<div className="px-4 pb-4">
<h3 className="text-white font-medium text-sm mb-3 flex items-center gap-2">
<Trophy className="w-4 h-4 text-yellow-500" />
Puanlarınız
</h3>
{isLoading ? (
<div className="grid grid-cols-2 gap-3">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-20 bg-gray-800" />
))}
</div>
) : scores ? (
<>
{/* Total Score Banner */}
<Card className="bg-gradient-to-r from-purple-600 to-pink-600 border-0 mb-3">
<CardContent className="p-4 flex items-center justify-between">
<div>
<p className="text-purple-100 text-xs">Toplam Skor</p>
<p className="text-white text-2xl font-bold">{scores.totalScore}</p>
</div>
<Badge className="bg-white/20 text-white border-0">
{getScoreRating(scores.totalScore)}
</Badge>
</CardContent>
</Card>
{/* Individual Scores */}
<div className="grid grid-cols-2 gap-3">
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-3">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center">
<Award className="w-4 h-4 text-purple-500" />
</div>
<span className="text-gray-400 text-xs">Trust</span>
</div>
<p className="text-white text-xl font-bold">{scores.trustScore}</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-3">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-lg bg-cyan-500/20 flex items-center justify-center">
<Users className="w-4 h-4 text-cyan-500" />
</div>
<span className="text-gray-400 text-xs">Referral</span>
</div>
<p className="text-white text-xl font-bold">{scores.referralScore}</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-3">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-lg bg-green-500/20 flex items-center justify-center">
<TrendingUp className="w-4 h-4 text-green-500" />
</div>
<span className="text-gray-400 text-xs">Staking</span>
</div>
<p className="text-white text-xl font-bold">{scores.stakingScore}</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-3">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-lg bg-pink-500/20 flex items-center justify-center">
<Star className="w-4 h-4 text-pink-500" />
</div>
<span className="text-gray-400 text-xs">Tiki</span>
</div>
<p className="text-white text-xl font-bold">{scores.tikiScore}</p>
</CardContent>
</Card>
</div>
</>
) : null}
</div>
{/* Staking Info */}
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 && (
<div className="px-4 pb-4">
<h3 className="text-white font-medium text-sm mb-3 flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-green-500" />
Staking Durumu
</h3>
<Card className="bg-gray-900 border-gray-800">
<CardContent className="p-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-gray-400 text-xs mb-1">Stake Edilmiş</p>
<p className="text-white font-bold">{stakingInfo.bonded} HEZ</p>
</div>
<div>
<p className="text-gray-400 text-xs mb-1">Aktif</p>
<p className="text-green-400 font-bold">{stakingInfo.active} HEZ</p>
</div>
{stakingInfo.stakingScore !== null && (
<div>
<p className="text-gray-400 text-xs mb-1">Staking Skoru</p>
<p className="text-purple-400 font-bold">{stakingInfo.stakingScore}/100</p>
</div>
)}
<div>
<p className="text-gray-400 text-xs mb-1">Nominasyonlar</p>
<p className="text-white font-bold">{stakingInfo.nominations.length}</p>
</div>
</div>
{/* PEZ Rewards */}
{stakingInfo.pezRewards?.hasPendingClaim && (
<div className="mt-4 pt-4 border-t border-gray-800">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-yellow-500" />
<span className="text-gray-400 text-sm">Bekleyen PEZ</span>
</div>
<span className="text-yellow-400 font-bold">
{stakingInfo.pezRewards.totalClaimable} PEZ
</span>
</div>
<Button
className="w-full mt-3 bg-yellow-600 hover:bg-yellow-700"
onClick={() => showAlert('Claim özelliği yakında!')}
>
Claim Yap
</Button>
</div>
)}
</CardContent>
</Card>
</div>
)}
{/* Disconnect Button */}
<div className="px-4 pb-6">
<Button
variant="outline"
className="w-full border-red-500/30 text-red-400 hover:bg-red-500/10"
onClick={() => {
hapticImpact('medium');
disconnectWallet();
}}
>
Bağlantıyı Kes
</Button>
</div>
</div>
);
}
export default WalletSection;
-8
View File
@@ -1,8 +0,0 @@
export { Sidebar } from './Sidebar';
export type { Section } from './Sidebar';
export { Announcements } from './Announcements';
export { Forum } from './Forum';
export { Rewards } from './Rewards';
export { APK } from './APK';
export { Wallet } from './Wallet';
-4
View File
@@ -1,4 +0,0 @@
export { useTelegram } from './useTelegram';
export type { TelegramUser, TelegramTheme } from './useTelegram';
export { usePezkuwiApi, getApiInstance } from './usePezkuwiApi';
-159
View File
@@ -1,159 +0,0 @@
import { useState, useEffect } from 'react';
import { ApiPromise, WsProvider } from '@pezkuwi/api';
// RPC endpoint - uses environment variable or falls back to mainnet
const RPC_ENDPOINT = import.meta.env.VITE_WS_ENDPOINT || 'wss://rpc.pezkuwichain.io:9944';
const FALLBACK_ENDPOINTS = [
RPC_ENDPOINT,
import.meta.env.VITE_WS_ENDPOINT_FALLBACK_1,
import.meta.env.VITE_WS_ENDPOINT_FALLBACK_2,
].filter(Boolean) as string[];
interface UsePezkuwiApiReturn {
api: ApiPromise | null;
isReady: boolean;
isConnecting: boolean;
error: string | null;
reconnect: () => Promise<void>;
}
// Singleton API instance to avoid multiple connections
let globalApi: ApiPromise | null = null;
let connectionPromise: Promise<ApiPromise> | null = null;
async function createApiConnection(): Promise<ApiPromise> {
// Return existing connection promise if one is in progress
if (connectionPromise) {
return connectionPromise;
}
// Return existing API if already connected
if (globalApi && globalApi.isConnected) {
return globalApi;
}
// Create new connection
connectionPromise = (async () => {
for (const endpoint of FALLBACK_ENDPOINTS) {
try {
if (import.meta.env.DEV) {
console.log('[PezkuwiApi] Connecting to:', endpoint);
}
const provider = new WsProvider(endpoint);
const api = await ApiPromise.create({ provider });
await api.isReady;
globalApi = api;
if (import.meta.env.DEV) {
const [chain, nodeName, nodeVersion] = await Promise.all([
api.rpc.system.chain(),
api.rpc.system.name(),
api.rpc.system.version(),
]);
console.log(`[PezkuwiApi] Connected to ${chain} (${nodeName} v${nodeVersion})`);
}
return api;
} catch (err) {
if (import.meta.env.DEV) {
console.warn(`[PezkuwiApi] Failed to connect to ${endpoint}:`, err);
}
continue;
}
}
throw new Error('Failed to connect to any endpoint');
})();
try {
return await connectionPromise;
} finally {
connectionPromise = null;
}
}
export function usePezkuwiApi(): UsePezkuwiApiReturn {
const [api, setApi] = useState<ApiPromise | null>(globalApi);
const [isReady, setIsReady] = useState(globalApi?.isConnected || false);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<string | null>(null);
const connect = async () => {
if (isConnecting) return;
setIsConnecting(true);
setError(null);
try {
const apiInstance = await createApiConnection();
setApi(apiInstance);
setIsReady(true);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Connection failed';
setError(errorMessage);
setIsReady(false);
} finally {
setIsConnecting(false);
}
};
const reconnect = async () => {
// Disconnect existing connection
if (globalApi) {
await globalApi.disconnect();
globalApi = null;
}
await connect();
};
useEffect(() => {
// If we already have a global API, use it
if (globalApi && globalApi.isConnected) {
setApi(globalApi);
setIsReady(true);
return;
}
// Otherwise, establish connection
connect();
// Cleanup on unmount - don't disconnect global API, just clean up local state
return () => {
// Note: We don't disconnect globalApi here to maintain connection across components
};
}, []);
// Handle disconnection events
useEffect(() => {
if (!api) return;
const handleDisconnected = () => {
if (import.meta.env.DEV) {
console.log('[PezkuwiApi] Disconnected, attempting to reconnect...');
}
setIsReady(false);
reconnect();
};
api.on('disconnected', handleDisconnected);
return () => {
api.off('disconnected', handleDisconnected);
};
}, [api]);
return {
api,
isReady,
isConnecting,
error,
reconnect,
};
}
// Export helper to get the global API instance (for non-hook usage)
export function getApiInstance(): ApiPromise | null {
return globalApi;
}
-314
View File
@@ -1,314 +0,0 @@
import { useEffect, useState, useCallback } from 'react';
import WebApp from '@twa-dev/sdk';
export interface TelegramUser {
id: number;
first_name: string;
last_name?: string;
username?: string;
language_code?: string;
is_premium?: boolean;
photo_url?: string;
}
export interface TelegramTheme {
bg_color: string;
text_color: string;
hint_color: string;
link_color: string;
button_color: string;
button_text_color: string;
secondary_bg_color: string;
}
interface UseTelegramReturn {
// State
isReady: boolean;
isTelegram: boolean;
user: TelegramUser | null;
startParam: string | null;
theme: TelegramTheme | null;
colorScheme: 'light' | 'dark';
viewportHeight: number;
viewportStableHeight: number;
isExpanded: boolean;
// Actions
ready: () => void;
expand: () => void;
close: () => void;
showAlert: (message: string) => void;
showConfirm: (message: string) => Promise<boolean>;
showPopup: (params: { title?: string; message: string; buttons?: Array<{ id: string; type?: string; text: string }> }) => Promise<string>;
openLink: (url: string, options?: { try_instant_view?: boolean }) => void;
openTelegramLink: (url: string) => void;
sendData: (data: string) => void;
enableClosingConfirmation: () => void;
disableClosingConfirmation: () => void;
setHeaderColor: (color: string) => void;
setBackgroundColor: (color: string) => void;
// Main Button
showMainButton: (text: string, onClick: () => void) => void;
hideMainButton: () => void;
setMainButtonLoading: (loading: boolean) => void;
// Back Button
showBackButton: (onClick: () => void) => void;
hideBackButton: () => void;
// Haptic Feedback
hapticImpact: (style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft') => void;
hapticNotification: (type: 'error' | 'success' | 'warning') => void;
hapticSelection: () => void;
}
export function useTelegram(): UseTelegramReturn {
const [isReady, setIsReady] = useState(false);
const [isTelegram, setIsTelegram] = useState(false);
const [user, setUser] = useState<TelegramUser | null>(null);
const [startParam, setStartParam] = useState<string | null>(null);
const [theme, setTheme] = useState<TelegramTheme | null>(null);
const [colorScheme, setColorScheme] = useState<'light' | 'dark'>('dark');
const [viewportHeight, setViewportHeight] = useState(window.innerHeight);
const [viewportStableHeight, setViewportStableHeight] = useState(window.innerHeight);
const [isExpanded, setIsExpanded] = useState(false);
// Initialize Telegram WebApp
useEffect(() => {
try {
// Check if running in Telegram WebApp environment
const tg = WebApp;
if (tg && tg.initData) {
setIsTelegram(true);
// Get user info
if (tg.initDataUnsafe?.user) {
setUser(tg.initDataUnsafe.user as TelegramUser);
}
// Get start parameter (referral code, etc.)
if (tg.initDataUnsafe?.start_param) {
setStartParam(tg.initDataUnsafe.start_param);
}
// Get theme
if (tg.themeParams) {
setTheme(tg.themeParams as TelegramTheme);
}
// Get color scheme
setColorScheme(tg.colorScheme as 'light' | 'dark' || 'dark');
// Get viewport
setViewportHeight(tg.viewportHeight || window.innerHeight);
setViewportStableHeight(tg.viewportStableHeight || window.innerHeight);
setIsExpanded(tg.isExpanded || false);
// Listen for viewport changes
tg.onEvent('viewportChanged', (event: { isStateStable: boolean }) => {
setViewportHeight(tg.viewportHeight);
if (event.isStateStable) {
setViewportStableHeight(tg.viewportStableHeight);
}
});
// Listen for theme changes
tg.onEvent('themeChanged', () => {
setTheme(tg.themeParams as TelegramTheme);
setColorScheme(tg.colorScheme as 'light' | 'dark' || 'dark');
});
// Signal that app is ready
tg.ready();
setIsReady(true);
// Expand by default for better UX
tg.expand();
if (import.meta.env.DEV) {
console.log('[Telegram] Mini App initialized');
console.log('[Telegram] User:', tg.initDataUnsafe?.user);
console.log('[Telegram] Start param:', tg.initDataUnsafe?.start_param);
}
} else {
// Not running in Telegram, but still mark as ready
setIsReady(true);
if (import.meta.env.DEV) {
console.log('[Telegram] Not running in Telegram WebApp environment');
}
}
} catch (err) {
console.error('[Telegram] Initialization error:', err);
setIsReady(true); // Mark as ready even on error for graceful fallback
}
}, []);
// Actions
const ready = useCallback(() => {
if (isTelegram) WebApp.ready();
}, [isTelegram]);
const expand = useCallback(() => {
if (isTelegram) {
WebApp.expand();
setIsExpanded(true);
}
}, [isTelegram]);
const close = useCallback(() => {
if (isTelegram) WebApp.close();
}, [isTelegram]);
const showAlert = useCallback((message: string) => {
if (isTelegram) {
WebApp.showAlert(message);
} else {
alert(message);
}
}, [isTelegram]);
const showConfirm = useCallback((message: string): Promise<boolean> => {
return new Promise((resolve) => {
if (isTelegram) {
WebApp.showConfirm(message, (confirmed) => {
resolve(confirmed);
});
} else {
resolve(confirm(message));
}
});
}, [isTelegram]);
const showPopup = useCallback((params: { title?: string; message: string; buttons?: Array<{ id: string; type?: string; text: string }> }): Promise<string> => {
return new Promise((resolve) => {
if (isTelegram) {
WebApp.showPopup(params, (buttonId) => {
resolve(buttonId || '');
});
} else {
// Fallback for non-Telegram environment
const result = confirm(params.message);
resolve(result ? 'ok' : 'cancel');
}
});
}, [isTelegram]);
const openLink = useCallback((url: string, options?: { try_instant_view?: boolean }) => {
if (isTelegram) {
WebApp.openLink(url, options);
} else {
window.open(url, '_blank');
}
}, [isTelegram]);
const openTelegramLink = useCallback((url: string) => {
if (isTelegram) {
WebApp.openTelegramLink(url);
} else {
window.open(url, '_blank');
}
}, [isTelegram]);
const sendData = useCallback((data: string) => {
if (isTelegram) WebApp.sendData(data);
}, [isTelegram]);
const enableClosingConfirmation = useCallback(() => {
if (isTelegram) WebApp.enableClosingConfirmation();
}, [isTelegram]);
const disableClosingConfirmation = useCallback(() => {
if (isTelegram) WebApp.disableClosingConfirmation();
}, [isTelegram]);
const setHeaderColor = useCallback((color: string) => {
if (isTelegram) WebApp.setHeaderColor(color as `#${string}`);
}, [isTelegram]);
const setBackgroundColor = useCallback((color: string) => {
if (isTelegram) WebApp.setBackgroundColor(color as `#${string}`);
}, [isTelegram]);
// Main Button
const showMainButton = useCallback((text: string, onClick: () => void) => {
if (isTelegram) {
WebApp.MainButton.setText(text);
WebApp.MainButton.onClick(onClick);
WebApp.MainButton.show();
}
}, [isTelegram]);
const hideMainButton = useCallback(() => {
if (isTelegram) WebApp.MainButton.hide();
}, [isTelegram]);
const setMainButtonLoading = useCallback((loading: boolean) => {
if (isTelegram) {
if (loading) {
WebApp.MainButton.showProgress();
} else {
WebApp.MainButton.hideProgress();
}
}
}, [isTelegram]);
// Back Button
const showBackButton = useCallback((onClick: () => void) => {
if (isTelegram) {
WebApp.BackButton.onClick(onClick);
WebApp.BackButton.show();
}
}, [isTelegram]);
const hideBackButton = useCallback(() => {
if (isTelegram) WebApp.BackButton.hide();
}, [isTelegram]);
// Haptic Feedback
const hapticImpact = useCallback((style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft') => {
if (isTelegram) WebApp.HapticFeedback.impactOccurred(style);
}, [isTelegram]);
const hapticNotification = useCallback((type: 'error' | 'success' | 'warning') => {
if (isTelegram) WebApp.HapticFeedback.notificationOccurred(type);
}, [isTelegram]);
const hapticSelection = useCallback(() => {
if (isTelegram) WebApp.HapticFeedback.selectionChanged();
}, [isTelegram]);
return {
isReady,
isTelegram,
user,
startParam,
theme,
colorScheme,
viewportHeight,
viewportStableHeight,
isExpanded,
ready,
expand,
close,
showAlert,
showConfirm,
showPopup,
openLink,
openTelegramLink,
sendData,
enableClosingConfirmation,
disableClosingConfirmation,
setHeaderColor,
setBackgroundColor,
showMainButton,
hideMainButton,
setMainButtonLoading,
showBackButton,
hideBackButton,
hapticImpact,
hapticNotification,
hapticSelection,
};
}
-3
View File
@@ -1,3 +0,0 @@
export { TelegramApp } from './TelegramApp';
export { useTelegram, usePezkuwiApi } from './hooks';
export type { TelegramUser, TelegramTheme } from './hooks';
@@ -386,7 +386,7 @@ serve(async (req) => {
}
// Success! Complete the withdrawal using database function
const { data: completeResult, error: completeError } = await serviceClient
const { error: completeError } = await serviceClient
.rpc('complete_withdraw', {
p_user_id: user.id,
p_token: token,