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 { useState, useEffect } from 'react';
import { useTelegram } from './hooks/useTelegram'; import { useTelegram } from './hooks/useTelegram';
import { usePezkuwiApi } from './hooks/usePezkuwiApi'; import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { Sidebar, Section, Announcements, Forum, Rewards, APK, Wallet } from './components'; import { useWallet } from '@/contexts/WalletContext';
import { Loader2, AlertCircle, RefreshCw } from 'lucide-react'; 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() { export function TelegramApp() {
const { const {
isReady: isTelegramReady, isReady: isTelegramReady,
isTelegram, isTelegram,
user,
startParam, startParam,
colorScheme,
setHeaderColor, setHeaderColor,
setBackgroundColor, setBackgroundColor,
enableClosingConfirmation, hapticSelection,
} = useTelegram(); } = 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 [activeSection, setActiveSection] = useState<Section>('announcements');
const [isRetrying, setIsRetrying] = useState(false);
// Handle referral from startParam // Handle referral from startParam
useEffect(() => { useEffect(() => {
if (startParam) { if (startParam) {
// Store referral address in localStorage for later use
localStorage.setItem('referrerAddress', startParam); localStorage.setItem('referrerAddress', startParam);
if (import.meta.env.DEV) { console.log('[TelegramApp] Referral from startParam:', startParam);
console.log('[TelegramApp] Referral address from startParam:', startParam);
}
} }
}, [startParam]); }, [startParam]);
// Setup Telegram theme colors // Setup Telegram theme
useEffect(() => { useEffect(() => {
if (isTelegram) { if (isTelegram) {
// Set header and background colors to match our dark theme setHeaderColor('#030712'); // gray-950
setHeaderColor('#111827'); // gray-900 setBackgroundColor('#030712');
setBackgroundColor('#111827');
// Enable closing confirmation when user has unsaved changes
// (disabled for now, can be enabled per-section)
// enableClosingConfirmation();
} }
}, [isTelegram, setHeaderColor, setBackgroundColor]); }, [isTelegram, setHeaderColor, setBackgroundColor]);
// Render the active section content const handleNavClick = (section: Section) => {
const renderContent = () => { if (isTelegram) hapticSelection();
setActiveSection(section);
};
const handleRetry = () => {
setIsRetrying(true);
window.location.reload();
};
// Render active section
const renderSection = () => {
switch (activeSection) { switch (activeSection) {
case 'announcements': case 'announcements':
return <Announcements />; return <AnnouncementsSection />;
case 'forum': case 'forum':
return <Forum />; return <ForumSection />;
case 'rewards': case 'rewards':
return <Rewards />; return <RewardsSection />;
case 'apk': case 'apk':
return <APK />; return <APKSection />;
case 'wallet': case 'wallet':
return <Wallet />; return <WalletSection />;
default: default:
return <Announcements />; return <AnnouncementsSection />;
} }
}; };
// Loading state // Loading state
if (!isTelegramReady) { if (!isTelegramReady) {
return ( return (
<div className="flex items-center justify-center min-h-screen bg-gray-950"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-950 p-6">
<div className="flex flex-col items-center gap-4"> <img
<Loader2 className="w-10 h-10 text-green-500 animate-spin" /> src="/shared/images/pezkuwi_wallet_logo.png"
<span className="text-gray-400">Loading...</span> alt="Pezkuwi"
</div> 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> </div>
); );
} }
// API connection error // API Error state
if (apiError && !isConnecting) { if (apiError && !isApiReady) {
return ( return (
<div className="flex items-center justify-center min-h-screen bg-gray-950 p-6"> <div className="flex flex-col 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-500/20 flex items-center justify-center mb-4">
<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" />
<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> </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> </div>
); );
} }
return ( return (
<div <div className="flex flex-col h-screen bg-gray-950 text-white overflow-hidden">
className={`flex h-screen bg-gray-950 text-white overflow-hidden ${ {/* Header */}
colorScheme === 'light' ? 'theme-light' : 'theme-dark' <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">
{/* Sidebar */} <span className="text-white font-bold text-sm">P</span>
<Sidebar </div>
activeSection={activeSection} <div>
onSectionChange={setActiveSection} <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 */} {isConnected && (
<main className="flex-1 flex flex-col overflow-hidden bg-gray-900"> <div className="flex items-center gap-2">
{/* API connecting indicator */} <div className="w-2 h-2 rounded-full bg-green-500" />
{!isApiReady && isConnecting && ( <span className="text-xs text-gray-400">Cüzdan Bağlı</span>
<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>
</div> </div>
)} )}
</header>
{/* Content area */} {/* API connecting banner */}
<div className="flex-1 overflow-hidden"> {!isApiReady && (
{renderContent()} <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> </div>
)}
{/* Main content */}
<main className="flex-1 overflow-hidden">
{renderSection()}
</main> </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> </div>
); );
} }
+295 -212
View File
@@ -1,6 +1,15 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Smartphone, Download, Clock, CheckCircle2, AlertCircle, ExternalLink, FileText, Shield } from 'lucide-react';
import { useTelegram } from '../../hooks/useTelegram'; 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'; import { cn } from '@/lib/utils';
interface AppVersion { interface AppVersion {
@@ -11,47 +20,52 @@ interface AppVersion {
changelog: string[]; changelog: string[];
isLatest?: boolean; isLatest?: boolean;
minAndroidVersion?: string; minAndroidVersion?: string;
downloads?: number;
} }
// Mock versions - will be replaced with GitHub API
const appVersions: AppVersion[] = [ const appVersions: AppVersion[] = [
{ {
version: '1.2.0', 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', downloadUrl: 'https://github.com/pezkuwichain/pezwallet/releases/download/v1.2.0/pezwallet-v1.2.0.apk',
size: '45.2 MB', size: '45.2 MB',
isLatest: true, isLatest: true,
minAndroidVersion: '7.0', minAndroidVersion: '7.0',
downloads: 1234,
changelog: [ changelog: [
'New: Telegram Mini App integration', 'Yeni: Telegram Mini App entegrasyonu',
'New: Improved staking interface', 'Yeni: Geliştirilmiş staking arayüzü',
'Fix: Balance refresh issues', 'Düzeltme: Bakiye yenileme sorunları',
'Fix: Transaction history loading', 'Düzeltme: İşlem geçmişi yüklemesi',
'Improved: Overall performance', 'İyileştirme: Genel performans',
], ],
}, },
{ {
version: '1.1.2', 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', downloadUrl: 'https://github.com/pezkuwichain/pezwallet/releases/download/v1.1.2/pezwallet-v1.1.2.apk',
size: '44.8 MB', size: '44.8 MB',
minAndroidVersion: '7.0', minAndroidVersion: '7.0',
downloads: 856,
changelog: [ changelog: [
'Fix: Critical security update', 'Düzeltme: Kritik güvenlik güncellemesi',
'Fix: Wallet connection stability', 'Düzeltme: Cüzdan bağlantı kararlılığı',
'Improved: Transaction signing', 'İyileştirme: İşlem imzalama',
], ],
}, },
{ {
version: '1.1.0', 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', downloadUrl: 'https://github.com/pezkuwichain/pezwallet/releases/download/v1.1.0/pezwallet-v1.1.0.apk',
size: '44.5 MB', size: '44.5 MB',
minAndroidVersion: '7.0', minAndroidVersion: '7.0',
downloads: 2341,
changelog: [ changelog: [
'New: Multi-language support', 'Yeni: Çoklu dil desteği',
'New: Dark theme improvements', 'Yeni: Geliştirilmiş karanlık tema',
'New: QR code scanning', 'Yeni: QR kod tarama',
'Fix: Various bug fixes', 'Düzeltme: Çeşitli hata düzeltmeleri',
], ],
}, },
]; ];
@@ -59,51 +73,168 @@ const appVersions: AppVersion[] = [
const features = [ const features = [
{ {
icon: <Shield className="w-5 h-5" />, icon: <Shield className="w-5 h-5" />,
title: 'Secure Wallet', title: 'Güvenli Cüzdan',
description: 'Your keys, your crypto. Full self-custody.', description: 'Anahtarlarınız, kriptonuz. Tam self-custody.',
color: 'text-green-500',
bgColor: 'bg-green-500/20',
}, },
{ {
icon: <FileText className="w-5 h-5" />, icon: <FileText className="w-5 h-5" />,
title: 'Citizenship Management', title: 'Vatandaşlık Yönetimi',
description: 'Apply for citizenship and manage your Tiki.', 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" />, icon: <Wifi className="w-5 h-5" />,
title: 'Offline Support', title: 'Çevrimdışı Destek',
description: 'View balances and history offline.', description: 'Bakiye ve geçmişi çevrimdışı görüntüleyin.',
color: 'text-blue-500',
bgColor: 'bg-blue-500/20',
}, },
]; ];
export function APK() { function VersionCard({
const { hapticImpact, openLink, showConfirm } = useTelegram(); version,
const [expandedVersion, setExpandedVersion] = useState<string | null>(appVersions[0]?.version || null); isExpanded,
const [downloading, setDownloading] = useState<string | null>(null); onToggle,
onDownload,
isDownloading
}: {
version: AppVersion;
isExpanded: boolean;
onToggle: () => void;
onDownload: () => void;
isDownloading: boolean;
}) {
const formatDate = (date: Date) => { const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', { return date.toLocaleDateString('tr-TR', {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
day: 'numeric', 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) => { const handleDownload = async (version: AppVersion) => {
hapticImpact('medium'); hapticImpact('medium');
const confirmed = await showConfirm( const confirmed = await showConfirm(
`Download Pezwallet v${version.version} (${version.size})?` `Pezwallet v${version.version} (${version.size}) indirilsin mi?`
); );
if (confirmed) { if (confirmed) {
setDownloading(version.version); setDownloading(version.version);
// Open download link
openLink(version.downloadUrl); openLink(version.downloadUrl);
setTimeout(() => setDownloading(null), 3000);
// Reset downloading state after a delay
setTimeout(() => {
setDownloading(null);
}, 3000);
} }
}; };
@@ -115,197 +246,149 @@ export function APK() {
const latestVersion = appVersions.find(v => v.isLatest); const latestVersion = appVersions.find(v => v.isLatest);
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800"> <div className="p-4 pb-0">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between mb-4">
<Smartphone className="w-5 h-5 text-green-500" /> <h2 className="text-white font-semibold text-lg flex items-center gap-2">
<h2 className="text-lg font-semibold text-white">Pezwallet APK</h2> <Smartphone className="w-5 h-5 text-green-500" />
Pezwallet APK
</h2>
<Button
variant="ghost"
size="icon"
onClick={handleOpenGitHub}
className="h-8 w-8"
>
<Github className="w-4 h-4 text-gray-400" />
</Button>
</div> </div>
<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> </div>
{/* Content */} {/* 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 */} {/* App Banner */}
<div className="bg-gradient-to-br from-green-600 to-emerald-700 rounded-lg p-4"> <Card className="bg-gradient-to-br from-green-600 to-emerald-700 border-0 overflow-hidden">
<div className="flex items-start gap-4"> <CardContent className="p-4">
{/* App Icon */} <div className="flex items-start gap-4">
<div className="w-16 h-16 rounded-2xl bg-white/10 flex items-center justify-center flex-shrink-0"> {/* App Icon */}
<span className="text-3xl font-bold text-white">P</span> <div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center flex-shrink-0">
</div> <span className="text-3xl font-bold text-white">P</span>
<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}
</div> </div>
<div> <div className="flex-1">
<h4 className="text-white font-medium text-sm">{feature.title}</h4> <h3 className="text-xl font-bold text-white mb-1">Pezwallet</h3>
<p className="text-gray-400 text-xs">{feature.description}</p> <p className="text-green-100 text-sm mb-3">
</div> Pezkuwichain için resmi cüzdan uygulaması
</div> </p>
))} {latestVersion && (
</div> <Button
onClick={() => handleDownload(latestVersion)}
{/* Installation Guide */} disabled={downloading === latestVersion.version}
<div className="bg-yellow-900/30 border border-yellow-700/50 rounded-lg p-4"> className="bg-white text-green-700 hover:bg-green-50"
<div className="flex items-start gap-2"> >
<AlertCircle className="w-5 h-5 text-yellow-500 flex-shrink-0 mt-0.5" /> {downloading === latestVersion.version ? (
<div> <>
<h4 className="text-yellow-500 font-medium text-sm mb-1">Installation Guide</h4> <Loader2 className="w-4 h-4 mr-2 animate-spin" />
<ol className="text-yellow-200/80 text-xs space-y-1 list-decimal list-inside"> İndiriliyor...
<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> <Download className="w-4 h-4 mr-2" />
<li>Follow the installation prompts</li> v{latestVersion.version} İndir
</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' : ''
)} )}
/> </Button>
</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>
)} )}
</div> </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> </CardContent>
</div> </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 */} {/* Footer note */}
<div className="text-center text-xs text-gray-500 pb-4"> <div className="text-center pb-4">
<p>Always verify downloads from official sources</p> <p className="text-xs text-gray-500">
<p className="mt-1"> Daima resmi kaynaklardan indirdiğinizi doğrulayın
<button
onClick={handleOpenGitHub}
className="text-green-500 hover:text-green-400"
>
View all releases on GitHub
</button>
</p> </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> </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 { useState, useEffect } from 'react';
import { Megaphone, RefreshCw } from 'lucide-react';
import { AnnouncementCard, Announcement } from './AnnouncementCard';
import { useTelegram } from '../../hooks/useTelegram'; 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 // Mock data - will be replaced with API calls
const mockAnnouncements: Announcement[] = [ const mockAnnouncements: Announcement[] = [
{ {
id: '1', id: '1',
title: 'Pezkuwichain Mainnet Launch!', title: 'Pezkuwichain Mainnet Yayında!',
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', 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 Team', author: 'Pezkuwi Ekibi',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
likes: 142, likes: 142,
dislikes: 3, dislikes: 3,
isPinned: true, isPinned: true,
}, },
{ {
id: '2', id: '2',
title: 'New Referral Program', title: 'Yeni 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.', 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 Team', author: 'Pezkuwi Ekibi',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
likes: 89, likes: 89,
dislikes: 5, dislikes: 5,
}, },
{ {
id: '3', id: '3',
title: 'Staking Rewards Update', title: 'Staking Ödülleri Güncellendi',
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', 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 Team', author: 'Pezkuwi Ekibi',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3), // 3 days ago createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3),
likes: 67, likes: 67,
dislikes: 2, 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 { hapticNotification } = useTelegram();
const [announcements, setAnnouncements] = useState<Announcement[]>(mockAnnouncements); const [announcements, setAnnouncements] = useState<Announcement[]>(mockAnnouncements);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -64,41 +231,68 @@ export function Announcements() {
const handleRefresh = async () => { const handleRefresh = async () => {
setIsLoading(true); setIsLoading(true);
hapticNotification('success'); hapticNotification('success');
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
// In production, this would fetch from API
setAnnouncements(mockAnnouncements); setAnnouncements(mockAnnouncements);
setIsLoading(false); 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 ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800"> <div className="p-4 pb-0">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between mb-4">
<Megaphone className="w-5 h-5 text-green-500" /> <h2 className="text-white font-semibold text-lg flex items-center gap-2">
<h2 className="text-lg font-semibold text-white">Duyurular</h2> <Megaphone className="w-5 h-5 text-yellow-500" />
Duyurular
</h2>
<Button
variant="ghost"
size="icon"
onClick={handleRefresh}
disabled={isLoading}
className="h-8 w-8"
>
<RefreshCw className={cn("w-4 h-4 text-gray-400", isLoading && "animate-spin")} />
</Button>
</div>
{/* Stats */}
<div className="flex items-center gap-4 mb-4">
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
<Bell className="w-4 h-4 text-yellow-500" />
<span className="text-gray-300 text-sm">{announcements.length} Duyuru</span>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-lg">
<Pin className="w-4 h-4 text-yellow-500" />
<span className="text-gray-300 text-sm">
{announcements.filter(a => a.isPinned).length} Sabitlenmiş
</span>
</div>
</div> </div>
<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> </div>
{/* Content */} {/* Announcements List */}
<div className="flex-1 overflow-y-auto p-4 space-y-4"> <div className="flex-1 p-4 pt-0 space-y-4">
{announcements.length === 0 ? ( {isLoading ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500"> <>
{[...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" /> <Megaphone className="w-12 h-12 mb-3 opacity-50" />
<p>No announcements yet</p> <p>Henüz duyuru yok</p>
</div> </div>
) : ( ) : (
announcements.map(announcement => ( sortedAnnouncements.map(announcement => (
<AnnouncementCard <AnnouncementCard
key={announcement.id} key={announcement.id}
announcement={announcement} 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 { 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 { useTelegram } from '../../hooks/useTelegram';
import { usePezkuwi } from '@/contexts/PezkuwiContext'; 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[] = [ const mockThreads: ForumThread[] = [
{ {
id: '1', id: '1',
title: 'Welcome to Pezkuwi Forum!', title: 'Pezkuwi Forum\'a Hoş Geldiniz!',
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.', 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', author: 'Admin',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7), createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
replyCount: 45, replyCount: 45,
viewCount: 1234, viewCount: 1234,
isPinned: true, isPinned: true,
tags: ['announcement', 'rules'], tags: ['duyuru', 'kurallar'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 30), lastReplyAt: new Date(Date.now() - 1000 * 60 * 30),
lastReplyAuthor: 'NewCitizen', lastReplyAuthor: 'YeniVatandaş',
}, },
{ {
id: '2', id: '2',
title: 'How to stake HEZ and earn rewards?', title: 'HEZ nasıl stake edilir ve ödül kazanılır?',
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?', 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: 'CryptoNewbie', author: 'KriptoYeni',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 5), createdAt: new Date(Date.now() - 1000 * 60 * 60 * 5),
replyCount: 12, replyCount: 12,
viewCount: 256, viewCount: 256,
tags: ['staking', 'help'], tags: ['staking', 'yardım'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 2), lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
lastReplyAuthor: 'StakingPro', lastReplyAuthor: 'StakingPro',
}, },
{ {
id: '3', id: '3',
title: 'Proposal: Add support for Kurdish language in the app', title: 'Öneri: Uygulamaya Kürtçe dil desteği eklenmeli',
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?', 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: 'KurdishDev', author: 'KürtGeliştirici',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
replyCount: 28, replyCount: 28,
viewCount: 567, viewCount: 567,
tags: ['proposal', 'localization'], tags: ['öneri', 'yerelleştirme'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 4), lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 4),
lastReplyAuthor: 'LanguageExpert', lastReplyAuthor: 'DilUzmanı',
}, },
{ {
id: '4', id: '4',
title: 'Bug report: Wallet balance not updating', title: 'Hata: Cüzdan bakiyesi güncellenmiyor',
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?', 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: 'TechUser', author: 'TeknikKullanıcı',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 12), createdAt: new Date(Date.now() - 1000 * 60 * 60 * 12),
replyCount: 8, replyCount: 8,
viewCount: 89, viewCount: 89,
tags: ['bug', 'wallet'], tags: ['hata', 'cüzdan'],
lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 6), lastReplyAt: new Date(Date.now() - 1000 * 60 * 60 * 6),
lastReplyAuthor: 'DevTeam', lastReplyAuthor: 'GeliştiriciEkibi',
}, },
]; ];
const mockReplies: ForumReply[] = [ const mockReplies: ForumReply[] = [
{ {
id: '1', id: '1',
content: 'Great to be here! Looking forward to participating in the community.', content: 'Burada olmak harika! Topluluğa katılmak için sabırsızlanıyorum.',
author: 'NewCitizen', author: 'YeniVatandaş',
createdAt: new Date(Date.now() - 1000 * 60 * 30), createdAt: new Date(Date.now() - 1000 * 60 * 30),
likes: 5, likes: 5,
}, },
{ {
id: '2', id: '2',
content: 'Welcome! Make sure to check out the staking guide in the docs section.', content: 'Hoş geldiniz! Dokümantasyon bölümündeki staking rehberini kontrol etmeyi unutmayın.',
author: 'Helper', author: 'Yardımcı',
createdAt: new Date(Date.now() - 1000 * 60 * 60), createdAt: new Date(Date.now() - 1000 * 60 * 60),
likes: 12, 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 { hapticNotification, showAlert } = useTelegram();
const { selectedAccount } = usePezkuwi(); const { selectedAccount } = usePezkuwi();
const { isConnected } = useWallet();
const [threads, setThreads] = useState<ForumThread[]>(mockThreads); const [threads, setThreads] = useState<ForumThread[]>(mockThreads);
const [selectedThread, setSelectedThread] = useState<ForumThread | null>(null); const [selectedThread, setSelectedThread] = useState<ForumThread | null>(null);
const [replies, setReplies] = useState<ForumReply[]>(mockReplies); const [replies, setReplies] = useState<ForumReply[]>(mockReplies);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const isConnected = !!selectedAccount;
const handleRefresh = async () => { const handleRefresh = async () => {
setIsLoading(true); setIsLoading(true);
hapticNotification('success'); hapticNotification('success');
@@ -96,11 +410,10 @@ export function Forum() {
const handleCreateThread = () => { const handleCreateThread = () => {
if (!isConnected) { 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; return;
} }
// TODO: Implement thread creation modal showAlert('Konu oluşturma özelliği yakında!');
showAlert('Thread creation coming soon!');
}; };
const handleReply = async (content: string) => { const handleReply = async (content: string) => {
@@ -109,7 +422,7 @@ export function Forum() {
const newReply: ForumReply = { const newReply: ForumReply = {
id: String(Date.now()), id: String(Date.now()),
content, content,
author: selectedAccount?.meta?.name || 'Anonymous', author: selectedAccount?.meta?.name || 'Anonim',
createdAt: new Date(), createdAt: new Date(),
likes: 0, likes: 0,
}; };
@@ -155,57 +468,81 @@ export function Forum() {
} }
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800"> <div className="p-4 pb-0">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between mb-4">
<MessageCircle className="w-5 h-5 text-green-500" /> <h2 className="text-white font-semibold text-lg flex items-center gap-2">
<h2 className="text-lg font-semibold text-white">Forum</h2> <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>
<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 */} {/* Search */}
<div className="p-4 border-b border-gray-800"> <div className="relative mb-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input <Input
type="text" type="text"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search threads..." placeholder="Konularda ara..."
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" className="pl-9 bg-gray-900 border-gray-800 text-white placeholder:text-gray-500"
/> />
</div> </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> </div>
{/* Content */} {/* Thread List */}
<div className="flex-1 overflow-y-auto p-4 space-y-3"> <div className="flex-1 p-4 pt-0 space-y-3">
{sortedThreads.length === 0 ? ( {isLoading ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500"> <>
{[...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" /> <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 && ( {!searchQuery && (
<button <Button
variant="link"
onClick={handleCreateThread} onClick={handleCreateThread}
className="mt-4 text-green-500 hover:text-green-400" className="mt-2 text-blue-500"
> >
Create the first thread İlk konuyu oluştur
</button> </Button>
)} )}
</div> </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 { 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 { usePezkuwi } from '@/contexts/PezkuwiContext';
import { getReferralStats, calculateReferralScore, ReferralStats } from '@shared/lib/referral'; import { useReferral } from '@/contexts/ReferralContext';
import { getAllScores, UserScores, getScoreRating, getScoreColor } from '@shared/lib/scores'; 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 { 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'; import { cn } from '@/lib/utils';
interface DailyTask { export function RewardsSection() {
id: string; const { api, isApiReady, selectedAccount } = usePezkuwi();
title: string; const { stats, myReferrals, loading: referralLoading, refreshStats } = useReferral();
description: string; const { isConnected } = useWallet();
reward: number; const { hapticNotification, hapticImpact, showAlert, openTelegramLink } = useTelegram();
completed: boolean;
progress?: number;
maxProgress?: number;
}
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 [stakingInfo, setStakingInfo] = useState<StakingInfo | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [claimingEpoch, setClaimingEpoch] = useState<number | null>(null);
const isConnected = !!selectedAccount;
const address = selectedAccount?.address; const address = selectedAccount?.address;
const referralLink = address ? `https://t.me/pezkuwichain_bot?start=${address}` : '';
// Generate referral link // Fetch staking data
const referralLink = address
? `https://t.me/pezkuwichain?start=${address}`
: '';
// Fetch data when connected
useEffect(() => { useEffect(() => {
if (!api || !isApiReady || !address) return; if (!api || !isApiReady || !address) return;
const fetchData = async () => { const fetchData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const [stats, scores, staking] = await Promise.all([ const staking = await getStakingInfo(api, address);
getReferralStats(api, address),
getAllScores(api, address),
getStakingInfo(api, address),
]);
setReferralStats(stats);
setUserScores(scores);
setStakingInfo(staking); setStakingInfo(staking);
} catch (err) { } catch (err) {
console.error('Failed to fetch rewards data:', err); console.error('Failed to fetch rewards data:', err);
@@ -90,267 +50,301 @@ export function Rewards() {
const handleCopyLink = async () => { const handleCopyLink = async () => {
if (!referralLink) return; if (!referralLink) return;
try { try {
await navigator.clipboard.writeText(referralLink); await navigator.clipboard.writeText(referralLink);
setCopied(true); setCopied(true);
hapticNotification('success'); hapticNotification('success');
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
} catch { } catch {
showAlert('Failed to copy link'); showAlert('Link kopyalanamadı');
} }
}; };
const handleShare = () => { const handleShare = () => {
if (!referralLink) return; if (!referralLink) return;
hapticImpact('medium'); hapticImpact('medium');
const text = `Join Pezkuwichain - The Digital Kurdish State! Use my referral link:`; const text = encodeURIComponent('Pezkuwichain - Kürt Dijital Devleti! Referans linkimle katıl:');
const shareUrl = `https://t.me/share/url?url=${encodeURIComponent(referralLink)}&text=${encodeURIComponent(text)}`; openTelegramLink(`https://t.me/share/url?url=${encodeURIComponent(referralLink)}&text=${text}`);
openTelegramLink(shareUrl);
}; };
const handleClaimEpoch = async (epoch: number) => { const handleRefresh = async () => {
if (!api || !selectedAccount) return; hapticNotification('success');
await refreshStats();
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);
}
}; };
if (!isConnected) { // Not connected state
if (!isConnected || !selectedAccount) {
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full overflow-y-auto">
<div className="flex items-center gap-2 p-4 border-b border-gray-800"> <div className="flex-1 flex flex-col items-center justify-center p-6">
<Gift className="w-5 h-5 text-green-500" /> <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">
<h2 className="text-lg font-semibold text-white">Rewards</h2> <Gift className="w-12 h-12 text-purple-500" />
</div> </div>
<div className="flex-1 flex flex-col items-center justify-center text-gray-500 p-6"> <h2 className="text-white font-semibold text-xl mb-2">Ödüller ve Referanslar</h2>
<Gift className="w-16 h-16 mb-4 opacity-50" /> <p className="text-gray-400 text-sm text-center mb-8 max-w-xs">
<p className="text-center mb-4">Connect your wallet to view rewards and referrals</p> 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>
</div> </div>
); );
} }
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */} {/* Header Stats */}
<div className="flex items-center gap-2 p-4 border-b border-gray-800"> <div className="p-4 pb-0">
<Gift className="w-5 h-5 text-green-500" /> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white">Rewards</h2> <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> </div>
{/* Content */} {/* Referral Invite Section */}
<div className="flex-1 overflow-y-auto p-4 space-y-4"> <div className="px-4 pb-4">
{isLoading ? ( <Card className="bg-gradient-to-br from-purple-600 to-pink-600 border-0">
<div className="flex items-center justify-center h-32"> <CardContent className="p-4">
<Loader2 className="w-8 h-8 text-green-500 animate-spin" /> <div className="flex items-center gap-3 mb-4">
</div> <div className="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center">
) : ( <UserPlus className="w-6 h-6 text-white" />
<>
{/* 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>
</div> </div>
)} <div>
<h3 className="text-white font-semibold">Arkadaşını Davet Et</h3>
{/* Referral Section */} <p className="text-purple-100 text-sm">Her referans için puan kazan!</p>
<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> </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> </div>
{/* Epoch Rewards */} {/* Referral Link */}
{stakingInfo?.pezRewards && stakingInfo.pezRewards.hasPendingClaim && ( <div className="bg-black/20 rounded-lg p-3 mb-3">
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700"> <p className="text-purple-200 text-xs mb-1">Referans Linkin</p>
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-green-500" /> <code className="flex-1 text-white text-xs truncate">{referralLink}</code>
<span className="text-white font-medium">Epoch Rewards</span> <button
</div> onClick={handleCopyLink}
className="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
<div className="bg-gray-900 rounded-lg p-3 mb-3"> >
<div className="flex items-center justify-between mb-2"> {copied ? (
<span className="text-gray-400 text-sm">Current Epoch</span> <Check className="w-4 h-4 text-green-400" />
<span className="text-white font-medium"> ) : (
#{stakingInfo.pezRewards.currentEpoch} <Copy className="w-4 h-4 text-white" />
</span> )}
</div> </button>
<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>
</div> </div>
)} </div>
{/* Daily Tasks */} <Button
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700"> onClick={handleShare}
<div className="flex items-center gap-2 mb-3"> className="w-full bg-white text-purple-600 hover:bg-purple-50"
<Star className="w-5 h-5 text-yellow-500" /> >
<span className="text-white font-medium">Daily Tasks</span> <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>
<div className="space-y-3"> <div className="bg-black/20 rounded-lg p-3 mb-3">
{dailyTasks.map(task => ( <p className="text-orange-100 text-xs mb-1">Bekleyen PEZ</p>
<div <p className="text-white text-2xl font-bold">
key={task.id} {stakingInfo.pezRewards.totalClaimable} PEZ
className={cn( </p>
'bg-gray-900 rounded-lg p-3 flex items-center justify-between', </div>
task.completed && 'opacity-60'
)} <div className="space-y-2 mb-3">
> {stakingInfo.pezRewards.claimableRewards.map((reward) => (
<div className="flex-1"> <div key={reward.epoch} className="flex items-center justify-between bg-black/10 rounded-lg p-2">
<div className="flex items-center gap-2"> <span className="text-orange-100 text-sm">Epoch #{reward.epoch}</span>
<span className={cn( <span className="text-white font-medium">{reward.amount} PEZ</span>
'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> </div>
))} ))}
</div> </div>
</div>
</> <Button
)} className="w-full bg-white text-orange-600 hover:bg-orange-50"
</div> 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> </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 { 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 { 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 { 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'; import { cn } from '@/lib/utils';
interface TokenBalance { export function WalletSection() {
symbol: string; const { api, isApiReady, selectedAccount, connectWallet, disconnectWallet, accounts } = usePezkuwi();
name: string; const { balances, refreshBalances, isConnected } = useWallet();
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() {
const { hapticNotification, hapticImpact, showAlert, openLink } = useTelegram(); 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 [stakingInfo, setStakingInfo] = useState<StakingInfo | null>(null);
const [transactions, setTransactions] = useState<Transaction[]>(mockTransactions);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const isConnected = !!selectedAccount;
const address = selectedAccount?.address; const address = selectedAccount?.address;
// Fetch balances when connected // Fetch data when connected
useEffect(() => { useEffect(() => {
if (!api || !isApiReady || !address) return; if (!api || !isApiReady || !address) return;
const fetchData = async () => { const fetchData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
// Fetch native balance const [userScores, staking] = await Promise.all([
const accountInfo = await api.query.system.account(address); getAllScores(api, address),
const { data } = accountInfo.toJSON() as { data: { free: string; reserved: string } }; getStakingInfo(api, address),
const freeBalance = BigInt(data.free || 0); ]);
setScores(userScores);
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);
setStakingInfo(staking); setStakingInfo(staking);
} catch (err) { } catch (err) {
console.error('Failed to fetch wallet data:', err); console.error('Failed to fetch wallet data:', err);
@@ -106,26 +53,19 @@ export function Wallet() {
const handleRefresh = async () => { const handleRefresh = async () => {
if (!api || !address || isRefreshing) return; if (!api || !address || isRefreshing) return;
setIsRefreshing(true); setIsRefreshing(true);
hapticNotification('success'); hapticNotification('success');
try { try {
const accountInfo = await api.query.system.account(address); await refreshBalances();
const { data } = accountInfo.toJSON() as { data: { free: string } }; const [userScores, staking] = await Promise.all([
const freeBalance = BigInt(data.free || 0); getAllScores(api, address),
getStakingInfo(api, address),
setBalances([{ ]);
symbol: CHAIN_CONFIG.symbol, setScores(userScores);
name: 'Hezar Token',
balance: formatBalance(freeBalance.toString()),
isNative: true,
}]);
const staking = await getStakingInfo(api, address);
setStakingInfo(staking); setStakingInfo(staking);
} catch (err) { } catch (err) {
showAlert('Failed to refresh'); showAlert('Yenileme başarısız');
} finally { } finally {
setIsRefreshing(false); setIsRefreshing(false);
} }
@@ -133,14 +73,13 @@ export function Wallet() {
const handleCopyAddress = async () => { const handleCopyAddress = async () => {
if (!address) return; if (!address) return;
try { try {
await navigator.clipboard.writeText(address); await navigator.clipboard.writeText(address);
setCopied(true); setCopied(true);
hapticNotification('success'); hapticNotification('success');
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
} catch { } catch {
showAlert('Failed to copy address'); showAlert('Adres kopyalanamadı');
} }
}; };
@@ -149,273 +88,307 @@ export function Wallet() {
connectWallet(); connectWallet();
}; };
const handleDisconnect = () => {
hapticImpact('medium');
disconnectWallet();
};
const handleOpenExplorer = () => { const handleOpenExplorer = () => {
if (!address) return; if (!address) return;
hapticImpact('light'); hapticImpact('light');
openLink(`https://explorer.pezkuwichain.io/account/${address}`); openLink(`https://explorer.pezkuwichain.io/account/${address}`);
}; };
const getTransactionIcon = (type: Transaction['type']) => { // Not connected state
switch (type) { if (!isConnected || !selectedAccount) {
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) {
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full overflow-y-auto">
<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-1 flex flex-col items-center justify-center p-6"> <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"> <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">
<WalletIcon className="w-10 h-10 text-gray-600" /> <Wallet className="w-12 h-12 text-cyan-500" />
</div> </div>
<h3 className="text-white font-medium mb-2">Connect Your Wallet</h3> <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-6"> <p className="text-gray-400 text-sm text-center mb-8 max-w-xs">
Connect your Pezkuwi wallet to view balances, stake tokens, and manage your assets. Bakiyelerinizi görüntülemek, stake etmek ve işlem yapmak için Pezkuwi cüzdanınızı bağlayın.
</p> </p>
<button <Button
onClick={handleConnect} 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" /> <Wallet className="w-5 h-5 mr-2" />
Connect Wallet Cüzdan Bağla
</button> </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>
</div> </div>
); );
} }
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full overflow-y-auto bg-gray-950">
{/* Header */} {/* Account Card */}
<div className="flex items-center justify-between p-4 border-b border-gray-800"> <div className="p-4">
<div className="flex items-center gap-2"> <Card className="bg-gradient-to-br from-gray-900 to-gray-800 border-gray-700">
<WalletIcon className="w-5 h-5 text-green-500" /> <CardContent className="p-4">
<h2 className="text-lg font-semibold text-white">Wallet</h2> <div className="flex items-center justify-between mb-3">
</div> <div className="flex items-center gap-2">
<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">
<button <span className="text-white font-bold">
onClick={handleRefresh} {selectedAccount?.meta?.name?.charAt(0) || 'P'}
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'}
</span> </span>
<button
onClick={handleOpenExplorer}
className="text-gray-500 hover:text-gray-400"
>
<ExternalLink className="w-4 h-4" />
</button>
</div> </div>
<div className="flex items-center gap-2"> <div>
<code className="text-white text-sm flex-1 truncate"> <p className="text-white font-medium text-sm">
{formatAddress(address || '')} {selectedAccount?.meta?.name || 'Pezkuwi Hesabı'}
</code> </p>
<button <div className="flex items-center gap-1">
onClick={handleCopyAddress} <code className="text-gray-400 text-xs">
className="p-2 rounded bg-gray-700 hover:bg-gray-600 transition-colors" {formatAddress(address || '')}
> </code>
{copied ? ( <button onClick={handleCopyAddress} className="p-1">
<Check className="w-4 h-4 text-green-500" /> {copied ? (
) : ( <Check className="w-3 h-3 text-green-500" />
<Copy className="w-4 h-4 text-gray-400" /> ) : (
)} <Copy className="w-3 h-3 text-gray-500" />
</button> )}
</button>
</div>
</div> </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> </div>
{/* Balance Card */} {/* Balance Display */}
<div className="px-4 pb-4"> <div className="bg-black/30 rounded-lg p-4 mb-3">
<div className="bg-gradient-to-br from-green-600 to-emerald-700 rounded-lg p-4"> <p className="text-gray-400 text-xs mb-1">Toplam Bakiye</p>
<div className="text-green-100 text-sm mb-1">Total Balance</div> {isLoading ? (
<div className="text-3xl font-bold text-white mb-1"> <Skeleton className="h-8 w-32 bg-gray-700" />
{balances[0]?.balance || '0.00'} {CHAIN_CONFIG.symbol} ) : (
<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> </div>
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 && ( )}
<div className="text-green-200 text-sm"> {balances?.PEZ && parseFloat(balances.PEZ) > 0 && (
Staked: {stakingInfo.bonded} {CHAIN_CONFIG.symbol} <p className="text-green-400 text-sm mt-1">
</div> + {balances.PEZ} PEZ
)} </p>
</div> )}
</div> </div>
{/* Quick Actions */} {/* Quick Actions */}
<div className="px-4 pb-4"> <div className="grid grid-cols-3 gap-2">
<div className="grid grid-cols-3 gap-3"> <Button
<button variant="outline"
onClick={() => showAlert('Send feature coming soon!')} className="flex flex-col items-center gap-1 h-auto py-3 bg-gray-800/50 border-gray-700 hover:bg-gray-700"
className="flex flex-col items-center gap-2 bg-gray-800 hover:bg-gray-700 rounded-lg p-3 transition-colors" onClick={() => showAlert('Gönder özelliği yakında!')}
> >
<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" />
<Send className="w-5 h-5 text-blue-400" /> <span className="text-xs">Gönder</span>
</div> </Button>
<span className="text-xs text-gray-400">Send</span> <Button
</button> variant="outline"
<button 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('Receive feature coming soon!')} onClick={() => showAlert('Al özelliği yakında!')}
className="flex flex-col items-center gap-2 bg-gray-800 hover:bg-gray-700 rounded-lg p-3 transition-colors" >
> <ArrowDownToLine className="w-5 h-5 text-green-400" />
<div className="w-10 h-10 rounded-full bg-green-600/20 flex items-center justify-center"> <span className="text-xs">Al</span>
<ArrowDownToLine className="w-5 h-5 text-green-400" /> </Button>
</div> <Button
<span className="text-xs text-gray-400">Receive</span> variant="outline"
</button> className="flex flex-col items-center gap-1 h-auto py-3 bg-gray-800/50 border-gray-700 hover:bg-gray-700"
<button onClick={() => showAlert('Stake özelliği yakında!')}
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" <TrendingUp className="w-5 h-5 text-purple-400" />
> <span className="text-xs">Stake</span>
<div className="w-10 h-10 rounded-full bg-purple-600/20 flex items-center justify-center"> </Button>
<TrendingUp className="w-5 h-5 text-purple-400" />
</div>
<span className="text-xs text-gray-400">Stake</span>
</button>
</div>
</div> </div>
</CardContent>
</Card>
</div>
{/* Staking Info */} {/* Scores Section */}
{stakingInfo && parseFloat(stakingInfo.bonded) > 0 && ( <div className="px-4 pb-4">
<div className="px-4 pb-4"> <h3 className="text-white font-medium text-sm mb-3 flex items-center gap-2">
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700"> <Trophy className="w-4 h-4 text-yellow-500" />
<div className="flex items-center gap-2 mb-3"> Puanlarınız
<TrendingUp className="w-5 h-5 text-purple-500" /> </h3>
<span className="text-white font-medium">Staking Overview</span>
</div> {isLoading ? (
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="bg-gray-900 rounded-lg p-3"> {[...Array(4)].map((_, i) => (
<div className="text-gray-400 text-xs mb-1">Bonded</div> <Skeleton key={i} className="h-20 bg-gray-800" />
<div className="text-white font-medium">{stakingInfo.bonded}</div> ))}
</div> </div>
<div className="bg-gray-900 rounded-lg p-3"> ) : scores ? (
<div className="text-gray-400 text-xs mb-1">Active</div> <>
<div className="text-white font-medium">{stakingInfo.active}</div> {/* Total Score Banner */}
</div> <Card className="bg-gradient-to-r from-purple-600 to-pink-600 border-0 mb-3">
{stakingInfo.stakingScore !== null && ( <CardContent className="p-4 flex items-center justify-between">
<div className="bg-gray-900 rounded-lg p-3"> <div>
<div className="text-gray-400 text-xs mb-1">Staking Score</div> <p className="text-purple-100 text-xs">Toplam Skor</p>
<div className="text-green-500 font-medium">{stakingInfo.stakingScore}</div> <p className="text-white text-2xl font-bold">{scores.totalScore}</p>
</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>
</div> </div>
</div> <Badge className="bg-white/20 text-white border-0">
)} {getScoreRating(scores.totalScore)}
</Badge>
</CardContent>
</Card>
{/* Recent Transactions */} {/* Individual Scores */}
<div className="px-4 pb-4"> <div className="grid grid-cols-2 gap-3">
<div className="bg-gray-800 rounded-lg border border-gray-700"> <Card className="bg-gray-900 border-gray-800">
<div className="p-4 border-b border-gray-700"> <CardContent className="p-3">
<h3 className="text-white font-medium">Recent Activity</h3> <div className="flex items-center gap-2 mb-2">
</div> <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 ? ( <Card className="bg-gray-900 border-gray-800">
<div className="p-6 text-center text-gray-500 text-sm"> <CardContent className="p-3">
No recent transactions <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>
) : ( <p className="text-white text-xl font-bold">{scores.referralScore}</p>
<div className="divide-y divide-gray-700"> </CardContent>
{transactions.map(tx => ( </Card>
<div key={tx.id} className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <Card className="bg-gray-900 border-gray-800">
<div className="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center"> <CardContent className="p-3">
{getTransactionIcon(tx.type)} <div className="flex items-center gap-2 mb-2">
</div> <div className="w-8 h-8 rounded-lg bg-green-500/20 flex items-center justify-center">
<div> <TrendingUp className="w-4 h-4 text-green-500" />
<div className="text-white text-sm font-medium capitalize"> </div>
{tx.type} <span className="text-gray-400 text-xs">Staking</span>
</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>
))}
</div> </div>
)} <p className="text-white text-xl font-bold">{scores.stakingScore}</p>
</div> </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> </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>
</div> </div>
); );
} }
export default Wallet; export default WalletSection;