feat(telegram): redesign all sections with production-quality UI

- Rewrite AnnouncementsSection with shadcn/ui Card components
- Rewrite ForumSection with proper thread/reply UI
- Rewrite APKSection with version history and download features
- All sections now use consistent design patterns
- Turkish localization for all UI text
- Bottom navigation instead of sidebar
- Remove obsolete sub-components (AnnouncementCard, ThreadCard, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 23:24:41 +03:00
parent bf85df1651
commit 3e76c9c95f
10 changed files with 1657 additions and 1590 deletions
+152 -70
View File
@@ -1,130 +1,212 @@
import { useState, useEffect } from 'react';
import { useTelegram } from './hooks/useTelegram';
import { usePezkuwiApi } from './hooks/usePezkuwiApi';
import { Sidebar, Section, Announcements, Forum, Rewards, APK, Wallet } from './components';
import { Loader2, AlertCircle, RefreshCw } from 'lucide-react';
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,
user,
startParam,
colorScheme,
setHeaderColor,
setBackgroundColor,
enableClosingConfirmation,
hapticSelection,
} = useTelegram();
const { isReady: isApiReady, error: apiError, reconnect, isConnecting } = usePezkuwiApi();
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) {
// Store referral address in localStorage for later use
localStorage.setItem('referrerAddress', startParam);
if (import.meta.env.DEV) {
console.log('[TelegramApp] Referral address from startParam:', startParam);
}
console.log('[TelegramApp] Referral from startParam:', startParam);
}
}, [startParam]);
// Setup Telegram theme colors
// Setup Telegram theme
useEffect(() => {
if (isTelegram) {
// Set header and background colors to match our dark theme
setHeaderColor('#111827'); // gray-900
setBackgroundColor('#111827');
// Enable closing confirmation when user has unsaved changes
// (disabled for now, can be enabled per-section)
// enableClosingConfirmation();
setHeaderColor('#030712'); // gray-950
setBackgroundColor('#030712');
}
}, [isTelegram, setHeaderColor, setBackgroundColor]);
// Render the active section content
const renderContent = () => {
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 <Announcements />;
return <AnnouncementsSection />;
case 'forum':
return <Forum />;
return <ForumSection />;
case 'rewards':
return <Rewards />;
return <RewardsSection />;
case 'apk':
return <APK />;
return <APKSection />;
case 'wallet':
return <Wallet />;
return <WalletSection />;
default:
return <Announcements />;
return <AnnouncementsSection />;
}
};
// Loading state
if (!isTelegramReady) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-950">
<div className="flex flex-col items-center gap-4">
<Loader2 className="w-10 h-10 text-green-500 animate-spin" />
<span className="text-gray-400">Loading...</span>
</div>
<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 connection error
if (apiError && !isConnecting) {
// API Error state
if (apiError && !isApiReady) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-950 p-6">
<div className="flex flex-col items-center gap-4 text-center">
<div className="w-16 h-16 rounded-full bg-red-600/20 flex items-center justify-center">
<AlertCircle className="w-8 h-8 text-red-500" />
</div>
<h2 className="text-white font-semibold">Connection Error</h2>
<p className="text-gray-400 text-sm max-w-xs">
Unable to connect to Pezkuwichain network. Please check your connection and try again.
</p>
<button
onClick={reconnect}
className="flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors"
>
<RefreshCw className="w-4 h-4" />
Retry
</button>
<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 h-screen bg-gray-950 text-white overflow-hidden ${
colorScheme === 'light' ? 'theme-light' : 'theme-dark'
}`}
>
{/* Sidebar */}
<Sidebar
activeSection={activeSection}
onSectionChange={setActiveSection}
/>
<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>
{/* Main content */}
<main className="flex-1 flex flex-col overflow-hidden bg-gray-900">
{/* API connecting indicator */}
{!isApiReady && isConnecting && (
<div className="bg-yellow-900/30 border-b border-yellow-700/50 px-4 py-2 flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 text-yellow-500 animate-spin" />
<span className="text-yellow-500 text-sm">Connecting to network...</span>
{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>
{/* Content area */}
<div className="flex-1 overflow-hidden">
{renderContent()}
{/* 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>
);
}
+295 -212
View File
@@ -1,6 +1,15 @@
import { useState } from 'react';
import { Smartphone, Download, Clock, CheckCircle2, AlertCircle, ExternalLink, FileText, Shield } from 'lucide-react';
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 {
@@ -11,47 +20,52 @@ interface AppVersion {
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), // 2 days ago
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: [
'New: Telegram Mini App integration',
'New: Improved staking interface',
'Fix: Balance refresh issues',
'Fix: Transaction history loading',
'Improved: Overall performance',
'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), // 14 days ago
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: [
'Fix: Critical security update',
'Fix: Wallet connection stability',
'Improved: Transaction signing',
'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), // 30 days ago
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: [
'New: Multi-language support',
'New: Dark theme improvements',
'New: QR code scanning',
'Fix: Various bug fixes',
'Yeni: Çoklu dil desteği',
'Yeni: Geliştirilmiş karanlık tema',
'Yeni: QR kod tarama',
'Düzeltme: Çeşitli hata düzeltmeleri',
],
},
];
@@ -59,51 +73,168 @@ const appVersions: AppVersion[] = [
const features = [
{
icon: <Shield className="w-5 h-5" />,
title: 'Secure Wallet',
description: 'Your keys, your crypto. Full self-custody.',
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: 'Citizenship Management',
description: 'Apply for citizenship and manage your Tiki.',
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: <Download className="w-5 h-5" />,
title: 'Offline Support',
description: 'View balances and history offline.',
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',
},
];
export function APK() {
const { hapticImpact, openLink, showConfirm } = useTelegram();
const [expandedVersion, setExpandedVersion] = useState<string | null>(appVersions[0]?.version || null);
const [downloading, setDownloading] = useState<string | null>(null);
function VersionCard({
version,
isExpanded,
onToggle,
onDownload,
isDownloading
}: {
version: AppVersion;
isExpanded: boolean;
onToggle: () => void;
onDownload: () => void;
isDownloading: boolean;
}) {
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', {
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(
`Download Pezwallet v${version.version} (${version.size})?`
`Pezwallet v${version.version} (${version.size}) indirilsin mi?`
);
if (confirmed) {
setDownloading(version.version);
// Open download link
openLink(version.downloadUrl);
// Reset downloading state after a delay
setTimeout(() => {
setDownloading(null);
}, 3000);
setTimeout(() => setDownloading(null), 3000);
}
};
@@ -115,197 +246,149 @@ export function APK() {
const latestVersion = appVersions.find(v => v.isLatest);
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800">
<div className="flex items-center gap-2">
<Smartphone className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-white">Pezwallet APK</h2>
<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>
<button
onClick={handleOpenGitHub}
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors"
>
<ExternalLink className="w-4 h-4 text-gray-400" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<div className="flex-1 p-4 pt-0 space-y-4">
{/* App Banner */}
<div className="bg-gradient-to-br from-green-600 to-emerald-700 rounded-lg p-4">
<div className="flex items-start gap-4">
{/* App Icon */}
<div className="w-16 h-16 rounded-2xl bg-white/10 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">
Official wallet app for Pezkuwichain
</p>
{latestVersion && (
<button
onClick={() => handleDownload(latestVersion)}
disabled={downloading === latestVersion.version}
className="flex items-center gap-2 bg-white text-green-700 px-4 py-2 rounded-lg font-medium hover:bg-green-50 transition-colors disabled:opacity-70"
>
{downloading === latestVersion.version ? (
<>
<span className="animate-pulse">Downloading...</span>
</>
) : (
<>
<Download className="w-4 h-4" />
Download v{latestVersion.version}
</>
)}
</button>
)}
</div>
</div>
</div>
{/* Features */}
<div className="grid grid-cols-1 gap-3">
{features.map((feature, index) => (
<div
key={index}
className="bg-gray-800 rounded-lg p-3 flex items-start gap-3 border border-gray-700"
>
<div className="w-10 h-10 rounded-lg bg-green-600/20 flex items-center justify-center text-green-500 flex-shrink-0">
{feature.icon}
<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>
<h4 className="text-white font-medium text-sm">{feature.title}</h4>
<p className="text-gray-400 text-xs">{feature.description}</p>
</div>
</div>
))}
</div>
{/* Installation Guide */}
<div className="bg-yellow-900/30 border border-yellow-700/50 rounded-lg p-4">
<div className="flex items-start gap-2">
<AlertCircle className="w-5 h-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<div>
<h4 className="text-yellow-500 font-medium text-sm mb-1">Installation Guide</h4>
<ol className="text-yellow-200/80 text-xs space-y-1 list-decimal list-inside">
<li>Download the APK file</li>
<li>Open your device's Settings</li>
<li>Enable "Install from unknown sources"</li>
<li>Open the downloaded APK file</li>
<li>Follow the installation prompts</li>
</ol>
</div>
</div>
</div>
{/* Version History */}
<div className="bg-gray-800 rounded-lg border border-gray-700">
<div className="p-4 border-b border-gray-700">
<h3 className="text-white font-medium">Version History</h3>
</div>
<div className="divide-y divide-gray-700">
{appVersions.map((version) => (
<div key={version.version}>
<button
onClick={() => setExpandedVersion(
expandedVersion === version.version ? null : version.version
)}
className="w-full p-4 flex items-center justify-between hover:bg-gray-700/50 transition-colors"
>
<div className="flex items-center gap-3">
<div className={cn(
'w-10 h-10 rounded-lg flex items-center justify-center',
version.isLatest ? 'bg-green-600' : 'bg-gray-700'
)}>
{version.isLatest ? (
<CheckCircle2 className="w-5 h-5 text-white" />
) : (
<Clock className="w-5 h-5 text-gray-400" />
)}
</div>
<div className="text-left">
<div className="flex items-center gap-2">
<span className="text-white font-medium">v{version.version}</span>
{version.isLatest && (
<span className="text-xs bg-green-600 text-white px-1.5 py-0.5 rounded">
Latest
</span>
)}
</div>
<div className="text-xs text-gray-400">
{formatDate(version.releaseDate)} • {version.size}
</div>
</div>
</div>
<Download
className={cn(
'w-5 h-5 transition-transform',
expandedVersion === version.version ? 'rotate-180' : ''
<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>
{/* Expanded content */}
{expandedVersion === version.version && (
<div className="px-4 pb-4">
<div className="bg-gray-900 rounded-lg p-3">
<h4 className="text-sm font-medium text-white mb-2">Changelog</h4>
<ul className="space-y-1">
{version.changelog.map((item, idx) => (
<li key={idx} className="text-xs text-gray-400 flex items-start gap-2">
<span className="text-green-500 mt-1">•</span>
{item}
</li>
))}
</ul>
{version.minAndroidVersion && (
<div className="mt-3 pt-3 border-t border-gray-800 text-xs text-gray-500">
Requires Android {version.minAndroidVersion} or higher
</div>
)}
<button
onClick={() => handleDownload(version)}
disabled={downloading === version.version}
className="mt-3 w-full flex items-center justify-center gap-2 bg-gray-700 hover:bg-gray-600 text-white py-2 rounded-lg text-sm transition-colors disabled:opacity-70"
>
{downloading === version.version ? (
'Downloading...'
) : (
<>
<Download className="w-4 h-4" />
Download v{version.version}
</>
)}
</button>
</div>
</div>
</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>
))}
</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 text-xs text-gray-500 pb-4">
<p>Always verify downloads from official sources</p>
<p className="mt-1">
<button
onClick={handleOpenGitHub}
className="text-green-500 hover:text-green-400"
>
View all releases on GitHub
</button>
<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 APK;
export default APKSection;
@@ -1,154 +0,0 @@
import { useState } from 'react';
import { ThumbsUp, ThumbsDown, Clock, User } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useTelegram } from '../../hooks/useTelegram';
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;
}
interface AnnouncementCardProps {
announcement: Announcement;
onReact: (id: string, reaction: 'like' | 'dislike') => void;
}
export function AnnouncementCard({ announcement, onReact }: AnnouncementCardProps) {
const { hapticImpact, isTelegram } = useTelegram();
const [isExpanded, setIsExpanded] = useState(false);
const handleReact = (reaction: 'like' | 'dislike') => {
if (isTelegram) {
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 'Just now';
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
};
const truncatedContent = announcement.content.length > 200 && !isExpanded
? announcement.content.slice(0, 200) + '...'
: announcement.content;
return (
<div className={cn(
'bg-gray-800 rounded-lg p-4 border border-gray-700',
announcement.isPinned && 'border-green-600 border-l-4'
)}>
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
{/* Author avatar */}
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center overflow-hidden">
{announcement.authorAvatar ? (
<img
src={announcement.authorAvatar}
alt={announcement.author}
className="w-full h-full object-cover"
/>
) : (
<User className="w-5 h-5 text-white" />
)}
</div>
<div>
<h3 className="font-semibold text-white text-sm">{announcement.author}</h3>
<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 && (
<span className="text-xs bg-green-600 text-white px-2 py-0.5 rounded-full">
Pinned
</span>
)}
</div>
{/* Title */}
<h4 className="text-white font-medium mb-2">{announcement.title}</h4>
{/* Content */}
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-wrap">
{truncatedContent}
</p>
{/* Show more/less button */}
{announcement.content.length > 200 && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-green-500 text-sm mt-2 hover:text-green-400"
>
{isExpanded ? 'Show less' : 'Show more'}
</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-700">
<button
onClick={() => handleReact('like')}
className={cn(
'flex items-center gap-1.5 text-sm transition-colors',
announcement.userReaction === 'like'
? 'text-green-500'
: 'text-gray-400 hover:text-green-500'
)}
>
<ThumbsUp className={cn(
'w-4 h-4',
announcement.userReaction === 'like' && 'fill-current'
)} />
<span>{announcement.likes}</span>
</button>
<button
onClick={() => handleReact('dislike')}
className={cn(
'flex items-center gap-1.5 text-sm transition-colors',
announcement.userReaction === 'dislike'
? 'text-red-500'
: 'text-gray-400 hover:text-red-500'
)}
>
<ThumbsDown className={cn(
'w-4 h-4',
announcement.userReaction === 'dislike' && 'fill-current'
)} />
<span>{announcement.dislikes}</span>
</button>
</div>
</div>
);
}
@@ -1,41 +1,208 @@
import { useState, useEffect } from 'react';
import { Megaphone, RefreshCw } from 'lucide-react';
import { AnnouncementCard, Announcement } from './AnnouncementCard';
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 Launch!',
content: 'We are excited to announce that Pezkuwichain mainnet is now live! This marks a significant milestone in our journey towards building a decentralized digital state for the Kurdish people.\n\nKey features:\n- Fast transaction finality (6 seconds)\n- Low gas fees\n- Native staking support\n- Democratic governance\n\nStart exploring at app.pezkuwichain.io',
author: 'Pezkuwi Team',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
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: 'New Referral Program',
content: 'Invite friends and earn rewards! For every friend who completes KYC, you will receive bonus points that contribute to your trust score.\n\nShare your referral link from the Rewards section.',
author: 'Pezkuwi Team',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago
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 Rewards Update',
content: 'We have updated the staking rewards mechanism. Citizens who stake HEZ will now earn PEZ tokens as rewards each epoch.\n\nMinimum stake: 10 HEZ\nEpoch duration: 7 days',
author: 'Pezkuwi Team',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3), // 3 days ago
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,
},
];
export function Announcements() {
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);
@@ -64,41 +231,68 @@ export function Announcements() {
const handleRefresh = async () => {
setIsLoading(true);
hapticNotification('success');
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// In production, this would fetch from API
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">
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800">
<div className="flex items-center gap-2">
<Megaphone className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-white">Duyurular</h2>
<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>
<button
onClick={handleRefresh}
disabled={isLoading}
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 text-gray-400 ${isLoading ? 'animate-spin' : ''}`} />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{announcements.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500">
{/* 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>No announcements yet</p>
<p>Henüz duyuru yok</p>
</div>
) : (
announcements.map(announcement => (
sortedAnnouncements.map(announcement => (
<AnnouncementCard
key={announcement.id}
announcement={announcement}
@@ -111,4 +305,4 @@ export function Announcements() {
);
}
export default Announcements;
export default AnnouncementsSection;
@@ -1,121 +0,0 @@
import { MessageCircle, Eye, Clock, User, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useTelegram } from '../../hooks/useTelegram';
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[];
}
interface ThreadCardProps {
thread: ForumThread;
onClick: () => void;
}
export function ThreadCard({ thread, onClick }: ThreadCardProps) {
const { hapticSelection, isTelegram } = useTelegram();
const handleClick = () => {
if (isTelegram) {
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 'Just now';
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
};
const truncatedContent = thread.content.length > 100
? thread.content.slice(0, 100) + '...'
: thread.content;
return (
<button
onClick={handleClick}
className={cn(
'w-full text-left bg-gray-800 rounded-lg p-4 border border-gray-700 hover:border-gray-600 transition-all',
thread.isPinned && 'border-l-4 border-l-green-600'
)}
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
{/* Title */}
<div className="flex items-center gap-2 mb-1">
{thread.isPinned && (
<span className="text-xs bg-green-600 text-white px-1.5 py-0.5 rounded">
Pinned
</span>
)}
<h3 className="font-medium text-white truncate">{thread.title}</h3>
</div>
{/* Preview */}
<p className="text-gray-400 text-sm mb-2 line-clamp-2">{truncatedContent}</p>
{/* Tags */}
{thread.tags && thread.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mb-2">
{thread.tags.slice(0, 3).map(tag => (
<span
key={tag}
className="text-xs bg-gray-700 text-gray-300 px-2 py-0.5 rounded-full"
>
{tag}
</span>
))}
</div>
)}
{/* Meta */}
<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-2 border-t border-gray-700 text-xs text-gray-500">
Last reply by <span className="text-gray-400">{thread.lastReplyAuthor}</span>{' '}
{formatDate(thread.lastReplyAt)}
</div>
)}
</button>
);
}
@@ -1,197 +0,0 @@
import { useState } from 'react';
import { ArrowLeft, Send, User, Clock, ThumbsUp } from 'lucide-react';
import { ForumThread } from './ThreadCard';
import { useTelegram } from '../../hooks/useTelegram';
export interface ForumReply {
id: string;
content: string;
author: string;
authorAddress?: string;
createdAt: Date;
likes: number;
userLiked?: boolean;
}
interface ThreadViewProps {
thread: ForumThread;
replies: ForumReply[];
onBack: () => void;
onReply: (content: string) => void;
onLikeReply: (replyId: string) => void;
isConnected: boolean;
}
export function ThreadView({
thread,
replies,
onBack,
onReply,
onLikeReply,
isConnected
}: ThreadViewProps) {
const { hapticImpact, showBackButton, hideBackButton, isTelegram } = useTelegram();
const [replyContent, setReplyContent] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
// Setup Telegram back button
useState(() => {
if (isTelegram) {
showBackButton(onBack);
return () => hideBackButton();
}
});
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);
if (isTelegram) {
hapticImpact('medium');
}
try {
await onReply(replyContent);
setReplyContent('');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex items-center gap-3 p-4 border-b border-gray-800">
<button
onClick={onBack}
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors"
>
<ArrowLeft className="w-4 h-4 text-gray-400" />
</button>
<h2 className="text-lg font-semibold text-white 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">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center">
<User className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="font-semibold text-white text-sm">{thread.author}</h3>
<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 mb-3">
{thread.tags.map(tag => (
<span
key={tag}
className="text-xs bg-gray-700 text-gray-300 px-2 py-0.5 rounded-full"
>
{tag}
</span>
))}
</div>
)}
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-wrap">
{thread.content}
</p>
</div>
{/* Replies */}
<div className="p-4">
<h4 className="text-sm font-medium text-gray-400 mb-4">
{replies.length} {replies.length === 1 ? 'Reply' : 'Replies'}
</h4>
<div className="space-y-4">
{replies.map(reply => (
<div key={reply.id} className="bg-gray-800 rounded-lg 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-700 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={`flex items-center gap-1 text-xs transition-colors ${
reply.userLiked ? 'text-green-500' : 'text-gray-500 hover:text-green-500'
}`}
>
<ThumbsUp className={`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>
</div>
))}
{replies.length === 0 && (
<div className="text-center text-gray-500 py-8">
No replies yet. Be the first to reply!
</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
type="text"
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
placeholder="Write a reply..."
className="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm placeholder-gray-500 focus:outline-none focus:border-green-500"
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmitReply();
}
}}
/>
<button
onClick={handleSubmitReply}
disabled={!replyContent.trim() || isSubmitting}
className="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-700 disabled:opacity-50 rounded-lg transition-colors"
>
<Send className="w-4 h-4 text-white" />
</button>
</div>
</div>
) : (
<div className="p-4 border-t border-gray-800 bg-gray-900 text-center text-gray-500 text-sm">
Connect wallet to reply
</div>
)}
</div>
);
}
+407 -70
View File
@@ -1,91 +1,405 @@
import { useState } from 'react';
import { MessageCircle, Plus, RefreshCw, Search } from 'lucide-react';
import { ThreadCard, ForumThread } from './ThreadCard';
import { ThreadView, ForumReply } from './ThreadView';
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';
// Mock data - will be replaced with API calls
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: 'Welcome to Pezkuwi Forum!',
content: 'This is the official community forum for Pezkuwi citizens. Feel free to discuss anything related to our digital state, governance, development, and more.\n\nPlease be respectful and follow our community guidelines.',
title: 'Pezkuwi Forum\'a Hoş Geldiniz!',
content: 'Bu, Pezkuwi vatandaşları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: ['announcement', 'rules'],
tags: ['duyuru', 'kurallar'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 30),
lastReplyAuthor: 'NewCitizen',
lastReplyAuthor: 'YeniVatandaş',
},
{
id: '2',
title: 'How to stake HEZ and earn rewards?',
content: 'Hi everyone! I just got my first HEZ tokens and want to start staking. Can someone explain the process step by step? What is the minimum amount required?',
author: 'CryptoNewbie',
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', 'help'],
tags: ['staking', 'yardım'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
lastReplyAuthor: 'StakingPro',
},
{
id: '3',
title: 'Proposal: Add support for Kurdish language in the app',
content: 'As a Kurdish digital state, I think it would be great to have full Kurdish language support (Kurmanci and Sorani) in all our applications.\n\nWhat do you think?',
author: 'KurdishDev',
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: ['proposal', 'localization'],
tags: ['öneri', 'yerelleştirme'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 4),
lastReplyAuthor: 'LanguageExpert',
lastReplyAuthor: 'DilUzmanı',
},
{
id: '4',
title: 'Bug report: Wallet balance not updating',
content: 'After making a transfer, my wallet balance doesn\'t update immediately. I have to refresh the page multiple times. Is anyone else experiencing this issue?',
author: 'TechUser',
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: ['bug', 'wallet'],
tags: ['hata', 'cüzdan'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 6),
lastReplyAuthor: 'DevTeam',
lastReplyAuthor: 'GeliştiriciEkibi',
},
];
const mockReplies: ForumReply[] = [
{
id: '1',
content: 'Great to be here! Looking forward to participating in the community.',
author: 'NewCitizen',
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: 'Welcome! Make sure to check out the staking guide in the docs section.',
author: 'Helper',
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,
},
];
export function Forum() {
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 isConnected = !!selectedAccount;
const handleRefresh = async () => {
setIsLoading(true);
hapticNotification('success');
@@ -96,11 +410,10 @@ export function Forum() {
const handleCreateThread = () => {
if (!isConnected) {
showAlert('Please connect your wallet to create a thread');
showAlert('Konu oluşturmak için cüzdanınızı bağlayın');
return;
}
// TODO: Implement thread creation modal
showAlert('Thread creation coming soon!');
showAlert('Konu oluşturma özelliği yakında!');
};
const handleReply = async (content: string) => {
@@ -109,7 +422,7 @@ export function Forum() {
const newReply: ForumReply = {
id: String(Date.now()),
content,
author: selectedAccount?.meta?.name || 'Anonymous',
author: selectedAccount?.meta?.name || 'Anonim',
createdAt: new Date(),
likes: 0,
};
@@ -155,57 +468,81 @@ export function Forum() {
}
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800">
<div className="flex items-center gap-2">
<MessageCircle className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-white">Forum</h2>
<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>
<div className="flex items-center gap-2">
<button
onClick={handleRefresh}
disabled={isLoading}
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 text-gray-400 ${isLoading ? 'animate-spin' : ''}`} />
</button>
<button
onClick={handleCreateThread}
className="p-2 rounded-lg bg-green-600 hover:bg-green-700 transition-colors"
>
<Plus className="w-4 h-4 text-white" />
</button>
</div>
</div>
{/* Search */}
<div className="p-4 border-b border-gray-800">
<div className="relative">
{/* 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
<Input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search threads..."
className="w-full bg-gray-800 border border-gray-700 rounded-lg pl-9 pr-3 py-2 text-white text-sm placeholder-gray-500 focus:outline-none focus:border-green-500"
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>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{sortedThreads.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500">
{/* 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 ? 'No threads found' : 'No threads yet'}</p>
<p>{searchQuery ? 'Konu bulunamadı' : 'Henüz konu yok'}</p>
{!searchQuery && (
<button
<Button
variant="link"
onClick={handleCreateThread}
className="mt-4 text-green-500 hover:text-green-400"
className="mt-2 text-blue-500"
>
Create the first thread
</button>
İlk konuyu oluştur
</Button>
)}
</div>
) : (
@@ -222,4 +559,4 @@ export function Forum() {
);
}
export default Forum;
export default ForumSection;
+279 -285
View File
@@ -1,82 +1,42 @@
import { useState, useEffect } from 'react';
import { Gift, Users, Trophy, Calendar, Copy, Check, Share2, Loader2, Star } from 'lucide-react';
import { useTelegram } from '../../hooks/useTelegram';
import { usePezkuwiApi } from '../../hooks/usePezkuwiApi';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { getReferralStats, calculateReferralScore, ReferralStats } from '@shared/lib/referral';
import { getAllScores, UserScores, getScoreRating, getScoreColor } from '@shared/lib/scores';
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';
interface DailyTask {
id: string;
title: string;
description: string;
reward: number;
completed: boolean;
progress?: number;
maxProgress?: number;
}
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 dailyTasks: DailyTask[] = [
{
id: 'login',
title: 'Daily Login',
description: 'Open the app daily',
reward: 5,
completed: true,
},
{
id: 'forum',
title: 'Forum Activity',
description: 'Post or reply in forum',
reward: 10,
completed: false,
},
{
id: 'referral',
title: 'Invite a Friend',
description: 'Invite a new user to join',
reward: 50,
completed: false,
},
];
export function Rewards() {
const { hapticNotification, hapticImpact, showAlert, user, openTelegramLink } = useTelegram();
const { api, isReady: isApiReady } = usePezkuwiApi();
const { selectedAccount } = usePezkuwi();
const [referralStats, setReferralStats] = useState<ReferralStats | null>(null);
const [userScores, setUserScores] = useState<UserScores | null>(null);
const [stakingInfo, setStakingInfo] = useState<StakingInfo | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [copied, setCopied] = useState(false);
const [claimingEpoch, setClaimingEpoch] = useState<number | null>(null);
const isConnected = !!selectedAccount;
const address = selectedAccount?.address;
const referralLink = address ? `https://t.me/pezkuwichain_bot?start=${address}` : '';
// Generate referral link
const referralLink = address
? `https://t.me/pezkuwichain?start=${address}`
: '';
// Fetch data when connected
// Fetch staking data
useEffect(() => {
if (!api || !isApiReady || !address) return;
const fetchData = async () => {
setIsLoading(true);
try {
const [stats, scores, staking] = await Promise.all([
getReferralStats(api, address),
getAllScores(api, address),
getStakingInfo(api, address),
]);
setReferralStats(stats);
setUserScores(scores);
const staking = await getStakingInfo(api, address);
setStakingInfo(staking);
} catch (err) {
console.error('Failed to fetch rewards data:', err);
@@ -90,267 +50,301 @@ export function Rewards() {
const handleCopyLink = async () => {
if (!referralLink) return;
try {
await navigator.clipboard.writeText(referralLink);
setCopied(true);
hapticNotification('success');
setTimeout(() => setCopied(false), 2000);
} catch {
showAlert('Failed to copy link');
showAlert('Link kopyalanamadı');
}
};
const handleShare = () => {
if (!referralLink) return;
hapticImpact('medium');
const text = `Join Pezkuwichain - The Digital Kurdish State! Use my referral link:`;
const shareUrl = `https://t.me/share/url?url=${encodeURIComponent(referralLink)}&text=${encodeURIComponent(text)}`;
openTelegramLink(shareUrl);
const text = encodeURIComponent('Pezkuwichain - Kürt Dijital Devleti! Referans linkimle katıl:');
openTelegramLink(`https://t.me/share/url?url=${encodeURIComponent(referralLink)}&text=${text}`);
};
const handleClaimEpoch = async (epoch: number) => {
if (!api || !selectedAccount) return;
setClaimingEpoch(epoch);
hapticImpact('medium');
try {
// TODO: Implement actual claim transaction
await new Promise(resolve => setTimeout(resolve, 2000));
hapticNotification('success');
showAlert(`Successfully claimed rewards for epoch ${epoch}!`);
} catch (err) {
hapticNotification('error');
showAlert('Failed to claim rewards');
} finally {
setClaimingEpoch(null);
}
const handleRefresh = async () => {
hapticNotification('success');
await refreshStats();
};
if (!isConnected) {
// Not connected state
if (!isConnected || !selectedAccount) {
return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-2 p-4 border-b border-gray-800">
<Gift className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-white">Rewards</h2>
</div>
<div className="flex-1 flex flex-col items-center justify-center text-gray-500 p-6">
<Gift className="w-16 h-16 mb-4 opacity-50" />
<p className="text-center mb-4">Connect your wallet to view rewards and referrals</p>
<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">
{/* Header */}
<div className="flex items-center gap-2 p-4 border-b border-gray-800">
<Gift className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-white">Rewards</h2>
<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>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{isLoading ? (
<div className="flex items-center justify-center h-32">
<Loader2 className="w-8 h-8 text-green-500 animate-spin" />
</div>
) : (
<>
{/* Score Overview */}
{userScores && (
<div className="bg-gradient-to-br from-green-600 to-emerald-700 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Trophy className="w-5 h-5 text-yellow-400" />
<span className="text-white font-medium">Trust Score</span>
</div>
<span className={cn('text-sm font-medium', getScoreColor(userScores.totalScore))}>
{getScoreRating(userScores.totalScore)}
</span>
</div>
<div className="text-4xl font-bold text-white mb-2">
{userScores.totalScore}
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="bg-black/20 rounded px-2 py-1">
<span className="text-green-200">Staking:</span>{' '}
<span className="text-white">{userScores.stakingScore}</span>
</div>
<div className="bg-black/20 rounded px-2 py-1">
<span className="text-green-200">Referral:</span>{' '}
<span className="text-white">{userScores.referralScore}</span>
</div>
<div className="bg-black/20 rounded px-2 py-1">
<span className="text-green-200">Tiki:</span>{' '}
<span className="text-white">{userScores.tikiScore}</span>
</div>
<div className="bg-black/20 rounded px-2 py-1">
<span className="text-green-200">Trust:</span>{' '}
<span className="text-white">{userScores.trustScore}</span>
</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>
)}
{/* Referral Section */}
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
<div className="flex items-center gap-2 mb-3">
<Users className="w-5 h-5 text-green-500" />
<span className="text-white font-medium">Referral Program</span>
<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>
{referralStats && (
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="bg-gray-900 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-white">
{referralStats.referralCount}
</div>
<div className="text-xs text-gray-400">Referrals</div>
</div>
<div className="bg-gray-900 rounded-lg p-3 text-center">
<div className="text-2xl font-bold text-green-500">
{referralStats.referralScore}
</div>
<div className="text-xs text-gray-400">Score</div>
</div>
</div>
)}
{/* Referral link */}
<div className="bg-gray-900 rounded-lg p-3 mb-3">
<div className="text-xs text-gray-400 mb-1">Your referral link</div>
<div className="flex items-center gap-2">
<code className="flex-1 text-sm text-gray-300 truncate">
{referralLink}
</code>
<button
onClick={handleCopyLink}
className="p-2 rounded bg-gray-700 hover:bg-gray-600 transition-colors"
>
{copied ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4 text-gray-400" />
)}
</button>
</div>
</div>
<button
onClick={handleShare}
className="w-full flex items-center justify-center gap-2 bg-green-600 hover:bg-green-700 text-white py-2.5 rounded-lg transition-colors"
>
<Share2 className="w-4 h-4" />
Share via Telegram
</button>
{/* Who invited me */}
{referralStats?.whoInvitedMe && (
<div className="mt-3 text-sm text-gray-400">
Invited by:{' '}
<span className="text-gray-300">
{referralStats.whoInvitedMe.slice(0, 8)}...{referralStats.whoInvitedMe.slice(-6)}
</span>
</div>
)}
</div>
{/* Epoch Rewards */}
{stakingInfo?.pezRewards && stakingInfo.pezRewards.hasPendingClaim && (
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
<div className="flex items-center gap-2 mb-3">
<Calendar className="w-5 h-5 text-green-500" />
<span className="text-white font-medium">Epoch Rewards</span>
</div>
<div className="bg-gray-900 rounded-lg p-3 mb-3">
<div className="flex items-center justify-between mb-2">
<span className="text-gray-400 text-sm">Current Epoch</span>
<span className="text-white font-medium">
#{stakingInfo.pezRewards.currentEpoch}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-400 text-sm">Claimable</span>
<span className="text-green-500 font-bold">
{stakingInfo.pezRewards.totalClaimable} PEZ
</span>
</div>
</div>
<div className="space-y-2">
{stakingInfo.pezRewards.claimableRewards.map(reward => (
<div
key={reward.epoch}
className="flex items-center justify-between bg-gray-900 rounded-lg p-3"
>
<div>
<div className="text-sm text-white">Epoch #{reward.epoch}</div>
<div className="text-xs text-green-500">{reward.amount} PEZ</div>
</div>
<button
onClick={() => handleClaimEpoch(reward.epoch)}
disabled={claimingEpoch === reward.epoch}
className="px-3 py-1.5 bg-green-600 hover:bg-green-700 disabled:bg-gray-700 text-white text-sm rounded-lg transition-colors"
>
{claimingEpoch === reward.epoch ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
'Claim'
)}
</button>
</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>
{/* Daily Tasks */}
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
<div className="flex items-center gap-2 mb-3">
<Star className="w-5 h-5 text-yellow-500" />
<span className="text-white font-medium">Daily Tasks</span>
<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="space-y-3">
{dailyTasks.map(task => (
<div
key={task.id}
className={cn(
'bg-gray-900 rounded-lg p-3 flex items-center justify-between',
task.completed && 'opacity-60'
)}
>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className={cn(
'text-sm font-medium',
task.completed ? 'text-gray-400 line-through' : 'text-white'
)}>
{task.title}
</span>
{task.completed && (
<Check className="w-4 h-4 text-green-500" />
)}
</div>
<div className="text-xs text-gray-500">{task.description}</div>
</div>
<div className="text-sm font-medium text-yellow-500">
+{task.reward} pts
</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>
</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 Rewards;
export default RewardsSection;
-124
View File
@@ -1,124 +0,0 @@
import { cn } from '@/lib/utils';
import { useTelegram } from '../hooks/useTelegram';
import {
Megaphone,
MessageCircle,
Gift,
Smartphone,
Wallet
} from 'lucide-react';
export type Section = 'announcements' | 'forum' | 'rewards' | 'apk' | 'wallet';
interface SidebarItem {
id: Section;
icon: React.ReactNode;
label: string;
emoji: string;
}
const sidebarItems: SidebarItem[] = [
{
id: 'announcements',
icon: <Megaphone className="w-5 h-5" />,
label: 'Duyurular',
emoji: '📢'
},
{
id: 'forum',
icon: <MessageCircle className="w-5 h-5" />,
label: 'Forum',
emoji: '💬'
},
{
id: 'rewards',
icon: <Gift className="w-5 h-5" />,
label: 'Rewards',
emoji: '🎁'
},
{
id: 'apk',
icon: <Smartphone className="w-5 h-5" />,
label: 'APK',
emoji: '📱'
},
{
id: 'wallet',
icon: <Wallet className="w-5 h-5" />,
label: 'Wallet',
emoji: '💳'
},
];
interface SidebarProps {
activeSection: Section;
onSectionChange: (section: Section) => void;
}
export function Sidebar({ activeSection, onSectionChange }: SidebarProps) {
const { hapticSelection, isTelegram } = useTelegram();
const handleClick = (section: Section) => {
if (isTelegram) {
hapticSelection();
}
onSectionChange(section);
};
return (
<div className="w-16 bg-gray-900 border-r border-gray-800 flex flex-col items-center py-3 gap-1">
{/* Logo at top */}
<div className="mb-4 p-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 text-lg">P</span>
</div>
</div>
{/* Divider */}
<div className="w-8 h-0.5 bg-gray-700 rounded-full mb-3" />
{/* Navigation items */}
<nav className="flex flex-col items-center gap-2 flex-1">
{sidebarItems.map((item) => (
<button
key={item.id}
onClick={() => handleClick(item.id)}
className={cn(
'relative w-12 h-12 rounded-2xl flex items-center justify-center transition-all duration-200',
'hover:rounded-xl hover:bg-green-600',
activeSection === item.id
? 'bg-green-600 rounded-xl'
: 'bg-gray-800 hover:bg-gray-700'
)}
title={item.label}
>
{/* Active indicator */}
<div
className={cn(
'absolute left-0 top-1/2 -translate-y-1/2 -translate-x-1/2 w-1 rounded-r-full bg-white transition-all duration-200',
activeSection === item.id ? 'h-10' : 'h-0 group-hover:h-5'
)}
/>
{/* Icon */}
<span className={cn(
'text-gray-400 transition-colors duration-200',
activeSection === item.id ? 'text-white' : 'hover:text-white'
)}>
{item.icon}
</span>
</button>
))}
</nav>
{/* Bottom section - could add settings or user avatar here */}
<div className="mt-auto pt-3 border-t border-gray-800">
<div className="w-10 h-10 rounded-full bg-gray-800 flex items-center justify-center">
<span className="text-xs text-gray-500">v1</span>
</div>
</div>
</div>
);
}
export default Sidebar;
+292 -319
View File
@@ -1,98 +1,45 @@
import { useState, useEffect } from 'react';
import { Wallet as WalletIcon, Send, ArrowDownToLine, RefreshCw, Copy, Check, Loader2, TrendingUp, Clock, ExternalLink } from 'lucide-react';
import { useTelegram } from '../../hooks/useTelegram';
import { usePezkuwiApi } from '../../hooks/usePezkuwiApi';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { formatBalance, CHAIN_CONFIG, formatAddress } from '@shared/lib/wallet';
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';
interface TokenBalance {
symbol: string;
name: string;
balance: string;
balanceUsd?: string;
icon?: string;
isNative?: boolean;
}
interface Transaction {
id: string;
type: 'send' | 'receive' | 'stake' | 'unstake' | 'claim';
amount: string;
symbol: string;
address?: string;
timestamp: Date;
status: 'pending' | 'confirmed' | 'failed';
}
// Mock recent transactions - will be replaced with actual data
const mockTransactions: Transaction[] = [
{
id: '1',
type: 'receive',
amount: '100.00',
symbol: 'HEZ',
address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2),
status: 'confirmed',
},
{
id: '2',
type: 'stake',
amount: '50.00',
symbol: 'HEZ',
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24),
status: 'confirmed',
},
{
id: '3',
type: 'claim',
amount: '5.25',
symbol: 'PEZ',
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3),
status: 'confirmed',
},
];
export function Wallet() {
export function WalletSection() {
const { api, isApiReady, selectedAccount, connectWallet, disconnectWallet, accounts } = usePezkuwi();
const { balances, refreshBalances, isConnected } = useWallet();
const { hapticNotification, hapticImpact, showAlert, openLink } = useTelegram();
const { api, isReady: isApiReady } = usePezkuwiApi();
const { selectedAccount, connectWallet, disconnectWallet } = usePezkuwi();
const [balances, setBalances] = useState<TokenBalance[]>([]);
const [scores, setScores] = useState<UserScores | null>(null);
const [stakingInfo, setStakingInfo] = useState<StakingInfo | null>(null);
const [transactions, setTransactions] = useState<Transaction[]>(mockTransactions);
const [isLoading, setIsLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [copied, setCopied] = useState(false);
const isConnected = !!selectedAccount;
const address = selectedAccount?.address;
// Fetch balances when connected
// Fetch data when connected
useEffect(() => {
if (!api || !isApiReady || !address) return;
const fetchData = async () => {
setIsLoading(true);
try {
// Fetch native balance
const accountInfo = await api.query.system.account(address);
const { data } = accountInfo.toJSON() as { data: { free: string; reserved: string } };
const freeBalance = BigInt(data.free || 0);
const nativeBalance: TokenBalance = {
symbol: CHAIN_CONFIG.symbol,
name: 'Hezar Token',
balance: formatBalance(freeBalance.toString()),
isNative: true,
};
setBalances([nativeBalance]);
// Fetch staking info
const staking = await getStakingInfo(api, address);
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);
@@ -106,26 +53,19 @@ export function Wallet() {
const handleRefresh = async () => {
if (!api || !address || isRefreshing) return;
setIsRefreshing(true);
hapticNotification('success');
try {
const accountInfo = await api.query.system.account(address);
const { data } = accountInfo.toJSON() as { data: { free: string } };
const freeBalance = BigInt(data.free || 0);
setBalances([{
symbol: CHAIN_CONFIG.symbol,
name: 'Hezar Token',
balance: formatBalance(freeBalance.toString()),
isNative: true,
}]);
const staking = await getStakingInfo(api, address);
await refreshBalances();
const [userScores, staking] = await Promise.all([
getAllScores(api, address),
getStakingInfo(api, address),
]);
setScores(userScores);
setStakingInfo(staking);
} catch (err) {
showAlert('Failed to refresh');
showAlert('Yenileme başarısız');
} finally {
setIsRefreshing(false);
}
@@ -133,14 +73,13 @@ export function Wallet() {
const handleCopyAddress = async () => {
if (!address) return;
try {
await navigator.clipboard.writeText(address);
setCopied(true);
hapticNotification('success');
setTimeout(() => setCopied(false), 2000);
} catch {
showAlert('Failed to copy address');
showAlert('Adres kopyalanamadı');
}
};
@@ -149,273 +88,307 @@ export function Wallet() {
connectWallet();
};
const handleDisconnect = () => {
hapticImpact('medium');
disconnectWallet();
};
const handleOpenExplorer = () => {
if (!address) return;
hapticImpact('light');
openLink(`https://explorer.pezkuwichain.io/account/${address}`);
};
const getTransactionIcon = (type: Transaction['type']) => {
switch (type) {
case 'send':
return <Send className="w-4 h-4 text-red-400" />;
case 'receive':
return <ArrowDownToLine className="w-4 h-4 text-green-400" />;
case 'stake':
return <TrendingUp className="w-4 h-4 text-blue-400" />;
case 'unstake':
return <Clock className="w-4 h-4 text-orange-400" />;
case 'claim':
return <ArrowDownToLine className="w-4 h-4 text-green-400" />;
default:
return null;
}
};
const formatTimestamp = (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 'Just now';
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return date.toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' });
};
if (!isConnected) {
// Not connected state
if (!isConnected || !selectedAccount) {
return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-2 p-4 border-b border-gray-800">
<WalletIcon className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-white">Wallet</h2>
</div>
<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-20 h-20 rounded-full bg-gray-800 flex items-center justify-center mb-4">
<WalletIcon className="w-10 h-10 text-gray-600" />
<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>
<h3 className="text-white font-medium mb-2">Connect Your Wallet</h3>
<p className="text-gray-400 text-sm text-center mb-6">
Connect your Pezkuwi wallet to view balances, stake tokens, and manage your assets.
<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
<Button
onClick={handleConnect}
className="flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-medium transition-colors"
className="bg-green-600 hover:bg-green-700 text-white px-8 py-6 text-base"
>
<WalletIcon className="w-5 h-5" />
Connect Wallet
</button>
<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">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800">
<div className="flex items-center gap-2">
<WalletIcon className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-white">Wallet</h2>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleRefresh}
disabled={isRefreshing}
className="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors disabled:opacity-50"
>
<RefreshCw className={cn('w-4 h-4 text-gray-400', isRefreshing && 'animate-spin')} />
</button>
<button
onClick={handleDisconnect}
className="text-xs text-gray-500 hover:text-gray-400"
>
Disconnect
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto">
{isLoading ? (
<div className="flex items-center justify-center h-32">
<Loader2 className="w-8 h-8 text-green-500 animate-spin" />
</div>
) : (
<>
{/* Address Card */}
<div className="p-4">
<div className="bg-gradient-to-br from-gray-800 to-gray-900 rounded-lg p-4 border border-gray-700">
<div className="flex items-center justify-between mb-3">
<span className="text-gray-400 text-sm">
{selectedAccount?.meta?.name || 'Account'}
<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>
<button
onClick={handleOpenExplorer}
className="text-gray-500 hover:text-gray-400"
>
<ExternalLink className="w-4 h-4" />
</button>
</div>
<div className="flex items-center gap-2">
<code className="text-white text-sm flex-1 truncate">
{formatAddress(address || '')}
</code>
<button
onClick={handleCopyAddress}
className="p-2 rounded bg-gray-700 hover:bg-gray-600 transition-colors"
>
{copied ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4 text-gray-400" />
)}
</button>
<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 Card */}
<div className="px-4 pb-4">
<div className="bg-gradient-to-br from-green-600 to-emerald-700 rounded-lg p-4">
<div className="text-green-100 text-sm mb-1">Total Balance</div>
<div className="text-3xl font-bold text-white mb-1">
{balances[0]?.balance || '0.00'} {CHAIN_CONFIG.symbol}
{/* 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>
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 && (
<div className="text-green-200 text-sm">
Staked: {stakingInfo.bonded} {CHAIN_CONFIG.symbol}
</div>
)}
</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="px-4 pb-4">
<div className="grid grid-cols-3 gap-3">
<button
onClick={() => showAlert('Send feature coming soon!')}
className="flex flex-col items-center gap-2 bg-gray-800 hover:bg-gray-700 rounded-lg p-3 transition-colors"
>
<div className="w-10 h-10 rounded-full bg-blue-600/20 flex items-center justify-center">
<Send className="w-5 h-5 text-blue-400" />
</div>
<span className="text-xs text-gray-400">Send</span>
</button>
<button
onClick={() => showAlert('Receive feature coming soon!')}
className="flex flex-col items-center gap-2 bg-gray-800 hover:bg-gray-700 rounded-lg p-3 transition-colors"
>
<div className="w-10 h-10 rounded-full bg-green-600/20 flex items-center justify-center">
<ArrowDownToLine className="w-5 h-5 text-green-400" />
</div>
<span className="text-xs text-gray-400">Receive</span>
</button>
<button
onClick={() => showAlert('Stake feature coming soon!')}
className="flex flex-col items-center gap-2 bg-gray-800 hover:bg-gray-700 rounded-lg p-3 transition-colors"
>
<div className="w-10 h-10 rounded-full bg-purple-600/20 flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-purple-400" />
</div>
<span className="text-xs text-gray-400">Stake</span>
</button>
</div>
<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>
{/* Staking Info */}
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 && (
<div className="px-4 pb-4">
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="w-5 h-5 text-purple-500" />
<span className="text-white font-medium">Staking Overview</span>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="bg-gray-900 rounded-lg p-3">
<div className="text-gray-400 text-xs mb-1">Bonded</div>
<div className="text-white font-medium">{stakingInfo.bonded}</div>
</div>
<div className="bg-gray-900 rounded-lg p-3">
<div className="text-gray-400 text-xs mb-1">Active</div>
<div className="text-white font-medium">{stakingInfo.active}</div>
</div>
{stakingInfo.stakingScore !== null && (
<div className="bg-gray-900 rounded-lg p-3">
<div className="text-gray-400 text-xs mb-1">Staking Score</div>
<div className="text-green-500 font-medium">{stakingInfo.stakingScore}</div>
</div>
)}
<div className="bg-gray-900 rounded-lg p-3">
<div className="text-gray-400 text-xs mb-1">Nominations</div>
<div className="text-white font-medium">{stakingInfo.nominations.length}</div>
</div>
</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>
</div>
)}
<Badge className="bg-white/20 text-white border-0">
{getScoreRating(scores.totalScore)}
</Badge>
</CardContent>
</Card>
{/* Recent Transactions */}
<div className="px-4 pb-4">
<div className="bg-gray-800 rounded-lg border border-gray-700">
<div className="p-4 border-b border-gray-700">
<h3 className="text-white font-medium">Recent Activity</h3>
</div>
{/* 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>
{transactions.length === 0 ? (
<div className="p-6 text-center text-gray-500 text-sm">
No recent transactions
<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>
) : (
<div className="divide-y divide-gray-700">
{transactions.map(tx => (
<div key={tx.id} className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center">
{getTransactionIcon(tx.type)}
</div>
<div>
<div className="text-white text-sm font-medium capitalize">
{tx.type}
</div>
<div className="text-gray-500 text-xs">
{formatTimestamp(tx.timestamp)}
</div>
</div>
</div>
<div className="text-right">
<div className={cn(
'font-medium text-sm',
tx.type === 'send' || tx.type === 'stake' ? 'text-red-400' : 'text-green-400'
)}>
{tx.type === 'send' || tx.type === 'stake' ? '-' : '+'}
{tx.amount} {tx.symbol}
</div>
<div className={cn(
'text-xs',
tx.status === 'confirmed' ? 'text-gray-500' :
tx.status === 'pending' ? 'text-yellow-500' : 'text-red-500'
)}>
{tx.status}
</div>
</div>
</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>
)}
</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 Wallet;
export default WalletSection;